diff --git a/README.md b/README.md index 98d9915..6705881 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,45 @@ -# Mandelbrot Viewer — runtime-cost removal build +# Mandelbrot Deep Zoom Viewer — root-cause deep stability build Production source of truth is `index.html`. -This build starts from the integrated five-improvement viewer and applies the A/B-supported runtime-cost removals. The numerical architecture remains direct f32 / perturbation + series / sparse recovery / finite continuation / CPU fallback. +この版は、報告された「微小な座標移動で形状が大きく変わる」「deep view の fresh load が全面暗転する」問題を、履歴依存ヒステリシスではなく数値判定とprogressive publicationの根本側で修正したものです。 -## Validation +## 主な変更 + +- adaptive primary iteration の履歴依存ヒステリシスを完全撤廃 +- FAST perturbation の escape に propagated error guard を追加 +- FAST/active pixel delta で high/low span・inverse-width を使用 +- FAST numerical failure を同一referenceのGPU double-single sparse correctionへ最初に送る +- fresh deep frameをstrip単位でprogressive表示 +- DS sparse correctionもbatch単位でprogressive表示 +- unsafe series approximationは引き続き無効 + +## 検証 ```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 +node experiments/root-cause/analyze.mjs --assert ``` -The optional real-browser WebGPU test is `tests/browser_smoke.mjs`; it requires Chromium with a working WebGPU backend. +実装意図、報告viewの再現値、制約は `docs/root-cause-fix/IMPLEMENTATION.md` を参照してください。 -## Current records +過去の性能監査・統合記録は `docs/performance-cost-audit/`, `docs/integrated-improvements/`, `docs/removal-pass/`, `docs/reported-view-bugfix/` に残しています。前回のヒステリシス版 `docs/deep-stability/` は現仕様と矛盾するため削除しました。 -- `docs/removal-pass/IMPLEMENTATION.md` — why each runtime cost was removed or retained -- `docs/removal-pass/results.json` — structural checks, hashes, and A/B evidence -- `docs/removal-pass/integrated-bench.json` — post-removal CPU/surrogate benchmark +## 2026-09-29 black-regression rollback -Historical investigation records remain under `docs/performance-cost-audit/` and `docs/integrated-improvements/`. +The attempted broad FAST error guard/progressive partial publication was rolled back after user testing showed near-total black rendering on deep views. See `docs/black-regression-rollback/IMPLEMENTATION.md`. Adaptive-primary hysteresis remains removed. -## Reported deep-view artifact fix (2026-09-29) +## 2026-09-29 reference-end root fix -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`. +Deep FAST rendering no longer shortens the requested primary iteration count to a coarse bucket when the selected reference orbit is shorter. Reference exhaustion remains `REFERENCE_END` and is repaired before publication, preventing large transparent/black regions that were incorrectly produced as `OPERATION_LIMIT`. No hysteresis is used. See `docs/reference-end-root-fix/IMPLEMENTATION.md`. + +## 2026-09-29 navigation-continuity regression (#8 only) + +Only proposal #8 was implemented. Production `index.html` is unchanged. A dedicated `tests/navigation_continuity.mjs` now verifies that a target view produces the same sampled FAST numerical field when reached with a fresh target reference or via a nearby pan/zoom reference that production allows to be reused. + +```bash +node tests/navigation_continuity.mjs # strict: must be path-independent +node tests/navigation_continuity.mjs --report # diagnostic report without failing +``` + +The current implementation intentionally fails strict continuity and therefore confirms the existing defect. See `docs/navigation-continuity/IMPLEMENTATION.md`. diff --git a/docs/black-regression-rollback/IMPLEMENTATION.md b/docs/black-regression-rollback/IMPLEMENTATION.md new file mode 100644 index 0000000..1be3109 --- /dev/null +++ b/docs/black-regression-rollback/IMPLEMENTATION.md @@ -0,0 +1,41 @@ +# Black regression rollback + +## Intent + +A previous deep-zoom root-cause attempt introduced a broad f32 perturbation error propagation guard and progressive publication of partially resolved FAST fields. User testing showed this regressed rendering severely: most of the viewport could become black. + +This pass is deliberately a rollback, not another numerical heuristic. It restores the pre-regression FAST perturbation and publication behavior while keeping the later removal of adaptive-primary hysteresis. + +## Removed from the failed attempt + +1. FAST primary propagated `errScaled` acceptance/rejection logic. +2. Low-part delta rewrite that was bundled with that unvalidated FAST change. +3. Per-strip `publishNumericProgress()` during FAST primary. +4. Per-batch `publishNumericProgress()` during sparse correction. +5. Same-reference DS correction pre-pass added solely to service the broad FAST guard. +6. Publication of a FAST primary before numerical failure repair is complete. + +## Preserved + +- No adaptive-primary hysteresis. +- Series approximation remains disabled. +- Sparse active state, dynamic references, CPU fallback and the prior supported removal pass remain. +- FAST frames are withheld until numerical failures are repaired, so UNKNOWN alpha=0 pixels are not intentionally exposed as a fresh-frame partial image. + +## Validation + +- `node tests/regression.mjs`: PASS. +- 19 WGSL kernels pinned by SHA-256. +- JavaScript syntax is checked by the regression suite. +- Static invariants: + - no `ADAPTIVE_PRIMARY_HYSTERESIS` + - no `stabilizeAdaptiveInitialChoice` + - no `lastPrimaryPolicy` + - no `nearbyAdaptiveViewSafe` + - no failed-attempt progressive FAST publication + - no `same-reference-ds-guided-precision` + - `deferPrimaryColor=deferColor||fastExtended` + +## Scope / remaining issue + +This rollback fixes the newly introduced near-all-black regression. It does **not** claim that the earlier deep-view reference-dependence / coordinate-shift instability is solved. That issue must be addressed from numerical validity (perturbation/rebasing/reference-domain correctness), not from view-history hysteresis. diff --git a/docs/black-regression-rollback/index.diff b/docs/black-regression-rollback/index.diff new file mode 100644 index 0000000..f9aa534 --- /dev/null +++ b/docs/black-regression-rollback/index.diff @@ -0,0 +1,149 @@ +--- /mnt/data/mandelbrot_rootcause_fix/index.html 2026-09-29 11:48:30.228740302 +0000 ++++ index.html 2026-09-29 12:06:01.379510157 +0000 +@@ -230,54 +230,34 @@ + if(reason>=1u && reason<=6u){atomicAdd(&unresolved[reason],1u);} + } + +-fn fast_delta(gx:f32,gy:f32)->vec2{ +- // Keep the low parts that are already present in Params. The old FAST path +- // discarded both invFullWLo and spanMantLo, making the perturbation depend +- // unnecessarily on f32 pixel-coordinate rounding. +- let ox=gx-p.refPixelX; +- let oy=p.refPixelY-gy; +- let dxHi=ox*p.invFullWHi; let dxLo=ox*p.invFullWLo; +- let dyHi=oy*p.invFullWHi; let dyLo=oy*p.invFullWLo; +- return vec2( +- p.spanMantHi*dxHi+(p.spanMantHi*dxLo+p.spanMantLo*dxHi), +- p.spanMantHi*dyHi+(p.spanMantHi*dyLo+p.spanMantLo*dyHi) +- ); +-} + fn render_fast(out:u32,gx:f32,gy:f32){ +- var d=fast_delta(gx,gy); ++ let dx=(gx-p.refPixelX)/f32(p.fullW); ++ let dy=(p.refPixelY-gy)/f32(p.fullW); ++ var d=vec2(p.spanMantHi*dx,p.spanMantHi*dy); + var w=vec2(0.0); + var scaleExp=p.spanExp; +- var errScaled=4.0*F32_U*(maxabs(d)+1.0e-30); + var n=0u; var m=0u; + if(p.strict==0u && series.enabled!=0u && series.jump>0u && series.jump<=p.refLen && series.jump4.0){ +- let errAbs=safe_abs_error(errScaled,scaleExp,z,delta); +- if(length(z)-errAbs>2.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,mag);return;} +- mark_fast_unresolved(out,n,REASON_ESCAPE_UNCERTAIN);return; +- } ++ if(mag>4.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,mag);return;} + if(n>=p.maxIter){mark_fast_unresolved(out,p.maxIter,REASON_OPERATION_LIMIT);return;} ++ // Reference-near-zero episodes are not an automatic failure. Continue the ++ // valid perturbation recurrence; explicit range/reference guards remain. + if(m>=p.refLen){mark_fast_unresolved(out,n,REASON_REFERENCE_END);return;} +- let refAbs=maxabs(rp.hi)+maxabs(rp.lo); +- let wAbs=maxabs(w); let dAbs=maxabs(d); let deltaAbs=maxabs(delta); +- let sq=scaled_to_f32(cmul(w,w),scaleExp); let sqAbs=maxabs(sq); +- let gain=2.0*refAbs+2.0*deltaAbs; +- let roundErr=1.0*F32_U*(2.0*refAbs*wAbs+sqAbs+dAbs+1.0e-30); +- errScaled=gain*errScaled+roundErr; +- let linear=2.0*(cmul(rp.hi,w)+cmul(rp.lo,w)); ++ let r=rp; ++ let linear=2.0*(cmul(r.hi,w)+cmul(r.lo,w)); ++ let sq=scaled_to_f32(cmul(w,w),scaleExp); + w=linear+sq+d; m+=1u; n+=1u; + let mm=max(maxabs(w),maxabs(d)); +- if(mm>=1.0e30 || mm!=mm || errScaled!=errScaled || errScaled>1.0e35){mark_fast_unresolved(out,n,REASON_RANGE);return;} +- if(mm>65536.0){w*=0.0000152587890625;d*=0.0000152587890625;errScaled*=0.0000152587890625;scaleExp+=16;} +- else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){w*=65536.0;d*=65536.0;errScaled*=65536.0;scaleExp-=16;} ++ if(mm>=1.0e30 || mm!=mm){mark_fast_unresolved(out,n,REASON_RANGE);return;} ++ if(mm>65536.0){w*=0.0000152587890625;d*=0.0000152587890625;scaleExp+=16;} ++ else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){w*=65536.0;d*=65536.0;scaleExp-=16;} + if(scaleExp>126){mark_fast_unresolved(out,n,REASON_RANGE);return;} + } + } +@@ -300,7 +280,7 @@ + struct Params{tileW:u32,tileH:u32,fullW:u32,fullH:u32,tileX:u32,tileY:u32,maxIter:u32,refLen:u32,strict:u32,unknownOnly:u32,outputStride:u32,outputBase:u32,spanMantHi:f32,spanExp:i32,sampleX:f32,sampleY:f32,refPixelX:f32,refPixelY:f32,spanMantLo:f32,invFullWHi:f32,invFullWLo:f32,targetIter:u32,chunkSize:u32,_numeric2:u32}; + struct DeepState{d:vec2,w:vec2,scaleExp:i32,n:u32,m:u32,errScaled:f32}; + @group(0) @binding(0) var p:Params;@group(0) @binding(1) var states:array;@group(0) @binding(2) var queue:array;@group(0) @binding(3) var activeMark:array>;@group(0) @binding(4) var pixelMap:array; +-@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) gid:vec3){let qi=gid.x;if(qi>=p.unknownOnly){return;}let slot=queue[qi];let out=pixelMap[slot];if(out>=p.fullW*p.fullH){return;}let px=out%p.fullW;let py=out/p.fullW;let gx=f32(px)+p.sampleX;let gy=f32(py)+p.sampleY;let ox=gx-p.refPixelX;let oy=p.refPixelY-gy;let dxHi=ox*p.invFullWHi;let dxLo=ox*p.invFullWLo;let dyHi=oy*p.invFullWHi;let dyLo=oy*p.invFullWLo;let d=vec2(p.spanMantHi*dxHi+(p.spanMantHi*dxLo+p.spanMantLo*dxHi),p.spanMantHi*dyHi+(p.spanMantHi*dyLo+p.spanMantLo*dyHi));states[slot]=DeepState(d,vec2(0.0),p.spanExp,0u,0u,4.0*F32_U*(maxabs(d)+1.0e-30));} ++@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) gid:vec3){let qi=gid.x;if(qi>=p.unknownOnly){return;}let slot=queue[qi];let out=pixelMap[slot];if(out>=p.fullW*p.fullH){return;}let px=out%p.fullW;let py=out/p.fullW;let gx=f32(px)+p.sampleX;let gy=f32(py)+p.sampleY;let dx=(gx-p.refPixelX)/f32(p.fullW);let dy=(p.refPixelY-gy)/f32(p.fullW);let d=vec2(p.spanMantHi*dx,p.spanMantHi*dy);states[slot]=DeepState(d,vec2(0.0),p.spanExp,0u,0u,1.0*F32_U*maxabs(d));} + `; + const DEEP_ACTIVE_CONTINUE_WGSL=COMMON+String.raw` + struct Params{tileW:u32,tileH:u32,fullW:u32,fullH:u32,tileX:u32,tileY:u32,maxIter:u32,refLen:u32,strict:u32,unknownOnly:u32,outputStride:u32,outputBase:u32,spanMantHi:f32,spanExp:i32,sampleX:f32,sampleY:f32,refPixelX:f32,refPixelY:f32,spanMantLo:f32,invFullWHi:f32,invFullWLo:f32,targetIter:u32,chunkSize:u32,_numeric2:u32}; +@@ -1395,13 +1375,7 @@ + const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4)); + const refX=referencePixel?.x??f.w*.5,refY=referencePixel?.y??f.h*.5,mirror=canUseRealAxisSymmetry(snap),computeH=mirror?Math.ceil(f.h/2):f.h,previous=state.fieldView,currentReferenceKey=this.fastCtx?.key||'',reusePriorField=!!previous&&sameViewSnapshot(previous,snap)&&previous.w===f.w&&previous.h===f.h&&previous.iter{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(); +- // A fresh deep view used to remain completely black until every FAST +- // strip finished. Publish already computed, numerically safe pixels at +- // bounded intervals; UNKNOWN/uncertain pixels stay transparent and are +- // repaired before the final frame is certified. +- if(!deferColor&&!await this.publishNumericProgress(snap,iter,token,'COVERING'))return false; +- return true}; ++ 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=burstLimit||burstPredictedMs>=budgetMs||performance.now()-burstStart>=budgetMs){if(!await flushBurst(computeH-(y+th)))return false}}y+=th} + 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 + } +@@ -1468,14 +1442,6 @@ + return true; + } + 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} +- async publishNumericProgress(snap,iter,token,drawState='REFINING',intervalMs=180){ +- if(state.deferNumericPublish||token!==state.token||performance.now()-(this.lastProvisionalAt||0)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()])} +@@ -1547,7 +1513,7 @@ + const built=await this.readDeepQueueStats();if(token!==state.token)return false; + const buildValid=built.selected===built.enqueued&&built.overflow===0&&built.invalidIndex===0;if(!buildValid){console.warn('Sparse correction build invariant failed; using bounded full scan',built);return this.correctUnknownFrameScan(snap,iter,token,referencePixel)} + const maxBatch=Math.max(1,Math.floor(PIXEL_FRONTIER_WORK/Math.max(1,iter)));let offset=0; +- while(offset0){const guided=await recoverNumericalGuided(r,snap,iter,token,stats);if(!guided||token!==state.token)return null;stats=guided.stats;attempted+=guided.attempted||0;passes+=guided.passes||0;} ++ const guided=await recoverNumericalGuided(r,snap,iter,token,stats);if(!guided||token!==state.token)return null;stats=guided.stats;attempted+=guided.attempted||0;passes+=guided.passes||0; + if(numericalFailureCount(stats)>0){const pf=await recoverResidualPrecision(r,snap,iter,token,stats);if(!pf||token!==state.token)return null;stats=pf.stats;attempted+=pf.attempted||0;applied+=pf.applied||0} +- return{stats,attempted,applied,passes,policy:'same-reference-ds-guided-precision'}; ++ return{stats,attempted,applied,passes,policy:'guided-precision'}; + } + function precisionBatchSize(iter,bits){return Math.max(1,Math.min(64,Math.floor(500000/(Math.max(1,iter)*5*Math.max(1,bits/128)))))} + async function recoverResidualPrecision(r,snap,iter,token,initialStats,terminalClass=-1){ +@@ -1842,8 +1803,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,primaryIter:iter,requestedIter}} + 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 covered=primaryIterCoveredByReference(iter,fastCtx?.refLen||0);if(covered0){const ordered=await recoverNumericalCostOrdered(r,snap,iter,token,stats,referencePixel);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 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,fastExtended?fastCtx?.source:null);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,primaryIter:iter,requestedIter} + } diff --git a/docs/black-regression-rollback/results.json b/docs/black-regression-rollback/results.json new file mode 100644 index 0000000..001a23e --- /dev/null +++ b/docs/black-regression-rollback/results.json @@ -0,0 +1,16 @@ +{ + "generatedAt": "2026-09-29T12:06:43Z", + "status": "rollback-complete", + "regression": "PASS", + "wgslKernels": 19, + "sha256": "e9bebe37ded03f56d8f36da41caedc658f5ba05274120d90141b1746ea2c634b", + "failedAttemptRemoved": [ + "fast-primary-propagated-error-guard", + "fast-progressive-partial-publication", + "sparse-correction-progressive-partial-publication", + "same-reference-ds-prepass" + ], + "hysteresisPresent": false, + "seriesApproximationEnabled": false, + "remainingKnownIssue": "deep-view reference-dependent numerical instability remains under investigation" +} diff --git a/docs/deep-stability/IMPLEMENTATION.md b/docs/deep-stability/IMPLEMENTATION.md new file mode 100644 index 0000000..7d53e8d --- /dev/null +++ b/docs/deep-stability/IMPLEMENTATION.md @@ -0,0 +1,114 @@ +# Deep zoom stability fix — implementation and evaluation + +Date: 2026-09-29 + +## Scope + +This pass targets three user-reported deep-zoom pathologies in the current production-derived `index.html`: + +1. A b=256 view that loads extremely slowly. +2. A nearby b=256 view where a small pan can produce a disproportionate visible/performance change. +3. A b=273 view where a small zoom-in can abruptly become much slower. + +The previous reported-view fix remains in place: series approximation is disabled and FAST numerical failures are not published before correction. + +## Root causes found + +### 1. Primary iteration can exceed the available FAST reference + +The first view selected an initial primary budget of 768 iterations, while the old FAST reference selector returned a reference orbit ending at iteration 614. In an 84-point exact sample, 8 points survived beyond iteration 614. Those pixels can enter `REFERENCE_END` during the primary pass and then require expensive guided-reference / high-precision correction. + +The old selector scanned an 8x6 grid in row-major order and could return the first early-escaping candidate far from the viewport center. + +### 2. Adaptive initial-iteration selection is discontinuous + +For the reported pan-sensitive view, tiny horizontal changes caused the raw selected primary budget to jump among 512, 768, 1024 and 1536 iterations. That changes provisional work, reference requirements and follow-on correction abruptly even though the camera motion is small. + +For the reported zoom-cliff view, changing the scale from 0.98 to 0.95 caused the raw primary choice to jump from 768 to 1536. At that 0.95 scale the old FAST reference ended at 940, so the larger primary target simultaneously increased work and increased `REFERENCE_END` risk. + +### 3. Sparse long-tail continuation over-shoots + +At the 0.95-scale zoom-cliff sample, only 1 of 84 exact sample points was still active after 1536 iterations. The old continuation rule doubled 1536 -> 3072 regardless of active-tail size. For a sparse tail this wastes work and can force a different reference, which in turn can reinitialize continuation state. + +## Implemented changes + +### Center-prioritized FAST reference selection + +`fastReferenceWorkerSource()` now: + +- samples a denser 12x8 candidate grid; +- orders candidates by distance from viewport center; +- prefers a nearby survivor that covers the requested primary iteration budget. + +For the slow-load reproduction this changes the reference from length 614 to 768 and removes all 8/84 sampled `REFERENCE_END` risks. + +### Primary budget cannot run beyond the selected reference + +`primaryIterCoveredByReference()` clamps the actual primary iteration to the largest safe adaptive bucket covered by the FAST reference. If reference construction cannot cover an aggressive probe choice, the renderer does less primary work and leaves the remaining pixels to the existing sparse continuation path instead of deliberately generating `REFERENCE_END` failures. + +### Hysteresis and step limiting for adaptive primary work + +The adaptive probe itself is retained because previous A/B testing showed that deleting it can increase work by up to about 33% on some views. + +The selected result is now stabilized using: + +- a per-quality primary cap (`fast=768`, `standard=1024`, `high=1536`); +- the previous successful nearby frame; +- a 15% cost hysteresis band; +- at most one adaptive bucket of change per nearby frame. + +This makes camera motion continuous without converting the renderer to a fixed iteration budget. + +### Tail-aware sparse continuation + +`nextContinuationTarget()` keeps the existing doubling behavior for dense/early work, but uses smaller 4/3 or 3/2 increments when the active frontier is already sparse. In the reported zoom-cliff case, the next target after 1536 becomes 2048 instead of 3072. + +### Reuse the primary FAST source for continuation + +The primary FAST reference source is passed into the first continuation session. This avoids immediately discarding a suitable primary source and independently selecting a different continuation source. + +## Measured reproductions + +### Slow-load view + +| Metric | Before | After | +|---|---:|---:| +| Primary iteration | 768 | 768 | +| FAST reference length | 614 | 768 | +| 84-sample points surviving past reference end | 8 | 0 | +| Median reference build surrogate | 18.28 ms | 15.01 ms | + +The CPU timing is a local surrogate and is not a WebGPU frame-time measurement. + +### Pan-sensitive view + +Across `dx = 0.00 .. 0.10` in 0.01 viewport-span increments: + +- Before selected primary budgets: 512 / 768 / 1024 / 1536 appeared. +- After, in a continuous navigation sequence: all steps stayed at 768. + +The raw probe remains variable; the change deliberately stabilizes the live policy rather than pretending the probe function itself became smooth. + +### Zoom-cliff view + +Across scale factors `1.00, 0.98, 0.95, 0.92, 0.90, 0.88, 0.85, 0.80`: + +- Before: `768, 768, 1536, 1024, 768, 768, 768, 768`. +- After continuous navigation stabilization: all eight select 768. +- At scale 0.95, FAST reference coverage changes from 940/1536 to 768/768. +- Sparse next target changes from 3072 to 2048 when only a very small tail remains. + +## Regression and compatibility + +- Production regression: PASS. +- WGSL kernels checked: 19. +- Existing direct periodicity, sparse active state, failure-guided multi-reference and CPU fallback tests still complete. +- Series approximation remains disabled after the previous reported-view numerical overflow bug. + +## Important limitations + +The current execution environment cannot provide a working real WebGPU browser backend, so actual GPU frame-time p50/p95 for these three URLs is not measured here. The evidence consists of the actual production control/reference code, exact-orbit sampling, worker/reference timing surrogates and regression tests. + +The isolated fresh-reference metric is not used as proof of better reference-position continuity: live navigation also has a nearby-reference cache, while the isolated script deliberately rebuilds each reference. The strong measured improvement is iteration-policy continuity and elimination of primary-reference under-coverage in the reported cases. + +These changes reduce pathological work and discontinuous control decisions; they do not turn the FAST perturbation path into a mathematically certified membership oracle. Strict/certified behavior remains separate as before. diff --git a/docs/deep-stability/build-summary.json b/docs/deep-stability/build-summary.json new file mode 100644 index 0000000..20bf821 --- /dev/null +++ b/docs/deep-stability/build-summary.json @@ -0,0 +1,11 @@ +{ + "date": "2026-09-29", + "beforeSha256": "c53c0dc4cb9fa2d2046aaa12ef03cc79c61a710b0c8db36dea1a87be2d9aedd8", + "afterSha256": "c35f514d5edd29d7e2aba43125cda8023034f4bf6d2e3104a97051950925529c", + "beforeBytes": 224955, + "afterBytes": 228503, + "beforeGzip9Bytes": 57958, + "afterGzip9Bytes": 59232, + "regression": "PASS: production JS, 19 WGSL kernels, staged scoring, lifecycle, export metadata", + "webgpuFrameTiming": "not measured; browser WebGPU backend unavailable in this environment" +} diff --git a/docs/deep-stability/index.diff b/docs/deep-stability/index.diff new file mode 100644 index 0000000..43381ae --- /dev/null +++ b/docs/deep-stability/index.diff @@ -0,0 +1,133 @@ +--- /mnt/data/mandelbrot_deep_stability/docs/index.before-deep-stability.html 2026-09-29 10:56:39.029999242 +0000 ++++ /mnt/data/mandelbrot_deep_stability/index.html 2026-09-29 11:00:10.948038400 +0000 +@@ -912,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}; ++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,lastPrimaryPolicy:null}; + let renderer=null,rendererInitPromise=null,raf=0,settleTimer=0,lastWrittenHash='',navigationHash=''; + + // ── exact fixed-point view state ───────────────────────────────────────── +@@ -943,6 +943,41 @@ + // ── iteration / quality policy ─────────────────────────────────────────── + const ADAPTIVE_ITER_QUANTUM=64,ADAPTIVE_PROBE_COLS=4,ADAPTIVE_PROBE_ROWS=3; + const ADAPTIVE_PROBE_CHOICES=[512,768,1024,1536,2048]; ++const ADAPTIVE_PRIMARY_CAP={fast:768,standard:1024,high:1536}; ++const ADAPTIVE_PRIMARY_HYSTERESIS=1.15; ++function nearbyAdaptiveViewSafe(old,snap,maxZoom=1.35,maxShift=.15){ ++ if(!old||!snap||old.span<=0n||snap.span<=0n)return false; ++ const b=Math.max(old.bits,snap.bits),os=align(old.span,old.bits,b),ns=align(snap.span,snap.bits,b),dr=align(snap.re,snap.bits,b)-align(old.re,old.bits,b),di=align(snap.im,snap.bits,b)-align(old.im,old.bits,b),scale=fixedRatio(ns,os),shift=Math.hypot(fixedRatio(dr,os),fixedRatio(di,os)); ++ if(!Number.isFinite(scale)||scale<=0||!Number.isFinite(shift))return false; ++ return Math.max(scale,1/scale)<=maxZoom&&shift<=maxShift; ++} ++function stabilizeAdaptiveInitialChoice(selected,snap){ ++ const cap=Math.max(state.baseIter,ADAPTIVE_PRIMARY_CAP[state.quality]||1024),eligible=(selected.work||[]).filter(x=>x.initial<=cap); ++ let best=eligible.length?eligible.reduce((a,b)=>b.totalx<=cap&&a.indexOf(x)===i).sort((a,b)=>a-b),prev=allowed.reduce((a,b)=>Math.abs(b-policy.iter)x.initial===prev); ++ if(prevWork&&prevWork.total<=best.total*ADAPTIVE_PRIMARY_HYSTERESIS)return{...selected,iter:prev,rawIter:selected.iter,stabilized:true,cap}; ++ const pi=allowed.indexOf(prev),bi=allowed.reduce((idx,x,i)=>Math.abs(x-best.initial)pi?Math.min(pi+1,allowed.length-1):bi=0&&bi>=0)return{...selected,iter:allowed[step],rawIter:selected.iter,stabilized:allowed[step]!==selected.iter,cap}; ++ } ++ return{...selected,iter:best.initial,rawIter:selected.iter,stabilized:best.initial!==selected.iter,cap}; ++} ++function nextContinuationTarget(iter,maxIter,session){ ++ if(iter>=maxIter)return maxIter; ++ if(iter<1024)return Math.min(maxIter,Math.max(iter+1,iter*2)); ++ const initial=Math.max(1,session?.initial||session?.active||1),active=Math.max(0,session?.active||0),ratio=active/initial; ++ let factor=2; ++ if(active<=8192||ratio<=.02)factor=4/3;else if(ratio<=.10)factor=1.5; ++ const target=Math.ceil(iter*factor/64)*64; ++ return Math.min(maxIter,Math.max(iter+64,target)); ++} ++function primaryIterCoveredByReference(iter,refLen){ ++ if(!Number.isFinite(refLen)||refLen<=0||refLen>=iter)return iter; ++ const choices=[state.baseIter,...ADAPTIVE_PROBE_CHOICES].filter((x,i,a)=>x<=refLen&&a.indexOf(x)===i); ++ if(choices.length)return Math.max(...choices); ++ return Math.max(1,Math.min(iter,Math.max(64,Math.floor(refLen/64)*64))); ++} + function adaptiveIterationBonus(screenBits,q,quantum=ADAPTIVE_ITER_QUANTUM){const raw=48*Math.max(0,Number(q)||0)*Math.max(0,Number(screenBits)||0);return raw>0?Math.ceil(raw/quantum)*quantum:0} + function maxIter(){if(!state.adaptive)return Math.min(state.baseIter,state.continuationBudget||state.baseIter);const z=zoomExp(),w=Math.max(1,canvas.width||innerWidth||1),screenBits=Math.max(0,Math.log2(w)+z*Math.LOG2E*Math.LN10),bonus=adaptiveIterationBonus(screenBits,1);return Math.min(state.continuationBudget||12000,12000,Math.max(state.baseIter,state.baseIter+bonus))} + function canUseStableReprojection(){ +@@ -1108,7 +1143,7 @@ + const sources=adaptiveProbeSources(snap,width,height),started=performance.now(); + try{ + const scored=await referenceScorer.scoreFixedCandidates(snap,adaptiveIter,width,height,sources);if(token!==state.token)return null; +- const selected=adaptiveProbeChoice(scored.escapes||[],adaptiveIter,state.baseIter),probeMs=performance.now()-started; ++ const raw=adaptiveProbeChoice(scored.escapes||[],adaptiveIter,state.baseIter),selected=stabilizeAdaptiveInitialChoice(raw,snap),probeMs=performance.now()-started; + + return{...selected,adaptiveIter,probeMs,samples:sources.length,escapes:scored.escapes||[]}; + }catch(error){ +@@ -1124,14 +1159,17 @@ + function orbit(bits,re,im,iter){const B=BigInt(bits),half=1n<<(B-1n),round=v=>{const neg=v<0n,a=neg?-v:v,q=(a+half)>>B;return neg?-q:q},BAIL=16n*(1n<>B;zi2=(zi*zi+half)>>B;if(zr2+zi2>BAIL)escape=n+1}rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);return{rr,ri,refLen:escape||iter,escape}} + function orbitEscape(bits,re,im,iter){const B=BigInt(bits),half=1n<<(B-1n),round=v=>{const neg=v<0n,a=neg?-v:v,q=(a+half)>>B;return neg?-q:q},BAIL=16n*(1n<>B;zi2=(zi*zi+half)>>B;if(zr2+zi2>BAIL)return n+1}return 0} + function chooseReference(bits,re,im,span,w,h,iter,centerEscape=0,probeBits=bits){ +- let best={re,im,escape:centerEscape,score:centerEscape||iter}; +- if(centerEscape===0||span<=0n||w<=0||h<=0)return best; +- // Score candidates at the packed-reference precision used by the orbit. +- const pb=Math.max(64,Math.min(bits,probeBits|0)),bw=BigInt(w),seen=new Set([re+':'+im]),goal=Math.max(4096,Math.floor(iter/3)); +- const probe=(sr,si)=>{const key=sr+':'+si;if(seen.has(key))return false;seen.add(key);const rr=requant(sr,bits,pb),ii=requant(si,bits,pb),escape=orbitEscape(pb,rr,ii,iter),score=escape||iter;if(score>best.score)best={re:sr,im:si,escape,score};return escape===0||score>=goal}; +- // Sample the actual pixel-centre domain with an 8x6 checkerboard. +- const div=(v,d)=>{const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q},den=2n*bw; +- for(let jy=0;jy<6;jy++)for(let ix=0;ix<8;ix++){if(((ix+jy)&1)!==1)continue;const x=Math.floor((ix+.5)*w/8),y=Math.floor((jy+.5)*h/6),sr=re+div(span*(2n*BigInt(x)+1n-bw),den),si=im+div(span*(BigInt(h)-2n*BigInt(y)-1n),den);if(probe(sr,si))return best} ++ let best={re,im,escape:centerEscape,score:centerEscape||iter,dist2:0}; ++ if(span<=0n||w<=0||h<=0)return best; ++ // The primary FAST reference is chosen from a denser 12x8 checkerboard, ++ // ordered from the viewport centre outwards. A nearby survivor therefore ++ // wins before a distant one, avoiding large reference jumps after tiny pans. ++ const pb=Math.max(64,Math.min(bits,probeBits|0)),bw=BigInt(w),seen=new Set([re+':'+im]),goal=Math.max(4096,Math.floor(iter/3)),div=(v,d)=>{const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q},den=2n*bw,candidates=[]; ++ for(let jy=0;jy<8;jy++)for(let ix=0;ix<12;ix++){if(((ix+jy)&1)!==1)continue;const x=Math.floor((ix+.5)*w/12),y=Math.floor((jy+.5)*h/8),dx=x+.5-w*.5,dy=y+.5-h*.5;candidates.push({x,y,dist2:dx*dx+dy*dy})} ++ candidates.sort((a,b)=>a.dist2-b.dist2||a.y-b.y||a.x-b.x); ++ const probe=(sr,si,dist2)=>{const key=sr+':'+si;if(seen.has(key))return false;seen.add(key);const rr=requant(sr,bits,pb),ii=requant(si,bits,pb),escape=orbitEscape(pb,rr,ii,iter),score=escape||iter;if(score>best.score||score===best.score&&dist2=goal}; ++ if(centerEscape===0)return best; ++ for(const c of candidates){const sr=re+div(span*(2n*BigInt(c.x)+1n-bw),den),si=im+div(span*(BigInt(h)-2n*BigInt(c.y)-1n),den);if(probe(sr,si,c.dist2))return best} + return best + } + function packRefs(rr,ri,refLen){const buf=new ArrayBuffer((refLen+1)*16),dv=new DataView(buf);for(let i=0;i<=refLen;i++){const hr=Math.fround(rr[i]),hi=Math.fround(ri[i]),lr=Math.fround(rr[i]-hr),li=Math.fround(ri[i]-hi),o=i*16;dv.setFloat32(o,hr,true);dv.setFloat32(o+4,hi,true);dv.setFloat32(o+8,lr,true);dv.setFloat32(o+12,li,true)}return buf} +@@ -1656,12 +1694,12 @@ + const guided=await tryOperationLimitGuidedReference(r,session,snap,target,token,stats,best,tried);if(!guided||token!==state.token)return null;if(guided.used){best=guided.ctx;bestSource=guided.ctx.source;if(best.refLen>=target)return{ctx:best,source:bestSource}} + return{ctx:best,source:bestSource}; + } +-async function refinePixelFrontier(r,snap,initialIter,token,initialStats,certified=0){ ++async function refinePixelFrontier(r,snap,initialIter,token,initialStats,certified=0,preferredSource=null){ + if(!state.continuationBudget)throw new Error('production quality requires a finite continuation budget'); + const maxIter=Math.max(initialIter,state.continuationBudget),minimumIter=maxIter; +- let session=await r.beginGpuOperationLimitContinuation(snap,token,{x:canvas.width*.5,y:canvas.height*.5},false,initialStats?.reasons?.operationLimit||0);if(!session||token!==state.token)return null;let iter=initialIter,stats=initialStats,rounds=0,processed=0,retired=0,ctx=null,fixedSource=null,converged=session.active===0||iter>=maxIter; ++ let session=await r.beginGpuOperationLimitContinuation(snap,token,{x:canvas.width*.5,y:canvas.height*.5},false,initialStats?.reasons?.operationLimit||0);if(!session||token!==state.token)return null;let iter=initialIter,stats=initialStats,rounds=0,processed=0,retired=0,ctx=null,fixedSource=preferredSource,converged=session.active===0||iter>=maxIter; + while(!converged&&iter0||!!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)}} ++ 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,primaryIter:iter,requestedIter}} ++ 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 covered=primaryIterCoveredByReference(iter,fastCtx?.refLen||0);if(covered0){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} ++ const frontier=await refinePixelFrontier(r,snap,iter,token,{total:state.unresolved,reasons:{...(state.unknownReasons||{})}},certified,fastExtended?fastCtx?.source:null);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,primaryIter:iter,requestedIter} + } + function numericFrameComplete(){return state.fieldView?.complete===true&&numericalFailureCount({reasons:state.unknownReasons||{}})===0} + async function renderFrame(){ +@@ -1799,6 +1837,7 @@ + if(!r){try{const ok=await renderCpuFallbackFrame(snap,iter,token);if(!ok||token!==state.token)return;state.rendering=false;state.drawState='COVERED';finishRenderClock(token);updateStats();return}catch(error){if(token!==state.token)return;state.gpuError=(state.gpuError?state.gpuError+' · ':'')+'CPU fallback失敗: '+String(error&&error.message||error);finishRenderClock(token,'error');state.rendering=false;state.drawState='ERROR';updateStats();return}} + state.temporalFill=!!r.historyReady;if(holdStable){r.presentFrame(r.presentTransform());state.drawState='REPROJECTED';updateStats()} + const result=await renderFinalFrame(r,snap,iter,token);if(!result||token!==state.token)return; ++ state.lastPrimaryPolicy={view:{...snap},iter:result.primaryIter??iter}; + state.rendering=false;state.drawState='COVERED';if(numericFrameComplete()){r.commitHistory(snap);state.temporalFill=false}else state.temporalFill=!!r.historyReady;r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0}); + state.rendering=true;await r.device.queue.onSubmittedWorkDone();if(token!==state.token)return;state.rendering=false;finishRenderClock(token); + updateStats();if(state.recolorPending&&!state.recoloring)queueMicrotask(recolor); diff --git a/docs/deep-stability/regression.txt b/docs/deep-stability/regression.txt new file mode 100644 index 0000000..aadf988 --- /dev/null +++ b/docs/deep-stability/regression.txt @@ -0,0 +1 @@ +PASS: single-source finite viewer regression (production JS, 19 WGSL kernels, staged scoring, lifecycle, export metadata) diff --git a/docs/deep-stability/results.json b/docs/deep-stability/results.json new file mode 100644 index 0000000..7afdccd --- /dev/null +++ b/docs/deep-stability/results.json @@ -0,0 +1,262 @@ +{ + "date": "2026-09-29T11:00:51.867Z", + "canvas": { + "width": 1365, + "height": 768 + }, + "slowLoad": { + "before": { + "initialIter": 768, + "fastRef": { + "refLen": 614, + "selectionEscape": 614, + "referencePixel": { + "x": 85.50000000000034, + "y": 704.4999999999998 + }, + "medianBuildMs": 18.279392, + "times": [ + 13.043343000000007, + 15.469174999999993, + 18.279392, + 59.971247000000005, + 66.900037 + ] + }, + "referenceEndRiskSamples": 8 + }, + "after": { + "initialIter": 768, + "fastRef": { + "refLen": 768, + "selectionEscape": 0, + "referencePixel": { + "x": 398.5000000000001, + "y": 432.49999999999983 + }, + "medianBuildMs": 15.013002000000029, + "times": [ + 11.409249000000045, + 14.228717999999958, + 15.013002000000029, + 15.405350999999968, + 17.648473999999965 + ] + }, + "referenceEndRiskSamples": 0 + }, + "sampleCount": 84 + }, + "shiftStability": { + "dx": [ + 0, + 0.01, + 0.02, + 0.03, + 0.04, + 0.05, + 0.06, + 0.07, + 0.08, + 0.09, + 0.1 + ], + "before": [ + { + "raw": 768, + "selected": 768 + }, + { + "raw": 1024, + "selected": 1024 + }, + { + "raw": 1024, + "selected": 1024 + }, + { + "raw": 512, + "selected": 512 + }, + { + "raw": 512, + "selected": 512 + }, + { + "raw": 1024, + "selected": 1024 + }, + { + "raw": 1024, + "selected": 1024 + }, + { + "raw": 768, + "selected": 768 + }, + { + "raw": 768, + "selected": 768 + }, + { + "raw": 1536, + "selected": 1536 + }, + { + "raw": 512, + "selected": 512 + } + ], + "after": [ + { + "raw": 768, + "selected": 768 + }, + { + "raw": 1024, + "selected": 768 + }, + { + "raw": 1024, + "selected": 768 + }, + { + "raw": 512, + "selected": 768 + }, + { + "raw": 512, + "selected": 768 + }, + { + "raw": 1024, + "selected": 768 + }, + { + "raw": 1024, + "selected": 768 + }, + { + "raw": 768, + "selected": 768 + }, + { + "raw": 768, + "selected": 768 + }, + { + "raw": 1536, + "selected": 768 + }, + { + "raw": 512, + "selected": 768 + } + ], + "beforeUnique": [ + 768, + 1024, + 512, + 1536 + ], + "afterUnique": [ + 768 + ], + "beforeReferenceMaxJumpPx": 342.00000000000034, + "afterReferenceMaxJumpPx": 447.11072454147165 + }, + "zoomCliff": { + "scales": [ + 1, + 0.98, + 0.95, + 0.92, + 0.9, + 0.88, + 0.85, + 0.8 + ], + "before": [ + { + "raw": 768, + "selected": 768 + }, + { + "raw": 768, + "selected": 768 + }, + { + "raw": 1536, + "selected": 1536 + }, + { + "raw": 1024, + "selected": 1024 + }, + { + "raw": 768, + "selected": 768 + }, + { + "raw": 768, + "selected": 768 + }, + { + "raw": 768, + "selected": 768 + }, + { + "raw": 768, + "selected": 768 + } + ], + "after": [ + { + "raw": 768, + "selected": 768 + }, + { + "raw": 768, + "selected": 768 + }, + { + "raw": 1536, + "selected": 768 + }, + { + "raw": 1024, + "selected": 768 + }, + { + "raw": 768, + "selected": 768 + }, + { + "raw": 768, + "selected": 768 + }, + { + "raw": 768, + "selected": 768 + }, + { + "raw": 768, + "selected": 768 + } + ], + "scale095": { + "beforeInitial": 1536, + "afterInitial": 768, + "beforeFastRefLen": 940, + "afterFastRefLen": 768, + "activeAfter1536Samples": 1, + "sampleCount": 84, + "currentStyleNextTarget": 3072, + "patchedNextTarget": 2048 + } + }, + "source": { + "beforeSha256": "c53c0dc4cb9fa2d2046aaa12ef03cc79c61a710b0c8db36dea1a87be2d9aedd8", + "afterSha256": "c35f514d5edd29d7e2aba43125cda8023034f4bf6d2e3104a97051950925529c" + } +} diff --git a/docs/index.before-deep-stability.html b/docs/index.before-deep-stability.html new file mode 100644 index 0000000..6ece015 --- /dev/null +++ b/docs/index.before-deep-stability.html @@ -0,0 +1,1975 @@ + + + + + + + +Mandelbrot Deep Zoom v24.6.0 + + + + +
MANDELBROT DEEP ZOOM333の部屋に戻る
+
+
+
+
+
+
0.0080
+
.18
+
+
+
起動中…
+
中心 …
+
倍率 … · 幅 … · 起動中…
+
+
+
+
+ +
+ +

高解像度 PNG 出力

+
+ + + + +
+ +
+
+
+ + + + diff --git a/docs/navigation-continuity/IMPLEMENTATION.md b/docs/navigation-continuity/IMPLEMENTATION.md new file mode 100644 index 0000000..293c792 --- /dev/null +++ b/docs/navigation-continuity/IMPLEMENTATION.md @@ -0,0 +1,75 @@ +# Navigation continuity regression (#8) + +## Intent + +Implement only proposal **#8: navigation-continuity testing**. No rendering algorithm, fallback policy, iteration policy, reference-selection policy, cache policy, or WGSL kernel is changed. + +The invariant under test is: + +> For the same target view, a FAST reference produced by a nearby view must not change the numerical result when production itself considers that reference reusable. + +This directly targets the observed symptom where a small pan or zoom changes the visible Mandelbrot region even though the target coordinates are otherwise equivalent. + +## Test architecture + +`tests/navigation_continuity.mjs` reads production `index.html` as the only source of truth and extracts the existing worker/helper functions. It does not maintain a test copy of the application. + +For each target view: + +1. Run the production adaptive initial-iteration probe. +2. Build a fresh FAST reference for the target. +3. Evaluate a deterministic `16 × 10 = 160` pixel grid using a JavaScript f32 surrogate of the production perturbation recurrence. +4. Construct eight predecessor views: + - pan ±0.10 span on X + - pan ±0.10 span on Y + - zoom predecessor scale 0.90 / 1.10 + - two diagonal pan predecessors +5. Build a FAST reference in each predecessor view. +6. Pass that context through production `reusableReferencePixel(..., allowNearby=true)`. +7. Only when production accepts it as reusable, evaluate the *same target pixels* and compare with the fresh-target-reference result. + +The strict continuity fields are: + +- classification / reason surrogate +- escape iteration + +Smooth escape is also compared, with a diagnostic tolerance of `1e-4`. + +## Corpus + +Five deep views are fixed from user-reported instability cases and the reference-end investigation: + +- `mini-1` +- `mini-2` +- `mini-3-black` +- `refend-zoom-black` +- `refend-undrawn` + +This is not a minibrot-specific rule. The test is keyed only to the production nearby-reference reuse gate and numerical outputs. + +## Current result + +Command: + +```bash +node tests/navigation_continuity.mjs --report +``` + +Current production result: + +- production-reusable navigation paths: **26** +- sampled target comparisons: **4160** +- classification differences: **7** +- escape-iteration differences (same class): **302** +- smooth differences above `1e-4`: **1604** +- maximum observed smooth delta: **0.98486328125** + +Therefore the test correctly exposes an existing navigation-dependent numerical defect. In strict mode (`node tests/navigation_continuity.mjs`) it exits non-zero until the defect is fixed. + +The existing `tests/regression.mjs` remains PASS; this change intentionally adds a new failing invariant test rather than changing production behavior. + +## Why the test is separate + +Adding the assertion directly to the existing fast regression suite would make every structural regression run fail because the product currently violates navigation continuity. Keeping it as a dedicated strict test makes the defect explicit while preserving the existing sanity suite for unrelated development. + +When the underlying reference-reuse defect is fixed, this test should pass without changing its thresholds or corpus. At that point it can be promoted into the default CI suite. diff --git a/docs/navigation-continuity/README.md b/docs/navigation-continuity/README.md new file mode 100644 index 0000000..8ec89cd --- /dev/null +++ b/docs/navigation-continuity/README.md @@ -0,0 +1,6 @@ +# Navigation continuity + +- `IMPLEMENTATION.md`: purpose, invariant, corpus, and current outcome. +- `results.json`: `--report` output from the current production source. +- `strict-test.log`: expected strict-mode failure showing that the test detects the current defect. +- `regression.log`: existing regression-suite result after adding the test. diff --git a/docs/navigation-continuity/index-sha256.txt b/docs/navigation-continuity/index-sha256.txt new file mode 100644 index 0000000..0be18f0 --- /dev/null +++ b/docs/navigation-continuity/index-sha256.txt @@ -0,0 +1,2 @@ +9118e61d6ce5486865533451991f99e901182ba9a7f708a31fff24e3750ced0e index.html +9118e61d6ce5486865533451991f99e901182ba9a7f708a31fff24e3750ced0e /mnt/data/mandelbrot_reference_end_fix/index.html diff --git a/docs/navigation-continuity/regression.log b/docs/navigation-continuity/regression.log new file mode 100644 index 0000000..aadf988 --- /dev/null +++ b/docs/navigation-continuity/regression.log @@ -0,0 +1 @@ +PASS: single-source finite viewer regression (production JS, 19 WGSL kernels, staged scoring, lifecycle, export metadata) diff --git a/docs/navigation-continuity/report.log b/docs/navigation-continuity/report.log new file mode 100644 index 0000000..29228f6 --- /dev/null +++ b/docs/navigation-continuity/report.log @@ -0,0 +1,2 @@ +REPORT: navigation continuity found 7 classification, 302 iteration, 1604 smooth differences across 4160 comparisons +elapsed=0:02.80 maxrss=78512KB diff --git a/docs/navigation-continuity/results.json b/docs/navigation-continuity/results.json new file mode 100644 index 0000000..a442b28 --- /dev/null +++ b/docs/navigation-continuity/results.json @@ -0,0 +1,2935 @@ +{ + "generatedAt": "2026-09-29T14:31:59.490Z", + "productionSha256": "9118e61d6ce5486865533451991f99e901182ba9a7f708a31fff24e3750ced0e", + "width": 1365, + "height": 768, + "samplesPerTarget": 160, + "cases": [ + { + "case": "mini-1", + "iter": 1024, + "adaptive": 1758, + "freshRefLen": 1024, + "paths": [ + { + "name": "pan-right-0.10", + "fx": 0.1, + "fy": 0, + "scale": 1, + "reusable": true, + "refLen": 1024, + "referencePixel": { + "x": 648.0000000000001, + "y": 720.4999999999998 + }, + "classDifferences": 0, + "iterationDifferences": 16, + "smoothDifferences": 96, + "maxSmoothDelta": 0.73321533203125, + "examples": [ + { + "index": 51912, + "x": 42, + "y": 38, + "fresh": { + "kind": "escape", + "n": 360, + "smooth": 360.8445739746094 + }, + "candidate": { + "kind": "escape", + "n": 360, + "smooth": 360.8450012207031 + }, + "smoothDelta": 0.00042724609375 + }, + { + "index": 52083, + "x": 213, + "y": 38, + "fresh": { + "kind": "escape", + "n": 470, + "smooth": 470.2359924316406 + }, + "candidate": { + "kind": "escape", + "n": 470, + "smooth": 470.23974609375 + }, + "smoothDelta": 0.003753662109375 + }, + { + "index": 52168, + "x": 298, + "y": 38, + "fresh": { + "kind": "escape", + "n": 468, + "smooth": 468.62896728515625 + }, + "candidate": { + "kind": "escape", + "n": 468, + "smooth": 468.646484375 + }, + "smoothDelta": 0.01751708984375 + }, + { + "index": 52253, + "x": 383, + "y": 38, + "fresh": { + "kind": "escape", + "n": 552, + "smooth": 551.78173828125 + }, + "candidate": { + "kind": "escape", + "n": 556, + "smooth": 556.14453125 + }, + "smoothDelta": 0 + }, + { + "index": 52339, + "x": 469, + "y": 38, + "fresh": { + "kind": "escape", + "n": 663, + "smooth": 663.2035522460938 + }, + "candidate": { + "kind": "escape", + "n": 663, + "smooth": 663.2073364257812 + }, + "smoothDelta": 0.0037841796875 + }, + { + "index": 52424, + "x": 554, + "y": 38, + "fresh": { + "kind": "escape", + "n": 648, + "smooth": 648.4779052734375 + }, + "candidate": { + "kind": "escape", + "n": 648, + "smooth": 648.4765014648438 + }, + "smoothDelta": 0.00140380859375 + }, + { + "index": 52509, + "x": 639, + "y": 38, + "fresh": { + "kind": "escape", + "n": 482, + "smooth": 482.25592041015625 + }, + "candidate": { + "kind": "escape", + "n": 481, + "smooth": 481.9993896484375 + }, + "smoothDelta": 0 + }, + { + "index": 52595, + "x": 725, + "y": 38, + "fresh": { + "kind": "escape", + "n": 1013, + "smooth": 1013.469970703125 + }, + "candidate": { + "kind": "escape", + "n": 984, + "smooth": 984.9177856445312 + }, + "smoothDelta": 0 + } + ] + }, + { + "name": "pan-left-0.10", + "fx": -0.1, + "fy": 0, + "scale": 1, + "reusable": true, + "refLen": 1024, + "referencePixel": { + "x": 603.0000000000002, + "y": 144.50000000000026 + }, + "classDifferences": 1, + "iterationDifferences": 18, + "smoothDifferences": 83, + "maxSmoothDelta": 0.4068603515625, + "examples": [ + { + "index": 51912, + "x": 42, + "y": 38, + "fresh": { + "kind": "escape", + "n": 360, + "smooth": 360.8445739746094 + }, + "candidate": { + "kind": "escape", + "n": 360, + "smooth": 360.8447265625 + }, + "smoothDelta": 0.000152587890625 + }, + { + "index": 52083, + "x": 213, + "y": 38, + "fresh": { + "kind": "escape", + "n": 470, + "smooth": 470.2359924316406 + }, + "candidate": { + "kind": "escape", + "n": 470, + "smooth": 470.236328125 + }, + "smoothDelta": 0.000335693359375 + }, + { + "index": 52168, + "x": 298, + "y": 38, + "fresh": { + "kind": "escape", + "n": 468, + "smooth": 468.62896728515625 + }, + "candidate": { + "kind": "escape", + "n": 468, + "smooth": 468.6162109375 + }, + "smoothDelta": 0.01275634765625 + }, + { + "index": 52253, + "x": 383, + "y": 38, + "fresh": { + "kind": "escape", + "n": 552, + "smooth": 551.78173828125 + }, + "candidate": { + "kind": "escape", + "n": 550, + "smooth": 550.6992797851562 + }, + "smoothDelta": 0 + }, + { + "index": 52339, + "x": 469, + "y": 38, + "fresh": { + "kind": "escape", + "n": 663, + "smooth": 663.2035522460938 + }, + "candidate": { + "kind": "escape", + "n": 663, + "smooth": 663.2030639648438 + }, + "smoothDelta": 0.00048828125 + }, + { + "index": 52424, + "x": 554, + "y": 38, + "fresh": { + "kind": "escape", + "n": 648, + "smooth": 648.4779052734375 + }, + "candidate": { + "kind": "escape", + "n": 648, + "smooth": 648.474365234375 + }, + "smoothDelta": 0.0035400390625 + }, + { + "index": 52509, + "x": 639, + "y": 38, + "fresh": { + "kind": "escape", + "n": 482, + "smooth": 482.25592041015625 + }, + "candidate": { + "kind": "escape", + "n": 481, + "smooth": 481.9964294433594 + }, + "smoothDelta": 0 + }, + { + "index": 52595, + "x": 725, + "y": 38, + "fresh": { + "kind": "escape", + "n": 1013, + "smooth": 1013.469970703125 + }, + "candidate": { + "kind": "escape", + "n": 985, + "smooth": 985.9537963867188 + }, + "smoothDelta": 0 + } + ] + }, + { + "name": "pan-up-0.10", + "fx": 0, + "fy": 0.1, + "scale": 1, + "reusable": true, + "refLen": 1024, + "referencePixel": { + "x": 398.5000000000001, + "y": 487.99999999999994 + }, + "classDifferences": 0, + "iterationDifferences": 17, + "smoothDifferences": 88, + "maxSmoothDelta": 0.48602294921875, + "examples": [ + { + "index": 51912, + "x": 42, + "y": 38, + "fresh": { + "kind": "escape", + "n": 360, + "smooth": 360.8445739746094 + }, + "candidate": { + "kind": "escape", + "n": 360, + "smooth": 360.8453674316406 + }, + "smoothDelta": 0.00079345703125 + }, + { + "index": 52083, + "x": 213, + "y": 38, + "fresh": { + "kind": "escape", + "n": 470, + "smooth": 470.2359924316406 + }, + "candidate": { + "kind": "escape", + "n": 470, + "smooth": 470.2371826171875 + }, + "smoothDelta": 0.001190185546875 + }, + { + "index": 52168, + "x": 298, + "y": 38, + "fresh": { + "kind": "escape", + "n": 468, + "smooth": 468.62896728515625 + }, + "candidate": { + "kind": "escape", + "n": 468, + "smooth": 468.6209716796875 + }, + "smoothDelta": 0.00799560546875 + }, + { + "index": 52253, + "x": 383, + "y": 38, + "fresh": { + "kind": "escape", + "n": 552, + "smooth": 551.78173828125 + }, + "candidate": { + "kind": "escape", + "n": 551, + "smooth": 551.3150024414062 + }, + "smoothDelta": 0 + }, + { + "index": 52339, + "x": 469, + "y": 38, + "fresh": { + "kind": "escape", + "n": 663, + "smooth": 663.2035522460938 + }, + "candidate": { + "kind": "escape", + "n": 663, + "smooth": 663.2047729492188 + }, + "smoothDelta": 0.001220703125 + }, + { + "index": 52424, + "x": 554, + "y": 38, + "fresh": { + "kind": "escape", + "n": 648, + "smooth": 648.4779052734375 + }, + "candidate": { + "kind": "escape", + "n": 648, + "smooth": 648.4752807617188 + }, + "smoothDelta": 0.00262451171875 + }, + { + "index": 52509, + "x": 639, + "y": 38, + "fresh": { + "kind": "escape", + "n": 482, + "smooth": 482.25592041015625 + }, + "candidate": { + "kind": "escape", + "n": 482, + "smooth": 482.25347900390625 + }, + "smoothDelta": 0.00244140625 + }, + { + "index": 52595, + "x": 725, + "y": 38, + "fresh": { + "kind": "escape", + "n": 1013, + "smooth": 1013.469970703125 + }, + "candidate": { + "kind": "escape", + "n": 990, + "smooth": 990.9667358398438 + }, + "smoothDelta": 0 + } + ] + }, + { + "name": "pan-down-0.10", + "fx": 0, + "fy": -0.1, + "scale": 1, + "reusable": true, + "refLen": 1024, + "referencePixel": { + "x": 511.5, + "y": 664.9999999999999 + }, + "classDifferences": 0, + "iterationDifferences": 17, + "smoothDifferences": 87, + "maxSmoothDelta": 0.186279296875, + "examples": [ + { + "index": 51912, + "x": 42, + "y": 38, + "fresh": { + "kind": "escape", + "n": 360, + "smooth": 360.8445739746094 + }, + "candidate": { + "kind": "escape", + "n": 360, + "smooth": 360.843994140625 + }, + "smoothDelta": 0.000579833984375 + }, + { + "index": 52083, + "x": 213, + "y": 38, + "fresh": { + "kind": "escape", + "n": 470, + "smooth": 470.2359924316406 + }, + "candidate": { + "kind": "escape", + "n": 470, + "smooth": 470.239013671875 + }, + "smoothDelta": 0.003021240234375 + }, + { + "index": 52168, + "x": 298, + "y": 38, + "fresh": { + "kind": "escape", + "n": 468, + "smooth": 468.62896728515625 + }, + "candidate": { + "kind": "escape", + "n": 468, + "smooth": 468.6336975097656 + }, + "smoothDelta": 0.004730224609375 + }, + { + "index": 52253, + "x": 383, + "y": 38, + "fresh": { + "kind": "escape", + "n": 552, + "smooth": 551.78173828125 + }, + "candidate": { + "kind": "escape", + "n": 550, + "smooth": 550.9351806640625 + }, + "smoothDelta": 0 + }, + { + "index": 52339, + "x": 469, + "y": 38, + "fresh": { + "kind": "escape", + "n": 663, + "smooth": 663.2035522460938 + }, + "candidate": { + "kind": "escape", + "n": 663, + "smooth": 663.1981201171875 + }, + "smoothDelta": 0.00543212890625 + }, + { + "index": 52424, + "x": 554, + "y": 38, + "fresh": { + "kind": "escape", + "n": 648, + "smooth": 648.4779052734375 + }, + "candidate": { + "kind": "escape", + "n": 648, + "smooth": 648.4767456054688 + }, + "smoothDelta": 0.00115966796875 + }, + { + "index": 52509, + "x": 639, + "y": 38, + "fresh": { + "kind": "escape", + "n": 482, + "smooth": 482.25592041015625 + }, + "candidate": { + "kind": "escape", + "n": 481, + "smooth": 481.991943359375 + }, + "smoothDelta": 0 + }, + { + "index": 52595, + "x": 725, + "y": 38, + "fresh": { + "kind": "escape", + "n": 1013, + "smooth": 1013.469970703125 + }, + "candidate": { + "kind": "escape", + "n": 999, + "smooth": 999.9511108398438 + }, + "smoothDelta": 0 + } + ] + }, + { + "name": "zoom-from-0.90", + "fx": 0, + "fy": 0, + "scale": 0.9, + "reusable": true, + "refLen": 1024, + "referencePixel": { + "x": 733.8, + "y": 168.45000000000005 + }, + "classDifferences": 0, + "iterationDifferences": 18, + "smoothDifferences": 88, + "maxSmoothDelta": 0.98486328125, + "examples": [ + { + "index": 51912, + "x": 42, + "y": 38, + "fresh": { + "kind": "escape", + "n": 360, + "smooth": 360.8445739746094 + }, + "candidate": { + "kind": "escape", + "n": 360, + "smooth": 360.84564208984375 + }, + "smoothDelta": 0.001068115234375 + }, + { + "index": 52083, + "x": 213, + "y": 38, + "fresh": { + "kind": "escape", + "n": 470, + "smooth": 470.2359924316406 + }, + "candidate": { + "kind": "escape", + "n": 470, + "smooth": 470.2364501953125 + }, + "smoothDelta": 0.000457763671875 + }, + { + "index": 52168, + "x": 298, + "y": 38, + "fresh": { + "kind": "escape", + "n": 468, + "smooth": 468.62896728515625 + }, + "candidate": { + "kind": "escape", + "n": 468, + "smooth": 468.6522521972656 + }, + "smoothDelta": 0.023284912109375 + }, + { + "index": 52253, + "x": 383, + "y": 38, + "fresh": { + "kind": "escape", + "n": 552, + "smooth": 551.78173828125 + }, + "candidate": { + "kind": "escape", + "n": 553, + "smooth": 552.7398681640625 + }, + "smoothDelta": 0 + }, + { + "index": 52339, + "x": 469, + "y": 38, + "fresh": { + "kind": "escape", + "n": 663, + "smooth": 663.2035522460938 + }, + "candidate": { + "kind": "escape", + "n": 663, + "smooth": 663.2036743164062 + }, + "smoothDelta": 0.0001220703125 + }, + { + "index": 52424, + "x": 554, + "y": 38, + "fresh": { + "kind": "escape", + "n": 648, + "smooth": 648.4779052734375 + }, + "candidate": { + "kind": "escape", + "n": 648, + "smooth": 648.4703369140625 + }, + "smoothDelta": 0.007568359375 + }, + { + "index": 52509, + "x": 639, + "y": 38, + "fresh": { + "kind": "escape", + "n": 482, + "smooth": 482.25592041015625 + }, + "candidate": { + "kind": "escape", + "n": 481, + "smooth": 481.99652099609375 + }, + "smoothDelta": 0 + }, + { + "index": 52595, + "x": 725, + "y": 38, + "fresh": { + "kind": "escape", + "n": 1013, + "smooth": 1013.469970703125 + }, + "candidate": { + "kind": "escape", + "n": 988, + "smooth": 988.0548095703125 + }, + "smoothDelta": 0 + } + ] + }, + { + "name": "zoom-from-1.10", + "fx": 0, + "fy": 0, + "scale": 1.1, + "reusable": true, + "refLen": 1024, + "referencePixel": { + "x": 370.10000000000014, + "y": 437.34999999999974 + }, + "classDifferences": 1, + "iterationDifferences": 16, + "smoothDifferences": 91, + "maxSmoothDelta": 0.6895751953125, + "examples": [ + { + "index": 52083, + "x": 213, + "y": 38, + "fresh": { + "kind": "escape", + "n": 470, + "smooth": 470.2359924316406 + }, + "candidate": { + "kind": "escape", + "n": 470, + "smooth": 470.2394714355469 + }, + "smoothDelta": 0.00347900390625 + }, + { + "index": 52168, + "x": 298, + "y": 38, + "fresh": { + "kind": "escape", + "n": 468, + "smooth": 468.62896728515625 + }, + "candidate": { + "kind": "escape", + "n": 468, + "smooth": 468.627197265625 + }, + "smoothDelta": 0.00177001953125 + }, + { + "index": 52253, + "x": 383, + "y": 38, + "fresh": { + "kind": "escape", + "n": 552, + "smooth": 551.78173828125 + }, + "candidate": { + "kind": "escape", + "n": 557, + "smooth": 557.842041015625 + }, + "smoothDelta": 0 + }, + { + "index": 52339, + "x": 469, + "y": 38, + "fresh": { + "kind": "escape", + "n": 663, + "smooth": 663.2035522460938 + }, + "candidate": { + "kind": "escape", + "n": 663, + "smooth": 663.2013549804688 + }, + "smoothDelta": 0.002197265625 + }, + { + "index": 52424, + "x": 554, + "y": 38, + "fresh": { + "kind": "escape", + "n": 648, + "smooth": 648.4779052734375 + }, + "candidate": { + "kind": "escape", + "n": 648, + "smooth": 648.4785766601562 + }, + "smoothDelta": 0.00067138671875 + }, + { + "index": 52509, + "x": 639, + "y": 38, + "fresh": { + "kind": "escape", + "n": 482, + "smooth": 482.25592041015625 + }, + "candidate": { + "kind": "escape", + "n": 482, + "smooth": 482.2550354003906 + }, + "smoothDelta": 0.000885009765625 + }, + { + "index": 52595, + "x": 725, + "y": 38, + "fresh": { + "kind": "escape", + "n": 1013, + "smooth": 1013.469970703125 + }, + "candidate": { + "kind": "operation-limit", + "n": 1024, + "smooth": 0 + }, + "smoothDelta": 0 + }, + { + "index": 52680, + "x": 810, + "y": 38, + "fresh": { + "kind": "escape", + "n": 678, + "smooth": 678.9059448242188 + }, + "candidate": { + "kind": "escape", + "n": 680, + "smooth": 680.234619140625 + }, + "smoothDelta": 0 + } + ] + }, + { + "name": "pan-diagonal-a", + "fx": 0.2, + "fy": 0.15, + "scale": 1, + "reusable": true, + "refLen": 1024, + "referencePixel": { + "x": 329.50000000000006, + "y": 131.75000000000028 + }, + "classDifferences": 0, + "iterationDifferences": 17, + "smoothDifferences": 91, + "maxSmoothDelta": 0.3402099609375, + "examples": [ + { + "index": 51912, + "x": 42, + "y": 38, + "fresh": { + "kind": "escape", + "n": 360, + "smooth": 360.8445739746094 + }, + "candidate": { + "kind": "escape", + "n": 360, + "smooth": 360.84442138671875 + }, + "smoothDelta": 0.000152587890625 + }, + { + "index": 52083, + "x": 213, + "y": 38, + "fresh": { + "kind": "escape", + "n": 470, + "smooth": 470.2359924316406 + }, + "candidate": { + "kind": "escape", + "n": 470, + "smooth": 470.2388000488281 + }, + "smoothDelta": 0.0028076171875 + }, + { + "index": 52168, + "x": 298, + "y": 38, + "fresh": { + "kind": "escape", + "n": 468, + "smooth": 468.62896728515625 + }, + "candidate": { + "kind": "escape", + "n": 468, + "smooth": 468.634765625 + }, + "smoothDelta": 0.00579833984375 + }, + { + "index": 52253, + "x": 383, + "y": 38, + "fresh": { + "kind": "escape", + "n": 552, + "smooth": 551.78173828125 + }, + "candidate": { + "kind": "escape", + "n": 553, + "smooth": 553.194091796875 + }, + "smoothDelta": 0 + }, + { + "index": 52339, + "x": 469, + "y": 38, + "fresh": { + "kind": "escape", + "n": 663, + "smooth": 663.2035522460938 + }, + "candidate": { + "kind": "escape", + "n": 663, + "smooth": 663.2039794921875 + }, + "smoothDelta": 0.00042724609375 + }, + { + "index": 52424, + "x": 554, + "y": 38, + "fresh": { + "kind": "escape", + "n": 648, + "smooth": 648.4779052734375 + }, + "candidate": { + "kind": "escape", + "n": 648, + "smooth": 648.4691772460938 + }, + "smoothDelta": 0.00872802734375 + }, + { + "index": 52509, + "x": 639, + "y": 38, + "fresh": { + "kind": "escape", + "n": 482, + "smooth": 482.25592041015625 + }, + "candidate": { + "kind": "escape", + "n": 481, + "smooth": 481.9989013671875 + }, + "smoothDelta": 0 + }, + { + "index": 52595, + "x": 725, + "y": 38, + "fresh": { + "kind": "escape", + "n": 1013, + "smooth": 1013.469970703125 + }, + "candidate": { + "kind": "escape", + "n": 989, + "smooth": 989.7955322265625 + }, + "smoothDelta": 0 + } + ] + }, + { + "name": "pan-diagonal-b", + "fx": -0.2, + "fy": -0.15, + "scale": 1, + "reusable": true, + "refLen": 1024, + "referencePixel": { + "x": 238.50000000000006, + "y": 541.2499999999998 + }, + "classDifferences": 1, + "iterationDifferences": 17, + "smoothDifferences": 85, + "maxSmoothDelta": 0.72686767578125, + "examples": [ + { + "index": 51912, + "x": 42, + "y": 38, + "fresh": { + "kind": "escape", + "n": 360, + "smooth": 360.8445739746094 + }, + "candidate": { + "kind": "escape", + "n": 360, + "smooth": 360.8449401855469 + }, + "smoothDelta": 0.0003662109375 + }, + { + "index": 52083, + "x": 213, + "y": 38, + "fresh": { + "kind": "escape", + "n": 470, + "smooth": 470.2359924316406 + }, + "candidate": { + "kind": "escape", + "n": 470, + "smooth": 470.23822021484375 + }, + "smoothDelta": 0.002227783203125 + }, + { + "index": 52168, + "x": 298, + "y": 38, + "fresh": { + "kind": "escape", + "n": 468, + "smooth": 468.62896728515625 + }, + "candidate": { + "kind": "escape", + "n": 468, + "smooth": 468.624755859375 + }, + "smoothDelta": 0.00421142578125 + }, + { + "index": 52253, + "x": 383, + "y": 38, + "fresh": { + "kind": "escape", + "n": 552, + "smooth": 551.78173828125 + }, + "candidate": { + "kind": "escape", + "n": 551, + "smooth": 551.2603149414062 + }, + "smoothDelta": 0 + }, + { + "index": 52339, + "x": 469, + "y": 38, + "fresh": { + "kind": "escape", + "n": 663, + "smooth": 663.2035522460938 + }, + "candidate": { + "kind": "escape", + "n": 663, + "smooth": 663.2005004882812 + }, + "smoothDelta": 0.0030517578125 + }, + { + "index": 52424, + "x": 554, + "y": 38, + "fresh": { + "kind": "escape", + "n": 648, + "smooth": 648.4779052734375 + }, + "candidate": { + "kind": "escape", + "n": 648, + "smooth": 648.4852294921875 + }, + "smoothDelta": 0.00732421875 + }, + { + "index": 52509, + "x": 639, + "y": 38, + "fresh": { + "kind": "escape", + "n": 482, + "smooth": 482.25592041015625 + }, + "candidate": { + "kind": "escape", + "n": 481, + "smooth": 481.9943542480469 + }, + "smoothDelta": 0 + }, + { + "index": 52595, + "x": 725, + "y": 38, + "fresh": { + "kind": "escape", + "n": 1013, + "smooth": 1013.469970703125 + }, + "candidate": { + "kind": "escape", + "n": 986, + "smooth": 986.9180908203125 + }, + "smoothDelta": 0 + } + ] + } + ] + }, + { + "case": "mini-2", + "iter": 1536, + "adaptive": 1886, + "freshRefLen": 1536, + "paths": [ + { + "name": "pan-right-0.10", + "fx": 0.1, + "fy": 0, + "scale": 1, + "reusable": true, + "refLen": 1536, + "referencePixel": { + "x": 421, + "y": 336.50000000000017 + }, + "classDifferences": 0, + "iterationDifferences": 19, + "smoothDifferences": 83, + "maxSmoothDelta": 0.467529296875, + "examples": [ + { + "index": 52083, + "x": 213, + "y": 38, + "fresh": { + "kind": "escape", + "n": 669, + "smooth": 669.814453125 + }, + "candidate": { + "kind": "escape", + "n": 669, + "smooth": 669.8153076171875 + }, + "smoothDelta": 0.0008544921875 + }, + { + "index": 52168, + "x": 298, + "y": 38, + "fresh": { + "kind": "escape", + "n": 691, + "smooth": 691.194091796875 + }, + "candidate": { + "kind": "escape", + "n": 691, + "smooth": 691.193359375 + }, + "smoothDelta": 0.000732421875 + }, + { + "index": 52424, + "x": 554, + "y": 38, + "fresh": { + "kind": "escape", + "n": 1269, + "smooth": 1269.619873046875 + }, + "candidate": { + "kind": "escape", + "n": 1270, + "smooth": 1270.353271484375 + }, + "smoothDelta": 0 + }, + { + "index": 52509, + "x": 639, + "y": 38, + "fresh": { + "kind": "escape", + "n": 643, + "smooth": 643.490234375 + }, + "candidate": { + "kind": "escape", + "n": 643, + "smooth": 643.4899291992188 + }, + "smoothDelta": 0.00030517578125 + }, + { + "index": 52595, + "x": 725, + "y": 38, + "fresh": { + "kind": "escape", + "n": 498, + "smooth": 498.9639587402344 + }, + "candidate": { + "kind": "escape", + "n": 496, + "smooth": 496.8247375488281 + }, + "smoothDelta": 0 + }, + { + "index": 52680, + "x": 810, + "y": 38, + "fresh": { + "kind": "escape", + "n": 425, + "smooth": 425.15325927734375 + }, + "candidate": { + "kind": "escape", + "n": 425, + "smooth": 425.15423583984375 + }, + "smoothDelta": 0.0009765625 + }, + { + "index": 52851, + "x": 981, + "y": 38, + "fresh": { + "kind": "escape", + "n": 357, + "smooth": 357.7349548339844 + }, + "candidate": { + "kind": "escape", + "n": 357, + "smooth": 357.7350769042969 + }, + "smoothDelta": 0.0001220703125 + }, + { + "index": 52936, + "x": 1066, + "y": 38, + "fresh": { + "kind": "escape", + "n": 345, + "smooth": 345.4112243652344 + }, + "candidate": { + "kind": "escape", + "n": 345, + "smooth": 345.41064453125 + }, + "smoothDelta": 0.000579833984375 + } + ] + }, + { + "name": "pan-left-0.10", + "fx": -0.1, + "fy": 0, + "scale": 1, + "reusable": true, + "refLen": 1536, + "referencePixel": { + "x": 489.0000000000001, + "y": 432.49999999999983 + }, + "classDifferences": 0, + "iterationDifferences": 17, + "smoothDifferences": 84, + "maxSmoothDelta": 0.6876220703125, + "examples": [ + { + "index": 52083, + "x": 213, + "y": 38, + "fresh": { + "kind": "escape", + "n": 669, + "smooth": 669.814453125 + }, + "candidate": { + "kind": "escape", + "n": 669, + "smooth": 669.8156127929688 + }, + "smoothDelta": 0.00115966796875 + }, + { + "index": 52168, + "x": 298, + "y": 38, + "fresh": { + "kind": "escape", + "n": 691, + "smooth": 691.194091796875 + }, + "candidate": { + "kind": "escape", + "n": 691, + "smooth": 691.1976928710938 + }, + "smoothDelta": 0.00360107421875 + }, + { + "index": 52424, + "x": 554, + "y": 38, + "fresh": { + "kind": "escape", + "n": 1269, + "smooth": 1269.619873046875 + }, + "candidate": { + "kind": "escape", + "n": 1276, + "smooth": 1276.3968505859375 + }, + "smoothDelta": 0 + }, + { + "index": 52595, + "x": 725, + "y": 38, + "fresh": { + "kind": "escape", + "n": 498, + "smooth": 498.9639587402344 + }, + "candidate": { + "kind": "escape", + "n": 495, + "smooth": 495.2680358886719 + }, + "smoothDelta": 0 + }, + { + "index": 52680, + "x": 810, + "y": 38, + "fresh": { + "kind": "escape", + "n": 425, + "smooth": 425.15325927734375 + }, + "candidate": { + "kind": "escape", + "n": 425, + "smooth": 425.1540832519531 + }, + "smoothDelta": 0.000823974609375 + }, + { + "index": 52851, + "x": 981, + "y": 38, + "fresh": { + "kind": "escape", + "n": 357, + "smooth": 357.7349548339844 + }, + "candidate": { + "kind": "escape", + "n": 357, + "smooth": 357.73565673828125 + }, + "smoothDelta": 0.000701904296875 + }, + { + "index": 53107, + "x": 1237, + "y": 38, + "fresh": { + "kind": "escape", + "n": 334, + "smooth": 334.77362060546875 + }, + "candidate": { + "kind": "escape", + "n": 334, + "smooth": 334.7734680175781 + }, + "smoothDelta": 0.000152587890625 + }, + { + "index": 157017, + "x": 42, + "y": 115, + "fresh": { + "kind": "escape", + "n": 1090, + "smooth": 1090.697998046875 + }, + "candidate": { + "kind": "escape", + "n": 1095, + "smooth": 1095.95751953125 + }, + "smoothDelta": 0 + } + ] + }, + { + "name": "pan-up-0.10", + "fx": 0, + "fy": 0.1, + "scale": 1, + "reusable": true, + "refLen": 1536, + "referencePixel": { + "x": 625.5000000000002, + "y": 487.99999999999994 + }, + "classDifferences": 0, + "iterationDifferences": 19, + "smoothDifferences": 82, + "maxSmoothDelta": 0.4599609375, + "examples": [ + { + "index": 52083, + "x": 213, + "y": 38, + "fresh": { + "kind": "escape", + "n": 669, + "smooth": 669.814453125 + }, + "candidate": { + "kind": "escape", + "n": 669, + "smooth": 669.8157958984375 + }, + "smoothDelta": 0.0013427734375 + }, + { + "index": 52168, + "x": 298, + "y": 38, + "fresh": { + "kind": "escape", + "n": 691, + "smooth": 691.194091796875 + }, + "candidate": { + "kind": "escape", + "n": 691, + "smooth": 691.1958618164062 + }, + "smoothDelta": 0.00177001953125 + }, + { + "index": 52424, + "x": 554, + "y": 38, + "fresh": { + "kind": "escape", + "n": 1269, + "smooth": 1269.619873046875 + }, + "candidate": { + "kind": "escape", + "n": 1270, + "smooth": 1270.866943359375 + }, + "smoothDelta": 0 + }, + { + "index": 52509, + "x": 639, + "y": 38, + "fresh": { + "kind": "escape", + "n": 643, + "smooth": 643.490234375 + }, + "candidate": { + "kind": "escape", + "n": 643, + "smooth": 643.4910278320312 + }, + "smoothDelta": 0.00079345703125 + }, + { + "index": 52595, + "x": 725, + "y": 38, + "fresh": { + "kind": "escape", + "n": 498, + "smooth": 498.9639587402344 + }, + "candidate": { + "kind": "escape", + "n": 495, + "smooth": 494.9775695800781 + }, + "smoothDelta": 0 + }, + { + "index": 52851, + "x": 981, + "y": 38, + "fresh": { + "kind": "escape", + "n": 357, + "smooth": 357.7349548339844 + }, + "candidate": { + "kind": "escape", + "n": 357, + "smooth": 357.73541259765625 + }, + "smoothDelta": 0.000457763671875 + }, + { + "index": 52936, + "x": 1066, + "y": 38, + "fresh": { + "kind": "escape", + "n": 345, + "smooth": 345.4112243652344 + }, + "candidate": { + "kind": "escape", + "n": 345, + "smooth": 345.4113464355469 + }, + "smoothDelta": 0.0001220703125 + }, + { + "index": 53107, + "x": 1237, + "y": 38, + "fresh": { + "kind": "escape", + "n": 334, + "smooth": 334.77362060546875 + }, + "candidate": { + "kind": "escape", + "n": 334, + "smooth": 334.7767333984375 + }, + "smoothDelta": 0.00311279296875 + } + ] + }, + { + "name": "pan-down-0.10", + "fx": 0, + "fy": -0.1, + "scale": 1, + "reusable": true, + "refLen": 1536, + "referencePixel": { + "x": 625.5000000000002, + "y": 569 + }, + "classDifferences": 0, + "iterationDifferences": 17, + "smoothDifferences": 86, + "maxSmoothDelta": 0.3787841796875, + "examples": [ + { + "index": 52083, + "x": 213, + "y": 38, + "fresh": { + "kind": "escape", + "n": 669, + "smooth": 669.814453125 + }, + "candidate": { + "kind": "escape", + "n": 669, + "smooth": 669.8157348632812 + }, + "smoothDelta": 0.00128173828125 + }, + { + "index": 52168, + "x": 298, + "y": 38, + "fresh": { + "kind": "escape", + "n": 691, + "smooth": 691.194091796875 + }, + "candidate": { + "kind": "escape", + "n": 691, + "smooth": 691.1937866210938 + }, + "smoothDelta": 0.00030517578125 + }, + { + "index": 52424, + "x": 554, + "y": 38, + "fresh": { + "kind": "escape", + "n": 1269, + "smooth": 1269.619873046875 + }, + "candidate": { + "kind": "escape", + "n": 1270, + "smooth": 1270.0855712890625 + }, + "smoothDelta": 0 + }, + { + "index": 52509, + "x": 639, + "y": 38, + "fresh": { + "kind": "escape", + "n": 643, + "smooth": 643.490234375 + }, + "candidate": { + "kind": "escape", + "n": 643, + "smooth": 643.4918212890625 + }, + "smoothDelta": 0.0015869140625 + }, + { + "index": 52595, + "x": 725, + "y": 38, + "fresh": { + "kind": "escape", + "n": 498, + "smooth": 498.9639587402344 + }, + "candidate": { + "kind": "escape", + "n": 492, + "smooth": 492.18212890625 + }, + "smoothDelta": 0 + }, + { + "index": 52680, + "x": 810, + "y": 38, + "fresh": { + "kind": "escape", + "n": 425, + "smooth": 425.15325927734375 + }, + "candidate": { + "kind": "escape", + "n": 425, + "smooth": 425.15301513671875 + }, + "smoothDelta": 0.000244140625 + }, + { + "index": 52765, + "x": 895, + "y": 38, + "fresh": { + "kind": "escape", + "n": 383, + "smooth": 383.6557312011719 + }, + "candidate": { + "kind": "escape", + "n": 383, + "smooth": 383.65557861328125 + }, + "smoothDelta": 0.000152587890625 + }, + { + "index": 52936, + "x": 1066, + "y": 38, + "fresh": { + "kind": "escape", + "n": 345, + "smooth": 345.4112243652344 + }, + "candidate": { + "kind": "escape", + "n": 345, + "smooth": 345.4107971191406 + }, + "smoothDelta": 0.00042724609375 + } + ] + }, + { + "name": "zoom-from-0.90", + "fx": 0, + "fy": 0, + "scale": 0.9, + "reusable": true, + "refLen": 1536, + "referencePixel": { + "x": 631.2, + "y": 427.6499999999999 + }, + "classDifferences": 0, + "iterationDifferences": 17, + "smoothDifferences": 85, + "maxSmoothDelta": 0.41064453125, + "examples": [ + { + "index": 52083, + "x": 213, + "y": 38, + "fresh": { + "kind": "escape", + "n": 669, + "smooth": 669.814453125 + }, + "candidate": { + "kind": "escape", + "n": 669, + "smooth": 669.81494140625 + }, + "smoothDelta": 0.00048828125 + }, + { + "index": 52168, + "x": 298, + "y": 38, + "fresh": { + "kind": "escape", + "n": 691, + "smooth": 691.194091796875 + }, + "candidate": { + "kind": "escape", + "n": 691, + "smooth": 691.1924438476562 + }, + "smoothDelta": 0.00164794921875 + }, + { + "index": 52424, + "x": 554, + "y": 38, + "fresh": { + "kind": "escape", + "n": 1269, + "smooth": 1269.619873046875 + }, + "candidate": { + "kind": "escape", + "n": 1320, + "smooth": 1320.162109375 + }, + "smoothDelta": 0 + }, + { + "index": 52509, + "x": 639, + "y": 38, + "fresh": { + "kind": "escape", + "n": 643, + "smooth": 643.490234375 + }, + "candidate": { + "kind": "escape", + "n": 643, + "smooth": 643.4906005859375 + }, + "smoothDelta": 0.0003662109375 + }, + { + "index": 52595, + "x": 725, + "y": 38, + "fresh": { + "kind": "escape", + "n": 498, + "smooth": 498.9639587402344 + }, + "candidate": { + "kind": "escape", + "n": 518, + "smooth": 518.0783081054688 + }, + "smoothDelta": 0 + }, + { + "index": 52680, + "x": 810, + "y": 38, + "fresh": { + "kind": "escape", + "n": 425, + "smooth": 425.15325927734375 + }, + "candidate": { + "kind": "escape", + "n": 425, + "smooth": 425.1537780761719 + }, + "smoothDelta": 0.000518798828125 + }, + { + "index": 52851, + "x": 981, + "y": 38, + "fresh": { + "kind": "escape", + "n": 357, + "smooth": 357.7349548339844 + }, + "candidate": { + "kind": "escape", + "n": 357, + "smooth": 357.7353210449219 + }, + "smoothDelta": 0.0003662109375 + }, + { + "index": 52936, + "x": 1066, + "y": 38, + "fresh": { + "kind": "escape", + "n": 345, + "smooth": 345.4112243652344 + }, + "candidate": { + "kind": "escape", + "n": 345, + "smooth": 345.4114074707031 + }, + "smoothDelta": 0.00018310546875 + } + ] + }, + { + "name": "zoom-from-1.10", + "fx": 0, + "fy": 0, + "scale": 1.1, + "reusable": true, + "refLen": 1536, + "referencePixel": { + "x": 745.1999999999999, + "y": 331.75000000000017 + }, + "classDifferences": 0, + "iterationDifferences": 16, + "smoothDifferences": 88, + "maxSmoothDelta": 0.53594970703125, + "examples": [ + { + "index": 52083, + "x": 213, + "y": 38, + "fresh": { + "kind": "escape", + "n": 669, + "smooth": 669.814453125 + }, + "candidate": { + "kind": "escape", + "n": 669, + "smooth": 669.813720703125 + }, + "smoothDelta": 0.000732421875 + }, + { + "index": 52168, + "x": 298, + "y": 38, + "fresh": { + "kind": "escape", + "n": 691, + "smooth": 691.194091796875 + }, + "candidate": { + "kind": "escape", + "n": 691, + "smooth": 691.1990966796875 + }, + "smoothDelta": 0.0050048828125 + }, + { + "index": 52424, + "x": 554, + "y": 38, + "fresh": { + "kind": "escape", + "n": 1269, + "smooth": 1269.619873046875 + }, + "candidate": { + "kind": "escape", + "n": 1268, + "smooth": 1268.4906005859375 + }, + "smoothDelta": 0 + }, + { + "index": 52509, + "x": 639, + "y": 38, + "fresh": { + "kind": "escape", + "n": 643, + "smooth": 643.490234375 + }, + "candidate": { + "kind": "escape", + "n": 643, + "smooth": 643.4898071289062 + }, + "smoothDelta": 0.00042724609375 + }, + { + "index": 52595, + "x": 725, + "y": 38, + "fresh": { + "kind": "escape", + "n": 498, + "smooth": 498.9639587402344 + }, + "candidate": { + "kind": "escape", + "n": 495, + "smooth": 495.0293273925781 + }, + "smoothDelta": 0 + }, + { + "index": 52680, + "x": 810, + "y": 38, + "fresh": { + "kind": "escape", + "n": 425, + "smooth": 425.15325927734375 + }, + "candidate": { + "kind": "escape", + "n": 425, + "smooth": 425.15313720703125 + }, + "smoothDelta": 0.0001220703125 + }, + { + "index": 52851, + "x": 981, + "y": 38, + "fresh": { + "kind": "escape", + "n": 357, + "smooth": 357.7349548339844 + }, + "candidate": { + "kind": "escape", + "n": 357, + "smooth": 357.73565673828125 + }, + "smoothDelta": 0.000701904296875 + }, + { + "index": 52936, + "x": 1066, + "y": 38, + "fresh": { + "kind": "escape", + "n": 345, + "smooth": 345.4112243652344 + }, + "candidate": { + "kind": "escape", + "n": 345, + "smooth": 345.4111022949219 + }, + "smoothDelta": 0.0001220703125 + } + ] + }, + { + "name": "pan-diagonal-a", + "fx": 0.2, + "fy": 0.15, + "scale": 1, + "reusable": true, + "refLen": 1536, + "referencePixel": { + "x": 557.5000000000001, + "y": 515.7499999999998 + }, + "classDifferences": 0, + "iterationDifferences": 18, + "smoothDifferences": 88, + "maxSmoothDelta": 0.69287109375, + "examples": [ + { + "index": 52083, + "x": 213, + "y": 38, + "fresh": { + "kind": "escape", + "n": 669, + "smooth": 669.814453125 + }, + "candidate": { + "kind": "escape", + "n": 669, + "smooth": 669.8131103515625 + }, + "smoothDelta": 0.0013427734375 + }, + { + "index": 52168, + "x": 298, + "y": 38, + "fresh": { + "kind": "escape", + "n": 691, + "smooth": 691.194091796875 + }, + "candidate": { + "kind": "escape", + "n": 691, + "smooth": 691.1971435546875 + }, + "smoothDelta": 0.0030517578125 + }, + { + "index": 52424, + "x": 554, + "y": 38, + "fresh": { + "kind": "escape", + "n": 1269, + "smooth": 1269.619873046875 + }, + "candidate": { + "kind": "escape", + "n": 1269, + "smooth": 1268.927001953125 + }, + "smoothDelta": 0.69287109375 + }, + { + "index": 52509, + "x": 639, + "y": 38, + "fresh": { + "kind": "escape", + "n": 643, + "smooth": 643.490234375 + }, + "candidate": { + "kind": "escape", + "n": 643, + "smooth": 643.4898071289062 + }, + "smoothDelta": 0.00042724609375 + }, + { + "index": 52595, + "x": 725, + "y": 38, + "fresh": { + "kind": "escape", + "n": 498, + "smooth": 498.9639587402344 + }, + "candidate": { + "kind": "escape", + "n": 497, + "smooth": 497.174560546875 + }, + "smoothDelta": 0 + }, + { + "index": 52680, + "x": 810, + "y": 38, + "fresh": { + "kind": "escape", + "n": 425, + "smooth": 425.15325927734375 + }, + "candidate": { + "kind": "escape", + "n": 425, + "smooth": 425.15301513671875 + }, + "smoothDelta": 0.000244140625 + }, + { + "index": 52765, + "x": 895, + "y": 38, + "fresh": { + "kind": "escape", + "n": 383, + "smooth": 383.6557312011719 + }, + "candidate": { + "kind": "escape", + "n": 383, + "smooth": 383.65606689453125 + }, + "smoothDelta": 0.000335693359375 + }, + { + "index": 52851, + "x": 981, + "y": 38, + "fresh": { + "kind": "escape", + "n": 357, + "smooth": 357.7349548339844 + }, + "candidate": { + "kind": "escape", + "n": 357, + "smooth": 357.73541259765625 + }, + "smoothDelta": 0.000457763671875 + } + ] + }, + { + "name": "pan-diagonal-b", + "fx": -0.2, + "fy": -0.15, + "scale": 1, + "reusable": true, + "refLen": 1536, + "referencePixel": { + "x": 352.5000000000003, + "y": 637.2499999999998 + }, + "classDifferences": 1, + "iterationDifferences": 17, + "smoothDifferences": 88, + "maxSmoothDelta": 0.32525634765625, + "examples": [ + { + "index": 52083, + "x": 213, + "y": 38, + "fresh": { + "kind": "escape", + "n": 669, + "smooth": 669.814453125 + }, + "candidate": { + "kind": "escape", + "n": 669, + "smooth": 669.8146362304688 + }, + "smoothDelta": 0.00018310546875 + }, + { + "index": 52168, + "x": 298, + "y": 38, + "fresh": { + "kind": "escape", + "n": 691, + "smooth": 691.194091796875 + }, + "candidate": { + "kind": "escape", + "n": 691, + "smooth": 691.1988525390625 + }, + "smoothDelta": 0.0047607421875 + }, + { + "index": 52424, + "x": 554, + "y": 38, + "fresh": { + "kind": "escape", + "n": 1269, + "smooth": 1269.619873046875 + }, + "candidate": { + "kind": "escape", + "n": 1285, + "smooth": 1285.6400146484375 + }, + "smoothDelta": 0 + }, + { + "index": 52509, + "x": 639, + "y": 38, + "fresh": { + "kind": "escape", + "n": 643, + "smooth": 643.490234375 + }, + "candidate": { + "kind": "escape", + "n": 643, + "smooth": 643.4920043945312 + }, + "smoothDelta": 0.00177001953125 + }, + { + "index": 52595, + "x": 725, + "y": 38, + "fresh": { + "kind": "escape", + "n": 498, + "smooth": 498.9639587402344 + }, + "candidate": { + "kind": "escape", + "n": 493, + "smooth": 493.38446044921875 + }, + "smoothDelta": 0 + }, + { + "index": 52680, + "x": 810, + "y": 38, + "fresh": { + "kind": "escape", + "n": 425, + "smooth": 425.15325927734375 + }, + "candidate": { + "kind": "escape", + "n": 425, + "smooth": 425.1556091308594 + }, + "smoothDelta": 0.002349853515625 + }, + { + "index": 52851, + "x": 981, + "y": 38, + "fresh": { + "kind": "escape", + "n": 357, + "smooth": 357.7349548339844 + }, + "candidate": { + "kind": "escape", + "n": 357, + "smooth": 357.734375 + }, + "smoothDelta": 0.000579833984375 + }, + { + "index": 53107, + "x": 1237, + "y": 38, + "fresh": { + "kind": "escape", + "n": 334, + "smooth": 334.77362060546875 + }, + "candidate": { + "kind": "escape", + "n": 334, + "smooth": 334.774658203125 + }, + "smoothDelta": 0.00103759765625 + } + ] + } + ] + }, + { + "case": "mini-3-black", + "iter": 350, + "adaptive": 2142, + "freshRefLen": 350, + "paths": [ + { + "name": "pan-right-0.10", + "fx": 0.1, + "fy": 0, + "scale": 1, + "reusable": true, + "refLen": 350, + "referencePixel": { + "x": 818.9999999999998, + "y": 384 + }, + "classDifferences": 0, + "iterationDifferences": 0, + "smoothDifferences": 0, + "maxSmoothDelta": 0, + "examples": [] + }, + { + "name": "pan-left-0.10", + "fx": -0.1, + "fy": 0, + "scale": 1, + "reusable": true, + "refLen": 350, + "referencePixel": { + "x": 546.0000000000002, + "y": 384 + }, + "classDifferences": 0, + "iterationDifferences": 0, + "smoothDifferences": 0, + "maxSmoothDelta": 0, + "examples": [] + }, + { + "name": "pan-up-0.10", + "fx": 0, + "fy": 0.1, + "scale": 1, + "reusable": true, + "refLen": 350, + "referencePixel": { + "x": 682.5, + "y": 247.50000000000017 + }, + "classDifferences": 0, + "iterationDifferences": 0, + "smoothDifferences": 0, + "maxSmoothDelta": 0, + "examples": [] + }, + { + "name": "pan-down-0.10", + "fx": 0, + "fy": -0.1, + "scale": 1, + "reusable": true, + "refLen": 350, + "referencePixel": { + "x": 682.5, + "y": 520.4999999999998 + }, + "classDifferences": 0, + "iterationDifferences": 0, + "smoothDifferences": 0, + "maxSmoothDelta": 0, + "examples": [] + }, + { + "name": "zoom-from-0.90", + "fx": 0, + "fy": 0, + "scale": 0.9, + "reusable": true, + "refLen": 350, + "referencePixel": { + "x": 682.5, + "y": 384 + }, + "classDifferences": 0, + "iterationDifferences": 0, + "smoothDifferences": 0, + "maxSmoothDelta": 0, + "examples": [] + }, + { + "name": "zoom-from-1.10", + "fx": 0, + "fy": 0, + "scale": 1.1, + "reusable": true, + "refLen": 350, + "referencePixel": { + "x": 682.5, + "y": 384 + }, + "classDifferences": 0, + "iterationDifferences": 0, + "smoothDifferences": 0, + "maxSmoothDelta": 0, + "examples": [] + }, + { + "name": "pan-diagonal-a", + "fx": 0.2, + "fy": 0.15, + "scale": 1, + "reusable": true, + "refLen": 350, + "referencePixel": { + "x": 955.5, + "y": 179.2500000000001 + }, + "classDifferences": 0, + "iterationDifferences": 0, + "smoothDifferences": 0, + "maxSmoothDelta": 0, + "examples": [] + }, + { + "name": "pan-diagonal-b", + "fx": -0.2, + "fy": -0.15, + "scale": 1, + "reusable": true, + "refLen": 350, + "referencePixel": { + "x": 409.50000000000006, + "y": 588.7499999999999 + }, + "classDifferences": 0, + "iterationDifferences": 0, + "smoothDifferences": 0, + "maxSmoothDelta": 0, + "examples": [] + } + ] + }, + { + "case": "refend-zoom-black", + "iter": 1024, + "adaptive": 2910, + "freshRefLen": 984, + "paths": [ + { + "name": "pan-right-0.10", + "fx": 0.1, + "fy": 0, + "scale": 1, + "reusable": false, + "refLen": 934 + }, + { + "name": "pan-left-0.10", + "fx": -0.1, + "fy": 0, + "scale": 1, + "reusable": true, + "refLen": 1024, + "referencePixel": { + "x": 489.0000000000001, + "y": 240.5000000000002 + }, + "classDifferences": 1, + "iterationDifferences": 13, + "smoothDifferences": 104, + "maxSmoothDelta": 0.76104736328125, + "examples": [ + { + "index": 51912, + "x": 42, + "y": 38, + "fresh": { + "kind": "escape", + "n": 756, + "smooth": 756.6598510742188 + }, + "candidate": { + "kind": "escape", + "n": 756, + "smooth": 756.6602172851562 + }, + "smoothDelta": 0.0003662109375 + }, + { + "index": 51997, + "x": 127, + "y": 38, + "fresh": { + "kind": "escape", + "n": 818, + "smooth": 818.4981079101562 + }, + "candidate": { + "kind": "escape", + "n": 818, + "smooth": 818.5093383789062 + }, + "smoothDelta": 0.01123046875 + }, + { + "index": 52083, + "x": 213, + "y": 38, + "fresh": { + "kind": "escape", + "n": 798, + "smooth": 797.8334350585938 + }, + "candidate": { + "kind": "escape", + "n": 798, + "smooth": 797.8346557617188 + }, + "smoothDelta": 0.001220703125 + }, + { + "index": 52168, + "x": 298, + "y": 38, + "fresh": { + "kind": "reference-end", + "n": 984, + "smooth": 0 + }, + "candidate": { + "kind": "operation-limit", + "n": 1024, + "smooth": 0 + }, + "smoothDelta": 0 + }, + { + "index": 52253, + "x": 383, + "y": 38, + "fresh": { + "kind": "escape", + "n": 834, + "smooth": 834.6159057617188 + }, + "candidate": { + "kind": "escape", + "n": 834, + "smooth": 834.615234375 + }, + "smoothDelta": 0.00067138671875 + }, + { + "index": 52339, + "x": 469, + "y": 38, + "fresh": { + "kind": "escape", + "n": 801, + "smooth": 801.908935546875 + }, + "candidate": { + "kind": "escape", + "n": 801, + "smooth": 801.9091796875 + }, + "smoothDelta": 0.000244140625 + }, + { + "index": 52424, + "x": 554, + "y": 38, + "fresh": { + "kind": "escape", + "n": 907, + "smooth": 907.4242553710938 + }, + "candidate": { + "kind": "escape", + "n": 827, + "smooth": 827.5067749023438 + }, + "smoothDelta": 0 + }, + { + "index": 52509, + "x": 639, + "y": 38, + "fresh": { + "kind": "escape", + "n": 749, + "smooth": 749.63037109375 + }, + "candidate": { + "kind": "escape", + "n": 749, + "smooth": 749.6300048828125 + }, + "smoothDelta": 0.0003662109375 + } + ] + }, + { + "name": "pan-up-0.10", + "fx": 0, + "fy": 0.1, + "scale": 1, + "reusable": false, + "refLen": 872 + }, + { + "name": "pan-down-0.10", + "fx": 0, + "fy": -0.1, + "scale": 1, + "reusable": false, + "refLen": 1021 + }, + { + "name": "zoom-from-0.90", + "fx": 0, + "fy": 0, + "scale": 0.9, + "reusable": false, + "refLen": 968 + }, + { + "name": "zoom-from-1.10", + "fx": 0, + "fy": 0, + "scale": 1.1, + "reusable": false, + "refLen": 970 + }, + { + "name": "pan-diagonal-a", + "fx": 0.2, + "fy": 0.15, + "scale": 1, + "reusable": true, + "refLen": 1024, + "referencePixel": { + "x": 671.5, + "y": 227.75000000000026 + }, + "classDifferences": 2, + "iterationDifferences": 13, + "smoothDifferences": 107, + "maxSmoothDelta": 0.7099609375, + "examples": [ + { + "index": 51912, + "x": 42, + "y": 38, + "fresh": { + "kind": "escape", + "n": 756, + "smooth": 756.6598510742188 + }, + "candidate": { + "kind": "escape", + "n": 756, + "smooth": 756.6600341796875 + }, + "smoothDelta": 0.00018310546875 + }, + { + "index": 51997, + "x": 127, + "y": 38, + "fresh": { + "kind": "escape", + "n": 818, + "smooth": 818.4981079101562 + }, + "candidate": { + "kind": "escape", + "n": 818, + "smooth": 818.51318359375 + }, + "smoothDelta": 0.01507568359375 + }, + { + "index": 52083, + "x": 213, + "y": 38, + "fresh": { + "kind": "escape", + "n": 798, + "smooth": 797.8334350585938 + }, + "candidate": { + "kind": "escape", + "n": 798, + "smooth": 797.8341064453125 + }, + "smoothDelta": 0.00067138671875 + }, + { + "index": 52168, + "x": 298, + "y": 38, + "fresh": { + "kind": "reference-end", + "n": 984, + "smooth": 0 + }, + "candidate": { + "kind": "operation-limit", + "n": 1024, + "smooth": 0 + }, + "smoothDelta": 0 + }, + { + "index": 52253, + "x": 383, + "y": 38, + "fresh": { + "kind": "escape", + "n": 834, + "smooth": 834.6159057617188 + }, + "candidate": { + "kind": "escape", + "n": 834, + "smooth": 834.6153564453125 + }, + "smoothDelta": 0.00054931640625 + }, + { + "index": 52339, + "x": 469, + "y": 38, + "fresh": { + "kind": "escape", + "n": 801, + "smooth": 801.908935546875 + }, + "candidate": { + "kind": "escape", + "n": 801, + "smooth": 801.9088134765625 + }, + "smoothDelta": 0.0001220703125 + }, + { + "index": 52424, + "x": 554, + "y": 38, + "fresh": { + "kind": "escape", + "n": 907, + "smooth": 907.4242553710938 + }, + "candidate": { + "kind": "operation-limit", + "n": 1024, + "smooth": 0 + }, + "smoothDelta": 0 + }, + { + "index": 52509, + "x": 639, + "y": 38, + "fresh": { + "kind": "escape", + "n": 749, + "smooth": 749.63037109375 + }, + "candidate": { + "kind": "escape", + "n": 749, + "smooth": 749.6301879882812 + }, + "smoothDelta": 0.00018310546875 + } + ] + }, + { + "name": "pan-diagonal-b", + "fx": -0.2, + "fy": -0.15, + "scale": 1, + "reusable": false, + "refLen": 1022 + } + ] + }, + { + "case": "refend-undrawn", + "iter": 4702, + "adaptive": 4702, + "freshRefLen": 2774, + "paths": [ + { + "name": "pan-right-0.10", + "fx": 0.1, + "fy": 0, + "scale": 1, + "reusable": false, + "refLen": 2813 + }, + { + "name": "pan-left-0.10", + "fx": -0.1, + "fy": 0, + "scale": 1, + "reusable": false, + "refLen": 2774 + }, + { + "name": "pan-up-0.10", + "fx": 0, + "fy": 0.1, + "scale": 1, + "reusable": false, + "refLen": 2967 + }, + { + "name": "pan-down-0.10", + "fx": 0, + "fy": -0.1, + "scale": 1, + "reusable": false, + "refLen": 3143 + }, + { + "name": "zoom-from-0.90", + "fx": 0, + "fy": 0, + "scale": 0.9, + "reusable": false, + "refLen": 2942 + }, + { + "name": "zoom-from-1.10", + "fx": 0, + "fy": 0, + "scale": 1.1, + "reusable": false, + "refLen": 2750 + }, + { + "name": "pan-diagonal-a", + "fx": 0.2, + "fy": 0.15, + "scale": 1, + "reusable": false, + "refLen": 2545 + }, + { + "name": "pan-diagonal-b", + "fx": -0.2, + "fy": -0.15, + "scale": 1, + "reusable": false, + "refLen": 2741 + } + ] + } + ], + "totalReusablePaths": 26, + "totalComparisons": 4160, + "discontinuities": { + "classification": 7, + "escapeIteration": 302, + "smooth": 1604, + "maxSmoothDelta": 0.98486328125 + }, + "smoothTolerance": 0.0001, + "invariant": "For any reference that production reusableReferencePixel(..., allowNearby=true) accepts, the target view must match a fresh target reference at sampled pixels. Classification and escape iteration are strict; smooth has a small diagnostic tolerance." +} diff --git a/docs/navigation-continuity/status.txt b/docs/navigation-continuity/status.txt new file mode 100644 index 0000000..282dfae --- /dev/null +++ b/docs/navigation-continuity/status.txt @@ -0,0 +1 @@ +navigation_strict_exit=1 diff --git a/docs/navigation-continuity/strict-test.log b/docs/navigation-continuity/strict-test.log new file mode 100644 index 0000000..a5f99d4 --- /dev/null +++ b/docs/navigation-continuity/strict-test.log @@ -0,0 +1,17 @@ +node:internal/modules/run_main:123 + triggerUncaughtException( + ^ + +AssertionError [ERR_ASSERTION]: navigation continuity failed: 7 classification + 302 escape-iteration differences across 4160 comparisons + +309 !== 0 + + at file:///mnt/data/mandelbrot_navigation_continuity/tests/navigation_continuity.mjs:181:10 { + generatedMessage: false, + code: 'ERR_ASSERTION', + actual: 309, + expected: 0, + operator: 'strictEqual' +} + +Node.js v22.16.0 diff --git a/docs/reference-end-root-fix/IMPLEMENTATION.md b/docs/reference-end-root-fix/IMPLEMENTATION.md new file mode 100644 index 0000000..6802f1e --- /dev/null +++ b/docs/reference-end-root-fix/IMPLEMENTATION.md @@ -0,0 +1,58 @@ +# Deep zoom black-region root fix + +## Summary + +The regression was caused by `primaryIterCoveredByReference()`, introduced after the earlier deep-stability pass. When the selected FAST reference orbit was shorter than the requested primary iteration count, it rounded the primary iteration count down to one of `350 / 512 / 768 / 1024 / 1536 / 2048`. + +That changed the semantic reason of unresolved pixels: + +- Correct behavior: requested iteration continues until the reference ends, then pixels become `REFERENCE_END` and enter the numerical-recovery path **before publication**. +- Regressed behavior: primary iteration was reduced first, so the same pixels became `OPERATION_LIMIT`. `OPERATION_LIMIT` is intentionally transparent in the color pass, so large regions appeared black / undrawn. + +The fix removes the iteration truncation entirely. Reference exhaustion is once again represented as `REFERENCE_END` and repaired by the existing guided-reference / precision-fallback pipeline before the first FAST provisional frame is published. + +No temporal hysteresis, previous-frame iteration feedback, or zoom-dependent smoothing is used. + +## Reported views + +Evaluation used 1365x768 and 160 evenly distributed sample pixels per view, using the production worker arithmetic. + +| View | Requested primary | FAST refLen | Old primary | Old transparent OP_LIMIT | Fixed FAST result before recovery | +|---|---:|---:|---:|---:|---| +| zoom-black (`b=273`, span ≈ 9.8e-13) | 1024 | 984 | 768 | 97/160 | 159 escape + 1 REFERENCE_END | +| black-at-specific-scale (`b=273`, span ≈ 9.08e-6) | 1536 | 1218 | 1024 | 20/160 | 144 escape + 16 REFERENCE_END | +| undrawn-area (`b=307`, span ≈ 5.22e-24) | 4702 | 2774 | 2048 | 119/160 | 156 escape + 4 REFERENCE_END | + +The remaining `REFERENCE_END` samples were sent through the same production precision-fallback worker: + +- zoom-black: 1/1 accepted (true operation-limit at 1024) +- black-at-specific-scale: 16/16 accepted (7 escape, 9 operation-limit at 1536) +- undrawn-area: 4/4 accepted as escape + +This is the intended distinction: true finite-iteration survivors remain `OPERATION_LIMIT`; insufficient reference length is repaired as a numerical issue instead of being displayed as a transparent hole. + +## Code change + +1. Removed `primaryIterCoveredByReference()`. +2. Removed the call that lowered `iter` after FAST reference construction. +3. Added an inline design comment documenting why `refLen` must not be converted into an earlier operation limit. +4. Added a regression assertion preventing the truncation helper from returning. + +No WGSL kernel changed. + +## Verification + +- `tests/regression.mjs`: **PASS** +- Production JavaScript syntax: covered by regression VM compilation +- WGSL hashes: unchanged, all **19 kernels** pinned by regression +- Integrated CPU/surrogate benchmark: completed; existing periodicity, sparse state, multi-reference, and CPU fallback checks remain intact +- Hysteresis symbols: absent +- Series approximation: remains disabled + +The legacy `tests/browser_smoke.mjs` is not a valid current production test because it still waits for the removed `__MANDEL_TEST__` hook. It was not used as evidence for this fix. + +## Files + +- `index.html`: fixed viewer +- `results.json`: measured comparison and recovery results +- `index.diff`: exact source diff against the rollback baseline diff --git a/docs/reference-end-root-fix/index.diff b/docs/reference-end-root-fix/index.diff new file mode 100644 index 0000000..f69a52a --- /dev/null +++ b/docs/reference-end-root-fix/index.diff @@ -0,0 +1,29 @@ +--- black-regression-rollback/index.html ++++ reference-end-root-fix/index.html +@@ -957,12 +957,6 @@ + const target=Math.ceil(iter*factor/64)*64; + return Math.min(maxIter,Math.max(iter+64,target)); + } +-function primaryIterCoveredByReference(iter,refLen){ +- if(!Number.isFinite(refLen)||refLen<=0||refLen>=iter)return iter; +- const choices=[state.baseIter,...ADAPTIVE_PROBE_CHOICES].filter((x,i,a)=>x<=refLen&&a.indexOf(x)===i); +- if(choices.length)return Math.max(...choices); +- return Math.max(1,Math.min(iter,Math.max(64,Math.floor(refLen/64)*64))); +-} + function adaptiveIterationBonus(screenBits,q,quantum=ADAPTIVE_ITER_QUANTUM){const raw=48*Math.max(0,Number(q)||0)*Math.max(0,Number(screenBits)||0);return raw>0?Math.ceil(raw/quantum)*quantum:0} + function maxIter(){if(!state.adaptive)return Math.min(state.baseIter,state.continuationBudget||state.baseIter);const z=zoomExp(),w=Math.max(1,canvas.width||innerWidth||1),screenBits=Math.max(0,Math.log2(w)+z*Math.LOG2E*Math.LN10),bonus=adaptiveIterationBonus(screenBits,1);return Math.min(state.continuationBudget||12000,12000,Math.max(state.baseIter,state.baseIter+bonus))} + function canUseStableReprojection(){ +@@ -1802,7 +1796,12 @@ + 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={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,primaryIter:iter,requestedIter}} +- 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 covered=primaryIterCoveredByReference(iter,fastCtx?.refLen||0);if(covered0){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,fastExtended?fastCtx?.source:null);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}} diff --git a/docs/reference-end-root-fix/results.json b/docs/reference-end-root-fix/results.json new file mode 100644 index 0000000..017fdab --- /dev/null +++ b/docs/reference-end-root-fix/results.json @@ -0,0 +1,518 @@ +{ + "generatedAt": "2026-09-29T12:26:52.531730+00:00", + "production": { + "baseSha256": "e9bebe37ded03f56d8f36da41caedc658f5ba05274120d90141b1746ea2c634b", + "fixedSha256": "9118e61d6ce5486865533451991f99e901182ba9a7f708a31fff24e3750ced0e", + "baseBytes": 226808, + "fixedBytes": 226745, + "regression": "PASS: production JS + 19 WGSL kernels", + "hysteresisPresent": false, + "seriesApproximationEnabled": false + }, + "reportedViewPolicyComparison": [ + { + "case": "zoom-black", + "mode": "old", + "requested": 1024, + "refLen": 984, + "iter": 768, + "samples": 160, + "oracleEscape": 63, + "fast": { + "escape": 63, + "op": 97 + } + }, + { + "case": "zoom-black", + "mode": "new", + "requested": 1024, + "refLen": 984, + "iter": 1024, + "samples": 160, + "oracleEscape": 159, + "fast": { + "escape": 159, + "refend": 1 + } + }, + { + "case": "specific-black-magnification", + "mode": "old", + "requested": 1536, + "refLen": 1218, + "iter": 1024, + "samples": 160, + "oracleEscape": 140, + "fast": { + "escape": 140, + "op": 20 + } + }, + { + "case": "specific-black-magnification", + "mode": "new", + "requested": 1536, + "refLen": 1218, + "iter": 1536, + "samples": 160, + "oracleEscape": 150, + "fast": { + "escape": 144, + "refend": 16 + } + }, + { + "case": "undrawn-area", + "mode": "old", + "requested": 4702, + "refLen": 2774, + "iter": 2048, + "samples": 160, + "oracleEscape": 40, + "fast": { + "op": 119, + "escape": 41 + } + }, + { + "case": "undrawn-area", + "mode": "new", + "requested": 4702, + "refLen": 2774, + "iter": 4702, + "samples": 160, + "oracleEscape": 160, + "fast": { + "escape": 156, + "refend": 4 + } + } + ], + "referenceEndRecovery": [ + { + "case": "zoom-black", + "iter": 1024, + "refLen": 984, + "samples": 160, + "referenceEnd": 1, + "precisionFallback": { + "accepted": 1, + "escaped": 0, + "operationLimit": 1, + "rejected": 0 + } + }, + { + "case": "specific-black-magnification", + "iter": 1536, + "refLen": 1218, + "samples": 160, + "referenceEnd": 16, + "precisionFallback": { + "accepted": 16, + "escaped": 7, + "operationLimit": 9, + "rejected": 0 + } + }, + { + "case": "undrawn-area", + "iter": 4702, + "refLen": 2774, + "samples": 160, + "referenceEnd": 4, + "precisionFallback": { + "accepted": 4, + "escaped": 4, + "operationLimit": 0, + "rejected": 0 + } + } + ], + "integratedBench": { + "date": "2026-09-29T12:25:38.978Z", + "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": 124.01649900000001 + }, + { + "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": 3054.664349 + }, + { + "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": 3367.845772 + }, + { + "view": "seahorse-boundary", + "productionBackend": "direct", + "pixels": 6144, + "baseWork": 410111, + "heuristicWork": 410111, + "workReduction": 0, + "candidates": 0, + "falseCandidates": 0, + "baseLimit": 0, + "heuristicLimit": 0, + "diagnosticIfForcedDirect": null, + "ms": 20.501296999999795 + }, + { + "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": 2318.3999639999993 + }, + { + "view": "exterior", + "productionBackend": "direct", + "pixels": 6144, + "baseWork": 686432, + "heuristicWork": 137063, + "workReduction": 0.8003254510279241, + "candidates": 72, + "falseCandidates": 0, + "baseLimit": 79, + "heuristicLimit": 7, + "diagnosticIfForcedDirect": null, + "ms": 46.87714600000072 + } + ], + "note": "Production DIRECT_F32 rule with two attracting-cycle confirmations. FAST/perturbation periodic early-stop is intentionally disabled." + }, + "series": { + "rows": [ + { + "span": 1e-05, + "jump": 0, + "errorLog2": 0, + "maxAbsError": 0, + "maxRelativeError": 0, + "refLen": 1200, + "buildMs": 8.115433999999368 + }, + { + "span": 1e-07, + "jump": 0, + "errorLog2": 0, + "maxAbsError": 0, + "maxRelativeError": 0, + "refLen": 1200, + "buildMs": 7.299462999999378 + }, + { + "span": 1e-09, + "jump": 0, + "errorLog2": 0, + "maxAbsError": 0, + "maxRelativeError": 0, + "refLen": 1200, + "buildMs": 6.741853999999876 + }, + { + "span": 1e-11, + "jump": 0, + "errorLog2": 0, + "maxAbsError": 0, + "maxRelativeError": 0, + "refLen": 1200, + "buildMs": 7.206070999998701 + } + ], + "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": 95.52713500000027, + "times": [ + 95.52713500000027, + 98.22418299999845, + 106.965193, + 87.50846400000046, + 86.81074499999886, + 98.30645499999991, + 94.21621600000071 + ], + "work": 2658084, + "escaped": 176648, + "operationLimit": 3394, + "heuristicInterior": 1654 + }, + "multi": { + "medianMs": 37.26759299999867, + "times": [ + 102.13415600000008, + 51.094177000000855, + 34.866469999999026, + 37.26759299999867, + 39.969904999999926, + 36.02580599999965, + 36.46932300000117 + ], + "work": 2658084, + "escaped": 176648, + "operationLimit": 3394, + "heuristicInterior": 1654 + }, + "speedup": 2.5632762223201184, + "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" + } + }, + "browserSmoke": { + "status": "not_applicable", + "reason": "legacy browser_smoke expects removed __MANDEL_TEST__ hook; production regression is the authoritative current test" + } +} \ No newline at end of file diff --git a/docs/root-cause-fix/IMPLEMENTATION.md b/docs/root-cause-fix/IMPLEMENTATION.md new file mode 100644 index 0000000..b57e78d --- /dev/null +++ b/docs/root-cause-fix/IMPLEMENTATION.md @@ -0,0 +1,98 @@ +# Deep zoom root-cause fix + +## 目的 + +報告された2種類の不具合を、前フレーム依存のヒステリシスや閾値固定ではなく、数値経路と公開順序そのものから修正する。 + +1. `b=256 ... q=high` 付近で、わずかな座標移動により描画されるフラクタル形状が大きく変化する。 +2. `b=273 ... q=high` で fresh load 時に画面が完全に暗転する。 + +production source of truth は `index.html`。 + +## 根本原因 1: FAST perturbation が誤った escape を確定できる + +従来の `FAST_PERTURB_WGSL` は、f32 perturbation で `|z|^2 > 4` になった時点で、そのまま `FIELD_ESCAPED` を確定していた。perturbation error の上限を計算していなかったため、reference や画素位置の変更によって低精度誤差が増幅された場合、実際にはその反復数まで脱出していない画素を escape と確定できた。 + +再現例として1件目の view を実部方向へ span の 0.5% 移動した 1365×768 / 1024 iter ケースでは、pixel `(255,230)` について: + +- high-precision fixed-point oracle: 1024 iter まで non-escape +- 旧 FAST f32 perturbation: 846 iter で escape +- 新 FAST guard: escape を確定せず numerical failure へ送る +- 同一 reference の GPU double-single 相当シミュレーション: 1024 iter まで non-escape + +40 sample 中、旧 FAST の false escape は 1、guard 後は 0。DS correction の分類不一致も 0。 + +### 修正 + +- FAST recurrence に、既存 `DEEP_ACTIVE_CONTINUE_WGSL` と同じ考え方の propagated error を追加。 +- `|z|>2` でも `|z|-error > 2` を満たす場合だけ escape を確定。 +- 不確実なら `ESCAPE_UNCERTAIN / RANGE` として sparse correction に送る。 +- FAST/active 初期 delta で、従来捨てていた `spanMantLo`, `invFullWLo` を使用。 +- numerical failure は、guided reference や CPU BigInt より先に **現在の同一 reference を使う GPU double-single sparse correction** で再評価。 + +この変更により、カメラ履歴や直前の iteration 選択に依存せず、数値的に不確かな escape 自体を確定しない。 + +## 根本原因 2: FAST deep frame の公開が全処理終了まで抑止されていた + +従来は `fastExtended` の場合、stable history の有無に関係なく `deferPrimaryColor=true` になり、FAST primary 全画面計算と numerical recovery が終わるまで新しい色を公開しなかった。fresh hash load では履歴画像がないため、その間は canvas の clear color(黒)のままになる。 + +2件目の報告 view を high-precision fixed-point oracle で40点調べると、1713×2448 / 1536 iter では 37/40 が escape。したがって「全黒」は数値結果ではない。 + +### 修正 + +- `fastExtended` だからという理由だけで全primary colorを抑止する処理を撤廃。 +- bounded FAST strip が完了するたび、180 ms 以上経過していれば計算済みの安全な画素を公開。 +- sparse GPU double-single correction も batch 間で progressive publication。 +- 未計算/不確実画素は従来どおり透明のままにし、誤った色で埋めない。 +- stable reprojection が安全に使える場合は従来どおり旧frameを保持し、無駄なちらつきを避ける。 + +## ヒステリシスの撤廃 + +前回追加した以下は完全に削除した。 + +- `ADAPTIVE_PRIMARY_HYSTERESIS` +- `ADAPTIVE_PRIMARY_CAP` +- `nearbyAdaptiveViewSafe()` +- `stabilizeAdaptiveInitialChoice()` +- `state.lastPrimaryPolicy` + +adaptive initial iteration は再び **現在の view の probe 結果だけ**で決まる。直前のカメラ状態は数値policyに入らない。 + +adaptive probe 自体は残している。以前のA/Bで固定350へ落とすと、一部viewで推定仕事量が 7–33%増えたためであり、これは履歴依存ではない。 + +## 報告viewの再評価 + +`experiments/root-cause/analyze.mjs` は production の FastReference worker / PrecisionFallback worker と同じ実装を読み込み、旧FAST式、新guard、DS correction相当を同一referenceで比較する。 + +主な結果: + +| case | size | oracle escape / 40 | old false escape | guarded false escape | DS classification mismatch | +|---|---:|---:|---:|---:|---:| +| reported view 1 | 1365×768 | 28 | 0 | 0 | 0 | +| reported view 1 + re 0.5% span shift | 1365×768 | 26 | **1** | **0** | **0** | +| reported view 2 blackout | 1713×2448 | **37** | 0 | 0 | 0 | + +詳細は `reported-view-analysis.json`。 + +## 性能上の注意 + +この修正は「速さのために誤escapeを許容する」方向にはしていない。数値的に不安定な領域では guard が多くの画素を sparse DS correction へ送る。 + +報告2の 1713×2448 sample では、40点中5点のみFAST段階で安全なescape、32点が不確実判定になった。このため最終計算量が増える可能性がある。一方で: + +- 誤escapeによる形状変動は避ける。 +- DS correction は同一referenceで最初に実行し、CPU BigIntへ直行しない。 +- primary/DS correctionともprogressive publicationするため、計算中の全面暗転を避ける。 + +今後さらに高速化する場合は guard を弱めるのではなく、**reference validity domain / multi-reference partition / explicit rebasing** で perturbation error 自体を小さくするのが次の構造的改善候補。 + +## 検証 + +- `node tests/regression.mjs`: PASS +- production JS parse: regression 内 `vm.Script` で PASS +- 19 WGSL kernel hash 更新済み +- `node experiments/root-cause/analyze.mjs --assert`: PASS +- series approximation: 引き続き無効 +- WebGPU実フレーム時間: このコンテナでは有効なheadless WebGPU backendを確立できず未測定 + +`tests/browser_smoke.mjs` の結果は `browser-smoke.txt` に保存。実GPU上では報告2 URLの「first visible pixel / stable frame p50」を追加測定する必要がある。 diff --git a/docs/root-cause-fix/browser-smoke.txt b/docs/root-cause-fix/browser-smoke.txt new file mode 100644 index 0000000..659afaa --- /dev/null +++ b/docs/root-cause-fix/browser-smoke.txt @@ -0,0 +1,8 @@ +file:///mnt/data/mandelbrot_rootcause_fix/tests/browser_smoke.mjs:51 + for(let i=0;i<200;i++){if(await evaluate('typeof globalThis.__MANDEL_TEST__ === "object"'))break;if(i===199)throw new Error('__MANDEL_TEST__ timeout');await sleep(50)} + ^ + +Error: __MANDEL_TEST__ timeout + at file:///mnt/data/mandelbrot_rootcause_fix/tests/browser_smoke.mjs:51:117 + +Node.js v22.16.0 diff --git a/docs/root-cause-fix/index.diff b/docs/root-cause-fix/index.diff new file mode 100644 index 0000000..ff2a83a --- /dev/null +++ b/docs/root-cause-fix/index.diff @@ -0,0 +1,221 @@ +--- /mnt/data/mandelbrot_deep_stability/index.html 2026-09-29 11:23:40.152053837 +0000 ++++ index.html 2026-09-29 11:48:30.228740302 +0000 +@@ -194,9 +194,10 @@ + if(!claim(out)){return;} + let q=atomicAdd(&count[0],1u);if(q>=p.unknownOnly){release(out);return;} + let px=out%p.fullW;let py=out/p.fullW;let gx=f32(px)+p.sampleX;let gy=f32(py)+p.sampleY; +- let dx=(gx-p.refPixelX)/f32(p.fullW);let dy=(p.refPixelY-gy)/f32(p.fullW); +- let d=vec2(p.spanMantHi*dx,p.spanMantHi*dy); +- states[q]=DeepState(d,vec2(0.0),p.spanExp,0u,0u,1.0*F32_U*maxabs(d));pixelMap[q]=out;queue[q]=q; ++ let ox=gx-p.refPixelX;let oy=p.refPixelY-gy; ++ let dxHi=ox*p.invFullWHi;let dxLo=ox*p.invFullWLo;let dyHi=oy*p.invFullWHi;let dyLo=oy*p.invFullWLo; ++ let d=vec2(p.spanMantHi*dxHi+(p.spanMantHi*dxLo+p.spanMantLo*dxHi),p.spanMantHi*dyHi+(p.spanMantHi*dyLo+p.spanMantLo*dyHi)); ++ states[q]=DeepState(d,vec2(0.0),p.spanExp,0u,0u,4.0*F32_U*(maxabs(d)+1.0e-30));pixelMap[q]=out;queue[q]=q; + } + `; + const FAST_PERTURB_WGSL=COMMON+String.raw` +@@ -229,34 +230,54 @@ + if(reason>=1u && reason<=6u){atomicAdd(&unresolved[reason],1u);} + } + ++fn fast_delta(gx:f32,gy:f32)->vec2{ ++ // Keep the low parts that are already present in Params. The old FAST path ++ // discarded both invFullWLo and spanMantLo, making the perturbation depend ++ // unnecessarily on f32 pixel-coordinate rounding. ++ let ox=gx-p.refPixelX; ++ let oy=p.refPixelY-gy; ++ let dxHi=ox*p.invFullWHi; let dxLo=ox*p.invFullWLo; ++ let dyHi=oy*p.invFullWHi; let dyLo=oy*p.invFullWLo; ++ return vec2( ++ p.spanMantHi*dxHi+(p.spanMantHi*dxLo+p.spanMantLo*dxHi), ++ p.spanMantHi*dyHi+(p.spanMantHi*dyLo+p.spanMantLo*dyHi) ++ ); ++} + fn render_fast(out:u32,gx:f32,gy:f32){ +- let dx=(gx-p.refPixelX)/f32(p.fullW); +- let dy=(p.refPixelY-gy)/f32(p.fullW); +- var d=vec2(p.spanMantHi*dx,p.spanMantHi*dy); ++ var d=fast_delta(gx,gy); + var w=vec2(0.0); + var scaleExp=p.spanExp; ++ var errScaled=4.0*F32_U*(maxabs(d)+1.0e-30); + var n=0u; var m=0u; + if(p.strict==0u && series.enabled!=0u && series.jump>0u && series.jump<=p.refLen && series.jump4.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,mag);return;} ++ if(mag>4.0){ ++ let errAbs=safe_abs_error(errScaled,scaleExp,z,delta); ++ if(length(z)-errAbs>2.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,mag);return;} ++ mark_fast_unresolved(out,n,REASON_ESCAPE_UNCERTAIN);return; ++ } + if(n>=p.maxIter){mark_fast_unresolved(out,p.maxIter,REASON_OPERATION_LIMIT);return;} +- // Reference-near-zero episodes are not an automatic failure. Continue the +- // valid perturbation recurrence; explicit range/reference guards remain. + if(m>=p.refLen){mark_fast_unresolved(out,n,REASON_REFERENCE_END);return;} +- let r=rp; +- let linear=2.0*(cmul(r.hi,w)+cmul(r.lo,w)); +- let sq=scaled_to_f32(cmul(w,w),scaleExp); ++ let refAbs=maxabs(rp.hi)+maxabs(rp.lo); ++ let wAbs=maxabs(w); let dAbs=maxabs(d); let deltaAbs=maxabs(delta); ++ let sq=scaled_to_f32(cmul(w,w),scaleExp); let sqAbs=maxabs(sq); ++ let gain=2.0*refAbs+2.0*deltaAbs; ++ let roundErr=1.0*F32_U*(2.0*refAbs*wAbs+sqAbs+dAbs+1.0e-30); ++ errScaled=gain*errScaled+roundErr; ++ let linear=2.0*(cmul(rp.hi,w)+cmul(rp.lo,w)); + w=linear+sq+d; m+=1u; n+=1u; + let mm=max(maxabs(w),maxabs(d)); +- if(mm>=1.0e30 || mm!=mm){mark_fast_unresolved(out,n,REASON_RANGE);return;} +- if(mm>65536.0){w*=0.0000152587890625;d*=0.0000152587890625;scaleExp+=16;} +- else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){w*=65536.0;d*=65536.0;scaleExp-=16;} ++ if(mm>=1.0e30 || mm!=mm || errScaled!=errScaled || errScaled>1.0e35){mark_fast_unresolved(out,n,REASON_RANGE);return;} ++ if(mm>65536.0){w*=0.0000152587890625;d*=0.0000152587890625;errScaled*=0.0000152587890625;scaleExp+=16;} ++ else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){w*=65536.0;d*=65536.0;errScaled*=65536.0;scaleExp-=16;} + if(scaleExp>126){mark_fast_unresolved(out,n,REASON_RANGE);return;} + } + } +@@ -279,7 +300,7 @@ + struct Params{tileW:u32,tileH:u32,fullW:u32,fullH:u32,tileX:u32,tileY:u32,maxIter:u32,refLen:u32,strict:u32,unknownOnly:u32,outputStride:u32,outputBase:u32,spanMantHi:f32,spanExp:i32,sampleX:f32,sampleY:f32,refPixelX:f32,refPixelY:f32,spanMantLo:f32,invFullWHi:f32,invFullWLo:f32,targetIter:u32,chunkSize:u32,_numeric2:u32}; + struct DeepState{d:vec2,w:vec2,scaleExp:i32,n:u32,m:u32,errScaled:f32}; + @group(0) @binding(0) var p:Params;@group(0) @binding(1) var states:array;@group(0) @binding(2) var queue:array;@group(0) @binding(3) var activeMark:array>;@group(0) @binding(4) var pixelMap:array; +-@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) gid:vec3){let qi=gid.x;if(qi>=p.unknownOnly){return;}let slot=queue[qi];let out=pixelMap[slot];if(out>=p.fullW*p.fullH){return;}let px=out%p.fullW;let py=out/p.fullW;let gx=f32(px)+p.sampleX;let gy=f32(py)+p.sampleY;let dx=(gx-p.refPixelX)/f32(p.fullW);let dy=(p.refPixelY-gy)/f32(p.fullW);let d=vec2(p.spanMantHi*dx,p.spanMantHi*dy);states[slot]=DeepState(d,vec2(0.0),p.spanExp,0u,0u,1.0*F32_U*maxabs(d));} ++@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) gid:vec3){let qi=gid.x;if(qi>=p.unknownOnly){return;}let slot=queue[qi];let out=pixelMap[slot];if(out>=p.fullW*p.fullH){return;}let px=out%p.fullW;let py=out/p.fullW;let gx=f32(px)+p.sampleX;let gy=f32(py)+p.sampleY;let ox=gx-p.refPixelX;let oy=p.refPixelY-gy;let dxHi=ox*p.invFullWHi;let dxLo=ox*p.invFullWLo;let dyHi=oy*p.invFullWHi;let dyLo=oy*p.invFullWLo;let d=vec2(p.spanMantHi*dxHi+(p.spanMantHi*dxLo+p.spanMantLo*dxHi),p.spanMantHi*dyHi+(p.spanMantHi*dyLo+p.spanMantLo*dyHi));states[slot]=DeepState(d,vec2(0.0),p.spanExp,0u,0u,4.0*F32_U*(maxabs(d)+1.0e-30));} + `; + const DEEP_ACTIVE_CONTINUE_WGSL=COMMON+String.raw` + struct Params{tileW:u32,tileH:u32,fullW:u32,fullH:u32,tileX:u32,tileY:u32,maxIter:u32,refLen:u32,strict:u32,unknownOnly:u32,outputStride:u32,outputBase:u32,spanMantHi:f32,spanExp:i32,sampleX:f32,sampleY:f32,refPixelX:f32,refPixelY:f32,spanMantLo:f32,invFullWHi:f32,invFullWLo:f32,targetIter:u32,chunkSize:u32,_numeric2:u32}; +@@ -912,7 +933,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,lastPrimaryPolicy: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 ───────────────────────────────────────── +@@ -943,26 +964,10 @@ + // ── iteration / quality policy ─────────────────────────────────────────── + const ADAPTIVE_ITER_QUANTUM=64,ADAPTIVE_PROBE_COLS=4,ADAPTIVE_PROBE_ROWS=3; + const ADAPTIVE_PROBE_CHOICES=[512,768,1024,1536,2048]; +-const ADAPTIVE_PRIMARY_CAP={fast:768,standard:1024,high:1536}; +-const ADAPTIVE_PRIMARY_HYSTERESIS=1.15; +-function nearbyAdaptiveViewSafe(old,snap,maxZoom=1.35,maxShift=.15){ +- if(!old||!snap||old.span<=0n||snap.span<=0n)return false; +- const b=Math.max(old.bits,snap.bits),os=align(old.span,old.bits,b),ns=align(snap.span,snap.bits,b),dr=align(snap.re,snap.bits,b)-align(old.re,old.bits,b),di=align(snap.im,snap.bits,b)-align(old.im,old.bits,b),scale=fixedRatio(ns,os),shift=Math.hypot(fixedRatio(dr,os),fixedRatio(di,os)); +- if(!Number.isFinite(scale)||scale<=0||!Number.isFinite(shift))return false; +- return Math.max(scale,1/scale)<=maxZoom&&shift<=maxShift; +-} +-function stabilizeAdaptiveInitialChoice(selected,snap){ +- const cap=Math.max(state.baseIter,ADAPTIVE_PRIMARY_CAP[state.quality]||1024),eligible=(selected.work||[]).filter(x=>x.initial<=cap); +- let best=eligible.length?eligible.reduce((a,b)=>b.totalx<=cap&&a.indexOf(x)===i).sort((a,b)=>a-b),prev=allowed.reduce((a,b)=>Math.abs(b-policy.iter)x.initial===prev); +- if(prevWork&&prevWork.total<=best.total*ADAPTIVE_PRIMARY_HYSTERESIS)return{...selected,iter:prev,rawIter:selected.iter,stabilized:true,cap}; +- const pi=allowed.indexOf(prev),bi=allowed.reduce((idx,x,i)=>Math.abs(x-best.initial)pi?Math.min(pi+1,allowed.length-1):bi=0&&bi>=0)return{...selected,iter:allowed[step],rawIter:selected.iter,stabilized:allowed[step]!==selected.iter,cap}; +- } +- return{...selected,iter:best.initial,rawIter:selected.iter,stabilized:best.initial!==selected.iter,cap}; +-} ++// The initial-iteration choice is a pure function of the current view/probe. ++// Never feed the previous frame's choice back into the numeric policy: doing ++// so makes identical coordinates history-dependent and can hide real numeric ++// discontinuities instead of fixing them. + function nextContinuationTarget(iter,maxIter,session){ + if(iter>=maxIter)return maxIter; + if(iter<1024)return Math.min(maxIter,Math.max(iter+1,iter*2)); +@@ -1143,8 +1148,7 @@ + const sources=adaptiveProbeSources(snap,width,height),started=performance.now(); + try{ + const scored=await referenceScorer.scoreFixedCandidates(snap,adaptiveIter,width,height,sources);if(token!==state.token)return null; +- const raw=adaptiveProbeChoice(scored.escapes||[],adaptiveIter,state.baseIter),selected=stabilizeAdaptiveInitialChoice(raw,snap),probeMs=performance.now()-started; +- ++ const selected=adaptiveProbeChoice(scored.escapes||[],adaptiveIter,state.baseIter),probeMs=performance.now()-started; + return{...selected,adaptiveIter,probeMs,samples:sources.length,escapes:scored.escapes||[]}; + }catch(error){ + if(token!==state.token)return null; +@@ -1391,7 +1395,13 @@ + const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4)); + const refX=referencePixel?.x??f.w*.5,refY=referencePixel?.y??f.h*.5,mirror=canUseRealAxisSymmetry(snap),computeH=mirror?Math.ceil(f.h/2):f.h,previous=state.fieldView,currentReferenceKey=this.fastCtx?.key||'',reusePriorField=!!previous&&sameViewSnapshot(previous,snap)&&previous.w===f.w&&previous.h===f.h&&previous.iter{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}; ++ 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(); ++ // A fresh deep view used to remain completely black until every FAST ++ // strip finished. Publish already computed, numerically safe pixels at ++ // bounded intervals; UNKNOWN/uncertain pixels stay transparent and are ++ // repaired before the final frame is certified. ++ if(!deferColor&&!await this.publishNumericProgress(snap,iter,token,'COVERING'))return false; ++ return true}; + for(let y=0;y=burstLimit||burstPredictedMs>=budgetMs||performance.now()-burstStart>=budgetMs){if(!await flushBurst(computeH-(y+th)))return false}}y+=th} + 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 + } +@@ -1458,6 +1468,14 @@ + return true; + } + 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} ++ async publishNumericProgress(snap,iter,token,drawState='REFINING',intervalMs=180){ ++ if(state.deferNumericPublish||token!==state.token||performance.now()-(this.lastProvisionalAt||0)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()])} +@@ -1529,7 +1547,7 @@ + const built=await this.readDeepQueueStats();if(token!==state.token)return false; + const buildValid=built.selected===built.enqueued&&built.overflow===0&&built.invalidIndex===0;if(!buildValid){console.warn('Sparse correction build invariant failed; using bounded full scan',built);return this.correctUnknownFrameScan(snap,iter,token,referencePixel)} + const maxBatch=Math.max(1,Math.floor(PIXEL_FRONTIER_WORK/Math.max(1,iter)));let offset=0; +- while(offset0){const guided=await recoverNumericalGuided(r,snap,iter,token,stats);if(!guided||token!==state.token)return null;stats=guided.stats;attempted+=guided.attempted||0;passes+=guided.passes||0;} + if(numericalFailureCount(stats)>0){const pf=await recoverResidualPrecision(r,snap,iter,token,stats);if(!pf||token!==state.token)return null;stats=pf.stats;attempted+=pf.attempted||0;applied+=pf.applied||0} +- return{stats,attempted,applied,passes,policy:'guided-precision'}; ++ return{stats,attempted,applied,passes,policy:'same-reference-ds-guided-precision'}; + } + function precisionBatchSize(iter,bits){return Math.max(1,Math.min(64,Math.floor(500000/(Math.max(1,iter)*5*Math.max(1,bits/128)))))} + async function recoverResidualPrecision(r,snap,iter,token,initialStats,terminalClass=-1){ +@@ -1819,8 +1842,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,primaryIter:iter,requestedIter}} + 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 covered=primaryIterCoveredByReference(iter,fastCtx?.refLen||0);if(covered0){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 deferColor=!!(r.historyReady&&canUseStableReprojection()),deferPrimaryColor=deferColor;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,referencePixel);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,fastExtended?fastCtx?.source:null);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,primaryIter:iter,requestedIter} + } +@@ -1837,7 +1860,6 @@ + if(!r){try{const ok=await renderCpuFallbackFrame(snap,iter,token);if(!ok||token!==state.token)return;state.rendering=false;state.drawState='COVERED';finishRenderClock(token);updateStats();return}catch(error){if(token!==state.token)return;state.gpuError=(state.gpuError?state.gpuError+' · ':'')+'CPU fallback失敗: '+String(error&&error.message||error);finishRenderClock(token,'error');state.rendering=false;state.drawState='ERROR';updateStats();return}} + state.temporalFill=!!r.historyReady;if(holdStable){r.presentFrame(r.presentTransform());state.drawState='REPROJECTED';updateStats()} + const result=await renderFinalFrame(r,snap,iter,token);if(!result||token!==state.token)return; +- state.lastPrimaryPolicy={view:{...snap},iter:result.primaryIter??iter}; + state.rendering=false;state.drawState='COVERED';if(numericFrameComplete()){r.commitHistory(snap);state.temporalFill=false}else state.temporalFill=!!r.historyReady;r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0}); + state.rendering=true;await r.device.queue.onSubmittedWorkDone();if(token!==state.token)return;state.rendering=false;finishRenderClock(token); + updateStats();if(state.recolorPending&&!state.recoloring)queueMicrotask(recolor); diff --git a/docs/root-cause-fix/index.sha256 b/docs/root-cause-fix/index.sha256 new file mode 100644 index 0000000..fcd621a --- /dev/null +++ b/docs/root-cause-fix/index.sha256 @@ -0,0 +1 @@ +cf9fef2bbfa6a80b0815e8b476e06fd9c229ea3db2df1eb86337f5fea0063598 index.html diff --git a/docs/root-cause-fix/regression.txt b/docs/root-cause-fix/regression.txt new file mode 100644 index 0000000..aadf988 --- /dev/null +++ b/docs/root-cause-fix/regression.txt @@ -0,0 +1 @@ +PASS: single-source finite viewer regression (production JS, 19 WGSL kernels, staged scoring, lifecycle, export metadata) diff --git a/docs/root-cause-fix/reported-view-analysis.json b/docs/root-cause-fix/reported-view-analysis.json new file mode 100644 index 0000000..f97276e --- /dev/null +++ b/docs/root-cause-fix/reported-view-analysis.json @@ -0,0 +1,995 @@ +{ + "generatedAt": "2026-09-29T11:49:55.036Z", + "results": [ + { + "case": "varying-range", + "w": 1365, + "h": 768, + "iter": 768, + "refLen": 768, + "referencePixel": { + "x": 625.5000000000002, + "y": 432.49999999999983 + }, + "samples": 40, + "oracleEscape": 28, + "oldFalseEscape": 0, + "guardedFalseEscape": 0, + "guardedUncertain": 21, + "guardedSafeEscape": 7, + "dsFalseEscape": 0, + "dsClassificationMismatch": 0, + "interesting": [ + { + "index": 103995, + "x": 255, + "y": 76, + "oracle": "escape", + "old": "escape", + "oldN": 349, + "new": "uncertain", + "newN": 349, + "ds": "escape", + "dsN": 349 + }, + { + "index": 104166, + "x": 426, + "y": 76, + "oracle": "escape", + "old": "escape", + "oldN": 447, + "new": "uncertain", + "newN": 446, + "ds": "escape", + "dsN": 446 + }, + { + "index": 104678, + "x": 938, + "y": 76, + "oracle": "escape", + "old": "escape", + "oldN": 383, + "new": "uncertain", + "newN": 383, + "ds": "escape", + "dsN": 383 + }, + { + "index": 104849, + "x": 1109, + "y": 76, + "oracle": "escape", + "old": "escape", + "oldN": 283, + "new": "uncertain", + "newN": 283, + "ds": "escape", + "dsN": 283 + }, + { + "index": 105019, + "x": 1279, + "y": 76, + "oracle": "escape", + "old": "escape", + "oldN": 308, + "new": "uncertain", + "newN": 308, + "ds": "escape", + "dsN": 308 + }, + { + "index": 314035, + "x": 85, + "y": 230, + "oracle": "escape", + "old": "escape", + "oldN": 562, + "new": "uncertain", + "newN": 562, + "ds": "escape", + "dsN": 562 + }, + { + "index": 314205, + "x": 255, + "y": 230, + "oracle": "escape", + "old": "escape", + "oldN": 426, + "new": "uncertain", + "newN": 426, + "ds": "escape", + "dsN": 426 + }, + { + "index": 314376, + "x": 426, + "y": 230, + "oracle": "escape", + "old": "escape", + "oldN": 338, + "new": "uncertain", + "newN": 338, + "ds": "escape", + "dsN": 338 + }, + { + "index": 314547, + "x": 597, + "y": 230, + "oracle": "escape", + "old": "escape", + "oldN": 399, + "new": "uncertain", + "newN": 399, + "ds": "escape", + "dsN": 399 + }, + { + "index": 314717, + "x": 767, + "y": 230, + "oracle": "escape", + "old": "escape", + "oldN": 389, + "new": "uncertain", + "newN": 389, + "ds": "escape", + "dsN": 389 + }, + { + "index": 314888, + "x": 938, + "y": 230, + "oracle": "escape", + "old": "escape", + "oldN": 448, + "new": "uncertain", + "newN": 448, + "ds": "escape", + "dsN": 448 + }, + { + "index": 315229, + "x": 1279, + "y": 230, + "oracle": "escape", + "old": "escape", + "oldN": 379, + "new": "uncertain", + "newN": 379, + "ds": "escape", + "dsN": 379 + } + ] + }, + { + "case": "varying-range", + "w": 1713, + "h": 2448, + "iter": 768, + "refLen": 768, + "referencePixel": { + "x": 785.5000000000002, + "y": 1377.4999999999998 + }, + "samples": 40, + "oracleEscape": 32, + "oldFalseEscape": 0, + "guardedFalseEscape": 0, + "guardedUncertain": 19, + "guardedSafeEscape": 13, + "dsFalseEscape": 0, + "dsClassificationMismatch": 0, + "interesting": [ + { + "index": 418079, + "x": 107, + "y": 244, + "oracle": "escape", + "old": "escape", + "oldN": 294, + "new": "uncertain", + "newN": 294, + "ds": "escape", + "dsN": 294 + }, + { + "index": 418293, + "x": 321, + "y": 244, + "oracle": "escape", + "old": "escape", + "oldN": 220, + "new": "uncertain", + "newN": 220, + "ds": "escape", + "dsN": 220 + }, + { + "index": 418507, + "x": 535, + "y": 244, + "oracle": "escape", + "old": "escape", + "oldN": 373, + "new": "uncertain", + "newN": 373, + "ds": "escape", + "dsN": 373 + }, + { + "index": 418721, + "x": 749, + "y": 244, + "oracle": "escape", + "old": "escape", + "oldN": 166, + "new": "uncertain", + "newN": 166, + "ds": "escape", + "dsN": 166 + }, + { + "index": 1257663, + "x": 321, + "y": 734, + "oracle": "escape", + "old": "escape", + "oldN": 406, + "new": "uncertain", + "newN": 406, + "ds": "escape", + "dsN": 406 + }, + { + "index": 1257877, + "x": 535, + "y": 734, + "oracle": "escape", + "old": "escape", + "oldN": 199, + "new": "uncertain", + "newN": 199, + "ds": "escape", + "dsN": 199 + }, + { + "index": 1258305, + "x": 963, + "y": 734, + "oracle": "escape", + "old": "escape", + "oldN": 358, + "new": "uncertain", + "newN": 358, + "ds": "escape", + "dsN": 358 + }, + { + "index": 1258519, + "x": 1177, + "y": 734, + "oracle": "escape", + "old": "escape", + "oldN": 398, + "new": "uncertain", + "newN": 398, + "ds": "escape", + "dsN": 398 + }, + { + "index": 1258733, + "x": 1391, + "y": 734, + "oracle": "escape", + "old": "escape", + "oldN": 291, + "new": "uncertain", + "newN": 291, + "ds": "escape", + "dsN": 291 + }, + { + "index": 1258947, + "x": 1605, + "y": 734, + "oracle": "escape", + "old": "escape", + "oldN": 236, + "new": "uncertain", + "newN": 236, + "ds": "escape", + "dsN": 236 + }, + { + "index": 2096819, + "x": 107, + "y": 1224, + "oracle": "escape", + "old": "escape", + "oldN": 561, + "new": "uncertain", + "newN": 561, + "ds": "escape", + "dsN": 566 + }, + { + "index": 2097033, + "x": 321, + "y": 1224, + "oracle": "escape", + "old": "escape", + "oldN": 768, + "new": "uncertain", + "newN": 768, + "ds": "escape", + "dsN": 768 + } + ] + }, + { + "case": "varying-range-shift-0.5pct", + "w": 1365, + "h": 768, + "iter": 1024, + "refLen": 1024, + "referencePixel": { + "x": 739.4999999999998, + "y": 528.4999999999998 + }, + "samples": 40, + "oracleEscape": 26, + "oldFalseEscape": 1, + "guardedFalseEscape": 0, + "guardedUncertain": 20, + "guardedSafeEscape": 6, + "dsFalseEscape": 0, + "dsClassificationMismatch": 0, + "interesting": [ + { + "index": 103825, + "x": 85, + "y": 76, + "oracle": "escape", + "old": "escape", + "oldN": 248, + "new": "uncertain", + "newN": 248, + "ds": "escape", + "dsN": 248 + }, + { + "index": 103995, + "x": 255, + "y": 76, + "oracle": "escape", + "old": "escape", + "oldN": 342, + "new": "uncertain", + "newN": 342, + "ds": "escape", + "dsN": 342 + }, + { + "index": 104166, + "x": 426, + "y": 76, + "oracle": "escape", + "old": "escape", + "oldN": 283, + "new": "uncertain", + "newN": 283, + "ds": "escape", + "dsN": 283 + }, + { + "index": 104678, + "x": 938, + "y": 76, + "oracle": "escape", + "old": "escape", + "oldN": 424, + "new": "uncertain", + "newN": 424, + "ds": "escape", + "dsN": 424 + }, + { + "index": 104849, + "x": 1109, + "y": 76, + "oracle": "escape", + "old": "escape", + "oldN": 282, + "new": "uncertain", + "newN": 282, + "ds": "escape", + "dsN": 282 + }, + { + "index": 105019, + "x": 1279, + "y": 76, + "oracle": "escape", + "old": "escape", + "oldN": 248, + "new": "uncertain", + "newN": 248, + "ds": "escape", + "dsN": 248 + }, + { + "index": 314035, + "x": 85, + "y": 230, + "oracle": "escape", + "old": "escape", + "oldN": 665, + "new": "uncertain", + "newN": 665, + "ds": "escape", + "dsN": 665 + }, + { + "index": 314205, + "x": 255, + "y": 230, + "oracle": "not-escape", + "old": "escape", + "oldN": 846, + "new": "range", + "newN": 845, + "ds": "op", + "dsN": 1024 + }, + { + "index": 314376, + "x": 426, + "y": 230, + "oracle": "escape", + "old": "escape", + "oldN": 312, + "new": "uncertain", + "newN": 312, + "ds": "escape", + "dsN": 312 + }, + { + "index": 314547, + "x": 597, + "y": 230, + "oracle": "escape", + "old": "escape", + "oldN": 609, + "new": "uncertain", + "newN": 611, + "ds": "escape", + "dsN": 614 + }, + { + "index": 314717, + "x": 767, + "y": 230, + "oracle": "escape", + "old": "escape", + "oldN": 354, + "new": "uncertain", + "newN": 354, + "ds": "escape", + "dsN": 354 + }, + { + "index": 314888, + "x": 938, + "y": 230, + "oracle": "escape", + "old": "escape", + "oldN": 462, + "new": "uncertain", + "newN": 462, + "ds": "escape", + "dsN": 462 + } + ] + }, + { + "case": "varying-range-shift-0.5pct", + "w": 1713, + "h": 2448, + "iter": 1024, + "refLen": 1024, + "referencePixel": { + "x": 785.5000000000002, + "y": 1377.4999999999998 + }, + "samples": 40, + "oracleEscape": 33, + "oldFalseEscape": 0, + "guardedFalseEscape": 0, + "guardedUncertain": 20, + "guardedSafeEscape": 13, + "dsFalseEscape": 0, + "dsClassificationMismatch": 0, + "interesting": [ + { + "index": 418079, + "x": 107, + "y": 244, + "oracle": "escape", + "old": "escape", + "oldN": 285, + "new": "uncertain", + "newN": 285, + "ds": "escape", + "dsN": 285 + }, + { + "index": 418293, + "x": 321, + "y": 244, + "oracle": "escape", + "old": "escape", + "oldN": 224, + "new": "uncertain", + "newN": 224, + "ds": "escape", + "dsN": 224 + }, + { + "index": 418507, + "x": 535, + "y": 244, + "oracle": "escape", + "old": "escape", + "oldN": 236, + "new": "uncertain", + "newN": 236, + "ds": "escape", + "dsN": 236 + }, + { + "index": 418935, + "x": 963, + "y": 244, + "oracle": "escape", + "old": "escape", + "oldN": 159, + "new": "uncertain", + "newN": 159, + "ds": "escape", + "dsN": 159 + }, + { + "index": 1257663, + "x": 321, + "y": 734, + "oracle": "escape", + "old": "escape", + "oldN": 483, + "new": "uncertain", + "newN": 483, + "ds": "escape", + "dsN": 483 + }, + { + "index": 1257877, + "x": 535, + "y": 734, + "oracle": "escape", + "old": "escape", + "oldN": 207, + "new": "uncertain", + "newN": 207, + "ds": "escape", + "dsN": 207 + }, + { + "index": 1258091, + "x": 749, + "y": 734, + "oracle": "escape", + "old": "escape", + "oldN": 221, + "new": "uncertain", + "newN": 221, + "ds": "escape", + "dsN": 221 + }, + { + "index": 1258305, + "x": 963, + "y": 734, + "oracle": "escape", + "old": "escape", + "oldN": 372, + "new": "uncertain", + "newN": 372, + "ds": "escape", + "dsN": 372 + }, + { + "index": 1258519, + "x": 1177, + "y": 734, + "oracle": "escape", + "old": "escape", + "oldN": 426, + "new": "uncertain", + "newN": 426, + "ds": "escape", + "dsN": 426 + }, + { + "index": 1258733, + "x": 1391, + "y": 734, + "oracle": "escape", + "old": "escape", + "oldN": 290, + "new": "uncertain", + "newN": 290, + "ds": "escape", + "dsN": 290 + }, + { + "index": 2096819, + "x": 107, + "y": 1224, + "oracle": "escape", + "old": "escape", + "oldN": 328, + "new": "uncertain", + "newN": 328, + "ds": "escape", + "dsN": 328 + }, + { + "index": 2097033, + "x": 321, + "y": 1224, + "oracle": "escape", + "old": "escape", + "oldN": 782, + "new": "uncertain", + "newN": 782, + "ds": "escape", + "dsN": 781 + } + ] + }, + { + "case": "blackout", + "w": 1365, + "h": 768, + "iter": 1536, + "refLen": 1536, + "referencePixel": { + "x": 682.5, + "y": 384 + }, + "samples": 40, + "oracleEscape": 31, + "oldFalseEscape": 0, + "guardedFalseEscape": 0, + "guardedUncertain": 24, + "guardedSafeEscape": 7, + "dsFalseEscape": 0, + "dsClassificationMismatch": 0, + "interesting": [ + { + "index": 103825, + "x": 85, + "y": 76, + "oracle": "escape", + "old": "escape", + "oldN": 1183, + "new": "uncertain", + "newN": 1183, + "ds": "escape", + "dsN": 1172 + }, + { + "index": 103995, + "x": 255, + "y": 76, + "oracle": "escape", + "old": "escape", + "oldN": 1156, + "new": "uncertain", + "newN": 1156, + "ds": "escape", + "dsN": 1154 + }, + { + "index": 104166, + "x": 426, + "y": 76, + "oracle": "escape", + "old": "escape", + "oldN": 1219, + "new": "uncertain", + "newN": 1219, + "ds": "escape", + "dsN": 1219 + }, + { + "index": 104507, + "x": 767, + "y": 76, + "oracle": "escape", + "old": "escape", + "oldN": 1489, + "new": "uncertain", + "newN": 1489, + "ds": "escape", + "dsN": 1489 + }, + { + "index": 104678, + "x": 938, + "y": 76, + "oracle": "escape", + "old": "escape", + "oldN": 1042, + "new": "uncertain", + "newN": 1042, + "ds": "escape", + "dsN": 1042 + }, + { + "index": 104849, + "x": 1109, + "y": 76, + "oracle": "escape", + "old": "escape", + "oldN": 953, + "new": "uncertain", + "newN": 953, + "ds": "escape", + "dsN": 953 + }, + { + "index": 105019, + "x": 1279, + "y": 76, + "oracle": "escape", + "old": "escape", + "oldN": 851, + "new": "uncertain", + "newN": 851, + "ds": "escape", + "dsN": 851 + }, + { + "index": 314205, + "x": 255, + "y": 230, + "oracle": "escape", + "old": "escape", + "oldN": 1325, + "new": "uncertain", + "newN": 1325, + "ds": "escape", + "dsN": 1327 + }, + { + "index": 314888, + "x": 938, + "y": 230, + "oracle": "escape", + "old": "escape", + "oldN": 1205, + "new": "uncertain", + "newN": 1205, + "ds": "escape", + "dsN": 1205 + }, + { + "index": 315059, + "x": 1109, + "y": 230, + "oracle": "escape", + "old": "escape", + "oldN": 931, + "new": "uncertain", + "newN": 931, + "ds": "escape", + "dsN": 931 + }, + { + "index": 524245, + "x": 85, + "y": 384, + "oracle": "escape", + "old": "escape", + "oldN": 939, + "new": "uncertain", + "newN": 939, + "ds": "escape", + "dsN": 939 + }, + { + "index": 524415, + "x": 255, + "y": 384, + "oracle": "escape", + "old": "escape", + "oldN": 1214, + "new": "uncertain", + "newN": 1214, + "ds": "escape", + "dsN": 1214 + } + ] + }, + { + "case": "blackout", + "w": 1713, + "h": 2448, + "iter": 1536, + "refLen": 1536, + "referencePixel": { + "x": 856.5, + "y": 1224 + }, + "samples": 40, + "oracleEscape": 37, + "oldFalseEscape": 0, + "guardedFalseEscape": 0, + "guardedUncertain": 32, + "guardedSafeEscape": 5, + "dsFalseEscape": 0, + "dsClassificationMismatch": 0, + "interesting": [ + { + "index": 418293, + "x": 321, + "y": 244, + "oracle": "escape", + "old": "escape", + "oldN": 773, + "new": "uncertain", + "newN": 773, + "ds": "escape", + "dsN": 773 + }, + { + "index": 418507, + "x": 535, + "y": 244, + "oracle": "escape", + "old": "escape", + "oldN": 765, + "new": "uncertain", + "newN": 765, + "ds": "escape", + "dsN": 765 + }, + { + "index": 418721, + "x": 749, + "y": 244, + "oracle": "escape", + "old": "escape", + "oldN": 835, + "new": "uncertain", + "newN": 835, + "ds": "escape", + "dsN": 835 + }, + { + "index": 418935, + "x": 963, + "y": 244, + "oracle": "escape", + "old": "escape", + "oldN": 771, + "new": "uncertain", + "newN": 771, + "ds": "escape", + "dsN": 771 + }, + { + "index": 419149, + "x": 1177, + "y": 244, + "oracle": "escape", + "old": "escape", + "oldN": 841, + "new": "uncertain", + "newN": 841, + "ds": "escape", + "dsN": 841 + }, + { + "index": 419363, + "x": 1391, + "y": 244, + "oracle": "escape", + "old": "escape", + "oldN": 736, + "new": "uncertain", + "newN": 736, + "ds": "escape", + "dsN": 736 + }, + { + "index": 419577, + "x": 1605, + "y": 244, + "oracle": "escape", + "old": "escape", + "oldN": 792, + "new": "uncertain", + "newN": 792, + "ds": "escape", + "dsN": 792 + }, + { + "index": 1257449, + "x": 107, + "y": 734, + "oracle": "escape", + "old": "escape", + "oldN": 820, + "new": "uncertain", + "newN": 820, + "ds": "escape", + "dsN": 820 + }, + { + "index": 1257663, + "x": 321, + "y": 734, + "oracle": "escape", + "old": "escape", + "oldN": 1093, + "new": "uncertain", + "newN": 1093, + "ds": "escape", + "dsN": 1093 + }, + { + "index": 1257877, + "x": 535, + "y": 734, + "oracle": "escape", + "old": "escape", + "oldN": 1003, + "new": "uncertain", + "newN": 1003, + "ds": "escape", + "dsN": 1003 + }, + { + "index": 1258091, + "x": 749, + "y": 734, + "oracle": "escape", + "old": "escape", + "oldN": 1135, + "new": "uncertain", + "newN": 1135, + "ds": "escape", + "dsN": 1135 + }, + { + "index": 1258305, + "x": 963, + "y": 734, + "oracle": "escape", + "old": "escape", + "oldN": 1187, + "new": "uncertain", + "newN": 1187, + "ds": "escape", + "dsN": 1187 + } + ] + } + ] +} diff --git a/docs/root-cause-fix/results.json b/docs/root-cause-fix/results.json new file mode 100644 index 0000000..2fd6c80 --- /dev/null +++ b/docs/root-cause-fix/results.json @@ -0,0 +1,110 @@ +{ + "base": { + "path": "mandelbrot_deep_stability/index.html", + "sha256": "c35f514d5edd29d7e2aba43125cda8023034f4bf6d2e3104a97051950925529c", + "bytes": 228503, + "gzip9": 59232 + }, + "rootCauseFix": { + "sha256": "cf9fef2bbfa6a80b0815e8b476e06fd9c229ea3db2df1eb86337f5fea0063598", + "bytes": 229939, + "gzip9": 59671 + }, + "hysteresisRemoved": true, + "seriesStillDisabled": true, + "fastEscapeGuard": true, + "sameReferenceDsFirst": true, + "progressivePrimaryAndCorrection": true, + "regression": "PASS", + "webgpuBrowserSmoke": "UNAVAILABLE: local headless browser did not expose the production test hook / usable WebGPU backend in this container", + "reportedViews": [ + { + "case": "varying-range", + "w": 1365, + "h": 768, + "iter": 768, + "samples": 40, + "oracleEscape": 28, + "oldFalseEscape": 0, + "guardedFalseEscape": 0, + "guardedUncertain": 21, + "guardedSafeEscape": 7, + "dsFalseEscape": 0, + "dsClassificationMismatch": 0 + }, + { + "case": "varying-range", + "w": 1713, + "h": 2448, + "iter": 768, + "samples": 40, + "oracleEscape": 32, + "oldFalseEscape": 0, + "guardedFalseEscape": 0, + "guardedUncertain": 19, + "guardedSafeEscape": 13, + "dsFalseEscape": 0, + "dsClassificationMismatch": 0 + }, + { + "case": "varying-range-shift-0.5pct", + "w": 1365, + "h": 768, + "iter": 1024, + "samples": 40, + "oracleEscape": 26, + "oldFalseEscape": 1, + "guardedFalseEscape": 0, + "guardedUncertain": 20, + "guardedSafeEscape": 6, + "dsFalseEscape": 0, + "dsClassificationMismatch": 0 + }, + { + "case": "varying-range-shift-0.5pct", + "w": 1713, + "h": 2448, + "iter": 1024, + "samples": 40, + "oracleEscape": 33, + "oldFalseEscape": 0, + "guardedFalseEscape": 0, + "guardedUncertain": 20, + "guardedSafeEscape": 13, + "dsFalseEscape": 0, + "dsClassificationMismatch": 0 + }, + { + "case": "blackout", + "w": 1365, + "h": 768, + "iter": 1536, + "samples": 40, + "oracleEscape": 31, + "oldFalseEscape": 0, + "guardedFalseEscape": 0, + "guardedUncertain": 24, + "guardedSafeEscape": 7, + "dsFalseEscape": 0, + "dsClassificationMismatch": 0 + }, + { + "case": "blackout", + "w": 1713, + "h": 2448, + "iter": 1536, + "samples": 40, + "oracleEscape": 37, + "oldFalseEscape": 0, + "guardedFalseEscape": 0, + "guardedUncertain": 32, + "guardedSafeEscape": 5, + "dsFalseEscape": 0, + "dsClassificationMismatch": 0 + } + ], + "limitations": [ + "No real WebGPU frame-time measurement in this container.", + "The conservative FAST error guard intentionally increases sparse DS correction work in numerically unstable regions." + ] +} diff --git a/experiments/deep-stability/analyze.mjs b/experiments/deep-stability/analyze.mjs new file mode 100644 index 0000000..b7b8dc3 --- /dev/null +++ b/experiments/deep-stability/analyze.mjs @@ -0,0 +1,31 @@ +import fs from 'node:fs/promises'; +import vm from 'node:vm'; +import {createHash} from 'node:crypto'; + +const beforePath=new URL('../../docs/index.before-deep-stability.html',import.meta.url); +const afterPath=new URL('../../index.html',import.meta.url); +const W=1365,H=768; +const views={ + slow:{bits:256,re:-205622114510507926800704643054286809254592769987563457279831046473325524359322n,im:513379446477500883677196015856714327336904219690358193317938348486510676186n,span:3078418461602202114708533300838559104565378571213513442445450374639665n}, + shift:{bits:256,re:-182215120433266355203010987622148276056382218624508989129623313366391792176971n,im:-2144608509479534944973188258037468495761432062726185186112796911558891n,span:534948936069955205492424267156491218237035352585723908563891386999955n}, + zoom:{bits:273,re:-29476405178773873431784472616251929746296615379738474285641794100531397955824039943n,im:221537265873922872266252656824773446182293138083067600042152072427939817789716n,span:42908678145762089950947452698100567019970259336840347677734662966834469639023n}, +}; +const D=1000000n; +function extract(html){return [...html.matchAll(/]*>([\s\S]*?)<\/script>/g)].map(m=>m[1])} +function load(html){const ss=extract(html),k={};vm.runInNewContext(ss[0],k);const el={width:W,height:H,clientWidth:W,clientHeight:H,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:W,height:H}}};const c={MANDEL_WEBGPU_KERNELS:k.MANDEL_WEBGPU_KERNELS,document:{querySelector:()=>el,getElementById:()=>el,documentElement:{classList:{toggle(){}}},body:{classList:{toggle(){}},appendChild(){}}},navigator:{hardwareConcurrency:8,deviceMemory:8},location:{search:'',hash:''},history:{replaceState(){}},performance,URLSearchParams,URL:{createObjectURL:()=>'',revokeObjectURL(){}},TextEncoder,Blob:class{},console,addEventListener(){},requestAnimationFrame:()=>1,cancelAnimationFrame(){},setTimeout,clearTimeout,setInterval,clearInterval,innerWidth:W,innerHeight:H,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}};const cut=ss[1].indexOf('// ── boot / teardown'),hasStable=ss[1].includes('stabilizeAdaptiveInitialChoice');const names=['referenceWorkerSource','fastReferenceWorkerSource','pixelPrecisionBits','fastReferenceBits','fixedSourceForPixel','referencePixelForSource','adaptiveProbeSources','adaptiveProbeChoice','log2FixedAt'];if(hasStable)names.push('stabilizeAdaptiveInitialChoice','nextContinuationTarget','primaryIterCoveredByReference');vm.runInNewContext(ss[1].slice(0,cut)+`\nglobalThis.I={${names.join(',')}};globalThis.S=state;})();`,c);return{I:c.I,S:c.S,html}} +function worker(src,msg){let result;const c={self:{},postMessage:x=>{result=x},performance};vm.runInNewContext(src,c);const t=performance.now();c.self.onmessage({data:msg});if(!result)throw new Error('worker returned nothing');if(result.type==='error')throw new Error(result.error);return{result,ms:performance.now()-t}} +function adaptiveIter(I,snap){const z=Math.max(0,Math.log10(3.4)-I.log2FixedAt(snap.span,snap.bits)/Math.log2(10)),screenBits=Math.max(0,Math.log2(W)+z*Math.LOG2E*Math.LN10),bonus=Math.ceil(48*screenBits/64)*64;return Math.min(12000,Math.max(350,350+bonus))} +function probe(app,snap){const {I,S}=app;S.baseIter=350;S.quality='standard';S.adaptive=true;S.continuationBudget=16384;const a=adaptiveIter(I,snap),sources=I.adaptiveProbeSources(snap,W,H),sc=worker(I.referenceWorkerSource(),{type:'score',id:1,bits:snap.bits,targetBits:I.pixelPrecisionBits(snap,W,40)+32,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:W,height:H,iter:a,candidates:sources.map(s=>({re:s.re.toString(),im:s.im.toString()}))}).result,raw=I.adaptiveProbeChoice(sc.escapes||[],a,350),selected=I.stabilizeAdaptiveInitialChoice?I.stabilizeAdaptiveInitialChoice(raw,snap):raw;return{adaptiveIter:a,raw,selected,escapes:sc.escapes||[]}} +function fastRef(app,snap,iter,runs=1){const {I}=app,bits=I.fastReferenceBits(snap,W),times=[];let result;for(let n=0;na-b)[Math.floor(times.length/2)],times}} +function exactGrid(app,snap,iter,cols=12,rows=7){const {I}=app,candidates=[];for(let jy=0;jy=iter)return 0;return escapes.filter(e=>e===0||e>refLen).length} +function shifted(v,dx){return{bits:v.bits,re:v.re+v.span*BigInt(Math.round(dx*1e6))/D,im:v.im,span:v.span}} +function zoomed(v,scale){return{bits:v.bits,re:v.re,im:v.im,span:v.span*BigInt(Math.round(scale*1e6))/D}} +function seqChoices(app,snaps){app.S.lastPrimaryPolicy=null;const out=[];for(const snap of snaps){const p=probe(app,snap),iter=p.selected.iter;out.push({raw:p.raw.iter,selected:iter});if(app.I.stabilizeAdaptiveInitialChoice)app.S.lastPrimaryPolicy={view:{...snap},iter}}return out} +function refSequence(app,snaps,iters){const points=[];for(let i=0;ishifted(views.shift,x)),shiftBefore=seqChoices(before,shiftSnaps),shiftAfter=seqChoices(after,shiftSnaps),shiftBeforeRefs=refSequence(before,shiftSnaps,shiftBefore.map(x=>x.selected)),shiftAfterRefs=refSequence(after,shiftSnaps,shiftAfter.map(x=>x.selected)); +const zoomScales=[1,.98,.95,.92,.90,.88,.85,.80],zoomSnaps=zoomScales.map(x=>zoomed(views.zoom,x)),zoomBefore=seqChoices(before,zoomSnaps),zoomAfter=seqChoices(after,zoomSnaps),z95=zoomed(views.zoom,.95),z95BeforeIter=seqChoices(before,[z95])[0].selected,z95AfterIter=seqChoices(after,[z95])[0].selected,z95BeforeRef=fastRef(before,z95,z95BeforeIter,3),z95AfterRef=fastRef(after,z95,z95AfterIter,3),z95Esc=exactGrid(after,z95,3072),z95Active1536=z95Esc.filter(e=>e===0||e>1536).length; +const output={date:new Date().toISOString(),canvas:{width:W,height:H},slowLoad:{before:{initialIter:slowBeforeProbe.selected.iter,fastRef:slowBeforeRef,referenceEndRiskSamples:refEndRisk(slowEsc,slowBeforeRef.refLen,slowBeforeProbe.selected.iter)},after:{initialIter:slowAfterProbe.selected.iter,fastRef:slowAfterRef,referenceEndRiskSamples:refEndRisk(slowEsc,slowAfterRef.refLen,slowAfterProbe.selected.iter)},sampleCount:slowEsc.length},shiftStability:{dx:shiftDx,before:shiftBefore,after:shiftAfter,beforeUnique:[...new Set(shiftBefore.map(x=>x.selected))],afterUnique:[...new Set(shiftAfter.map(x=>x.selected))],beforeReferenceMaxJumpPx:shiftBeforeRefs.maxJump,afterReferenceMaxJumpPx:shiftAfterRefs.maxJump},zoomCliff:{scales:zoomScales,before:zoomBefore,after:zoomAfter,scale095:{beforeInitial:z95BeforeIter,afterInitial:z95AfterIter,beforeFastRefLen:z95BeforeRef.refLen,afterFastRefLen:z95AfterRef.refLen,activeAfter1536Samples:z95Active1536,sampleCount:z95Esc.length,currentStyleNextTarget:3072,patchedNextTarget:after.I.nextContinuationTarget?after.I.nextContinuationTarget(1536,16384,{initial:z95Esc.filter(e=>e===0||e>z95AfterIter).length,active:z95Active1536}):null}},source:{beforeSha256:createHash('sha256').update(beforeHtml).digest('hex'),afterSha256:createHash('sha256').update(afterHtml).digest('hex')}}; +console.log(JSON.stringify(output,null,2)); diff --git a/experiments/root-cause/analyze.mjs b/experiments/root-cause/analyze.mjs new file mode 100644 index 0000000..9b23baf --- /dev/null +++ b/experiments/root-cause/analyze.mjs @@ -0,0 +1,64 @@ +import fs from 'node:fs'; +import vm from 'node:vm'; + +const prodHtml=fs.readFileSync(new URL('../../index.html',import.meta.url),'utf8'); +const scripts=[...prodHtml.matchAll(/]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]); +const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS; +function appContext(){ + const element={width:800,height:600,clientWidth:800,clientHeight:600,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:800,height:600}}}; + 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:800,innerHeight:600,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}}; + const cut=scripts[1].indexOf('// ── boot / teardown'); + const code=scripts[1].slice(0,cut)+`\nglobalThis.internal={state,fastReferenceWorkerSource,precisionFallbackWorkerSource,fastReferenceBits,pixelPrecisionBits,referencePixelForSource,spanMantExp,adaptiveProbeChoice};})();`; + vm.runInNewContext(code,context);return context.internal; +} +const app=appContext(); +function runWorker(source,data){let result;const c={self:{},postMessage:d=>{result=d},performance,console,BigInt,Math,ArrayBuffer,DataView,Float32Array,Uint32Array,Uint8Array};vm.runInNewContext(source,c);c.self.onmessage({data});if(!result)throw new Error('worker returned no result');if(result.type==='error')throw new Error(result.error);return result;} +function buildFast(snap,iter,w,h){const targetBits=app.fastReferenceBits(snap,w),d=runWorker(app.fastReferenceWorkerSource(),{type:'build',id:1,key:'analysis',sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter});d.source={bits:snap.bits,re:BigInt(d.referenceRe),im:BigInt(d.referenceIm),viewRe:snap.re,viewIm:snap.im,span:snap.span,iter};return d;} +function oracle(snap,iter,w,h,indices){const targetBits=app.pixelPrecisionBits(snap,w,40);return runWorker(app.precisionFallbackWorkerSource(),{type:'solve',id:2,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter,terminalClass:-1,indices:Uint32Array.from(indices).buffer});} +const f=Math.fround,U=5.960464477539063e-8; +const add=(a,b)=>f(f(a)+f(b)), mul=(a,b)=>f(f(a)*f(b)); +function cmul(a,b){return [add(mul(a[0],b[0]),-mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]} +const maxabs=v=>Math.max(Math.abs(v[0]),Math.abs(v[1])); +function ld(v,e){return f(v*Math.pow(2,e))} +function refsFrom(buf,n){const dv=new DataView(buf),a=[];for(let i=0;i<=n;i++){const o=i*16;a.push([[dv.getFloat32(o,true),dv.getFloat32(o+4,true)],[dv.getFloat32(o+8,true),dv.getFloat32(o+12,true)]])}return a} +function dsQuick(a,b){const q=f(a+b),e=f(b-f(q-a));return [q,e]} +function dsSum(a,b){const q=f(a+b),bb=f(q-a),e=f(f(a-f(q-bb))+f(b-bb));return [q,e]} +function dsProd(a,b){const q=f(a*b),ca=f(4097*a),ah=f(ca-f(ca-a)),al=f(a-ah),cb=f(4097*b),bh=f(cb-f(cb-b)),bl=f(b-bh);let e=f(f(ah*bh)-q);e=f(e+f(ah*bl));e=f(e+f(al*bh));e=f(e+f(al*bl));return [q,e]} +function dsAdd(a,b){const q=dsSum(a[0],b[0]);return dsQuick(q[0],f(q[1]+f(a[1]+b[1])))} +const dsNeg=a=>[-a[0],-a[1]],dsSub=(a,b)=>dsAdd(a,dsNeg(b)); +function dsMul(a,b){const q=dsProd(a[0],b[0]);let e=f(q[1]+f(a[0]*b[1]));e=f(e+f(a[1]*b[0]));e=f(e+f(a[1]*b[1]));return dsQuick(q[0],e)} +function dsScale(a,b){const q=dsProd(a[0],b);return dsQuick(q[0],f(q[1]+f(a[1]*b)))} +const dsPow2=(a,e)=>[ld(a[0],e),ld(a[1],e)],dsVal=a=>f(a[0]+a[1]); +const cdsAdd=(a,b)=>[dsAdd(a[0],b[0]),dsAdd(a[1],b[1])]; +const cdsMul=(a,b)=>[dsSub(dsMul(a[0],b[0]),dsMul(a[1],b[1])),dsAdd(dsMul(a[0],b[1]),dsMul(a[1],b[0]))]; +const cdsScale=(a,b)=>[dsScale(a[0],b),dsScale(a[1],b)],cdsPow2=(a,e)=>[dsPow2(a[0],e),dsPow2(a[1],e)]; +const cdsMag2=a=>dsAdd(dsMul(a[0],a[0]),dsMul(a[1],a[1])),cdsMax=a=>Math.max(Math.abs(dsVal(a[0])),Math.abs(dsVal(a[1]))); +function simulateDS(snap,w,h,iter,ctx,index){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=1/w,iwh=f(iw),iwl=f(iw-iwh),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy),inv=[iwh,iwl],sm=[mh,ml],dx=dsScale(inv,ox),dy=dsScale(inv,oy),d0=[dsMul(sm,dx),dsMul(sm,dy)];let d=d0,ww=[[0,0],[0,0]],scaleExp=se.exp,n=0,m=0;const refs=refsFrom(ctx.refs,ctx.refLen);while(true){const rp=refs[m],r=[[rp[0][0],rp[1][0]],[rp[0][1],rp[1][1]]],delta=cdsPow2(ww,scaleExp),z=cdsAdd(r,delta),mag=cdsMag2(z);if(mag[0]>4||(mag[0]===4&&mag[1]>0))return{kind:'escape',n};if(n>=iter)return{kind:'op',n};if(m>=ctx.refLen)return{kind:'refend',n};const linear=cdsScale(cdsMul(r,ww),2),sq=cdsPow2(cdsMul(ww,ww),scaleExp);ww=cdsAdd(cdsAdd(linear,sq),d);m++;n++;const mm=Math.max(cdsMax(ww),cdsMax(d));if(!Number.isFinite(mm)||mm>=1e30)return{kind:'range',n};if(mm>65536){ww=cdsScale(ww,1/65536);d=cdsScale(d,1/65536);scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=cdsScale(ww,65536);d=cdsScale(d,65536);scaleExp-=16}if(scaleExp>126)return{kind:'range',n};}} +function simulate(snap,w,h,iter,ctx,index,guarded){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=1/w,iwh=f(iw),iwl=f(iw-iwh),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy);let d; + if(guarded){const dxh=mul(ox,iwh),dxl=mul(ox,iwl),dyh=mul(oy,iwh),dyl=mul(oy,iwl);d=[add(mul(mh,dxh),add(mul(mh,dxl),mul(ml,dxh))),add(mul(mh,dyh),add(mul(mh,dyl),mul(ml,dyh)))];} + else {const dx=f(ox/f(w)),dy=f(oy/f(w));d=[mul(mh,dx),mul(mh,dy)];} + let ww=[0,0],scaleExp=se.exp,n=0,m=0,err=4*U*(maxabs(d)+1e-30);const refs=refsFrom(ctx.refs,ctx.refLen); + while(true){const rp=refs[Math.min(m,ctx.refLen)],delta=[ld(ww[0],scaleExp),ld(ww[1],scaleExp)],z=[add(rp[0][0],add(rp[1][0],delta[0])),add(rp[0][1],add(rp[1][1],delta[1]))],mag=add(mul(z[0],z[0]),mul(z[1],z[1])); + if(mag>4){if(!guarded)return{kind:'escape',n};const errAbs=Math.abs(ld(err,scaleExp))+U*(maxabs(z)+maxabs(delta)+1e-30);if(Math.hypot(z[0],z[1])-errAbs>2)return{kind:'escape',n};return{kind:'uncertain',n};} + if(n>=iter)return{kind:'op',n};if(m>=ctx.refLen)return{kind:'refend',n}; + const refAbs=maxabs(rp[0])+maxabs(rp[1]),wAbs=maxabs(ww),dAbs=maxabs(d),deltaAbs=maxabs(delta),ww2=cmul(ww,ww),sq=[ld(ww2[0],scaleExp),ld(ww2[1],scaleExp)],sqAbs=maxabs(sq),gain=2*refAbs+2*deltaAbs,roundErr=U*(2*refAbs*wAbs+sqAbs+dAbs+1e-30);if(guarded)err=f(gain*err+roundErr); + const l1=cmul(rp[0],ww),l2=cmul(rp[1],ww),linear=[f(2*f(l1[0]+l2[0])),f(2*f(l1[1]+l2[1]))];ww=[add(add(linear[0],sq[0]),d[0]),add(add(linear[1],sq[1]),d[1])];m++;n++;const mm=Math.max(maxabs(ww),maxabs(d));if(!Number.isFinite(mm)||mm>=1e30||guarded&&(!Number.isFinite(err)||err>1e35))return{kind:'range',n};if(mm>65536){ww=[mul(ww[0],1/65536),mul(ww[1],1/65536)];d=[mul(d[0],1/65536),mul(d[1],1/65536)];if(guarded)err=f(err/65536);scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=[mul(ww[0],65536),mul(ww[1],65536)];d=[mul(d[0],65536),mul(d[1],65536)];if(guarded)err=f(err*65536);scaleExp-=16}if(scaleExp>126)return{kind:'range',n}; + } +} +function grid(w,h,nx=8,ny=5){const out=[];for(let j=0;j>>28)&3,oe=cls===1,old=simulate(snap,w,h,c.iter,ctx,indices[k],false),neu=simulate(snap,w,h,c.iter,ctx,indices[k],true),ds=simulateDS(snap,w,h,c.iter,ctx,indices[k]);oracleEscape+=oe?1:0;oldFalse+=old.kind==='escape'&&!oe?1:0;newFalse+=neu.kind==='escape'&&!oe?1:0;uncertain+=neu.kind==='uncertain'?1:0;safeEsc+=neu.kind==='escape'?1:0;if((old.kind==='escape')!==oe||neu.kind==='uncertain'||ds.kind==='escape'!==oe)rows.push({index:indices[k],x:indices[k]%w,y:Math.floor(indices[k]/w),oracle:oe?'escape':'not-escape',old:old.kind,oldN:old.n,new:neu.kind,newN:neu.n,ds:ds.kind,dsN:ds.n});} + const dsFalse=rows.filter(r=>r.ds==='escape'&&r.oracle==='not-escape').length,dsMismatch=rows.filter(r=>(r.ds==='escape')!==(r.oracle==='escape')).length;results.push({case:c.name,w,h,iter:c.iter,refLen:ctx.refLen,referencePixel:app.referencePixelForSource(ctx.source,snap,w,h),samples:indices.length,oracleEscape,oldFalseEscape:oldFalse,guardedFalseEscape:newFalse,guardedUncertain:uncertain,guardedSafeEscape:safeEsc,dsFalseEscape:dsFalse,dsClassificationMismatch:dsMismatch,interesting:rows.slice(0,12)});}} +const output={generatedAt:new Date().toISOString(),results}; +if(process.argv.includes('--assert')){ + const shifted=results.find(r=>r.case==='varying-range-shift-0.5pct'&&r.w===1365&&r.h===768); + const dark=results.find(r=>r.case==='blackout'&&r.w===1713&&r.h===2448); + if(!shifted||shifted.oldFalseEscape<1||shifted.guardedFalseEscape!==0||shifted.dsFalseEscape!==0)throw new Error('reported coordinate-shift false escape was not caught by the FAST guard/DS correction path'); + if(!dark||dark.oracleEscape<1||dark.guardedSafeEscape<1)throw new Error('blackout view must contain visible escaped samples and progressive safe output'); +} +console.log(JSON.stringify(output,null,2)); diff --git a/experiments/root-cause/compensated_z_test.tmp.mjs b/experiments/root-cause/compensated_z_test.tmp.mjs new file mode 100644 index 0000000..01b7482 --- /dev/null +++ b/experiments/root-cause/compensated_z_test.tmp.mjs @@ -0,0 +1,34 @@ +import fs from 'node:fs'; +import vm from 'node:vm'; +const prodHtml=fs.readFileSync(new URL('../../index.html',import.meta.url),'utf8'); +const scripts=[...prodHtml.matchAll(/]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]); +const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS; +function appContext(){ + const element={width:1365,height:768,clientWidth:1365,clientHeight:768,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:1365,height:768}}}; + 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:1365,innerHeight:768,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}}; + const cut=scripts[1].indexOf('// ── boot / teardown'); + const code=scripts[1].slice(0,cut)+`\nglobalThis.internal={state,fastReferenceWorkerSource,referenceWorkerSource,precisionFallbackWorkerSource,fastReferenceBits,pixelPrecisionBits,referencePixelForSource,spanMantExp,adaptiveProbeSources,adaptiveProbeChoice,adaptiveIterationBonus,fixedSourceForPixel,log2FixedAt};})();`; + vm.runInNewContext(code,context);return context.internal; +} +const app=appContext(); +function runWorker(source,data){let result;const c={self:{},postMessage:d=>{result=d},performance,console,BigInt,Math,ArrayBuffer,DataView,Float32Array,Uint32Array,Uint8Array};vm.runInNewContext(source,c);c.self.onmessage({data});if(!result)throw new Error('no result');if(result.type==='error')throw new Error(result.error);return result;} +function maxAdaptive(snap,w){const spanLog2=app.log2FixedAt(snap.span,snap.bits),z=Math.max(0,Math.log10(3.4)-spanLog2/Math.log2(10)),screenBits=Math.max(0,Math.log2(w)+z*Math.LOG2E*Math.LN10),bonus=app.adaptiveIterationBonus(screenBits,1);return Math.min(16384,Math.max(350,350+bonus));} +function initialIter(snap,w,h){const adaptive=maxAdaptive(snap,w),sources=app.adaptiveProbeSources(snap,w,h),targetBits=Math.max(app.pixelPrecisionBits(snap,w,40)+32),scored=runWorker(app.referenceWorkerSource(),{type:'score',id:1,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter:adaptive,candidates:sources.map(c=>({re:c.re.toString(),im:c.im.toString()}))});return {adaptive,...app.adaptiveProbeChoice(scored.escapes||[],adaptive,350),escapes:scored.escapes};} +function buildFast(snap,iter,w,h){const targetBits=app.fastReferenceBits(snap,w),d=runWorker(app.fastReferenceWorkerSource(),{type:'build',id:2,key:'analysis',sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter});d.source={bits:snap.bits,re:BigInt(d.referenceRe),im:BigInt(d.referenceIm),viewRe:snap.re,viewIm:snap.im,span:snap.span,iter};return d;} +function oracle(snap,iter,w,h,indices){const targetBits=app.pixelPrecisionBits(snap,w,40);return runWorker(app.precisionFallbackWorkerSource(),{type:'solve',id:3,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter,terminalClass:-1,indices:Uint32Array.from(indices).buffer});} +const f=Math.fround;const add=(a,b)=>f(f(a)+f(b)),mul=(a,b)=>f(f(a)*f(b));const maxabs=v=>Math.max(Math.abs(v[0]),Math.abs(v[1])); +function cmul(a,b){return [add(mul(a[0],b[0]),-mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]} +function ld(v,e){return f(v*Math.pow(2,e))} +function refsFrom(buf,n){const dv=new DataView(buf),a=[];for(let i=0;i<=n;i++){const o=i*16;a.push([[dv.getFloat32(o,true),dv.getFloat32(o+4,true)],[dv.getFloat32(o+8,true),dv.getFloat32(o+12,true)]])}return a} +function simulate(snap,w,h,iter,ctx,index,{rebase=false,glitchG=0}={}){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=f(1/w),iwlo=f(1/w-iw),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy),dxh=mul(ox,iw),dxl=mul(ox,iwlo),dyh=mul(oy,iw),dyl=mul(oy,iwlo);let d=[add(mul(mh,dxh),add(mul(mh,dxl),mul(ml,dxh))),add(mul(mh,dyh),add(mul(mh,dyl),mul(ml,dyh)))];let ww=[0,0],scaleExp=se.exp,n=0,m=0,rebases=0,glitches=0,minRatio=Infinity;const refs=refsFrom(ctx.refs,ctx.refLen); + while(true){const rp=refs[Math.min(m,ctx.refLen)],delta=[ld(ww[0],scaleExp),ld(ww[1],scaleExp)],rv=[add(rp[0][0],rp[1][0]),add(rp[0][1],rp[1][1])],z=[f(Number(rp[0][0])+Number(rp[1][0])+Number(delta[0])),f(Number(rp[0][1])+Number(rp[1][1])+Number(delta[1]))],mag=add(mul(z[0],z[0]),mul(z[1],z[1])),rmag=add(mul(rv[0],rv[0]),mul(rv[1],rv[1])),dmag=add(mul(delta[0],delta[0]),mul(delta[1],delta[1]));if(rmag>0)minRatio=Math.min(minRatio,mag/rmag); + if(m>0&&glitchG>0&&rmag>0&&mag4)return{kind:'escape',n,rebases,glitches,minRatio};if(n>=iter)return{kind:'op',n,rebases,glitches,minRatio};if(m>=ctx.refLen)return{kind:'refend',n,rebases,glitches,minRatio}; + if(rebase&&m>0&&mag=1e30)return{kind:'range',n,rebases,glitches,minRatio};if(mm>65536){ww=[mul(ww[0],1/65536),mul(ww[1],1/65536)];d=[mul(d[0],1/65536),mul(d[1],1/65536)];scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=[mul(ww[0],65536),mul(ww[1],65536)];d=[mul(d[0],65536),mul(d[1],65536)];scaleExp-=16}if(scaleExp>126)return{kind:'range',n,rebases,glitches,minRatio};}} +function grid(w,h,nx=16,ny=10){const out=[];for(let j=0;j>>28)&3)===1,q=simulate(snap,w,h,sel.iter,ctx,indices[k],mode),fast=q.kind==='escape';counts[q.kind]=(counts[q.kind]||0)+1;rebases+=q.rebases||0;if(fast!==exact){mismatch++;if(fast)falseEscape++;else falseNon++;details.push({idx:indices[k],x:indices[k]%w,y:Math.floor(indices[k]/w),exact,q});}}rows.push({case:c.name,initial:sel.iter,mode:mode.name,counts,mismatch,falseEscape,falseNon,rebases,details});}}console.log(JSON.stringify(rows,null,2)); diff --git a/experiments/root-cause/continuation_cases.mjs b/experiments/root-cause/continuation_cases.mjs new file mode 100644 index 0000000..bfe05b5 --- /dev/null +++ b/experiments/root-cause/continuation_cases.mjs @@ -0,0 +1,61 @@ +import fs from 'node:fs'; +import vm from 'node:vm'; + +const prodHtml=fs.readFileSync(new URL('../../index.html',import.meta.url),'utf8'); +const scripts=[...prodHtml.matchAll(/]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]); +const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS; +function appContext(){ + const element={width:800,height:600,clientWidth:800,clientHeight:600,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:800,height:600}}}; + 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:800,innerHeight:600,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}}; + const cut=scripts[1].indexOf('// ── boot / teardown'); + const code=scripts[1].slice(0,cut)+`\nglobalThis.internal={state,fastReferenceWorkerSource,referenceWorkerSource,precisionFallbackWorkerSource,fastReferenceBits,pixelPrecisionBits,referencePixelForSource,spanMantExp,adaptiveProbeChoice};})();`; + vm.runInNewContext(code,context);return context.internal; +} +const app=appContext(); +function runWorker(source,data){let result;const c={self:{},postMessage:d=>{result=d},performance,console,BigInt,Math,ArrayBuffer,DataView,Float32Array,Uint32Array,Uint8Array};vm.runInNewContext(source,c);c.self.onmessage({data});if(!result)throw new Error('worker returned no result');if(result.type==='error')throw new Error(result.error);return result;} +function buildFast(snap,iter,w,h){const targetBits=app.fastReferenceBits(snap,w),d=runWorker(app.fastReferenceWorkerSource(),{type:'build',id:1,key:'analysis',sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter});d.source={bits:snap.bits,re:BigInt(d.referenceRe),im:BigInt(d.referenceIm),viewRe:snap.re,viewIm:snap.im,span:snap.span,iter};return d;} +function buildRef(snap,iter,w,h){const targetBits=app.pixelPrecisionBits(snap,w,40),d=runWorker(app.referenceWorkerSource(),{type:'build',id:3,key:'ref',bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter});d.source={bits:snap.bits,re:BigInt(d.referenceRe),im:BigInt(d.referenceIm),viewRe:snap.re,viewIm:snap.im,span:snap.span,iter,precisionBits:d.precisionBits,referenceId:d.referenceId};return d;} +function oracle(snap,iter,w,h,indices){const targetBits=app.pixelPrecisionBits(snap,w,40);return runWorker(app.precisionFallbackWorkerSource(),{type:'solve',id:2,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter,terminalClass:-1,indices:Uint32Array.from(indices).buffer});} +const f=Math.fround,U=5.960464477539063e-8; +const add=(a,b)=>f(f(a)+f(b)), mul=(a,b)=>f(f(a)*f(b)); +function cmul(a,b){return [add(mul(a[0],b[0]),-mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]} +const maxabs=v=>Math.max(Math.abs(v[0]),Math.abs(v[1])); +function ld(v,e){return f(v*Math.pow(2,e))} +function refsFrom(buf,n){const dv=new DataView(buf),a=[];for(let i=0;i<=n;i++){const o=i*16;a.push([[dv.getFloat32(o,true),dv.getFloat32(o+4,true)],[dv.getFloat32(o+8,true),dv.getFloat32(o+12,true)]])}return a} +function dsQuick(a,b){const q=f(a+b),e=f(b-f(q-a));return [q,e]} +function dsSum(a,b){const q=f(a+b),bb=f(q-a),e=f(f(a-f(q-bb))+f(b-bb));return [q,e]} +function dsProd(a,b){const q=f(a*b),ca=f(4097*a),ah=f(ca-f(ca-a)),al=f(a-ah),cb=f(4097*b),bh=f(cb-f(cb-b)),bl=f(b-bh);let e=f(f(ah*bh)-q);e=f(e+f(ah*bl));e=f(e+f(al*bh));e=f(e+f(al*bl));return [q,e]} +function dsAdd(a,b){const q=dsSum(a[0],b[0]);return dsQuick(q[0],f(q[1]+f(a[1]+b[1])))} +const dsNeg=a=>[-a[0],-a[1]],dsSub=(a,b)=>dsAdd(a,dsNeg(b)); +function dsMul(a,b){const q=dsProd(a[0],b[0]);let e=f(q[1]+f(a[0]*b[1]));e=f(e+f(a[1]*b[0]));e=f(e+f(a[1]*b[1]));return dsQuick(q[0],e)} +function dsScale(a,b){const q=dsProd(a[0],b);return dsQuick(q[0],f(q[1]+f(a[1]*b)))} +const dsPow2=(a,e)=>[ld(a[0],e),ld(a[1],e)],dsVal=a=>f(a[0]+a[1]); +const cdsAdd=(a,b)=>[dsAdd(a[0],b[0]),dsAdd(a[1],b[1])]; +const cdsMul=(a,b)=>[dsSub(dsMul(a[0],b[0]),dsMul(a[1],b[1])),dsAdd(dsMul(a[0],b[1]),dsMul(a[1],b[0]))]; +const cdsScale=(a,b)=>[dsScale(a[0],b),dsScale(a[1],b)],cdsPow2=(a,e)=>[dsPow2(a[0],e),dsPow2(a[1],e)]; +const cdsMag2=a=>dsAdd(dsMul(a[0],a[0]),dsMul(a[1],a[1])),cdsMax=a=>Math.max(Math.abs(dsVal(a[0])),Math.abs(dsVal(a[1]))); +function simulateDS(snap,w,h,iter,ctx,index){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=1/w,iwh=f(iw),iwl=f(iw-iwh),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy),inv=[iwh,iwl],sm=[mh,ml],dx=dsScale(inv,ox),dy=dsScale(inv,oy),d0=[dsMul(sm,dx),dsMul(sm,dy)];let d=d0,ww=[[0,0],[0,0]],scaleExp=se.exp,n=0,m=0;const refs=refsFrom(ctx.refs,ctx.refLen);while(true){const rp=refs[m],r=[[rp[0][0],rp[1][0]],[rp[0][1],rp[1][1]]],delta=cdsPow2(ww,scaleExp),z=cdsAdd(r,delta),mag=cdsMag2(z);if(mag[0]>4||(mag[0]===4&&mag[1]>0))return{kind:'escape',n};if(n>=iter)return{kind:'op',n};if(m>=ctx.refLen)return{kind:'refend',n};const linear=cdsScale(cdsMul(r,ww),2),sq=cdsPow2(cdsMul(ww,ww),scaleExp);ww=cdsAdd(cdsAdd(linear,sq),d);m++;n++;const mm=Math.max(cdsMax(ww),cdsMax(d));if(!Number.isFinite(mm)||mm>=1e30)return{kind:'range',n};if(mm>65536){ww=cdsScale(ww,1/65536);d=cdsScale(d,1/65536);scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=cdsScale(ww,65536);d=cdsScale(d,65536);scaleExp-=16}if(scaleExp>126)return{kind:'range',n};}} +function simulate(snap,w,h,iter,ctx,index,guarded){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=1/w,iwh=f(iw),iwl=f(iw-iwh),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy);let d; + if(guarded){const dxh=mul(ox,iwh),dxl=mul(ox,iwl),dyh=mul(oy,iwh),dyl=mul(oy,iwl);d=[add(mul(mh,dxh),add(mul(mh,dxl),mul(ml,dxh))),add(mul(mh,dyh),add(mul(mh,dyl),mul(ml,dyh)))];} + else {const dx=f(ox/f(w)),dy=f(oy/f(w));d=[mul(mh,dx),mul(mh,dy)];} + let ww=[0,0],scaleExp=se.exp,n=0,m=0,err=4*U*(maxabs(d)+1e-30);const refs=refsFrom(ctx.refs,ctx.refLen); + while(true){const rp=refs[Math.min(m,ctx.refLen)],delta=[ld(ww[0],scaleExp),ld(ww[1],scaleExp)],z=[add(rp[0][0],add(rp[1][0],delta[0])),add(rp[0][1],add(rp[1][1],delta[1]))],mag=add(mul(z[0],z[0]),mul(z[1],z[1])); + if(mag>4){if(!guarded)return{kind:'escape',n};const errAbs=Math.abs(ld(err,scaleExp))+U*(maxabs(z)+maxabs(delta)+1e-30);if(Math.hypot(z[0],z[1])-errAbs>2)return{kind:'escape',n};return{kind:'uncertain',n};} + if(n>=iter)return{kind:'op',n};if(m>=ctx.refLen)return{kind:'refend',n}; + const refAbs=maxabs(rp[0])+maxabs(rp[1]),wAbs=maxabs(ww),dAbs=maxabs(d),deltaAbs=maxabs(delta),ww2=cmul(ww,ww),sq=[ld(ww2[0],scaleExp),ld(ww2[1],scaleExp)],sqAbs=maxabs(sq),gain=2*refAbs+2*deltaAbs,roundErr=U*(2*refAbs*wAbs+sqAbs+dAbs+1e-30);if(guarded)err=f(gain*err+roundErr); + const l1=cmul(rp[0],ww),l2=cmul(rp[1],ww),linear=[f(2*f(l1[0]+l2[0])),f(2*f(l1[1]+l2[1]))];ww=[add(add(linear[0],sq[0]),d[0]),add(add(linear[1],sq[1]),d[1])];m++;n++;const mm=Math.max(maxabs(ww),maxabs(d));if(!Number.isFinite(mm)||mm>=1e30||guarded&&(!Number.isFinite(err)||err>1e35))return{kind:'range',n};if(mm>65536){ww=[mul(ww[0],1/65536),mul(ww[1],1/65536)];d=[mul(d[0],1/65536),mul(d[1],1/65536)];if(guarded)err=f(err/65536);scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=[mul(ww[0],65536),mul(ww[1],65536)];d=[mul(d[0],65536),mul(d[1],65536)];if(guarded)err=f(err*65536);scaleExp-=16}if(scaleExp>126)return{kind:'range',n}; + } +} +function grid(w,h,nx=8,ny=5){const out=[];for(let j=0;j]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]); +const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS; +function appContext(){ + const element={width:1365,height:768,clientWidth:1365,clientHeight:768,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:1365,height:768}}}; + 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:1365,innerHeight:768,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}}; + const cut=scripts[1].indexOf('// ── boot / teardown'); + const code=scripts[1].slice(0,cut)+`\nglobalThis.internal={state,fastReferenceWorkerSource,referenceWorkerSource,precisionFallbackWorkerSource,fastReferenceBits,pixelPrecisionBits,referencePixelForSource,spanMantExp,adaptiveProbeSources,adaptiveProbeChoice,adaptiveIterationBonus,fixedSourceForPixel,log2FixedAt};})();`; + vm.runInNewContext(code,context);return context.internal; +} +const app=appContext(); +function runWorker(source,data){let result;const c={self:{},postMessage:d=>{result=d},performance,console,BigInt,Math,ArrayBuffer,DataView,Float32Array,Uint32Array,Uint8Array};vm.runInNewContext(source,c);c.self.onmessage({data});if(!result)throw new Error('no result');if(result.type==='error')throw new Error(result.error);return result;} +function maxAdaptive(snap,w){const spanLog2=app.log2FixedAt(snap.span,snap.bits),z=Math.max(0,Math.log10(3.4)-spanLog2/Math.log2(10)),screenBits=Math.max(0,Math.log2(w)+z*Math.LOG2E*Math.LN10),bonus=app.adaptiveIterationBonus(screenBits,1);return Math.min(16384,Math.max(350,350+bonus));} +function initialIter(snap,w,h){const adaptive=maxAdaptive(snap,w),sources=app.adaptiveProbeSources(snap,w,h),targetBits=Math.max(app.pixelPrecisionBits(snap,w,40)+32),scored=runWorker(app.referenceWorkerSource(),{type:'score',id:1,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter:adaptive,candidates:sources.map(c=>({re:c.re.toString(),im:c.im.toString()}))});return {adaptive,...app.adaptiveProbeChoice(scored.escapes||[],adaptive,350),escapes:scored.escapes};} +function buildFast(snap,iter,w,h){const targetBits=app.fastReferenceBits(snap,w),d=runWorker(app.fastReferenceWorkerSource(),{type:'build',id:2,key:'analysis',sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter});d.source={bits:snap.bits,re:BigInt(d.referenceRe),im:BigInt(d.referenceIm),viewRe:snap.re,viewIm:snap.im,span:snap.span,iter};return d;} +function oracle(snap,iter,w,h,indices){const targetBits=app.pixelPrecisionBits(snap,w,40);return runWorker(app.precisionFallbackWorkerSource(),{type:'solve',id:3,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter,terminalClass:-1,indices:Uint32Array.from(indices).buffer});} +const f=Math.fround;const add=(a,b)=>f(f(a)+f(b)),mul=(a,b)=>f(f(a)*f(b));const maxabs=v=>Math.max(Math.abs(v[0]),Math.abs(v[1])); +function cmul(a,b){return [add(mul(a[0],b[0]),-mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]} +function ld(v,e){return f(v*Math.pow(2,e))} +function refsFrom(buf,n){const dv=new DataView(buf),a=[];for(let i=0;i<=n;i++){const o=i*16;a.push([[dv.getFloat32(o,true),dv.getFloat32(o+4,true)],[dv.getFloat32(o+8,true),dv.getFloat32(o+12,true)]])}return a} +function simulate(snap,w,h,iter,ctx,index,{rebase=false,glitchG=0}={}){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=f(1/w),iwlo=f(1/w-iw),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy),dxh=mul(ox,iw),dxl=mul(ox,iwlo),dyh=mul(oy,iw),dyl=mul(oy,iwlo);let d=[add(mul(mh,dxh),add(mul(mh,dxl),mul(ml,dxh))),add(mul(mh,dyh),add(mul(mh,dyl),mul(ml,dyh)))];let ww=[0,0],scaleExp=se.exp,n=0,m=0,rebases=0,glitches=0,minRatio=Infinity;const refs=refsFrom(ctx.refs,ctx.refLen); + let firstRiskN=-1;while(true){const rp=refs[Math.min(m,ctx.refLen)],delta=[ld(ww[0],scaleExp),ld(ww[1],scaleExp)],rv=[add(rp[0][0],rp[1][0]),add(rp[0][1],rp[1][1])],z=[add(rp[0][0],add(rp[1][0],delta[0])),add(rp[0][1],add(rp[1][1],delta[1]))],mag=add(mul(z[0],z[0]),mul(z[1],z[1])),rmag=add(mul(rv[0],rv[0]),mul(rv[1],rv[1])),dmag=add(mul(delta[0],delta[0]),mul(delta[1],delta[1]));if(firstRiskN<0&&m>0&&Number.isFinite(mag)&&Number.isFinite(dmag)&&mag0)minRatio=Math.min(minRatio,mag/rmag); + if(m>0&&glitchG>0&&rmag>0&&mag4)return{kind:'escape',n,rebases,glitches,minRatio,firstRiskN};if(n>=iter)return{kind:'op',n,rebases,glitches,minRatio,firstRiskN};if(m>=ctx.refLen)return{kind:'refend',n,rebases,glitches,minRatio,firstRiskN}; + if(rebase&&m>0&&mag=1e30)return{kind:'range',n,rebases,glitches,minRatio,firstRiskN};if(mm>65536){ww=[mul(ww[0],1/65536),mul(ww[1],1/65536)];d=[mul(d[0],1/65536),mul(d[1],1/65536)];scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=[mul(ww[0],65536),mul(ww[1],65536)];d=[mul(d[0],65536),mul(d[1],65536)];scaleExp-=16}if(scaleExp>126)return{kind:'range',n,rebases,glitches,minRatio,firstRiskN};}} +function grid(w,h,nx=16,ny=10){const out=[];for(let j=0;j=0)vals.push({first:q.firstRiskN,end:q.n,kind:q.kind,remaining:q.n-q.firstRiskN});}const first=vals.map(x=>x.first).sort((a,b)=>a-b),rem=vals.map(x=>x.remaining).sort((a,b)=>a-b);const q=(a,p)=>a.length?a[Math.floor((a.length-1)*p)]:null;rows.push({case:c.name,initial:sel.iter,risk:vals.length,first:{min:q(first,0),p25:q(first,.25),p50:q(first,.5),p75:q(first,.75),p90:q(first,.9),max:q(first,1)},remaining:{min:q(rem,0),p25:q(rem,.25),p50:q(rem,.5),p75:q(rem,.75),p90:q(rem,.9),max:q(rem,1)}})}console.log(JSON.stringify(rows,null,2)); diff --git a/experiments/root-cause/glitch_tmp.mjs b/experiments/root-cause/glitch_tmp.mjs new file mode 100644 index 0000000..c9e90ab --- /dev/null +++ b/experiments/root-cause/glitch_tmp.mjs @@ -0,0 +1,76 @@ +import fs from 'node:fs'; +import vm from 'node:vm'; + +const prodHtml=fs.readFileSync(new URL('../../index.html',import.meta.url),'utf8'); +const scripts=[...prodHtml.matchAll(/]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]); +const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS; +function appContext(){ + const element={width:800,height:600,clientWidth:800,clientHeight:600,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:800,height:600}}}; + 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:800,innerHeight:600,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}}; + const cut=scripts[1].indexOf('// ── boot / teardown'); + const code=scripts[1].slice(0,cut)+`\nglobalThis.internal={state,fastReferenceWorkerSource,precisionFallbackWorkerSource,fastReferenceBits,pixelPrecisionBits,referencePixelForSource,spanMantExp,adaptiveProbeChoice};})();`; + vm.runInNewContext(code,context);return context.internal; +} +const app=appContext(); +function runWorker(source,data){let result;const c={self:{},postMessage:d=>{result=d},performance,console,BigInt,Math,ArrayBuffer,DataView,Float32Array,Uint32Array,Uint8Array};vm.runInNewContext(source,c);c.self.onmessage({data});if(!result)throw new Error('worker returned no result');if(result.type==='error')throw new Error(result.error);return result;} +function buildFast(snap,iter,w,h){const targetBits=app.fastReferenceBits(snap,w),d=runWorker(app.fastReferenceWorkerSource(),{type:'build',id:1,key:'analysis',sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter});d.source={bits:snap.bits,re:BigInt(d.referenceRe),im:BigInt(d.referenceIm),viewRe:snap.re,viewIm:snap.im,span:snap.span,iter};return d;} +function oracle(snap,iter,w,h,indices){const targetBits=app.pixelPrecisionBits(snap,w,40);return runWorker(app.precisionFallbackWorkerSource(),{type:'solve',id:2,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter,terminalClass:-1,indices:Uint32Array.from(indices).buffer});} +const f=Math.fround,U=5.960464477539063e-8; +const add=(a,b)=>f(f(a)+f(b)), mul=(a,b)=>f(f(a)*f(b)); +function cmul(a,b){return [add(mul(a[0],b[0]),-mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]} +const maxabs=v=>Math.max(Math.abs(v[0]),Math.abs(v[1])); +function ld(v,e){return f(v*Math.pow(2,e))} +function refsFrom(buf,n){const dv=new DataView(buf),a=[];for(let i=0;i<=n;i++){const o=i*16;a.push([[dv.getFloat32(o,true),dv.getFloat32(o+4,true)],[dv.getFloat32(o+8,true),dv.getFloat32(o+12,true)]])}return a} +function dsQuick(a,b){const q=f(a+b),e=f(b-f(q-a));return [q,e]} +function dsSum(a,b){const q=f(a+b),bb=f(q-a),e=f(f(a-f(q-bb))+f(b-bb));return [q,e]} +function dsProd(a,b){const q=f(a*b),ca=f(4097*a),ah=f(ca-f(ca-a)),al=f(a-ah),cb=f(4097*b),bh=f(cb-f(cb-b)),bl=f(b-bh);let e=f(f(ah*bh)-q);e=f(e+f(ah*bl));e=f(e+f(al*bh));e=f(e+f(al*bl));return [q,e]} +function dsAdd(a,b){const q=dsSum(a[0],b[0]);return dsQuick(q[0],f(q[1]+f(a[1]+b[1])))} +const dsNeg=a=>[-a[0],-a[1]],dsSub=(a,b)=>dsAdd(a,dsNeg(b)); +function dsMul(a,b){const q=dsProd(a[0],b[0]);let e=f(q[1]+f(a[0]*b[1]));e=f(e+f(a[1]*b[0]));e=f(e+f(a[1]*b[1]));return dsQuick(q[0],e)} +function dsScale(a,b){const q=dsProd(a[0],b);return dsQuick(q[0],f(q[1]+f(a[1]*b)))} +const dsPow2=(a,e)=>[ld(a[0],e),ld(a[1],e)],dsVal=a=>f(a[0]+a[1]); +const cdsAdd=(a,b)=>[dsAdd(a[0],b[0]),dsAdd(a[1],b[1])]; +const cdsMul=(a,b)=>[dsSub(dsMul(a[0],b[0]),dsMul(a[1],b[1])),dsAdd(dsMul(a[0],b[1]),dsMul(a[1],b[0]))]; +const cdsScale=(a,b)=>[dsScale(a[0],b),dsScale(a[1],b)],cdsPow2=(a,e)=>[dsPow2(a[0],e),dsPow2(a[1],e)]; +const cdsMag2=a=>dsAdd(dsMul(a[0],a[0]),dsMul(a[1],a[1])),cdsMax=a=>Math.max(Math.abs(dsVal(a[0])),Math.abs(dsVal(a[1]))); +function simulateDS(snap,w,h,iter,ctx,index){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=1/w,iwh=f(iw),iwl=f(iw-iwh),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy),inv=[iwh,iwl],sm=[mh,ml],dx=dsScale(inv,ox),dy=dsScale(inv,oy),d0=[dsMul(sm,dx),dsMul(sm,dy)];let d=d0,ww=[[0,0],[0,0]],scaleExp=se.exp,n=0,m=0;const refs=refsFrom(ctx.refs,ctx.refLen);while(true){const rp=refs[m],r=[[rp[0][0],rp[1][0]],[rp[0][1],rp[1][1]]],delta=cdsPow2(ww,scaleExp),z=cdsAdd(r,delta),mag=cdsMag2(z);if(mag[0]>4||(mag[0]===4&&mag[1]>0))return{kind:'escape',n};if(n>=iter)return{kind:'op',n};if(m>=ctx.refLen)return{kind:'refend',n};const linear=cdsScale(cdsMul(r,ww),2),sq=cdsPow2(cdsMul(ww,ww),scaleExp);ww=cdsAdd(cdsAdd(linear,sq),d);m++;n++;const mm=Math.max(cdsMax(ww),cdsMax(d));if(!Number.isFinite(mm)||mm>=1e30)return{kind:'range',n};if(mm>65536){ww=cdsScale(ww,1/65536);d=cdsScale(d,1/65536);scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=cdsScale(ww,65536);d=cdsScale(d,65536);scaleExp-=16}if(scaleExp>126)return{kind:'range',n};}} +function simulate(snap,w,h,iter,ctx,index,guarded){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=1/w,iwh=f(iw),iwl=f(iw-iwh),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy);let d; + if(guarded){const dxh=mul(ox,iwh),dxl=mul(ox,iwl),dyh=mul(oy,iwh),dyl=mul(oy,iwl);d=[add(mul(mh,dxh),add(mul(mh,dxl),mul(ml,dxh))),add(mul(mh,dyh),add(mul(mh,dyl),mul(ml,dyh)))];} + else {const dx=f(ox/f(w)),dy=f(oy/f(w));d=[mul(mh,dx),mul(mh,dy)];} + let ww=[0,0],scaleExp=se.exp,n=0,m=0,err=4*U*(maxabs(d)+1e-30);const refs=refsFrom(ctx.refs,ctx.refLen); + while(true){const rp=refs[Math.min(m,ctx.refLen)],delta=[ld(ww[0],scaleExp),ld(ww[1],scaleExp)],z=[add(rp[0][0],add(rp[1][0],delta[0])),add(rp[0][1],add(rp[1][1],delta[1]))],mag=add(mul(z[0],z[0]),mul(z[1],z[1])); + if(mag>4){if(!guarded)return{kind:'escape',n};const errAbs=Math.abs(ld(err,scaleExp))+U*(maxabs(z)+maxabs(delta)+1e-30);if(Math.hypot(z[0],z[1])-errAbs>2)return{kind:'escape',n};return{kind:'uncertain',n};} + if(n>=iter)return{kind:'op',n};if(m>=ctx.refLen)return{kind:'refend',n}; + const refAbs=maxabs(rp[0])+maxabs(rp[1]),wAbs=maxabs(ww),dAbs=maxabs(d),deltaAbs=maxabs(delta),ww2=cmul(ww,ww),sq=[ld(ww2[0],scaleExp),ld(ww2[1],scaleExp)],sqAbs=maxabs(sq),gain=2*refAbs+2*deltaAbs,roundErr=U*(2*refAbs*wAbs+sqAbs+dAbs+1e-30);if(guarded)err=f(gain*err+roundErr); + const l1=cmul(rp[0],ww),l2=cmul(rp[1],ww),linear=[f(2*f(l1[0]+l2[0])),f(2*f(l1[1]+l2[1]))];ww=[add(add(linear[0],sq[0]),d[0]),add(add(linear[1],sq[1]),d[1])];m++;n++;const mm=Math.max(maxabs(ww),maxabs(d));if(!Number.isFinite(mm)||mm>=1e30||guarded&&(!Number.isFinite(err)||err>1e35))return{kind:'range',n};if(mm>65536){ww=[mul(ww[0],1/65536),mul(ww[1],1/65536)];d=[mul(d[0],1/65536),mul(d[1],1/65536)];if(guarded)err=f(err/65536);scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=[mul(ww[0],65536),mul(ww[1],65536)];d=[mul(d[0],65536),mul(d[1],65536)];if(guarded)err=f(err*65536);scaleExp-=16}if(scaleExp>126)return{kind:'range',n}; + } +} + +function simulateGlitch(snap,w,h,iter,ctx,index){ + const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy),dx=f(ox/f(w)),dy=f(oy/f(w));let d=[mul(mh,dx),mul(mh,dy)],ww=[0,0],scaleExp=se.exp,n=0,m=0;const refs=refsFrom(ctx.refs,ctx.refLen);let minRatio=Infinity,minN=-1,maxDeltaRef=0; + while(true){const rp=refs[Math.min(m,ctx.refLen)],delta=[ld(ww[0],scaleExp),ld(ww[1],scaleExp)],z=[add(rp[0][0],add(rp[1][0],delta[0])),add(rp[0][1],add(rp[1][1],delta[1]))],mag=add(mul(z[0],z[0]),mul(z[1],z[1]));const rr=add(rp[0][0],rp[1][0]),ri=add(rp[0][1],rp[1][1]),rmag=add(mul(rr,rr),mul(ri,ri));if(rmag>0){const ratio=mag/rmag;if(ratio4)return{kind:'escape',n,minRatio,minN,maxDeltaRef};if(n>=iter)return{kind:'op',n,minRatio,minN,maxDeltaRef};if(m>=ctx.refLen)return{kind:'refend',n,minRatio,minN,maxDeltaRef};const sq0=cmul(ww,ww),sq=[ld(sq0[0],scaleExp),ld(sq0[1],scaleExp)],l1=cmul(rp[0],ww),l2=cmul(rp[1],ww),linear=[f(2*f(l1[0]+l2[0])),f(2*f(l1[1]+l2[1]))];ww=[add(add(linear[0],sq[0]),d[0]),add(add(linear[1],sq[1]),d[1])];m++;n++;const mm=Math.max(maxabs(ww),maxabs(d));if(!Number.isFinite(mm)||mm>=1e30)return{kind:'range',n,minRatio,minN,maxDeltaRef};if(mm>65536){ww=[mul(ww[0],1/65536),mul(ww[1],1/65536)];d=[mul(d[0],1/65536),mul(d[1],1/65536)];scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=[mul(ww[0],65536),mul(ww[1],65536)];d=[mul(d[0],65536),mul(d[1],65536)];scaleExp-=16}if(scaleExp>126)return{kind:'range',n,minRatio,minN,maxDeltaRef};} +} +function grid(w,h,nx=8,ny=5){const out=[];for(let j=0;j>>28)&3,oe=cls===1,old=simulate(snap,w,h,c.iter,ctx,indices[k],false),neu=simulate(snap,w,h,c.iter,ctx,indices[k],true),ds=simulateDS(snap,w,h,c.iter,ctx,indices[k]);oracleEscape+=oe?1:0;oldFalse+=old.kind==='escape'&&!oe?1:0;newFalse+=neu.kind==='escape'&&!oe?1:0;uncertain+=neu.kind==='uncertain'?1:0;safeEsc+=neu.kind==='escape'?1:0;if((old.kind==='escape')!==oe||neu.kind==='uncertain'||ds.kind==='escape'!==oe)rows.push({index:indices[k],x:indices[k]%w,y:Math.floor(indices[k]/w),oracle:oe?'escape':'not-escape',old:old.kind,oldN:old.n,new:neu.kind,newN:neu.n,ds:ds.kind,dsN:ds.n});} + const dsFalse=rows.filter(r=>r.ds==='escape'&&r.oracle==='not-escape').length,dsMismatch=rows.filter(r=>(r.ds==='escape')!==(r.oracle==='escape')).length;results.push({case:c.name,w,h,iter:c.iter,refLen:ctx.refLen,referencePixel:app.referencePixelForSource(ctx.source,snap,w,h),samples:indices.length,oracleEscape,oldFalseEscape:oldFalse,guardedFalseEscape:newFalse,guardedUncertain:uncertain,guardedSafeEscape:safeEsc,dsFalseEscape:dsFalse,dsClassificationMismatch:dsMismatch,interesting:rows.slice(0,12)});}} + +// focused glitch metrics +{ + const c=cases.find(x=>x.name==='varying-range-shift-0.5pct');const snap={bits:c.bits,re:BigInt(c.re),im:BigInt(c.im),span:BigInt(c.sp)};const w=1365,h=768,ctx=buildFast(snap,c.iter,w,h);for(const idx of [314205,103825,103995,104166,104678]){console.error('GLITCH',idx,simulateGlitch(snap,w,h,c.iter,ctx,idx));} +} + +const output={generatedAt:new Date().toISOString(),results}; +if(process.argv.includes('--assert')){ + const shifted=results.find(r=>r.case==='varying-range-shift-0.5pct'&&r.w===1365&&r.h===768); + const dark=results.find(r=>r.case==='blackout'&&r.w===1713&&r.h===2448); + if(!shifted||shifted.oldFalseEscape<1||shifted.guardedFalseEscape!==0||shifted.dsFalseEscape!==0)throw new Error('reported coordinate-shift false escape was not caught by the FAST guard/DS correction path'); + if(!dark||dark.oracleEscape<1||dark.guardedSafeEscape<1)throw new Error('blackout view must contain visible escaped samples and progressive safe output'); +} +console.log(JSON.stringify(output,null,2)); diff --git a/experiments/root-cause/grid_detection_test.tmp.mjs b/experiments/root-cause/grid_detection_test.tmp.mjs new file mode 100644 index 0000000..442e584 --- /dev/null +++ b/experiments/root-cause/grid_detection_test.tmp.mjs @@ -0,0 +1,36 @@ +import fs from 'node:fs'; +import vm from 'node:vm'; +const prodHtml=fs.readFileSync(new URL('../../index.html',import.meta.url),'utf8'); +const scripts=[...prodHtml.matchAll(/]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]); +const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS; +function appContext(){ + const element={width:1365,height:768,clientWidth:1365,clientHeight:768,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:1365,height:768}}}; + 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:1365,innerHeight:768,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}}; + const cut=scripts[1].indexOf('// ── boot / teardown'); + const code=scripts[1].slice(0,cut)+`\nglobalThis.internal={state,fastReferenceWorkerSource,referenceWorkerSource,precisionFallbackWorkerSource,fastReferenceBits,pixelPrecisionBits,referencePixelForSource,spanMantExp,adaptiveProbeSources,adaptiveProbeChoice,adaptiveIterationBonus,fixedSourceForPixel,log2FixedAt};})();`; + vm.runInNewContext(code,context);return context.internal; +} +const app=appContext(); +function runWorker(source,data){let result;const c={self:{},postMessage:d=>{result=d},performance,console,BigInt,Math,ArrayBuffer,DataView,Float32Array,Uint32Array,Uint8Array};vm.runInNewContext(source,c);c.self.onmessage({data});if(!result)throw new Error('no result');if(result.type==='error')throw new Error(result.error);return result;} +function maxAdaptive(snap,w){const spanLog2=app.log2FixedAt(snap.span,snap.bits),z=Math.max(0,Math.log10(3.4)-spanLog2/Math.log2(10)),screenBits=Math.max(0,Math.log2(w)+z*Math.LOG2E*Math.LN10),bonus=app.adaptiveIterationBonus(screenBits,1);return Math.min(16384,Math.max(350,350+bonus));} +function initialIter(snap,w,h){const adaptive=maxAdaptive(snap,w),sources=app.adaptiveProbeSources(snap,w,h),targetBits=Math.max(app.pixelPrecisionBits(snap,w,40)+32),scored=runWorker(app.referenceWorkerSource(),{type:'score',id:1,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter:adaptive,candidates:sources.map(c=>({re:c.re.toString(),im:c.im.toString()}))});return {adaptive,...app.adaptiveProbeChoice(scored.escapes||[],adaptive,350),escapes:scored.escapes};} +function buildFast(snap,iter,w,h){const targetBits=app.fastReferenceBits(snap,w),d=runWorker(app.fastReferenceWorkerSource(),{type:'build',id:2,key:'analysis',sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter});d.source={bits:snap.bits,re:BigInt(d.referenceRe),im:BigInt(d.referenceIm),viewRe:snap.re,viewIm:snap.im,span:snap.span,iter};return d;} +function oracle(snap,iter,w,h,indices){const targetBits=app.pixelPrecisionBits(snap,w,40);return runWorker(app.precisionFallbackWorkerSource(),{type:'solve',id:3,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter,terminalClass:-1,indices:Uint32Array.from(indices).buffer});} +const f=Math.fround;const add=(a,b)=>f(f(a)+f(b)),mul=(a,b)=>f(f(a)*f(b));const maxabs=v=>Math.max(Math.abs(v[0]),Math.abs(v[1])); +function cmul(a,b){return [add(mul(a[0],b[0]),-mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]} +function ld(v,e){return f(v*Math.pow(2,e))} +function refsFrom(buf,n){const dv=new DataView(buf),a=[];for(let i=0;i<=n;i++){const o=i*16;a.push([[dv.getFloat32(o,true),dv.getFloat32(o+4,true)],[dv.getFloat32(o+8,true),dv.getFloat32(o+12,true)]])}return a} +function simulate(snap,w,h,iter,ctx,index,{rebase=false,glitchG=0}={}){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=f(1/w),iwlo=f(1/w-iw),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy),dxh=mul(ox,iw),dxl=mul(ox,iwlo),dyh=mul(oy,iw),dyl=mul(oy,iwlo);let d=[add(mul(mh,dxh),add(mul(mh,dxl),mul(ml,dxh))),add(mul(mh,dyh),add(mul(mh,dyl),mul(ml,dyh)))];let ww=[0,0],scaleExp=se.exp,n=0,m=0,rebases=0,glitches=0,minRatio=Infinity;const refs=refsFrom(ctx.refs,ctx.refLen); + while(true){const rp=refs[Math.min(m,ctx.refLen)],delta=[ld(ww[0],scaleExp),ld(ww[1],scaleExp)],rv=[add(rp[0][0],rp[1][0]),add(rp[0][1],rp[1][1])],z=[add(rp[0][0],add(rp[1][0],delta[0])),add(rp[0][1],add(rp[1][1],delta[1]))],mag=add(mul(z[0],z[0]),mul(z[1],z[1])),rmag=add(mul(rv[0],rv[0]),mul(rv[1],rv[1])),dmag=add(mul(delta[0],delta[0]),mul(delta[1],delta[1]));if(rmag>0)minRatio=Math.min(minRatio,mag/rmag); + if(m>0&&glitchG>0&&rmag>0&&mag4)return{kind:'escape',n,rebases,glitches,minRatio};if(n>=iter)return{kind:'op',n,rebases,glitches,minRatio};if(m>=ctx.refLen)return{kind:'refend',n,rebases,glitches,minRatio}; + if(rebase&&m>0&&mag=1e30)return{kind:'range',n,rebases,glitches,minRatio};if(mm>65536){ww=[mul(ww[0],1/65536),mul(ww[1],1/65536)];d=[mul(d[0],1/65536),mul(d[1],1/65536)];scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=[mul(ww[0],65536),mul(ww[1],65536)];d=[mul(d[0],65536),mul(d[1],65536)];scaleExp-=16}if(scaleExp>126)return{kind:'range',n,rebases,glitches,minRatio};}} +function grid(w,h,nx=16,ny=10){const out=[];for(let j=0;j>>28)&3)===1,q=simulate(snap,w,h,sel.iter,ctx,indices[k],{}),fast=q.kind==='escape';if(fast!==exact){mismatch++;if(fast)falseEscape++;}}tests.push({nx,ny,samples:indices.length,mismatch,falseEscape});}rows.push({case:c.name,initial:sel.iter,tests});} +console.log(JSON.stringify(rows,null,2)); diff --git a/experiments/root-cause/hilo_accuracy.mjs b/experiments/root-cause/hilo_accuracy.mjs new file mode 100644 index 0000000..74f02d1 --- /dev/null +++ b/experiments/root-cause/hilo_accuracy.mjs @@ -0,0 +1,67 @@ +import fs from 'node:fs'; +import vm from 'node:vm'; + +const prodHtml=fs.readFileSync(new URL('../../index.html',import.meta.url),'utf8'); +const scripts=[...prodHtml.matchAll(/]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]); +const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS; +function appContext(){ + const element={width:800,height:600,clientWidth:800,clientHeight:600,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:800,height:600}}}; + 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:800,innerHeight:600,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}}; + const cut=scripts[1].indexOf('// ── boot / teardown'); + const code=scripts[1].slice(0,cut)+`\nglobalThis.internal={state,fastReferenceWorkerSource,precisionFallbackWorkerSource,fastReferenceBits,pixelPrecisionBits,referencePixelForSource,spanMantExp,adaptiveProbeChoice};})();`; + vm.runInNewContext(code,context);return context.internal; +} +const app=appContext(); +function runWorker(source,data){let result;const c={self:{},postMessage:d=>{result=d},performance,console,BigInt,Math,ArrayBuffer,DataView,Float32Array,Uint32Array,Uint8Array};vm.runInNewContext(source,c);c.self.onmessage({data});if(!result)throw new Error('worker returned no result');if(result.type==='error')throw new Error(result.error);return result;} +function buildFast(snap,iter,w,h){const targetBits=app.fastReferenceBits(snap,w),d=runWorker(app.fastReferenceWorkerSource(),{type:'build',id:1,key:'analysis',sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter});d.source={bits:snap.bits,re:BigInt(d.referenceRe),im:BigInt(d.referenceIm),viewRe:snap.re,viewIm:snap.im,span:snap.span,iter};return d;} +function oracle(snap,iter,w,h,indices){const targetBits=app.pixelPrecisionBits(snap,w,40);return runWorker(app.precisionFallbackWorkerSource(),{type:'solve',id:2,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter,terminalClass:-1,indices:Uint32Array.from(indices).buffer});} +const f=Math.fround,U=5.960464477539063e-8; +const add=(a,b)=>f(f(a)+f(b)), mul=(a,b)=>f(f(a)*f(b)); +function cmul(a,b){return [add(mul(a[0],b[0]),-mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]} +const maxabs=v=>Math.max(Math.abs(v[0]),Math.abs(v[1])); +function ld(v,e){return f(v*Math.pow(2,e))} +function refsFrom(buf,n){const dv=new DataView(buf),a=[];for(let i=0;i<=n;i++){const o=i*16;a.push([[dv.getFloat32(o,true),dv.getFloat32(o+4,true)],[dv.getFloat32(o+8,true),dv.getFloat32(o+12,true)]])}return a} +function dsQuick(a,b){const q=f(a+b),e=f(b-f(q-a));return [q,e]} +function dsSum(a,b){const q=f(a+b),bb=f(q-a),e=f(f(a-f(q-bb))+f(b-bb));return [q,e]} +function dsProd(a,b){const q=f(a*b),ca=f(4097*a),ah=f(ca-f(ca-a)),al=f(a-ah),cb=f(4097*b),bh=f(cb-f(cb-b)),bl=f(b-bh);let e=f(f(ah*bh)-q);e=f(e+f(ah*bl));e=f(e+f(al*bh));e=f(e+f(al*bl));return [q,e]} +function dsAdd(a,b){const q=dsSum(a[0],b[0]);return dsQuick(q[0],f(q[1]+f(a[1]+b[1])))} +const dsNeg=a=>[-a[0],-a[1]],dsSub=(a,b)=>dsAdd(a,dsNeg(b)); +function dsMul(a,b){const q=dsProd(a[0],b[0]);let e=f(q[1]+f(a[0]*b[1]));e=f(e+f(a[1]*b[0]));e=f(e+f(a[1]*b[1]));return dsQuick(q[0],e)} +function dsScale(a,b){const q=dsProd(a[0],b);return dsQuick(q[0],f(q[1]+f(a[1]*b)))} +const dsPow2=(a,e)=>[ld(a[0],e),ld(a[1],e)],dsVal=a=>f(a[0]+a[1]); +const cdsAdd=(a,b)=>[dsAdd(a[0],b[0]),dsAdd(a[1],b[1])]; +const cdsMul=(a,b)=>[dsSub(dsMul(a[0],b[0]),dsMul(a[1],b[1])),dsAdd(dsMul(a[0],b[1]),dsMul(a[1],b[0]))]; +const cdsScale=(a,b)=>[dsScale(a[0],b),dsScale(a[1],b)],cdsPow2=(a,e)=>[dsPow2(a[0],e),dsPow2(a[1],e)]; +const cdsMag2=a=>dsAdd(dsMul(a[0],a[0]),dsMul(a[1],a[1])),cdsMax=a=>Math.max(Math.abs(dsVal(a[0])),Math.abs(dsVal(a[1]))); +function simulateDS(snap,w,h,iter,ctx,index){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=1/w,iwh=f(iw),iwl=f(iw-iwh),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy),inv=[iwh,iwl],sm=[mh,ml],dx=dsScale(inv,ox),dy=dsScale(inv,oy),d0=[dsMul(sm,dx),dsMul(sm,dy)];let d=d0,ww=[[0,0],[0,0]],scaleExp=se.exp,n=0,m=0;const refs=refsFrom(ctx.refs,ctx.refLen);while(true){const rp=refs[m],r=[[rp[0][0],rp[1][0]],[rp[0][1],rp[1][1]]],delta=cdsPow2(ww,scaleExp),z=cdsAdd(r,delta),mag=cdsMag2(z);if(mag[0]>4||(mag[0]===4&&mag[1]>0))return{kind:'escape',n};if(n>=iter)return{kind:'op',n};if(m>=ctx.refLen)return{kind:'refend',n};const linear=cdsScale(cdsMul(r,ww),2),sq=cdsPow2(cdsMul(ww,ww),scaleExp);ww=cdsAdd(cdsAdd(linear,sq),d);m++;n++;const mm=Math.max(cdsMax(ww),cdsMax(d));if(!Number.isFinite(mm)||mm>=1e30)return{kind:'range',n};if(mm>65536){ww=cdsScale(ww,1/65536);d=cdsScale(d,1/65536);scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=cdsScale(ww,65536);d=cdsScale(d,65536);scaleExp-=16}if(scaleExp>126)return{kind:'range',n};}} +function simulate(snap,w,h,iter,ctx,index,guarded){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=1/w,iwh=f(iw),iwl=f(iw-iwh),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy);let d; + if(guarded){const dxh=mul(ox,iwh),dxl=mul(ox,iwl),dyh=mul(oy,iwh),dyl=mul(oy,iwl);d=[add(mul(mh,dxh),add(mul(mh,dxl),mul(ml,dxh))),add(mul(mh,dyh),add(mul(mh,dyl),mul(ml,dyh)))];} + else {const dx=f(ox/f(w)),dy=f(oy/f(w));d=[mul(mh,dx),mul(mh,dy)];} + let ww=[0,0],scaleExp=se.exp,n=0,m=0,err=4*U*(maxabs(d)+1e-30);const refs=refsFrom(ctx.refs,ctx.refLen); + while(true){const rp=refs[Math.min(m,ctx.refLen)],delta=[ld(ww[0],scaleExp),ld(ww[1],scaleExp)],z=[add(rp[0][0],add(rp[1][0],delta[0])),add(rp[0][1],add(rp[1][1],delta[1]))],mag=add(mul(z[0],z[0]),mul(z[1],z[1])); + if(mag>4){if(!guarded)return{kind:'escape',n};const errAbs=Math.abs(ld(err,scaleExp))+U*(maxabs(z)+maxabs(delta)+1e-30);if(Math.hypot(z[0],z[1])-errAbs>2)return{kind:'escape',n};return{kind:'uncertain',n};} + if(n>=iter)return{kind:'op',n};if(m>=ctx.refLen)return{kind:'refend',n}; + const refAbs=maxabs(rp[0])+maxabs(rp[1]),wAbs=maxabs(ww),dAbs=maxabs(d),deltaAbs=maxabs(delta),ww2=cmul(ww,ww),sq=[ld(ww2[0],scaleExp),ld(ww2[1],scaleExp)],sqAbs=maxabs(sq),gain=2*refAbs+2*deltaAbs,roundErr=U*(2*refAbs*wAbs+sqAbs+dAbs+1e-30);if(guarded)err=f(gain*err+roundErr); + const l1=cmul(rp[0],ww),l2=cmul(rp[1],ww),linear=[f(2*f(l1[0]+l2[0])),f(2*f(l1[1]+l2[1]))];ww=[add(add(linear[0],sq[0]),d[0]),add(add(linear[1],sq[1]),d[1])];m++;n++;const mm=Math.max(maxabs(ww),maxabs(d));if(!Number.isFinite(mm)||mm>=1e30||guarded&&(!Number.isFinite(err)||err>1e35))return{kind:'range',n};if(mm>65536){ww=[mul(ww[0],1/65536),mul(ww[1],1/65536)];d=[mul(d[0],1/65536),mul(d[1],1/65536)];if(guarded)err=f(err/65536);scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=[mul(ww[0],65536),mul(ww[1],65536)];d=[mul(d[0],65536),mul(d[1],65536)];if(guarded)err=f(err*65536);scaleExp-=16}if(scaleExp>126)return{kind:'range',n}; + } +} +function simulateHi(snap,w,h,iter,ctx,index){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=1/w,iwh=f(iw),iwl=f(iw-iwh),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy);let d; + if(true){const dxh=mul(ox,iwh),dxl=mul(ox,iwl),dyh=mul(oy,iwh),dyl=mul(oy,iwl);d=[add(mul(mh,dxh),add(mul(mh,dxl),mul(ml,dxh))),add(mul(mh,dyh),add(mul(mh,dyl),mul(ml,dyh)))];} + else {const dx=f(ox/f(w)),dy=f(oy/f(w));d=[mul(mh,dx),mul(mh,dy)];} + let ww=[0,0],scaleExp=se.exp,n=0,m=0,err=4*U*(maxabs(d)+1e-30);const refs=refsFrom(ctx.refs,ctx.refLen); + while(true){const rp=refs[Math.min(m,ctx.refLen)],delta=[ld(ww[0],scaleExp),ld(ww[1],scaleExp)],z=[add(rp[0][0],add(rp[1][0],delta[0])),add(rp[0][1],add(rp[1][1],delta[1]))],mag=add(mul(z[0],z[0]),mul(z[1],z[1])); + if(mag>4){return{kind:'escape',n};} + if(n>=iter)return{kind:'op',n};if(m>=ctx.refLen)return{kind:'refend',n}; + const refAbs=maxabs(rp[0])+maxabs(rp[1]),wAbs=maxabs(ww),dAbs=maxabs(d),deltaAbs=maxabs(delta),ww2=cmul(ww,ww),sq=[ld(ww2[0],scaleExp),ld(ww2[1],scaleExp)],sqAbs=maxabs(sq),gain=2*refAbs+2*deltaAbs,roundErr=U*(2*refAbs*wAbs+sqAbs+dAbs+1e-30); + const l1=cmul(rp[0],ww),l2=cmul(rp[1],ww),linear=[f(2*f(l1[0]+l2[0])),f(2*f(l1[1]+l2[1]))];ww=[add(add(linear[0],sq[0]),d[0]),add(add(linear[1],sq[1]),d[1])];m++;n++;const mm=Math.max(maxabs(ww),maxabs(d));if(!Number.isFinite(mm)||mm>=1e30)return{kind:'range',n};if(mm>65536){ww=[mul(ww[0],1/65536),mul(ww[1],1/65536)];d=[mul(d[0],1/65536),mul(d[1],1/65536)];scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=[mul(ww[0],65536),mul(ww[1],65536)];d=[mul(d[0],65536),mul(d[1],65536)];scaleExp-=16}if(scaleExp>126)return{kind:'range',n}; + } +} +function grid(w,h,nx=8,ny=5){const out=[];for(let j=0;j>>28)&3)===1;for(const [name,q] of [['base',simulate(snap,w,h,iter,ctx,indices[k],false)],['hi',simulateHi(snap,w,h,iter,ctx,indices[k])]]){const f=q.kind==='escape';if(f&&!exact)(name==='base'?base:hi).fp++;if(!f&&exact)(name==='base'?base:hi).fn++;}}rows.push({case:c.name,refLen:ctx.refLen,base,hi})}console.log(JSON.stringify(rows,null,2)); \ No newline at end of file diff --git a/experiments/root-cause/minibrot_detail_tmp.mjs b/experiments/root-cause/minibrot_detail_tmp.mjs new file mode 100644 index 0000000..c85bd79 --- /dev/null +++ b/experiments/root-cause/minibrot_detail_tmp.mjs @@ -0,0 +1,35 @@ +import fs from 'node:fs'; +import vm from 'node:vm'; +const prodHtml=fs.readFileSync(new URL('../../index.html',import.meta.url),'utf8'); +const scripts=[...prodHtml.matchAll(/]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]); +const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS; +function appContext(){ + const element={width:1365,height:768,clientWidth:1365,clientHeight:768,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:1365,height:768}}}; + 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:1365,innerHeight:768,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}}; + const cut=scripts[1].indexOf('// ── boot / teardown'); + const code=scripts[1].slice(0,cut)+`\nglobalThis.internal={state,fastReferenceWorkerSource,referenceWorkerSource,precisionFallbackWorkerSource,fastReferenceBits,pixelPrecisionBits,referencePixelForSource,spanMantExp,adaptiveProbeSources,adaptiveProbeChoice,adaptiveIterationBonus,fixedSourceForPixel,log2FixedAt};})();`; + vm.runInNewContext(code,context);return context.internal; +} +const app=appContext(); +function runWorker(source,data){let result;const c={self:{},postMessage:d=>{result=d},performance,console,BigInt,Math,ArrayBuffer,DataView,Float32Array,Uint32Array,Uint8Array};vm.runInNewContext(source,c);c.self.onmessage({data});if(!result)throw new Error('no result');if(result.type==='error')throw new Error(result.error);return result;} +function maxAdaptive(snap,w){const spanLog2=app.log2FixedAt(snap.span,snap.bits),z=Math.max(0,Math.log10(3.4)-spanLog2/Math.log2(10)),screenBits=Math.max(0,Math.log2(w)+z*Math.LOG2E*Math.LN10),bonus=app.adaptiveIterationBonus(screenBits,1);return Math.min(16384,Math.max(350,350+bonus));} +function initialIter(snap,w,h){const adaptive=maxAdaptive(snap,w),sources=app.adaptiveProbeSources(snap,w,h),targetBits=Math.max(app.pixelPrecisionBits(snap,w,40)+32),scored=runWorker(app.referenceWorkerSource(),{type:'score',id:1,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter:adaptive,candidates:sources.map(c=>({re:c.re.toString(),im:c.im.toString()}))});return {adaptive,...app.adaptiveProbeChoice(scored.escapes||[],adaptive,350),escapes:scored.escapes};} +function buildFast(snap,iter,w,h){const targetBits=app.fastReferenceBits(snap,w),d=runWorker(app.fastReferenceWorkerSource(),{type:'build',id:2,key:'analysis',sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter});d.source={bits:snap.bits,re:BigInt(d.referenceRe),im:BigInt(d.referenceIm),viewRe:snap.re,viewIm:snap.im,span:snap.span,iter};return d;} +function oracle(snap,iter,w,h,indices){const targetBits=app.pixelPrecisionBits(snap,w,40);return runWorker(app.precisionFallbackWorkerSource(),{type:'solve',id:3,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter,terminalClass:-1,indices:Uint32Array.from(indices).buffer});} +const f=Math.fround;const add=(a,b)=>f(f(a)+f(b)),mul=(a,b)=>f(f(a)*f(b));const maxabs=v=>Math.max(Math.abs(v[0]),Math.abs(v[1])); +function cmul(a,b){return [add(mul(a[0],b[0]),-mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]} +function ld(v,e){return f(v*Math.pow(2,e))} +function refsFrom(buf,n){const dv=new DataView(buf),a=[];for(let i=0;i<=n;i++){const o=i*16;a.push([[dv.getFloat32(o,true),dv.getFloat32(o+4,true)],[dv.getFloat32(o+8,true),dv.getFloat32(o+12,true)]])}return a} +function simulate(snap,w,h,iter,ctx,index,{rebase=false,glitchG=0}={}){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=f(1/w),iwlo=f(1/w-iw),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy),dxh=mul(ox,iw),dxl=mul(ox,iwlo),dyh=mul(oy,iw),dyl=mul(oy,iwlo);let d=[add(mul(mh,dxh),add(mul(mh,dxl),mul(ml,dxh))),add(mul(mh,dyh),add(mul(mh,dyl),mul(ml,dyh)))];let ww=[0,0],scaleExp=se.exp,n=0,m=0,rebases=0,glitches=0,minRatio=Infinity;const refs=refsFrom(ctx.refs,ctx.refLen); + while(true){const rp=refs[Math.min(m,ctx.refLen)],delta=[ld(ww[0],scaleExp),ld(ww[1],scaleExp)],rv=[add(rp[0][0],rp[1][0]),add(rp[0][1],rp[1][1])],z=[add(rp[0][0],add(rp[1][0],delta[0])),add(rp[0][1],add(rp[1][1],delta[1]))],mag=add(mul(z[0],z[0]),mul(z[1],z[1])),rmag=add(mul(rv[0],rv[0]),mul(rv[1],rv[1])),dmag=add(mul(delta[0],delta[0]),mul(delta[1],delta[1]));if(rmag>0)minRatio=Math.min(minRatio,mag/rmag); + if(m>0&&glitchG>0&&rmag>0&&mag4)return{kind:'escape',n,rebases,glitches,minRatio};if(n>=iter)return{kind:'op',n,rebases,glitches,minRatio};if(m>=ctx.refLen)return{kind:'refend',n,rebases,glitches,minRatio}; + if(rebase&&m>0&&mag=1e30)return{kind:'range',n,rebases,glitches,minRatio};if(mm>65536){ww=[mul(ww[0],1/65536),mul(ww[1],1/65536)];d=[mul(d[0],1/65536),mul(d[1],1/65536)];scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=[mul(ww[0],65536),mul(ww[1],65536)];d=[mul(d[0],65536),mul(d[1],65536)];scaleExp-=16}if(scaleExp>126)return{kind:'range',n,rebases,glitches,minRatio};}} +function grid(w,h,nx=16,ny=10){const out=[];for(let j=0;j>>28)&3)===1,q=simulate(snap,1365,768,sel.iter,ctx,indices[k],mode),fast=q.kind==='escape';counts[q.kind]=(counts[q.kind]||0)+1;rebases+=q.rebases||0;minRatio=Math.min(minRatio,q.minRatio??Infinity);if(fast!==exact){mismatch++;if(fast)falseEscape++;else falseNon++;}}if(mode.name==='base'){for(let k=0;k>>28)&3)===1,q=simulate(snap,1365,768,sel.iter,ctx,indices[k],mode),fast=q.kind==='escape';if(fast!==exact)console.error(JSON.stringify({case:c.name,x:indices[k]%1365,y:Math.floor(indices[k]/1365),exact,q}));}} rows.push({case:c.name,initial:sel.iter,adaptive:sel.adaptive,refLen:ctx.refLen,selectionEscape:ctx.selectionEscape,refPixel:app.referencePixelForSource(ctx.source,snap,1365,768),mode:mode.name,counts,mismatch,falseEscape,falseNon,rebases,minRatio});}} +console.log(JSON.stringify(rows,null,2)); diff --git a/experiments/root-cause/minibrot_ds_rebase.mjs b/experiments/root-cause/minibrot_ds_rebase.mjs new file mode 100644 index 0000000..6c75e28 --- /dev/null +++ b/experiments/root-cause/minibrot_ds_rebase.mjs @@ -0,0 +1,13 @@ +import fs from 'node:fs';import vm from 'node:vm'; +const html=fs.readFileSync(new URL('../../index.html',import.meta.url),'utf8'),scripts=[...html.matchAll(/]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]);const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS; +function appContext(){const e={width:1365,height:768,clientWidth:1365,clientHeight:768,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:1365,height:768}}},c={MANDEL_WEBGPU_KERNELS:kernels,document:{querySelector:()=>e,getElementById:()=>e,documentElement:{classList:{toggle(){}}},body:{classList:{toggle(){}},appendChild(){}}},navigator:{hardwareConcurrency:4,deviceMemory:8},location:{search:'',hash:''},history:{replaceState(){}},performance,URLSearchParams,URL:{createObjectURL:()=>'',revokeObjectURL(){}},TextEncoder,Blob:class{},console,addEventListener(){},requestAnimationFrame:()=>1,cancelAnimationFrame(){},setTimeout,clearTimeout,setInterval,clearInterval,innerWidth:1365,innerHeight:768,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}},cut=scripts[1].indexOf('// ── boot / teardown');vm.runInNewContext(scripts[1].slice(0,cut)+`\nglobalThis.I={fastReferenceWorkerSource,referenceWorkerSource,precisionFallbackWorkerSource,fastReferenceBits,pixelPrecisionBits,referencePixelForSource,spanMantExp,adaptiveProbeSources,adaptiveProbeChoice,adaptiveIterationBonus,log2FixedAt};})();`,c);return c.I}const app=appContext(); +function runWorker(source,data){let r;const c={self:{},postMessage:d=>r=d,performance,console,BigInt,Math,ArrayBuffer,DataView,Float32Array,Uint32Array,Uint8Array};vm.runInNewContext(source,c);c.self.onmessage({data});if(r.type==='error')throw new Error(r.error);return r} +function maxAdaptive(s,w){const l=app.log2FixedAt(s.span,s.bits),z=Math.max(0,Math.log10(3.4)-l/Math.log2(10)),sb=Math.max(0,Math.log2(w)+z*Math.LOG2E*Math.LN10),bonus=app.adaptiveIterationBonus(sb,1);return Math.min(16384,Math.max(350,350+bonus))} +function initialIter(s,w,h){const a=maxAdaptive(s,w),src=app.adaptiveProbeSources(s,w,h),tb=app.pixelPrecisionBits(s,w,40)+32,r=runWorker(app.referenceWorkerSource(),{type:'score',id:1,bits:s.bits,targetBits:tb,re:s.re.toString(),im:s.im.toString(),span:s.span.toString(),width:w,height:h,iter:a,candidates:src.map(c=>({re:c.re.toString(),im:c.im.toString()}))});return app.adaptiveProbeChoice(r.escapes||[],a,350).iter} +function build(s,iter,w,h){const tb=app.fastReferenceBits(s,w),r=runWorker(app.fastReferenceWorkerSource(),{type:'build',id:2,key:'x',sourceBits:s.bits,targetBits:tb,re:s.re.toString(),im:s.im.toString(),span:s.span.toString(),width:w,height:h,iter});r.source={bits:s.bits,re:BigInt(r.referenceRe),im:BigInt(r.referenceIm),viewRe:s.re,viewIm:s.im,span:s.span};return r} +function oracle(s,iter,w,h,idx){const tb=app.pixelPrecisionBits(s,w,40),r=runWorker(app.precisionFallbackWorkerSource(),{type:'solve',id:3,bits:s.bits,targetBits:tb,re:s.re.toString(),im:s.im.toString(),span:s.span.toString(),width:w,height:h,iter,terminalClass:-1,indices:Uint32Array.from(idx).buffer});return new Uint32Array(r.meta)} +const f=Math.fround;function dsQuick(a,b){const q=f(a+b),e=f(b-f(q-a));return[q,e]}function dsSum(a,b){const q=f(a+b),bb=f(q-a),e=f(f(a-f(q-bb))+f(b-bb));return[q,e]}function dsProd(a,b){const q=f(a*b),ca=f(4097*a),ah=f(ca-f(ca-a)),al=f(a-ah),cb=f(4097*b),bh=f(cb-f(cb-b)),bl=f(b-bh);let e=f(f(ah*bh)-q);e=f(e+f(ah*bl));e=f(e+f(al*bh));e=f(e+f(al*bl));return[q,e]}function dsAdd(a,b){const q=dsSum(a[0],b[0]);return dsQuick(q[0],f(q[1]+f(a[1]+b[1])))}const dsNeg=a=>[-a[0],-a[1]],dsSub=(a,b)=>dsAdd(a,dsNeg(b));function dsMul(a,b){const q=dsProd(a[0],b[0]);let e=f(q[1]+f(a[0]*b[1]));e=f(e+f(a[1]*b[0]));e=f(e+f(a[1]*b[1]));return dsQuick(q[0],e)}function dsScale(a,b){const q=dsProd(a[0],b);return dsQuick(q[0],f(q[1]+f(a[1]*b)))}function dsPow2(a,e){return[f(a[0]*Math.pow(2,e)),f(a[1]*Math.pow(2,e))]}const dsVal=a=>f(a[0]+a[1]);const cdsAdd=(a,b)=>[dsAdd(a[0],b[0]),dsAdd(a[1],b[1])],cdsMul=(a,b)=>[dsSub(dsMul(a[0],b[0]),dsMul(a[1],b[1])),dsAdd(dsMul(a[0],b[1]),dsMul(a[1],b[0]))],cdsScale=(a,b)=>[dsScale(a[0],b),dsScale(a[1],b)],cdsPow2=(a,e)=>[dsPow2(a[0],e),dsPow2(a[1],e)],cdsMag2=a=>dsAdd(dsMul(a[0],a[0]),dsMul(a[1],a[1])),cdsMax=a=>Math.max(Math.abs(dsVal(a[0])),Math.abs(dsVal(a[1]))); +function refsFrom(buf,n){const d=new DataView(buf),a=[];for(let i=0;i<=n;i++){const o=i*16;a.push([[d.getFloat32(o,true),d.getFloat32(o+8,true)],[d.getFloat32(o+4,true),d.getFloat32(o+12,true)]])}return a} +function simDS(s,w,h,iter,ctx,index,rebase){const px=index%w,py=Math.floor(index/w),rp=app.referencePixelForSource(ctx.source,s,w,h),se=app.spanMantExp(s),mh=f(se.mant),ml=f(se.mant-mh),iwh=f(1/w),iwl=f(1/w-iwh),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rp.x)),oy=f(f(rp.y)-gy),inv=[iwh,iwl],sm=[mh,ml],dx=dsScale(inv,ox),dy=dsScale(inv,oy),d0=[dsMul(sm,dx),dsMul(sm,dy)];let d=d0,zdel=[[0,0],[0,0]],exp=se.exp,n=0,m=0,rb=0;const refs=refsFrom(ctx.refs,ctx.refLen);while(true){const rr=refs[m],r=[rr[0],rr[1]],delta=cdsPow2(zdel,exp),z=cdsAdd(r,delta),mag=cdsMag2(z),dmag=cdsMag2(delta);if(dsVal(mag)>4)return{kind:'escape',n,rb};if(n>=iter)return{kind:'op',n,rb};if(m>=ctx.refLen)return{kind:'refend',n,rb};if(rebase&&m>0&&dsVal(mag)=1e30)return{kind:'range',n,rb};if(mm>65536){zdel=cdsScale(zdel,1/65536);d=cdsScale(d,1/65536);exp+=16}else if(mm>0&&mm<1/65536&&exp>se.exp){zdel=cdsScale(zdel,65536);d=cdsScale(d,65536);exp-=16}if(exp>126)return{kind:'range',n,rb}}} +function grid(w,h,nx=16,ny=10){let a=[];for(let j=0;j>>28)&3)===1,q=simDS(s,1365,768,iter,ctx,idx[i],rb),got=q.kind==='escape';kinds[q.kind]=(kinds[q.kind]||0)+1;rbs+=q.rb;if(got!==ex){mis++;got?fe++:fn++}}out.push({case:c.name,iter,refLen:ctx.refLen,rebase:rb,kinds,mis,fe,fn,rbs})}}console.log(JSON.stringify(out,null,2)); diff --git a/experiments/root-cause/minibrot_escape_guard.mjs b/experiments/root-cause/minibrot_escape_guard.mjs new file mode 100644 index 0000000..e7fcdf4 --- /dev/null +++ b/experiments/root-cause/minibrot_escape_guard.mjs @@ -0,0 +1,34 @@ +import fs from 'node:fs'; +import vm from 'node:vm'; +const prodHtml=fs.readFileSync(new URL('../../index.html',import.meta.url),'utf8'); +const scripts=[...prodHtml.matchAll(/]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]); +const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS; +function appContext(){ + const element={width:1365,height:768,clientWidth:1365,clientHeight:768,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:1365,height:768}}}; + 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:1365,innerHeight:768,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}}; + const cut=scripts[1].indexOf('// ── boot / teardown'); + const code=scripts[1].slice(0,cut)+`\nglobalThis.internal={state,fastReferenceWorkerSource,referenceWorkerSource,precisionFallbackWorkerSource,fastReferenceBits,pixelPrecisionBits,referencePixelForSource,spanMantExp,adaptiveProbeSources,adaptiveProbeChoice,adaptiveIterationBonus,fixedSourceForPixel,log2FixedAt};})();`; + vm.runInNewContext(code,context);return context.internal; +} +const app=appContext(); +function runWorker(source,data){let result;const c={self:{},postMessage:d=>{result=d},performance,console,BigInt,Math,ArrayBuffer,DataView,Float32Array,Uint32Array,Uint8Array};vm.runInNewContext(source,c);c.self.onmessage({data});if(!result)throw new Error('no result');if(result.type==='error')throw new Error(result.error);return result;} +function maxAdaptive(snap,w){const spanLog2=app.log2FixedAt(snap.span,snap.bits),z=Math.max(0,Math.log10(3.4)-spanLog2/Math.log2(10)),screenBits=Math.max(0,Math.log2(w)+z*Math.LOG2E*Math.LN10),bonus=app.adaptiveIterationBonus(screenBits,1);return Math.min(16384,Math.max(350,350+bonus));} +function initialIter(snap,w,h){const adaptive=maxAdaptive(snap,w),sources=app.adaptiveProbeSources(snap,w,h),targetBits=Math.max(app.pixelPrecisionBits(snap,w,40)+32),scored=runWorker(app.referenceWorkerSource(),{type:'score',id:1,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter:adaptive,candidates:sources.map(c=>({re:c.re.toString(),im:c.im.toString()}))});return {adaptive,...app.adaptiveProbeChoice(scored.escapes||[],adaptive,350),escapes:scored.escapes};} +function buildFast(snap,iter,w,h){const targetBits=app.fastReferenceBits(snap,w),d=runWorker(app.fastReferenceWorkerSource(),{type:'build',id:2,key:'analysis',sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter});d.source={bits:snap.bits,re:BigInt(d.referenceRe),im:BigInt(d.referenceIm),viewRe:snap.re,viewIm:snap.im,span:snap.span,iter};return d;} +function oracle(snap,iter,w,h,indices){const targetBits=app.pixelPrecisionBits(snap,w,40);return runWorker(app.precisionFallbackWorkerSource(),{type:'solve',id:3,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter,terminalClass:-1,indices:Uint32Array.from(indices).buffer});} +const f=Math.fround;const add=(a,b)=>f(f(a)+f(b)),mul=(a,b)=>f(f(a)*f(b));const maxabs=v=>Math.max(Math.abs(v[0]),Math.abs(v[1])); +function cmul(a,b){return [add(mul(a[0],b[0]),-mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]} +function ld(v,e){return f(v*Math.pow(2,e))} +function refsFrom(buf,n){const dv=new DataView(buf),a=[];for(let i=0;i<=n;i++){const o=i*16;a.push([[dv.getFloat32(o,true),dv.getFloat32(o+4,true)],[dv.getFloat32(o+8,true),dv.getFloat32(o+12,true)]])}return a} +function simulate(snap,w,h,iter,ctx,index,{rebase=false,glitchG=0}={}){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=f(1/w),iwlo=f(1/w-iw),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy),dxh=mul(ox,iw),dxl=mul(ox,iwlo),dyh=mul(oy,iw),dyl=mul(oy,iwlo);let d=[add(mul(mh,dxh),add(mul(mh,dxl),mul(ml,dxh))),add(mul(mh,dyh),add(mul(mh,dyl),mul(ml,dyh)))];let ww=[0,0],scaleExp=se.exp,n=0,m=0,rebases=0,glitches=0,minRatio=Infinity;const refs=refsFrom(ctx.refs,ctx.refLen); + while(true){const rp=refs[Math.min(m,ctx.refLen)],delta=[ld(ww[0],scaleExp),ld(ww[1],scaleExp)],rv=[add(rp[0][0],rp[1][0]),add(rp[0][1],rp[1][1])],z=[add(rp[0][0],add(rp[1][0],delta[0])),add(rp[0][1],add(rp[1][1],delta[1]))],mag=add(mul(z[0],z[0]),mul(z[1],z[1])),rmag=add(mul(rv[0],rv[0]),mul(rv[1],rv[1])),dmag=add(mul(delta[0],delta[0]),mul(delta[1],delta[1]));if(rmag>0)minRatio=Math.min(minRatio,mag/rmag); + if(mag>4){if(glitchG>0&&minRatio=iter)return{kind:'op',n,rebases,glitches,minRatio};if(m>=ctx.refLen)return{kind:'refend',n,rebases,glitches,minRatio}; + if(rebase&&m>0&&mag=1e30)return{kind:'range',n,rebases,glitches,minRatio};if(mm>65536){ww=[mul(ww[0],1/65536),mul(ww[1],1/65536)];d=[mul(d[0],1/65536),mul(d[1],1/65536)];scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=[mul(ww[0],65536),mul(ww[1],65536)];d=[mul(d[0],65536),mul(d[1],65536)];scaleExp-=16}if(scaleExp>126)return{kind:'range',n,rebases,glitches,minRatio};}} +function grid(w,h,nx=16,ny=10){const out=[];for(let j=0;j>>28)&3)===1,q=simulate(snap,1365,768,sel.iter,ctx,indices[k],mode),fast=q.kind==='escape';counts[q.kind]=(counts[q.kind]||0)+1;rebases+=q.rebases||0;minRatio=Math.min(minRatio,q.minRatio??Infinity);if(fast!==exact){mismatch++;if(fast)falseEscape++;else falseNon++;}}rows.push({case:c.name,initial:sel.iter,adaptive:sel.adaptive,refLen:ctx.refLen,selectionEscape:ctx.selectionEscape,refPixel:app.referencePixelForSource(ctx.source,snap,1365,768),mode:mode.name,counts,mismatch,falseEscape,falseNon,rebases,minRatio});}} +console.log(JSON.stringify(rows,null,2)); diff --git a/experiments/root-cause/minibrot_escape_profile.mjs b/experiments/root-cause/minibrot_escape_profile.mjs new file mode 100644 index 0000000..cc1deb7 --- /dev/null +++ b/experiments/root-cause/minibrot_escape_profile.mjs @@ -0,0 +1,6 @@ +import fs from 'node:fs';import vm from 'node:vm'; +const html=fs.readFileSync(new URL('../../index.html',import.meta.url),'utf8');const scripts=[...html.matchAll(/]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]);const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS; +const el={width:1365,height:768,clientWidth:1365,clientHeight:768,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:1365,height:768}}};const c={MANDEL_WEBGPU_KERNELS:kernels,document:{querySelector:()=>el,getElementById:()=>el,documentElement:{classList:{toggle(){}}},body:{classList:{toggle(){}},appendChild(){}}},navigator:{hardwareConcurrency:4,deviceMemory:8},location:{search:'',hash:''},history:{replaceState(){}},performance,URLSearchParams,URL:{createObjectURL:()=>'',revokeObjectURL(){}},TextEncoder,Blob:class{},console,addEventListener(){},requestAnimationFrame:()=>1,cancelAnimationFrame(){},setTimeout,clearTimeout,setInterval,clearInterval,innerWidth:1365,innerHeight:768,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}};const cut=scripts[1].indexOf('// ── boot / teardown');vm.runInNewContext(scripts[1].slice(0,cut)+`\nglobalThis.I={precisionFallbackWorkerSource,pixelPrecisionBits};})();`,c);const I=c.I; +function run(source,data){let r;const v={self:{},postMessage:d=>r=d,performance,console,BigInt,Math,ArrayBuffer,DataView,Float32Array,Uint32Array,Uint8Array};vm.runInNewContext(source,v);v.self.onmessage({data});return r} +function grid(w,h,nx=16,ny=10){let a=[];for(let j=0;j>>28)&3)===1)esc++;out.push({case:q.name,iter,escape:esc,non:idx.length-esc})}}console.log(JSON.stringify(out,null,2)); diff --git a/experiments/root-cause/minibrot_instability.mjs b/experiments/root-cause/minibrot_instability.mjs new file mode 100644 index 0000000..3ea152d --- /dev/null +++ b/experiments/root-cause/minibrot_instability.mjs @@ -0,0 +1,35 @@ +import fs from 'node:fs'; +import vm from 'node:vm'; +const prodHtml=fs.readFileSync(new URL('../../index.html',import.meta.url),'utf8'); +const scripts=[...prodHtml.matchAll(/]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]); +const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS; +function appContext(){ + const element={width:1365,height:768,clientWidth:1365,clientHeight:768,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:1365,height:768}}}; + 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:1365,innerHeight:768,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}}; + const cut=scripts[1].indexOf('// ── boot / teardown'); + const code=scripts[1].slice(0,cut)+`\nglobalThis.internal={state,fastReferenceWorkerSource,referenceWorkerSource,precisionFallbackWorkerSource,fastReferenceBits,pixelPrecisionBits,referencePixelForSource,spanMantExp,adaptiveProbeSources,adaptiveProbeChoice,adaptiveIterationBonus,fixedSourceForPixel,log2FixedAt};})();`; + vm.runInNewContext(code,context);return context.internal; +} +const app=appContext(); +function runWorker(source,data){let result;const c={self:{},postMessage:d=>{result=d},performance,console,BigInt,Math,ArrayBuffer,DataView,Float32Array,Uint32Array,Uint8Array};vm.runInNewContext(source,c);c.self.onmessage({data});if(!result)throw new Error('no result');if(result.type==='error')throw new Error(result.error);return result;} +function maxAdaptive(snap,w){const spanLog2=app.log2FixedAt(snap.span,snap.bits),z=Math.max(0,Math.log10(3.4)-spanLog2/Math.log2(10)),screenBits=Math.max(0,Math.log2(w)+z*Math.LOG2E*Math.LN10),bonus=app.adaptiveIterationBonus(screenBits,1);return Math.min(16384,Math.max(350,350+bonus));} +function initialIter(snap,w,h){const adaptive=maxAdaptive(snap,w),sources=app.adaptiveProbeSources(snap,w,h),targetBits=Math.max(app.pixelPrecisionBits(snap,w,40)+32),scored=runWorker(app.referenceWorkerSource(),{type:'score',id:1,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter:adaptive,candidates:sources.map(c=>({re:c.re.toString(),im:c.im.toString()}))});return {adaptive,...app.adaptiveProbeChoice(scored.escapes||[],adaptive,350),escapes:scored.escapes};} +function buildFast(snap,iter,w,h){const targetBits=app.fastReferenceBits(snap,w),d=runWorker(app.fastReferenceWorkerSource(),{type:'build',id:2,key:'analysis',sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter});d.source={bits:snap.bits,re:BigInt(d.referenceRe),im:BigInt(d.referenceIm),viewRe:snap.re,viewIm:snap.im,span:snap.span,iter};return d;} +function oracle(snap,iter,w,h,indices){const targetBits=app.pixelPrecisionBits(snap,w,40);return runWorker(app.precisionFallbackWorkerSource(),{type:'solve',id:3,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter,terminalClass:-1,indices:Uint32Array.from(indices).buffer});} +const f=Math.fround;const add=(a,b)=>f(f(a)+f(b)),mul=(a,b)=>f(f(a)*f(b));const maxabs=v=>Math.max(Math.abs(v[0]),Math.abs(v[1])); +function cmul(a,b){return [add(mul(a[0],b[0]),-mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]} +function ld(v,e){return f(v*Math.pow(2,e))} +function refsFrom(buf,n){const dv=new DataView(buf),a=[];for(let i=0;i<=n;i++){const o=i*16;a.push([[dv.getFloat32(o,true),dv.getFloat32(o+4,true)],[dv.getFloat32(o+8,true),dv.getFloat32(o+12,true)]])}return a} +function simulate(snap,w,h,iter,ctx,index,{rebase=false,glitchG=0}={}){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=f(1/w),iwlo=f(1/w-iw),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy),dxh=mul(ox,iw),dxl=mul(ox,iwlo),dyh=mul(oy,iw),dyl=mul(oy,iwlo);let d=[add(mul(mh,dxh),add(mul(mh,dxl),mul(ml,dxh))),add(mul(mh,dyh),add(mul(mh,dyl),mul(ml,dyh)))];let ww=[0,0],scaleExp=se.exp,n=0,m=0,rebases=0,glitches=0,minRatio=Infinity;const refs=refsFrom(ctx.refs,ctx.refLen); + while(true){const rp=refs[Math.min(m,ctx.refLen)],delta=[ld(ww[0],scaleExp),ld(ww[1],scaleExp)],rv=[add(rp[0][0],rp[1][0]),add(rp[0][1],rp[1][1])],z=[add(rp[0][0],add(rp[1][0],delta[0])),add(rp[0][1],add(rp[1][1],delta[1]))],mag=add(mul(z[0],z[0]),mul(z[1],z[1])),rmag=add(mul(rv[0],rv[0]),mul(rv[1],rv[1])),dmag=add(mul(delta[0],delta[0]),mul(delta[1],delta[1]));if(rmag>0)minRatio=Math.min(minRatio,mag/rmag); + if(m>0&&glitchG>0&&rmag>0&&mag4)return{kind:'escape',n,rebases,glitches,minRatio};if(n>=iter)return{kind:'op',n,rebases,glitches,minRatio};if(m>=ctx.refLen)return{kind:'refend',n,rebases,glitches,minRatio}; + if(rebase&&m>0&&mag=1e30)return{kind:'range',n,rebases,glitches,minRatio};if(mm>65536){ww=[mul(ww[0],1/65536),mul(ww[1],1/65536)];d=[mul(d[0],1/65536),mul(d[1],1/65536)];scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=[mul(ww[0],65536),mul(ww[1],65536)];d=[mul(d[0],65536),mul(d[1],65536)];scaleExp-=16}if(scaleExp>126)return{kind:'range',n,rebases,glitches,minRatio};}} +function grid(w,h,nx=16,ny=10){const out=[];for(let j=0;j>>28)&3)===1,q=simulate(snap,1365,768,sel.iter,ctx,indices[k],mode),fast=q.kind==='escape';counts[q.kind]=(counts[q.kind]||0)+1;rebases+=q.rebases||0;minRatio=Math.min(minRatio,q.minRatio??Infinity);if(fast!==exact){mismatch++;if(fast)falseEscape++;else falseNon++;}}rows.push({case:c.name,initial:sel.iter,adaptive:sel.adaptive,refLen:ctx.refLen,selectionEscape:ctx.selectionEscape,refPixel:app.referencePixelForSource(ctx.source,snap,1365,768),mode:mode.name,counts,mismatch,falseEscape,falseNon,rebases,minRatio});}} +console.log(JSON.stringify(rows,null,2)); diff --git a/experiments/root-cause/minibrot_multiref.mjs b/experiments/root-cause/minibrot_multiref.mjs new file mode 100644 index 0000000..c7dad5e --- /dev/null +++ b/experiments/root-cause/minibrot_multiref.mjs @@ -0,0 +1,11 @@ +import fs from 'node:fs';import vm from 'node:vm'; +// Reuse most helper code by importing source text from minibrot_instability and evaluating? Simpler: load it and extract impossible. Duplicate minimal. +const html=fs.readFileSync(new URL('../../index.html',import.meta.url),'utf8'),scripts=[...html.matchAll(/]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]);const k={};vm.runInNewContext(scripts[0],k);const kernels=k.MANDEL_WEBGPU_KERNELS; +const e={width:1365,height:768,clientWidth:1365,clientHeight:768,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:1365,height:768}}},c={MANDEL_WEBGPU_KERNELS:kernels,document:{querySelector:()=>e,getElementById:()=>e,documentElement:{classList:{toggle(){}}},body:{classList:{toggle(){}},appendChild(){}}},navigator:{hardwareConcurrency:4,deviceMemory:8},location:{search:'',hash:''},history:{replaceState(){}},performance,URLSearchParams,URL:{createObjectURL:()=>'',revokeObjectURL(){}},TextEncoder,Blob:class{},console,addEventListener(){},requestAnimationFrame:()=>1,cancelAnimationFrame(){},setTimeout,clearTimeout,setInterval,clearInterval,innerWidth:1365,innerHeight:768,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}},cut=scripts[1].indexOf('// ── boot / teardown');vm.runInNewContext(scripts[1].slice(0,cut)+`\nglobalThis.I={referenceWorkerSource,precisionFallbackWorkerSource,pixelPrecisionBits,referencePixelForSource,spanMantExp,fixedSourceForPixel};})();`,c);const I=c.I; +function run(src,data){let r;const v={self:{},postMessage:d=>r=d,performance,console,BigInt,Math,ArrayBuffer,DataView,Float32Array,Uint32Array,Uint8Array};vm.runInNewContext(src,v);v.self.onmessage({data});if(r.type==='error')throw new Error(r.error);return r} +const f=Math.fround,add=(a,b)=>f(f(a)+f(b)),mul=(a,b)=>f(f(a)*f(b)),maxabs=v=>Math.max(Math.abs(v[0]),Math.abs(v[1]));function cmul(a,b){return[add(mul(a[0],b[0]),-mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]}function ld(v,e){return f(v*Math.pow(2,e))}function refsFrom(buf,n){const d=new DataView(buf),a=[];for(let i=0;i<=n;i++){const o=i*16;a.push([[d.getFloat32(o,true),d.getFloat32(o+4,true)],[d.getFloat32(o+8,true),d.getFloat32(o+12,true)]])}return a} +function buildFixed(s,iter,w,h,source){const tb=I.pixelPrecisionBits(s,w,40)+32,r=run(I.referenceWorkerSource(),{type:'build',id:1,key:'x',bits:s.bits,sourceBits:s.bits,targetBits:tb,re:s.re.toString(),im:s.im.toString(),span:s.span.toString(),width:w,height:h,iter,fixedRe:source.re.toString(),fixedIm:source.im.toString()});r.source={...source,bits:s.bits,viewRe:s.re,viewIm:s.im,span:s.span};return r} +function oracle(s,iter,w,h,idx){const tb=I.pixelPrecisionBits(s,w,40),r=run(I.precisionFallbackWorkerSource(),{type:'solve',id:2,bits:s.bits,targetBits:tb,re:s.re.toString(),im:s.im.toString(),span:s.span.toString(),width:w,height:h,iter,terminalClass:-1,indices:Uint32Array.from(idx).buffer});return new Uint32Array(r.meta)} +function sim(s,w,h,iter,ctx,index){const px=index%w,py=Math.floor(index/w),rp=I.referencePixelForSource(ctx.source,s,w,h),se=I.spanMantExp(s),mh=f(se.mant),ml=f(se.mant-mh),iwh=f(1/w),iwl=f(1/w-iwh),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rp.x)),oy=f(f(rp.y)-gy),dxh=mul(ox,iwh),dxl=mul(ox,iwl),dyh=mul(oy,iwh),dyl=mul(oy,iwl);let d=[add(mul(mh,dxh),add(mul(mh,dxl),mul(ml,dxh))),add(mul(mh,dyh),add(mul(mh,dyl),mul(ml,dyh)))],ww=[0,0],exp=se.exp,n=0,m=0,refs=refsFrom(ctx.refs,ctx.refLen);while(true){const rp0=refs[Math.min(m,ctx.refLen)],delta=[ld(ww[0],exp),ld(ww[1],exp)],z=[add(rp0[0][0],add(rp0[1][0],delta[0])),add(rp0[0][1],add(rp0[1][1],delta[1]))],mag=add(mul(z[0],z[0]),mul(z[1],z[1]));if(mag>4)return true;if(n>=iter||m>=ctx.refLen)return false;const l1=cmul(rp0[0],ww),l2=cmul(rp0[1],ww),lin=[f(2*f(l1[0]+l2[0])),f(2*f(l1[1]+l2[1]))],sq0=cmul(ww,ww),sq=[ld(sq0[0],exp),ld(sq0[1],exp)];ww=[add(add(lin[0],sq[0]),d[0]),add(add(lin[1],sq[1]),d[1])];m++;n++;const mm=Math.max(maxabs(ww),maxabs(d));if(!Number.isFinite(mm)||mm>=1e30)return false;if(mm>65536){ww=[mul(ww[0],1/65536),mul(ww[1],1/65536)];d=[mul(d[0],1/65536),mul(d[1],1/65536)];exp+=16}else if(mm>0&&mm<1/65536&&exp>se.exp){ww=[mul(ww[0],65536),mul(ww[1],65536)];d=[mul(d[0],65536),mul(d[1],65536)];exp-=16}}} +function grid(w,h,nx=16,ny=10){let a=[];for(let j=0;jI.fixedSourceForPixel(Math.floor(y)*w+Math.floor(x),c1,w,h)),ctxs=anchors.map(s=>buildFixed(c1,iter,w,h,s));let mis=0,fe=0;for(let i=0;i=w/2?1:0)+(y>=h/2?2:0),got=sim(c1,w,h,iter,ctxs[qi],idx[i]),ex=((om[i]>>>28)&3)===1;if(got!==ex){mis++;if(got)fe++}}console.log(JSON.stringify({mis,fe,refLens:ctxs.map(c=>c.refLen),refPixels:ctxs.map(c=>I.referencePixelForSource(c.source,c1,w,h))},null,2)); diff --git a/experiments/root-cause/minibrot_sel_tmp.mjs b/experiments/root-cause/minibrot_sel_tmp.mjs new file mode 100644 index 0000000..d5796f8 --- /dev/null +++ b/experiments/root-cause/minibrot_sel_tmp.mjs @@ -0,0 +1,35 @@ +import fs from 'node:fs'; +import vm from 'node:vm'; +const prodHtml=fs.readFileSync(new URL('../../index.html',import.meta.url),'utf8'); +const scripts=[...prodHtml.matchAll(/]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]); +const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS; +function appContext(){ + const element={width:1365,height:768,clientWidth:1365,clientHeight:768,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:1365,height:768}}}; + 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:1365,innerHeight:768,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}}; + const cut=scripts[1].indexOf('// ── boot / teardown'); + const code=scripts[1].slice(0,cut)+`\nglobalThis.internal={state,fastReferenceWorkerSource,referenceWorkerSource,precisionFallbackWorkerSource,fastReferenceBits,pixelPrecisionBits,referencePixelForSource,spanMantExp,adaptiveProbeSources,adaptiveProbeChoice,adaptiveIterationBonus,fixedSourceForPixel,log2FixedAt};})();`; + vm.runInNewContext(code,context);return context.internal; +} +const app=appContext(); +function runWorker(source,data){let result;const c={self:{},postMessage:d=>{result=d},performance,console,BigInt,Math,ArrayBuffer,DataView,Float32Array,Uint32Array,Uint8Array};vm.runInNewContext(source,c);c.self.onmessage({data});if(!result)throw new Error('no result');if(result.type==='error')throw new Error(result.error);return result;} +function maxAdaptive(snap,w){const spanLog2=app.log2FixedAt(snap.span,snap.bits),z=Math.max(0,Math.log10(3.4)-spanLog2/Math.log2(10)),screenBits=Math.max(0,Math.log2(w)+z*Math.LOG2E*Math.LN10),bonus=app.adaptiveIterationBonus(screenBits,1);return Math.min(16384,Math.max(350,350+bonus));} +function initialIter(snap,w,h){const adaptive=maxAdaptive(snap,w),sources=app.adaptiveProbeSources(snap,w,h),targetBits=Math.max(app.pixelPrecisionBits(snap,w,40)+32),scored=runWorker(app.referenceWorkerSource(),{type:'score',id:1,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter:adaptive,candidates:sources.map(c=>({re:c.re.toString(),im:c.im.toString()}))});return {adaptive,...app.adaptiveProbeChoice(scored.escapes||[],adaptive,350),escapes:scored.escapes};} +function buildFast(snap,iter,w,h){const targetBits=app.fastReferenceBits(snap,w),d=runWorker(app.fastReferenceWorkerSource(),{type:'build',id:2,key:'analysis',sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter});d.source={bits:snap.bits,re:BigInt(d.referenceRe),im:BigInt(d.referenceIm),viewRe:snap.re,viewIm:snap.im,span:snap.span,iter};return d;} +function oracle(snap,iter,w,h,indices){const targetBits=app.pixelPrecisionBits(snap,w,40);return runWorker(app.precisionFallbackWorkerSource(),{type:'solve',id:3,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter,terminalClass:-1,indices:Uint32Array.from(indices).buffer});} +const f=Math.fround;const add=(a,b)=>f(f(a)+f(b)),mul=(a,b)=>f(f(a)*f(b));const maxabs=v=>Math.max(Math.abs(v[0]),Math.abs(v[1])); +function cmul(a,b){return [add(mul(a[0],b[0]),-mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]} +function ld(v,e){return f(v*Math.pow(2,e))} +function refsFrom(buf,n){const dv=new DataView(buf),a=[];for(let i=0;i<=n;i++){const o=i*16;a.push([[dv.getFloat32(o,true),dv.getFloat32(o+4,true)],[dv.getFloat32(o+8,true),dv.getFloat32(o+12,true)]])}return a} +function simulate(snap,w,h,iter,ctx,index,{rebase=false,glitchG=0}={}){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=f(1/w),iwlo=f(1/w-iw),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy),dxh=mul(ox,iw),dxl=mul(ox,iwlo),dyh=mul(oy,iw),dyl=mul(oy,iwlo);let d=[add(mul(mh,dxh),add(mul(mh,dxl),mul(ml,dxh))),add(mul(mh,dyh),add(mul(mh,dyl),mul(ml,dyh)))];let ww=[0,0],scaleExp=se.exp,n=0,m=0,rebases=0,glitches=0,minRatio=Infinity;const refs=refsFrom(ctx.refs,ctx.refLen); + while(true){const rp=refs[Math.min(m,ctx.refLen)],delta=[ld(ww[0],scaleExp),ld(ww[1],scaleExp)],rv=[add(rp[0][0],rp[1][0]),add(rp[0][1],rp[1][1])],z=[add(rp[0][0],add(rp[1][0],delta[0])),add(rp[0][1],add(rp[1][1],delta[1]))],mag=add(mul(z[0],z[0]),mul(z[1],z[1])),rmag=add(mul(rv[0],rv[0]),mul(rv[1],rv[1])),dmag=add(mul(delta[0],delta[0]),mul(delta[1],delta[1]));if(rmag>0)minRatio=Math.min(minRatio,mag/rmag); + if(m>0&&glitchG>0&&rmag>0&&mag4)return{kind:'escape',n,rebases,glitches,minRatio};if(n>=iter)return{kind:'op',n,rebases,glitches,minRatio};if(m>=ctx.refLen)return{kind:'refend',n,rebases,glitches,minRatio}; + if(rebase&&m>0&&mag=1e30)return{kind:'range',n,rebases,glitches,minRatio};if(mm>65536){ww=[mul(ww[0],1/65536),mul(ww[1],1/65536)];d=[mul(d[0],1/65536),mul(d[1],1/65536)];scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=[mul(ww[0],65536),mul(ww[1],65536)];d=[mul(d[0],65536),mul(d[1],65536)];scaleExp-=16}if(scaleExp>126)return{kind:'range',n,rebases,glitches,minRatio};}} +function grid(w,h,nx=16,ny=10){const out=[];for(let j=0;j>>28)&3)===1,q=simulate(snap,1365,768,sel.iter,ctx,indices[k],mode),fast=q.kind==='escape';counts[q.kind]=(counts[q.kind]||0)+1;rebases+=q.rebases||0;minRatio=Math.min(minRatio,q.minRatio??Infinity);if(fast!==exact){mismatch++;if(fast)falseEscape++;else falseNon++;}}rows.push({case:c.name,initial:sel.iter,adaptive:sel.adaptive,refLen:ctx.refLen,selectionEscape:ctx.selectionEscape,refPixel:app.referencePixelForSource(ctx.source,snap,1365,768),mode:mode.name,counts,mismatch,falseEscape,falseNon,rebases,minRatio});}} +console.log(JSON.stringify(rows,null,2)); diff --git a/experiments/root-cause/minibrot_thresholds.mjs b/experiments/root-cause/minibrot_thresholds.mjs new file mode 100644 index 0000000..e097dea --- /dev/null +++ b/experiments/root-cause/minibrot_thresholds.mjs @@ -0,0 +1,35 @@ +import fs from 'node:fs'; +import vm from 'node:vm'; +const prodHtml=fs.readFileSync(new URL('../../index.html',import.meta.url),'utf8'); +const scripts=[...prodHtml.matchAll(/]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]); +const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS; +function appContext(){ + const element={width:1365,height:768,clientWidth:1365,clientHeight:768,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:1365,height:768}}}; + 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:1365,innerHeight:768,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}}; + const cut=scripts[1].indexOf('// ── boot / teardown'); + const code=scripts[1].slice(0,cut)+`\nglobalThis.internal={state,fastReferenceWorkerSource,referenceWorkerSource,precisionFallbackWorkerSource,fastReferenceBits,pixelPrecisionBits,referencePixelForSource,spanMantExp,adaptiveProbeSources,adaptiveProbeChoice,adaptiveIterationBonus,fixedSourceForPixel,log2FixedAt};})();`; + vm.runInNewContext(code,context);return context.internal; +} +const app=appContext(); +function runWorker(source,data){let result;const c={self:{},postMessage:d=>{result=d},performance,console,BigInt,Math,ArrayBuffer,DataView,Float32Array,Uint32Array,Uint8Array};vm.runInNewContext(source,c);c.self.onmessage({data});if(!result)throw new Error('no result');if(result.type==='error')throw new Error(result.error);return result;} +function maxAdaptive(snap,w){const spanLog2=app.log2FixedAt(snap.span,snap.bits),z=Math.max(0,Math.log10(3.4)-spanLog2/Math.log2(10)),screenBits=Math.max(0,Math.log2(w)+z*Math.LOG2E*Math.LN10),bonus=app.adaptiveIterationBonus(screenBits,1);return Math.min(16384,Math.max(350,350+bonus));} +function initialIter(snap,w,h){const adaptive=maxAdaptive(snap,w),sources=app.adaptiveProbeSources(snap,w,h),targetBits=Math.max(app.pixelPrecisionBits(snap,w,40)+32),scored=runWorker(app.referenceWorkerSource(),{type:'score',id:1,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter:adaptive,candidates:sources.map(c=>({re:c.re.toString(),im:c.im.toString()}))});return {adaptive,...app.adaptiveProbeChoice(scored.escapes||[],adaptive,350),escapes:scored.escapes};} +function buildFast(snap,iter,w,h){const targetBits=app.fastReferenceBits(snap,w),d=runWorker(app.fastReferenceWorkerSource(),{type:'build',id:2,key:'analysis',sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter});d.source={bits:snap.bits,re:BigInt(d.referenceRe),im:BigInt(d.referenceIm),viewRe:snap.re,viewIm:snap.im,span:snap.span,iter};return d;} +function oracle(snap,iter,w,h,indices){const targetBits=app.pixelPrecisionBits(snap,w,40);return runWorker(app.precisionFallbackWorkerSource(),{type:'solve',id:3,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter,terminalClass:-1,indices:Uint32Array.from(indices).buffer});} +const f=Math.fround;const add=(a,b)=>f(f(a)+f(b)),mul=(a,b)=>f(f(a)*f(b));const maxabs=v=>Math.max(Math.abs(v[0]),Math.abs(v[1])); +function cmul(a,b){return [add(mul(a[0],b[0]),-mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]} +function ld(v,e){return f(v*Math.pow(2,e))} +function refsFrom(buf,n){const dv=new DataView(buf),a=[];for(let i=0;i<=n;i++){const o=i*16;a.push([[dv.getFloat32(o,true),dv.getFloat32(o+4,true)],[dv.getFloat32(o+8,true),dv.getFloat32(o+12,true)]])}return a} +function simulate(snap,w,h,iter,ctx,index,{rebase=false,glitchG=0}={}){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=f(1/w),iwlo=f(1/w-iw),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy),dxh=mul(ox,iw),dxl=mul(ox,iwlo),dyh=mul(oy,iw),dyl=mul(oy,iwlo);let d=[add(mul(mh,dxh),add(mul(mh,dxl),mul(ml,dxh))),add(mul(mh,dyh),add(mul(mh,dyl),mul(ml,dyh)))];let ww=[0,0],scaleExp=se.exp,n=0,m=0,rebases=0,glitches=0,minRatio=Infinity;const refs=refsFrom(ctx.refs,ctx.refLen); + while(true){const rp=refs[Math.min(m,ctx.refLen)],delta=[ld(ww[0],scaleExp),ld(ww[1],scaleExp)],rv=[add(rp[0][0],rp[1][0]),add(rp[0][1],rp[1][1])],z=[add(rp[0][0],add(rp[1][0],delta[0])),add(rp[0][1],add(rp[1][1],delta[1]))],mag=add(mul(z[0],z[0]),mul(z[1],z[1])),rmag=add(mul(rv[0],rv[0]),mul(rv[1],rv[1])),dmag=add(mul(delta[0],delta[0]),mul(delta[1],delta[1]));if(rmag>0)minRatio=Math.min(minRatio,mag/rmag); + if(m>0&&glitchG>0&&rmag>0&&mag4)return{kind:'escape',n,rebases,glitches,minRatio};if(n>=iter)return{kind:'op',n,rebases,glitches,minRatio};if(m>=ctx.refLen)return{kind:'refend',n,rebases,glitches,minRatio}; + if(rebase&&m>0&&mag=1e30)return{kind:'range',n,rebases,glitches,minRatio};if(mm>65536){ww=[mul(ww[0],1/65536),mul(ww[1],1/65536)];d=[mul(d[0],1/65536),mul(d[1],1/65536)];scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=[mul(ww[0],65536),mul(ww[1],65536)];d=[mul(d[0],65536),mul(d[1],65536)];scaleExp-=16}if(scaleExp>126)return{kind:'range',n,rebases,glitches,minRatio};}} +function grid(w,h,nx=16,ny=10){const out=[];for(let j=0;j>>28)&3)===1,q=simulate(snap,1365,768,sel.iter,ctx,indices[k],mode),fast=q.kind==='escape';counts[q.kind]=(counts[q.kind]||0)+1;rebases+=q.rebases||0;minRatio=Math.min(minRatio,q.minRatio??Infinity);if(fast!==exact){mismatch++;if(fast)falseEscape++;else falseNon++;}}rows.push({case:c.name,initial:sel.iter,adaptive:sel.adaptive,refLen:ctx.refLen,selectionEscape:ctx.selectionEscape,refPixel:app.referencePixelForSource(ctx.source,snap,1365,768),mode:mode.name,counts,mismatch,falseEscape,falseNon,rebases,minRatio});}} +console.log(JSON.stringify(rows,null,2)); diff --git a/experiments/root-cause/nearby_reuse_effect.mjs b/experiments/root-cause/nearby_reuse_effect.mjs new file mode 100644 index 0000000..2c805d6 --- /dev/null +++ b/experiments/root-cause/nearby_reuse_effect.mjs @@ -0,0 +1,35 @@ +import fs from 'node:fs'; +import vm from 'node:vm'; +const prodHtml=fs.readFileSync(new URL('../../index.html',import.meta.url),'utf8'); +const scripts=[...prodHtml.matchAll(/]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]); +const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS; +function appContext(){ + const element={width:1365,height:768,clientWidth:1365,clientHeight:768,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:1365,height:768}}}; + 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:1365,innerHeight:768,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}}; + const cut=scripts[1].indexOf('// ── boot / teardown'); + const code=scripts[1].slice(0,cut)+`\nglobalThis.internal={state,fastReferenceWorkerSource,referenceWorkerSource,precisionFallbackWorkerSource,fastReferenceBits,pixelPrecisionBits,referencePixelForSource,spanMantExp,adaptiveProbeSources,adaptiveProbeChoice,adaptiveIterationBonus,fixedSourceForPixel,log2FixedAt};})();`; + vm.runInNewContext(code,context);return context.internal; +} +const app=appContext(); +function runWorker(source,data){let result;const c={self:{},postMessage:d=>{result=d},performance,console,BigInt,Math,ArrayBuffer,DataView,Float32Array,Uint32Array,Uint8Array};vm.runInNewContext(source,c);c.self.onmessage({data});if(!result)throw new Error('no result');if(result.type==='error')throw new Error(result.error);return result;} +function maxAdaptive(snap,w){const spanLog2=app.log2FixedAt(snap.span,snap.bits),z=Math.max(0,Math.log10(3.4)-spanLog2/Math.log2(10)),screenBits=Math.max(0,Math.log2(w)+z*Math.LOG2E*Math.LN10),bonus=app.adaptiveIterationBonus(screenBits,1);return Math.min(16384,Math.max(350,350+bonus));} +function initialIter(snap,w,h){const adaptive=maxAdaptive(snap,w),sources=app.adaptiveProbeSources(snap,w,h),targetBits=Math.max(app.pixelPrecisionBits(snap,w,40)+32),scored=runWorker(app.referenceWorkerSource(),{type:'score',id:1,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter:adaptive,candidates:sources.map(c=>({re:c.re.toString(),im:c.im.toString()}))});return {adaptive,...app.adaptiveProbeChoice(scored.escapes||[],adaptive,350),escapes:scored.escapes};} +function buildFast(snap,iter,w,h){const targetBits=app.fastReferenceBits(snap,w),d=runWorker(app.fastReferenceWorkerSource(),{type:'build',id:2,key:'analysis',sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter});d.source={bits:snap.bits,re:BigInt(d.referenceRe),im:BigInt(d.referenceIm),viewRe:snap.re,viewIm:snap.im,span:snap.span,iter};return d;} +function oracle(snap,iter,w,h,indices){const targetBits=app.pixelPrecisionBits(snap,w,40);return runWorker(app.precisionFallbackWorkerSource(),{type:'solve',id:3,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter,terminalClass:-1,indices:Uint32Array.from(indices).buffer});} +const f=Math.fround;const add=(a,b)=>f(f(a)+f(b)),mul=(a,b)=>f(f(a)*f(b));const maxabs=v=>Math.max(Math.abs(v[0]),Math.abs(v[1])); +function cmul(a,b){return [add(mul(a[0],b[0]),-mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]} +function ld(v,e){return f(v*Math.pow(2,e))} +function refsFrom(buf,n){const dv=new DataView(buf),a=[];for(let i=0;i<=n;i++){const o=i*16;a.push([[dv.getFloat32(o,true),dv.getFloat32(o+4,true)],[dv.getFloat32(o+8,true),dv.getFloat32(o+12,true)]])}return a} +function simulate(snap,w,h,iter,ctx,index,{rebase=false,glitchG=0}={}){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=f(1/w),iwlo=f(1/w-iw),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy),dxh=mul(ox,iw),dxl=mul(ox,iwlo),dyh=mul(oy,iw),dyl=mul(oy,iwlo);let d=[add(mul(mh,dxh),add(mul(mh,dxl),mul(ml,dxh))),add(mul(mh,dyh),add(mul(mh,dyl),mul(ml,dyh)))];let ww=[0,0],scaleExp=se.exp,n=0,m=0,rebases=0,glitches=0,minRatio=Infinity;const refs=refsFrom(ctx.refs,ctx.refLen); + while(true){const rp=refs[Math.min(m,ctx.refLen)],delta=[ld(ww[0],scaleExp),ld(ww[1],scaleExp)],rv=[add(rp[0][0],rp[1][0]),add(rp[0][1],rp[1][1])],z=[add(rp[0][0],add(rp[1][0],delta[0])),add(rp[0][1],add(rp[1][1],delta[1]))],mag=add(mul(z[0],z[0]),mul(z[1],z[1])),rmag=add(mul(rv[0],rv[0]),mul(rv[1],rv[1])),dmag=add(mul(delta[0],delta[0]),mul(delta[1],delta[1]));if(rmag>0)minRatio=Math.min(minRatio,mag/rmag); + if(m>0&&glitchG>0&&rmag>0&&mag4)return{kind:'escape',n,rebases,glitches,minRatio};if(n>=iter)return{kind:'op',n,rebases,glitches,minRatio};if(m>=ctx.refLen)return{kind:'refend',n,rebases,glitches,minRatio}; + if(rebase&&m>0&&mag=1e30)return{kind:'range',n,rebases,glitches,minRatio};if(mm>65536){ww=[mul(ww[0],1/65536),mul(ww[1],1/65536)];d=[mul(d[0],1/65536),mul(d[1],1/65536)];scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=[mul(ww[0],65536),mul(ww[1],65536)];d=[mul(d[0],65536),mul(d[1],65536)];scaleExp-=16}if(scaleExp>126)return{kind:'range',n,rebases,glitches,minRatio};}} +function grid(w,h,nx=16,ny=10){const out=[];for(let j=0;j>>28)&3)===1,q=simulate(s,w,h,sel.iter,ctx,idx[i],{}),got=q.kind==='escape';kinds[q.kind]=(kinds[q.kind]||0)+1;if(got!==ex){mis++;got?fe++:fn++}}out.push({case:c.name,shift:[fx,fy],iter:sel.iter,refLen:ctx.refLen,refPixel:app.referencePixelForSource(ctx.source,s,w,h),mis,fe,fn,kinds})}}console.log(JSON.stringify(out,null,2)); diff --git a/experiments/root-cause/newcases.mjs b/experiments/root-cause/newcases.mjs new file mode 100644 index 0000000..667c722 --- /dev/null +++ b/experiments/root-cause/newcases.mjs @@ -0,0 +1,61 @@ +import fs from 'node:fs'; +import vm from 'node:vm'; + +const prodHtml=fs.readFileSync(new URL('../../index.html',import.meta.url),'utf8'); +const scripts=[...prodHtml.matchAll(/]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]); +const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS; +function appContext(){ + const element={width:800,height:600,clientWidth:800,clientHeight:600,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:800,height:600}}}; + 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:800,innerHeight:600,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}}; + const cut=scripts[1].indexOf('// ── boot / teardown'); + const code=scripts[1].slice(0,cut)+`\nglobalThis.internal={state,fastReferenceWorkerSource,precisionFallbackWorkerSource,fastReferenceBits,pixelPrecisionBits,referencePixelForSource,spanMantExp,adaptiveProbeChoice};})();`; + vm.runInNewContext(code,context);return context.internal; +} +const app=appContext(); +function runWorker(source,data){let result;const c={self:{},postMessage:d=>{result=d},performance,console,BigInt,Math,ArrayBuffer,DataView,Float32Array,Uint32Array,Uint8Array};vm.runInNewContext(source,c);c.self.onmessage({data});if(!result)throw new Error('worker returned no result');if(result.type==='error')throw new Error(result.error);return result;} +function buildFast(snap,iter,w,h){const targetBits=app.fastReferenceBits(snap,w),d=runWorker(app.fastReferenceWorkerSource(),{type:'build',id:1,key:'analysis',sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter});d.source={bits:snap.bits,re:BigInt(d.referenceRe),im:BigInt(d.referenceIm),viewRe:snap.re,viewIm:snap.im,span:snap.span,iter};return d;} +function oracle(snap,iter,w,h,indices){const targetBits=app.pixelPrecisionBits(snap,w,40);return runWorker(app.precisionFallbackWorkerSource(),{type:'solve',id:2,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter,terminalClass:-1,indices:Uint32Array.from(indices).buffer});} +const f=Math.fround,U=5.960464477539063e-8; +const add=(a,b)=>f(f(a)+f(b)), mul=(a,b)=>f(f(a)*f(b)); +function cmul(a,b){return [add(mul(a[0],b[0]),-mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]} +const maxabs=v=>Math.max(Math.abs(v[0]),Math.abs(v[1])); +function ld(v,e){return f(v*Math.pow(2,e))} +function refsFrom(buf,n){const dv=new DataView(buf),a=[];for(let i=0;i<=n;i++){const o=i*16;a.push([[dv.getFloat32(o,true),dv.getFloat32(o+4,true)],[dv.getFloat32(o+8,true),dv.getFloat32(o+12,true)]])}return a} +function dsQuick(a,b){const q=f(a+b),e=f(b-f(q-a));return [q,e]} +function dsSum(a,b){const q=f(a+b),bb=f(q-a),e=f(f(a-f(q-bb))+f(b-bb));return [q,e]} +function dsProd(a,b){const q=f(a*b),ca=f(4097*a),ah=f(ca-f(ca-a)),al=f(a-ah),cb=f(4097*b),bh=f(cb-f(cb-b)),bl=f(b-bh);let e=f(f(ah*bh)-q);e=f(e+f(ah*bl));e=f(e+f(al*bh));e=f(e+f(al*bl));return [q,e]} +function dsAdd(a,b){const q=dsSum(a[0],b[0]);return dsQuick(q[0],f(q[1]+f(a[1]+b[1])))} +const dsNeg=a=>[-a[0],-a[1]],dsSub=(a,b)=>dsAdd(a,dsNeg(b)); +function dsMul(a,b){const q=dsProd(a[0],b[0]);let e=f(q[1]+f(a[0]*b[1]));e=f(e+f(a[1]*b[0]));e=f(e+f(a[1]*b[1]));return dsQuick(q[0],e)} +function dsScale(a,b){const q=dsProd(a[0],b);return dsQuick(q[0],f(q[1]+f(a[1]*b)))} +const dsPow2=(a,e)=>[ld(a[0],e),ld(a[1],e)],dsVal=a=>f(a[0]+a[1]); +const cdsAdd=(a,b)=>[dsAdd(a[0],b[0]),dsAdd(a[1],b[1])]; +const cdsMul=(a,b)=>[dsSub(dsMul(a[0],b[0]),dsMul(a[1],b[1])),dsAdd(dsMul(a[0],b[1]),dsMul(a[1],b[0]))]; +const cdsScale=(a,b)=>[dsScale(a[0],b),dsScale(a[1],b)],cdsPow2=(a,e)=>[dsPow2(a[0],e),dsPow2(a[1],e)]; +const cdsMag2=a=>dsAdd(dsMul(a[0],a[0]),dsMul(a[1],a[1])),cdsMax=a=>Math.max(Math.abs(dsVal(a[0])),Math.abs(dsVal(a[1]))); +function simulateDS(snap,w,h,iter,ctx,index){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=1/w,iwh=f(iw),iwl=f(iw-iwh),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy),inv=[iwh,iwl],sm=[mh,ml],dx=dsScale(inv,ox),dy=dsScale(inv,oy),d0=[dsMul(sm,dx),dsMul(sm,dy)];let d=d0,ww=[[0,0],[0,0]],scaleExp=se.exp,n=0,m=0;const refs=refsFrom(ctx.refs,ctx.refLen);while(true){const rp=refs[m],r=[[rp[0][0],rp[1][0]],[rp[0][1],rp[1][1]]],delta=cdsPow2(ww,scaleExp),z=cdsAdd(r,delta),mag=cdsMag2(z);if(mag[0]>4||(mag[0]===4&&mag[1]>0))return{kind:'escape',n};if(n>=iter)return{kind:'op',n};if(m>=ctx.refLen)return{kind:'refend',n};const linear=cdsScale(cdsMul(r,ww),2),sq=cdsPow2(cdsMul(ww,ww),scaleExp);ww=cdsAdd(cdsAdd(linear,sq),d);m++;n++;const mm=Math.max(cdsMax(ww),cdsMax(d));if(!Number.isFinite(mm)||mm>=1e30)return{kind:'range',n};if(mm>65536){ww=cdsScale(ww,1/65536);d=cdsScale(d,1/65536);scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=cdsScale(ww,65536);d=cdsScale(d,65536);scaleExp-=16}if(scaleExp>126)return{kind:'range',n};}} +function simulate(snap,w,h,iter,ctx,index,guarded){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=1/w,iwh=f(iw),iwl=f(iw-iwh),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy);let d; + if(guarded){const dxh=mul(ox,iwh),dxl=mul(ox,iwl),dyh=mul(oy,iwh),dyl=mul(oy,iwl);d=[add(mul(mh,dxh),add(mul(mh,dxl),mul(ml,dxh))),add(mul(mh,dyh),add(mul(mh,dyl),mul(ml,dyh)))];} + else {const dx=f(ox/f(w)),dy=f(oy/f(w));d=[mul(mh,dx),mul(mh,dy)];} + let ww=[0,0],scaleExp=se.exp,n=0,m=0,err=4*U*(maxabs(d)+1e-30);const refs=refsFrom(ctx.refs,ctx.refLen); + while(true){const rp=refs[Math.min(m,ctx.refLen)],delta=[ld(ww[0],scaleExp),ld(ww[1],scaleExp)],z=[add(rp[0][0],add(rp[1][0],delta[0])),add(rp[0][1],add(rp[1][1],delta[1]))],mag=add(mul(z[0],z[0]),mul(z[1],z[1])); + if(mag>4){if(!guarded)return{kind:'escape',n};const errAbs=Math.abs(ld(err,scaleExp))+U*(maxabs(z)+maxabs(delta)+1e-30);if(Math.hypot(z[0],z[1])-errAbs>2)return{kind:'escape',n};return{kind:'uncertain',n};} + if(n>=iter)return{kind:'op',n};if(m>=ctx.refLen)return{kind:'refend',n}; + const refAbs=maxabs(rp[0])+maxabs(rp[1]),wAbs=maxabs(ww),dAbs=maxabs(d),deltaAbs=maxabs(delta),ww2=cmul(ww,ww),sq=[ld(ww2[0],scaleExp),ld(ww2[1],scaleExp)],sqAbs=maxabs(sq),gain=2*refAbs+2*deltaAbs,roundErr=U*(2*refAbs*wAbs+sqAbs+dAbs+1e-30);if(guarded)err=f(gain*err+roundErr); + const l1=cmul(rp[0],ww),l2=cmul(rp[1],ww),linear=[f(2*f(l1[0]+l2[0])),f(2*f(l1[1]+l2[1]))];ww=[add(add(linear[0],sq[0]),d[0]),add(add(linear[1],sq[1]),d[1])];m++;n++;const mm=Math.max(maxabs(ww),maxabs(d));if(!Number.isFinite(mm)||mm>=1e30||guarded&&(!Number.isFinite(err)||err>1e35))return{kind:'range',n};if(mm>65536){ww=[mul(ww[0],1/65536),mul(ww[1],1/65536)];d=[mul(d[0],1/65536),mul(d[1],1/65536)];if(guarded)err=f(err/65536);scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=[mul(ww[0],65536),mul(ww[1],65536)];d=[mul(d[0],65536),mul(d[1],65536)];if(guarded)err=f(err*65536);scaleExp-=16}if(scaleExp>126)return{kind:'range',n}; + } +} +function grid(w,h,nx=8,ny=5){const out=[];for(let j=0;j>>0,cls=(m>>>28)&3,n=m&0xfffff;if(cls===1){esc++;ns.push(n)}else op++;}row.budgets[iter]={escape:esc,nonEscape:op,minEscape:ns.length?Math.min(...ns):null,maxEscape:ns.length?Math.max(...ns):null};} +const ctx=buildFast(snap,1536,w,h);let counts={escape:0,op:0,refend:0,range:0};for(const idx of indices){const q=simulate(snap,w,h,1536,ctx,idx,false);counts[q.kind]=(counts[q.kind]||0)+1;}row.fast1536={refLen:ctx.refLen,referencePixel:app.referencePixelForSource(ctx.source,snap,w,h),counts};results.push(row)}} +console.log(JSON.stringify({results},null,2)); diff --git a/experiments/root-cause/primary_refs.mjs b/experiments/root-cause/primary_refs.mjs new file mode 100644 index 0000000..fe24530 --- /dev/null +++ b/experiments/root-cause/primary_refs.mjs @@ -0,0 +1,56 @@ +import fs from 'node:fs'; +import vm from 'node:vm'; + +const prodHtml=fs.readFileSync(new URL('../../index.html',import.meta.url),'utf8'); +const scripts=[...prodHtml.matchAll(/]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]); +const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS; +function appContext(){ + const element={width:800,height:600,clientWidth:800,clientHeight:600,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:800,height:600}}}; + 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:800,innerHeight:600,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}}; + const cut=scripts[1].indexOf('// ── boot / teardown'); + const code=scripts[1].slice(0,cut)+`\nglobalThis.internal={state,fastReferenceWorkerSource,precisionFallbackWorkerSource,fastReferenceBits,pixelPrecisionBits,referencePixelForSource,spanMantExp,adaptiveProbeChoice};})();`; + vm.runInNewContext(code,context);return context.internal; +} +const app=appContext(); +function runWorker(source,data){let result;const c={self:{},postMessage:d=>{result=d},performance,console,BigInt,Math,ArrayBuffer,DataView,Float32Array,Uint32Array,Uint8Array};vm.runInNewContext(source,c);c.self.onmessage({data});if(!result)throw new Error('worker returned no result');if(result.type==='error')throw new Error(result.error);return result;} +function buildFast(snap,iter,w,h){const targetBits=app.fastReferenceBits(snap,w),d=runWorker(app.fastReferenceWorkerSource(),{type:'build',id:1,key:'analysis',sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter});d.source={bits:snap.bits,re:BigInt(d.referenceRe),im:BigInt(d.referenceIm),viewRe:snap.re,viewIm:snap.im,span:snap.span,iter};return d;} +function oracle(snap,iter,w,h,indices){const targetBits=app.pixelPrecisionBits(snap,w,40);return runWorker(app.precisionFallbackWorkerSource(),{type:'solve',id:2,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter,terminalClass:-1,indices:Uint32Array.from(indices).buffer});} +const f=Math.fround,U=5.960464477539063e-8; +const add=(a,b)=>f(f(a)+f(b)), mul=(a,b)=>f(f(a)*f(b)); +function cmul(a,b){return [add(mul(a[0],b[0]),-mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]} +const maxabs=v=>Math.max(Math.abs(v[0]),Math.abs(v[1])); +function ld(v,e){return f(v*Math.pow(2,e))} +function refsFrom(buf,n){const dv=new DataView(buf),a=[];for(let i=0;i<=n;i++){const o=i*16;a.push([[dv.getFloat32(o,true),dv.getFloat32(o+4,true)],[dv.getFloat32(o+8,true),dv.getFloat32(o+12,true)]])}return a} +function dsQuick(a,b){const q=f(a+b),e=f(b-f(q-a));return [q,e]} +function dsSum(a,b){const q=f(a+b),bb=f(q-a),e=f(f(a-f(q-bb))+f(b-bb));return [q,e]} +function dsProd(a,b){const q=f(a*b),ca=f(4097*a),ah=f(ca-f(ca-a)),al=f(a-ah),cb=f(4097*b),bh=f(cb-f(cb-b)),bl=f(b-bh);let e=f(f(ah*bh)-q);e=f(e+f(ah*bl));e=f(e+f(al*bh));e=f(e+f(al*bl));return [q,e]} +function dsAdd(a,b){const q=dsSum(a[0],b[0]);return dsQuick(q[0],f(q[1]+f(a[1]+b[1])))} +const dsNeg=a=>[-a[0],-a[1]],dsSub=(a,b)=>dsAdd(a,dsNeg(b)); +function dsMul(a,b){const q=dsProd(a[0],b[0]);let e=f(q[1]+f(a[0]*b[1]));e=f(e+f(a[1]*b[0]));e=f(e+f(a[1]*b[1]));return dsQuick(q[0],e)} +function dsScale(a,b){const q=dsProd(a[0],b);return dsQuick(q[0],f(q[1]+f(a[1]*b)))} +const dsPow2=(a,e)=>[ld(a[0],e),ld(a[1],e)],dsVal=a=>f(a[0]+a[1]); +const cdsAdd=(a,b)=>[dsAdd(a[0],b[0]),dsAdd(a[1],b[1])]; +const cdsMul=(a,b)=>[dsSub(dsMul(a[0],b[0]),dsMul(a[1],b[1])),dsAdd(dsMul(a[0],b[1]),dsMul(a[1],b[0]))]; +const cdsScale=(a,b)=>[dsScale(a[0],b),dsScale(a[1],b)],cdsPow2=(a,e)=>[dsPow2(a[0],e),dsPow2(a[1],e)]; +const cdsMag2=a=>dsAdd(dsMul(a[0],a[0]),dsMul(a[1],a[1])),cdsMax=a=>Math.max(Math.abs(dsVal(a[0])),Math.abs(dsVal(a[1]))); +function simulateDS(snap,w,h,iter,ctx,index){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=1/w,iwh=f(iw),iwl=f(iw-iwh),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy),inv=[iwh,iwl],sm=[mh,ml],dx=dsScale(inv,ox),dy=dsScale(inv,oy),d0=[dsMul(sm,dx),dsMul(sm,dy)];let d=d0,ww=[[0,0],[0,0]],scaleExp=se.exp,n=0,m=0;const refs=refsFrom(ctx.refs,ctx.refLen);while(true){const rp=refs[m],r=[[rp[0][0],rp[1][0]],[rp[0][1],rp[1][1]]],delta=cdsPow2(ww,scaleExp),z=cdsAdd(r,delta),mag=cdsMag2(z);if(mag[0]>4||(mag[0]===4&&mag[1]>0))return{kind:'escape',n};if(n>=iter)return{kind:'op',n};if(m>=ctx.refLen)return{kind:'refend',n};const linear=cdsScale(cdsMul(r,ww),2),sq=cdsPow2(cdsMul(ww,ww),scaleExp);ww=cdsAdd(cdsAdd(linear,sq),d);m++;n++;const mm=Math.max(cdsMax(ww),cdsMax(d));if(!Number.isFinite(mm)||mm>=1e30)return{kind:'range',n};if(mm>65536){ww=cdsScale(ww,1/65536);d=cdsScale(d,1/65536);scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=cdsScale(ww,65536);d=cdsScale(d,65536);scaleExp-=16}if(scaleExp>126)return{kind:'range',n};}} +function simulate(snap,w,h,iter,ctx,index,guarded){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=1/w,iwh=f(iw),iwl=f(iw-iwh),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy);let d; + if(guarded){const dxh=mul(ox,iwh),dxl=mul(ox,iwl),dyh=mul(oy,iwh),dyl=mul(oy,iwl);d=[add(mul(mh,dxh),add(mul(mh,dxl),mul(ml,dxh))),add(mul(mh,dyh),add(mul(mh,dyl),mul(ml,dyh)))];} + else {const dx=f(ox/f(w)),dy=f(oy/f(w));d=[mul(mh,dx),mul(mh,dy)];} + let ww=[0,0],scaleExp=se.exp,n=0,m=0,err=4*U*(maxabs(d)+1e-30);const refs=refsFrom(ctx.refs,ctx.refLen); + while(true){const rp=refs[Math.min(m,ctx.refLen)],delta=[ld(ww[0],scaleExp),ld(ww[1],scaleExp)],z=[add(rp[0][0],add(rp[1][0],delta[0])),add(rp[0][1],add(rp[1][1],delta[1]))],mag=add(mul(z[0],z[0]),mul(z[1],z[1])); + if(mag>4){if(!guarded)return{kind:'escape',n};const errAbs=Math.abs(ld(err,scaleExp))+U*(maxabs(z)+maxabs(delta)+1e-30);if(Math.hypot(z[0],z[1])-errAbs>2)return{kind:'escape',n};return{kind:'uncertain',n};} + if(n>=iter)return{kind:'op',n};if(m>=ctx.refLen)return{kind:'refend',n}; + const refAbs=maxabs(rp[0])+maxabs(rp[1]),wAbs=maxabs(ww),dAbs=maxabs(d),deltaAbs=maxabs(delta),ww2=cmul(ww,ww),sq=[ld(ww2[0],scaleExp),ld(ww2[1],scaleExp)],sqAbs=maxabs(sq),gain=2*refAbs+2*deltaAbs,roundErr=U*(2*refAbs*wAbs+sqAbs+dAbs+1e-30);if(guarded)err=f(gain*err+roundErr); + const l1=cmul(rp[0],ww),l2=cmul(rp[1],ww),linear=[f(2*f(l1[0]+l2[0])),f(2*f(l1[1]+l2[1]))];ww=[add(add(linear[0],sq[0]),d[0]),add(add(linear[1],sq[1]),d[1])];m++;n++;const mm=Math.max(maxabs(ww),maxabs(d));if(!Number.isFinite(mm)||mm>=1e30||guarded&&(!Number.isFinite(err)||err>1e35))return{kind:'range',n};if(mm>65536){ww=[mul(ww[0],1/65536),mul(ww[1],1/65536)];d=[mul(d[0],1/65536),mul(d[1],1/65536)];if(guarded)err=f(err/65536);scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=[mul(ww[0],65536),mul(ww[1],65536)];d=[mul(d[0],65536),mul(d[1],65536)];if(guarded)err=f(err*65536);scaleExp-=16}if(scaleExp>126)return{kind:'range',n}; + } +} +function grid(w,h,nx=8,ny=5){const out=[];for(let j=0;j]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]); +const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS; +function appContext(){ + const element={width:800,height:600,clientWidth:800,clientHeight:600,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:800,height:600}}}; + 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:800,innerHeight:600,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}}; + const cut=scripts[1].indexOf('// ── boot / teardown'); + const code=scripts[1].slice(0,cut)+`\nglobalThis.internal={state,fastReferenceWorkerSource,precisionFallbackWorkerSource,fastReferenceBits,pixelPrecisionBits,referencePixelForSource,spanMantExp,adaptiveProbeChoice};})();`; + vm.runInNewContext(code,context);return context.internal; +} +const app=appContext(); +function runWorker(source,data){let result;const c={self:{},postMessage:d=>{result=d},performance,console,BigInt,Math,ArrayBuffer,DataView,Float32Array,Uint32Array,Uint8Array};vm.runInNewContext(source,c);c.self.onmessage({data});if(!result)throw new Error('worker returned no result');if(result.type==='error')throw new Error(result.error);return result;} +function buildFast(snap,iter,w,h){const targetBits=app.fastReferenceBits(snap,w),d=runWorker(app.fastReferenceWorkerSource(),{type:'build',id:1,key:'analysis',sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter});d.source={bits:snap.bits,re:BigInt(d.referenceRe),im:BigInt(d.referenceIm),viewRe:snap.re,viewIm:snap.im,span:snap.span,iter};return d;} +function oracle(snap,iter,w,h,indices){const targetBits=app.pixelPrecisionBits(snap,w,40);return runWorker(app.precisionFallbackWorkerSource(),{type:'solve',id:2,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter,terminalClass:-1,indices:Uint32Array.from(indices).buffer});} +const f=Math.fround,U=5.960464477539063e-8; +const add=(a,b)=>f(f(a)+f(b)), mul=(a,b)=>f(f(a)*f(b)); +function cmul(a,b){return [add(mul(a[0],b[0]),-mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]} +const maxabs=v=>Math.max(Math.abs(v[0]),Math.abs(v[1])); +function ld(v,e){return f(v*Math.pow(2,e))} +function refsFrom(buf,n){const dv=new DataView(buf),a=[];for(let i=0;i<=n;i++){const o=i*16;a.push([[dv.getFloat32(o,true),dv.getFloat32(o+4,true)],[dv.getFloat32(o+8,true),dv.getFloat32(o+12,true)]])}return a} +function dsQuick(a,b){const q=f(a+b),e=f(b-f(q-a));return [q,e]} +function dsSum(a,b){const q=f(a+b),bb=f(q-a),e=f(f(a-f(q-bb))+f(b-bb));return [q,e]} +function dsProd(a,b){const q=f(a*b),ca=f(4097*a),ah=f(ca-f(ca-a)),al=f(a-ah),cb=f(4097*b),bh=f(cb-f(cb-b)),bl=f(b-bh);let e=f(f(ah*bh)-q);e=f(e+f(ah*bl));e=f(e+f(al*bh));e=f(e+f(al*bl));return [q,e]} +function dsAdd(a,b){const q=dsSum(a[0],b[0]);return dsQuick(q[0],f(q[1]+f(a[1]+b[1])))} +const dsNeg=a=>[-a[0],-a[1]],dsSub=(a,b)=>dsAdd(a,dsNeg(b)); +function dsMul(a,b){const q=dsProd(a[0],b[0]);let e=f(q[1]+f(a[0]*b[1]));e=f(e+f(a[1]*b[0]));e=f(e+f(a[1]*b[1]));return dsQuick(q[0],e)} +function dsScale(a,b){const q=dsProd(a[0],b);return dsQuick(q[0],f(q[1]+f(a[1]*b)))} +const dsPow2=(a,e)=>[ld(a[0],e),ld(a[1],e)],dsVal=a=>f(a[0]+a[1]); +const cdsAdd=(a,b)=>[dsAdd(a[0],b[0]),dsAdd(a[1],b[1])]; +const cdsMul=(a,b)=>[dsSub(dsMul(a[0],b[0]),dsMul(a[1],b[1])),dsAdd(dsMul(a[0],b[1]),dsMul(a[1],b[0]))]; +const cdsScale=(a,b)=>[dsScale(a[0],b),dsScale(a[1],b)],cdsPow2=(a,e)=>[dsPow2(a[0],e),dsPow2(a[1],e)]; +const cdsMag2=a=>dsAdd(dsMul(a[0],a[0]),dsMul(a[1],a[1])),cdsMax=a=>Math.max(Math.abs(dsVal(a[0])),Math.abs(dsVal(a[1]))); +function simulateDS(snap,w,h,iter,ctx,index){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=1/w,iwh=f(iw),iwl=f(iw-iwh),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy),inv=[iwh,iwl],sm=[mh,ml],dx=dsScale(inv,ox),dy=dsScale(inv,oy),d0=[dsMul(sm,dx),dsMul(sm,dy)];let d=d0,ww=[[0,0],[0,0]],scaleExp=se.exp,n=0,m=0;const refs=refsFrom(ctx.refs,ctx.refLen);while(true){const rp=refs[m],r=[[rp[0][0],rp[1][0]],[rp[0][1],rp[1][1]]],delta=cdsPow2(ww,scaleExp),z=cdsAdd(r,delta),mag=cdsMag2(z);if(mag[0]>4||(mag[0]===4&&mag[1]>0))return{kind:'escape',n};if(n>=iter)return{kind:'op',n};if(m>=ctx.refLen)return{kind:'refend',n};const linear=cdsScale(cdsMul(r,ww),2),sq=cdsPow2(cdsMul(ww,ww),scaleExp);ww=cdsAdd(cdsAdd(linear,sq),d);m++;n++;const mm=Math.max(cdsMax(ww),cdsMax(d));if(!Number.isFinite(mm)||mm>=1e30)return{kind:'range',n};if(mm>65536){ww=cdsScale(ww,1/65536);d=cdsScale(d,1/65536);scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=cdsScale(ww,65536);d=cdsScale(d,65536);scaleExp-=16}if(scaleExp>126)return{kind:'range',n};}} +function simulate(snap,w,h,iter,ctx,index,guarded){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=1/w,iwh=f(iw),iwl=f(iw-iwh),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy);let d; + if(guarded){const dxh=mul(ox,iwh),dxl=mul(ox,iwl),dyh=mul(oy,iwh),dyl=mul(oy,iwl);d=[add(mul(mh,dxh),add(mul(mh,dxl),mul(ml,dxh))),add(mul(mh,dyh),add(mul(mh,dyl),mul(ml,dyh)))];} + else {const dx=f(ox/f(w)),dy=f(oy/f(w));d=[mul(mh,dx),mul(mh,dy)];} + let ww=[0,0],scaleExp=se.exp,n=0,m=0,err=4*U*(maxabs(d)+1e-30);const refs=refsFrom(ctx.refs,ctx.refLen); + while(true){const rp=refs[Math.min(m,ctx.refLen)],delta=[ld(ww[0],scaleExp),ld(ww[1],scaleExp)],z=[add(rp[0][0],add(rp[1][0],delta[0])),add(rp[0][1],add(rp[1][1],delta[1]))],mag=add(mul(z[0],z[0]),mul(z[1],z[1])); + if(mag>4){if(!guarded)return{kind:'escape',n};const errAbs=Math.abs(ld(err,scaleExp))+U*(maxabs(z)+maxabs(delta)+1e-30);if(Math.hypot(z[0],z[1])-errAbs>2)return{kind:'escape',n};return{kind:'uncertain',n};} + if(n>=iter)return{kind:'op',n};if(m>=ctx.refLen)return{kind:'refend',n}; + const refAbs=maxabs(rp[0])+maxabs(rp[1]),wAbs=maxabs(ww),dAbs=maxabs(d),deltaAbs=maxabs(delta),ww2=cmul(ww,ww),sq=[ld(ww2[0],scaleExp),ld(ww2[1],scaleExp)],sqAbs=maxabs(sq),gain=2*refAbs+2*deltaAbs,roundErr=U*(2*refAbs*wAbs+sqAbs+dAbs+1e-30);if(guarded)err=f(gain*err+roundErr); + const l1=cmul(rp[0],ww),l2=cmul(rp[1],ww),linear=[f(2*f(l1[0]+l2[0])),f(2*f(l1[1]+l2[1]))];ww=[add(add(linear[0],sq[0]),d[0]),add(add(linear[1],sq[1]),d[1])];m++;n++;const mm=Math.max(maxabs(ww),maxabs(d));if(!Number.isFinite(mm)||mm>=1e30||guarded&&(!Number.isFinite(err)||err>1e35))return{kind:'range',n};if(mm>65536){ww=[mul(ww[0],1/65536),mul(ww[1],1/65536)];d=[mul(d[0],1/65536),mul(d[1],1/65536)];if(guarded)err=f(err/65536);scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=[mul(ww[0],65536),mul(ww[1],65536)];d=[mul(d[0],65536),mul(d[1],65536)];if(guarded)err=f(err*65536);scaleExp-=16}if(scaleExp>126)return{kind:'range',n}; + } +} +function grid(w,h,nx=8,ny=5){const out=[];for(let j=0;j>>0,cls=(m>>>28)&3;esc.push(cls===1?(m&0xfffff):0)}const choice=app.adaptiveProbeChoice(esc,adaptive,350);rows.push({case:c.name,adaptive,escapes:esc,choice})} +console.log(JSON.stringify(rows,null,2)); \ No newline at end of file diff --git a/experiments/root-cause/rebase_age_test.tmp.mjs b/experiments/root-cause/rebase_age_test.tmp.mjs new file mode 100644 index 0000000..36c8c3e --- /dev/null +++ b/experiments/root-cause/rebase_age_test.tmp.mjs @@ -0,0 +1,45 @@ +import fs from 'node:fs'; +import vm from 'node:vm'; +const prodHtml=fs.readFileSync(new URL('../../index.html',import.meta.url),'utf8'); +const scripts=[...prodHtml.matchAll(/]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]); +const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS; +function appContext(){ + const element={width:1365,height:768,clientWidth:1365,clientHeight:768,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:1365,height:768}}}; + 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:1365,innerHeight:768,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}}; + const cut=scripts[1].indexOf('// ── boot / teardown'); + const code=scripts[1].slice(0,cut)+`\nglobalThis.internal={state,fastReferenceWorkerSource,referenceWorkerSource,precisionFallbackWorkerSource,fastReferenceBits,pixelPrecisionBits,referencePixelForSource,spanMantExp,adaptiveProbeSources,adaptiveProbeChoice,adaptiveIterationBonus,fixedSourceForPixel,log2FixedAt};})();`; + vm.runInNewContext(code,context);return context.internal; +} +const app=appContext(); +function runWorker(source,data){let result;const c={self:{},postMessage:d=>{result=d},performance,console,BigInt,Math,ArrayBuffer,DataView,Float32Array,Uint32Array,Uint8Array};vm.runInNewContext(source,c);c.self.onmessage({data});if(!result)throw new Error('no result');if(result.type==='error')throw new Error(result.error);return result;} +function maxAdaptive(snap,w){const spanLog2=app.log2FixedAt(snap.span,snap.bits),z=Math.max(0,Math.log10(3.4)-spanLog2/Math.log2(10)),screenBits=Math.max(0,Math.log2(w)+z*Math.LOG2E*Math.LN10),bonus=app.adaptiveIterationBonus(screenBits,1);return Math.min(16384,Math.max(350,350+bonus));} +function initialIter(snap,w,h){const adaptive=maxAdaptive(snap,w),sources=app.adaptiveProbeSources(snap,w,h),targetBits=Math.max(app.pixelPrecisionBits(snap,w,40)+32),scored=runWorker(app.referenceWorkerSource(),{type:'score',id:1,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter:adaptive,candidates:sources.map(c=>({re:c.re.toString(),im:c.im.toString()}))});return {adaptive,...app.adaptiveProbeChoice(scored.escapes||[],adaptive,350),escapes:scored.escapes};} +function buildFast(snap,iter,w,h){const targetBits=app.fastReferenceBits(snap,w),d=runWorker(app.fastReferenceWorkerSource(),{type:'build',id:2,key:'analysis',sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter});d.source={bits:snap.bits,re:BigInt(d.referenceRe),im:BigInt(d.referenceIm),viewRe:snap.re,viewIm:snap.im,span:snap.span,iter};return d;} +function oracle(snap,iter,w,h,indices){const targetBits=app.pixelPrecisionBits(snap,w,40);return runWorker(app.precisionFallbackWorkerSource(),{type:'solve',id:3,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter,terminalClass:-1,indices:Uint32Array.from(indices).buffer});} +const f=Math.fround;const add=(a,b)=>f(f(a)+f(b)),mul=(a,b)=>f(f(a)*f(b));const maxabs=v=>Math.max(Math.abs(v[0]),Math.abs(v[1])); +function cmul(a,b){return [add(mul(a[0],b[0]),-mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]} +function ld(v,e){return f(v*Math.pow(2,e))} +function refsFrom(buf,n){const dv=new DataView(buf),a=[];for(let i=0;i<=n;i++){const o=i*16;a.push([[dv.getFloat32(o,true),dv.getFloat32(o+4,true)],[dv.getFloat32(o+8,true),dv.getFloat32(o+12,true)]])}return a} +function simulate(snap,w,h,iter,ctx,index,{rebase=false,glitchG=0}={}){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=f(1/w),iwlo=f(1/w-iw),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy),dxh=mul(ox,iw),dxl=mul(ox,iwlo),dyh=mul(oy,iw),dyl=mul(oy,iwlo);let d=[add(mul(mh,dxh),add(mul(mh,dxl),mul(ml,dxh))),add(mul(mh,dyh),add(mul(mh,dyl),mul(ml,dyh)))];let ww=[0,0],scaleExp=se.exp,n=0,m=0,rebases=0,glitches=0,minRatio=Infinity;const refs=refsFrom(ctx.refs,ctx.refLen); + let lastRiskN=-999999;let riskCount=0;while(true){const rp=refs[Math.min(m,ctx.refLen)],delta=[ld(ww[0],scaleExp),ld(ww[1],scaleExp)],rv=[add(rp[0][0],rp[1][0]),add(rp[0][1],rp[1][1])],z=[add(rp[0][0],add(rp[1][0],delta[0])),add(rp[0][1],add(rp[1][1],delta[1]))],mag=add(mul(z[0],z[0]),mul(z[1],z[1])),rmag=add(mul(rv[0],rv[0]),mul(rv[1],rv[1])),dmag=add(mul(delta[0],delta[0]),mul(delta[1],delta[1]));if(m>0&&Number.isFinite(mag)&&Number.isFinite(dmag)&&mag0)minRatio=Math.min(minRatio,mag/rmag); + if(m>0&&glitchG>0&&rmag>0&&mag4)return{kind:'escape',n,rebases,glitches,minRatio,lastRiskN,riskCount,riskAge:n-lastRiskN,currentRisk:(m>0&&mag=iter)return{kind:'op',n,rebases,glitches,minRatio};if(m>=ctx.refLen)return{kind:'refend',n,rebases,glitches,minRatio}; + if(rebase&&m>0&&mag=1e30)return{kind:'range',n,rebases,glitches,minRatio};if(mm>65536){ww=[mul(ww[0],1/65536),mul(ww[1],1/65536)];d=[mul(d[0],1/65536),mul(d[1],1/65536)];scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=[mul(ww[0],65536),mul(ww[1],65536)];d=[mul(d[0],65536),mul(d[1],65536)];scaleExp-=16}if(scaleExp>126)return{kind:'range',n,rebases,glitches,minRatio};}} +function grid(w,h,nx=16,ny=10){const out=[];for(let j=0;j>>28)&3)===1,q=simulate(snap,w,h,sel.iter,ctx,indices[k],{}); + if(q.kind==='escape')esc.push({idx:indices[k],exact,...q}); + } + const ages=esc.map(x=>x.riskAge).sort((a,b)=>a-b); + rows.push({case:c.name,initial:sel.iter,escapes:esc.length,falseEscapes:esc.filter(x=>!x.exact),currentRisk:esc.filter(x=>x.currentRisk).length,ageLe1:esc.filter(x=>x.riskAge<=1).length,ageLe2:esc.filter(x=>x.riskAge<=2).length,ageLe4:esc.filter(x=>x.riskAge<=4).length,ageLe8:esc.filter(x=>x.riskAge<=8).length,ageLe16:esc.filter(x=>x.riskAge<=16).length,ageLe32:esc.filter(x=>x.riskAge<=32).length,falseDetails:esc.filter(x=>!x.exact)}); +} +console.log(JSON.stringify(rows,null,2)); diff --git a/experiments/root-cause/rebase_count_test.tmp.mjs b/experiments/root-cause/rebase_count_test.tmp.mjs new file mode 100644 index 0000000..fda994c --- /dev/null +++ b/experiments/root-cause/rebase_count_test.tmp.mjs @@ -0,0 +1,34 @@ +import fs from 'node:fs'; +import vm from 'node:vm'; +const prodHtml=fs.readFileSync(new URL('../../index.html',import.meta.url),'utf8'); +const scripts=[...prodHtml.matchAll(/]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]); +const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS; +function appContext(){ + const element={width:1365,height:768,clientWidth:1365,clientHeight:768,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:1365,height:768}}}; + 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:1365,innerHeight:768,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}}; + const cut=scripts[1].indexOf('// ── boot / teardown'); + const code=scripts[1].slice(0,cut)+`\nglobalThis.internal={state,fastReferenceWorkerSource,referenceWorkerSource,precisionFallbackWorkerSource,fastReferenceBits,pixelPrecisionBits,referencePixelForSource,spanMantExp,adaptiveProbeSources,adaptiveProbeChoice,adaptiveIterationBonus,fixedSourceForPixel,log2FixedAt};})();`; + vm.runInNewContext(code,context);return context.internal; +} +const app=appContext(); +function runWorker(source,data){let result;const c={self:{},postMessage:d=>{result=d},performance,console,BigInt,Math,ArrayBuffer,DataView,Float32Array,Uint32Array,Uint8Array};vm.runInNewContext(source,c);c.self.onmessage({data});if(!result)throw new Error('no result');if(result.type==='error')throw new Error(result.error);return result;} +function maxAdaptive(snap,w){const spanLog2=app.log2FixedAt(snap.span,snap.bits),z=Math.max(0,Math.log10(3.4)-spanLog2/Math.log2(10)),screenBits=Math.max(0,Math.log2(w)+z*Math.LOG2E*Math.LN10),bonus=app.adaptiveIterationBonus(screenBits,1);return Math.min(16384,Math.max(350,350+bonus));} +function initialIter(snap,w,h){const adaptive=maxAdaptive(snap,w),sources=app.adaptiveProbeSources(snap,w,h),targetBits=Math.max(app.pixelPrecisionBits(snap,w,40)+32),scored=runWorker(app.referenceWorkerSource(),{type:'score',id:1,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter:adaptive,candidates:sources.map(c=>({re:c.re.toString(),im:c.im.toString()}))});return {adaptive,...app.adaptiveProbeChoice(scored.escapes||[],adaptive,350),escapes:scored.escapes};} +function buildFast(snap,iter,w,h){const targetBits=app.fastReferenceBits(snap,w),d=runWorker(app.fastReferenceWorkerSource(),{type:'build',id:2,key:'analysis',sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter});d.source={bits:snap.bits,re:BigInt(d.referenceRe),im:BigInt(d.referenceIm),viewRe:snap.re,viewIm:snap.im,span:snap.span,iter};return d;} +function oracle(snap,iter,w,h,indices){const targetBits=app.pixelPrecisionBits(snap,w,40);return runWorker(app.precisionFallbackWorkerSource(),{type:'solve',id:3,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter,terminalClass:-1,indices:Uint32Array.from(indices).buffer});} +const f=Math.fround;const add=(a,b)=>f(f(a)+f(b)),mul=(a,b)=>f(f(a)*f(b));const maxabs=v=>Math.max(Math.abs(v[0]),Math.abs(v[1])); +function cmul(a,b){return [add(mul(a[0],b[0]),-mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]} +function ld(v,e){return f(v*Math.pow(2,e))} +function refsFrom(buf,n){const dv=new DataView(buf),a=[];for(let i=0;i<=n;i++){const o=i*16;a.push([[dv.getFloat32(o,true),dv.getFloat32(o+4,true)],[dv.getFloat32(o+8,true),dv.getFloat32(o+12,true)]])}return a} +function simulate(snap,w,h,iter,ctx,index,{rebase=false,glitchG=0}={}){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=f(1/w),iwlo=f(1/w-iw),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy),dxh=mul(ox,iw),dxl=mul(ox,iwlo),dyh=mul(oy,iw),dyl=mul(oy,iwlo);let d=[add(mul(mh,dxh),add(mul(mh,dxl),mul(ml,dxh))),add(mul(mh,dyh),add(mul(mh,dyl),mul(ml,dyh)))];let ww=[0,0],scaleExp=se.exp,n=0,m=0,rebases=0,glitches=0,minRatio=Infinity;const refs=refsFrom(ctx.refs,ctx.refLen); + while(true){const rp=refs[Math.min(m,ctx.refLen)],delta=[ld(ww[0],scaleExp),ld(ww[1],scaleExp)],rv=[add(rp[0][0],rp[1][0]),add(rp[0][1],rp[1][1])],z=[add(rp[0][0],add(rp[1][0],delta[0])),add(rp[0][1],add(rp[1][1],delta[1]))],mag=add(mul(z[0],z[0]),mul(z[1],z[1])),rmag=add(mul(rv[0],rv[0]),mul(rv[1],rv[1])),dmag=add(mul(delta[0],delta[0]),mul(delta[1],delta[1]));if(rmag>0)minRatio=Math.min(minRatio,mag/rmag); + if(m>0&&glitchG>0&&rmag>0&&mag4)return{kind:'escape',n,rebases,glitches,minRatio};if(n>=iter)return{kind:'op',n,rebases,glitches,minRatio};if(m>=ctx.refLen)return{kind:'refend',n,rebases,glitches,minRatio}; + if(rebase&&m>0&&mag=1e30)return{kind:'range',n,rebases,glitches,minRatio};if(mm>65536){ww=[mul(ww[0],1/65536),mul(ww[1],1/65536)];d=[mul(d[0],1/65536),mul(d[1],1/65536)];scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=[mul(ww[0],65536),mul(ww[1],65536)];d=[mul(d[0],65536),mul(d[1],65536)];scaleExp-=16}if(scaleExp>126)return{kind:'range',n,rebases,glitches,minRatio};}} +function grid(w,h,nx=16,ny=10){const out=[];for(let j=0;j>>28)&3)===1,q=simulate(snap,w,h,sel.iter,ctx,indices[k],{rebase:true});if(q.kind==='escape')esc.push({...q,exact:ex});}const dist={};for(const q of esc)dist[q.rebases]=(dist[q.rebases]||0)+1;const thresholds=[];for(const t of [1,2,3,4,5,6,7,8,9,10,12,16,20,32]){const a=esc.filter(q=>q.rebases>=t);thresholds.push({t,count:a.length,falseCaught:a.filter(q=>!q.exact).length});}rows.push({case:c.name,initial:sel.iter,escapes:esc.length,falseEscapes:esc.filter(q=>!q.exact).length,dist,thresholds});}console.log(JSON.stringify(rows,null,2)); diff --git a/experiments/root-cause/rebase_false_detail.tmp.mjs b/experiments/root-cause/rebase_false_detail.tmp.mjs new file mode 100644 index 0000000..e88a9ea --- /dev/null +++ b/experiments/root-cause/rebase_false_detail.tmp.mjs @@ -0,0 +1,36 @@ +import fs from 'node:fs'; +import vm from 'node:vm'; +const prodHtml=fs.readFileSync(new URL('../../index.html',import.meta.url),'utf8'); +const scripts=[...prodHtml.matchAll(/]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]); +const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS; +function appContext(){ + const element={width:1365,height:768,clientWidth:1365,clientHeight:768,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:1365,height:768}}}; + 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:1365,innerHeight:768,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}}; + const cut=scripts[1].indexOf('// ── boot / teardown'); + const code=scripts[1].slice(0,cut)+`\nglobalThis.internal={state,fastReferenceWorkerSource,referenceWorkerSource,precisionFallbackWorkerSource,fastReferenceBits,pixelPrecisionBits,referencePixelForSource,spanMantExp,adaptiveProbeSources,adaptiveProbeChoice,adaptiveIterationBonus,fixedSourceForPixel,log2FixedAt};})();`; + vm.runInNewContext(code,context);return context.internal; +} +const app=appContext(); +function runWorker(source,data){let result;const c={self:{},postMessage:d=>{result=d},performance,console,BigInt,Math,ArrayBuffer,DataView,Float32Array,Uint32Array,Uint8Array};vm.runInNewContext(source,c);c.self.onmessage({data});if(!result)throw new Error('no result');if(result.type==='error')throw new Error(result.error);return result;} +function maxAdaptive(snap,w){const spanLog2=app.log2FixedAt(snap.span,snap.bits),z=Math.max(0,Math.log10(3.4)-spanLog2/Math.log2(10)),screenBits=Math.max(0,Math.log2(w)+z*Math.LOG2E*Math.LN10),bonus=app.adaptiveIterationBonus(screenBits,1);return Math.min(16384,Math.max(350,350+bonus));} +function initialIter(snap,w,h){const adaptive=maxAdaptive(snap,w),sources=app.adaptiveProbeSources(snap,w,h),targetBits=Math.max(app.pixelPrecisionBits(snap,w,40)+32),scored=runWorker(app.referenceWorkerSource(),{type:'score',id:1,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter:adaptive,candidates:sources.map(c=>({re:c.re.toString(),im:c.im.toString()}))});return {adaptive,...app.adaptiveProbeChoice(scored.escapes||[],adaptive,350),escapes:scored.escapes};} +function buildFast(snap,iter,w,h){const targetBits=app.fastReferenceBits(snap,w),d=runWorker(app.fastReferenceWorkerSource(),{type:'build',id:2,key:'analysis',sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter});d.source={bits:snap.bits,re:BigInt(d.referenceRe),im:BigInt(d.referenceIm),viewRe:snap.re,viewIm:snap.im,span:snap.span,iter};return d;} +function oracle(snap,iter,w,h,indices){const targetBits=app.pixelPrecisionBits(snap,w,40);return runWorker(app.precisionFallbackWorkerSource(),{type:'solve',id:3,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter,terminalClass:-1,indices:Uint32Array.from(indices).buffer});} +const f=Math.fround;const add=(a,b)=>f(f(a)+f(b)),mul=(a,b)=>f(f(a)*f(b));const maxabs=v=>Math.max(Math.abs(v[0]),Math.abs(v[1])); +function cmul(a,b){return [add(mul(a[0],b[0]),-mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]} +function ld(v,e){return f(v*Math.pow(2,e))} +function refsFrom(buf,n){const dv=new DataView(buf),a=[];for(let i=0;i<=n;i++){const o=i*16;a.push([[dv.getFloat32(o,true),dv.getFloat32(o+4,true)],[dv.getFloat32(o+8,true),dv.getFloat32(o+12,true)]])}return a} +function simulate(snap,w,h,iter,ctx,index,{rebase=false,glitchG=0}={}){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=f(1/w),iwlo=f(1/w-iw),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy),dxh=mul(ox,iw),dxl=mul(ox,iwlo),dyh=mul(oy,iw),dyl=mul(oy,iwlo);let d=[add(mul(mh,dxh),add(mul(mh,dxl),mul(ml,dxh))),add(mul(mh,dyh),add(mul(mh,dyl),mul(ml,dyh)))];let ww=[0,0],scaleExp=se.exp,n=0,m=0,rebases=0,glitches=0,minRatio=Infinity;const refs=refsFrom(ctx.refs,ctx.refLen); + while(true){const rp=refs[Math.min(m,ctx.refLen)],delta=[ld(ww[0],scaleExp),ld(ww[1],scaleExp)],rv=[add(rp[0][0],rp[1][0]),add(rp[0][1],rp[1][1])],z=[add(rp[0][0],add(rp[1][0],delta[0])),add(rp[0][1],add(rp[1][1],delta[1]))],mag=add(mul(z[0],z[0]),mul(z[1],z[1])),rmag=add(mul(rv[0],rv[0]),mul(rv[1],rv[1])),dmag=add(mul(delta[0],delta[0]),mul(delta[1],delta[1]));if(rmag>0)minRatio=Math.min(minRatio,mag/rmag); + if(m>0&&glitchG>0&&rmag>0&&mag4)return{kind:'escape',n,m,rebases,glitches,minRatio,currentRisk:(m>0&&mag=iter)return{kind:'op',n,rebases,glitches,minRatio};if(m>=ctx.refLen)return{kind:'refend',n,rebases,glitches,minRatio}; + if(rebase&&m>0&&mag=1e30)return{kind:'range',n,rebases,glitches,minRatio};if(mm>65536){ww=[mul(ww[0],1/65536),mul(ww[1],1/65536)];d=[mul(d[0],1/65536),mul(d[1],1/65536)];scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=[mul(ww[0],65536),mul(ww[1],65536)];d=[mul(d[0],65536),mul(d[1],65536)];scaleExp-=16}if(scaleExp>126)return{kind:'range',n,rebases,glitches,minRatio};}} +function grid(w,h,nx=16,ny=10){const out=[];for(let j=0;j>>28)&3)===1,q=simulate(snap,w,h,sel.iter,ctx,indices[k],{rebase:true});const fast=q.kind==='escape';if(fast!==exact){mismatch++;details.push({index:indices[k],x:indices[k]%w,y:Math.floor(indices[k]/w),exact,q});}}rows.push({case:c.name,initial:sel.iter,mismatch,details});} +console.log(JSON.stringify(rows,null,2)); diff --git a/experiments/root-cause/rebase_risk_corpus.tmp.mjs b/experiments/root-cause/rebase_risk_corpus.tmp.mjs new file mode 100644 index 0000000..dca6aa3 --- /dev/null +++ b/experiments/root-cause/rebase_risk_corpus.tmp.mjs @@ -0,0 +1,38 @@ +import fs from 'node:fs'; +import vm from 'node:vm'; +const prodHtml=fs.readFileSync(new URL('../../index.html',import.meta.url),'utf8'); +const scripts=[...prodHtml.matchAll(/]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]); +const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS; +function appContext(){ + const element={width:1365,height:768,clientWidth:1365,clientHeight:768,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:1365,height:768}}}; + 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:1365,innerHeight:768,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}}; + const cut=scripts[1].indexOf('// ── boot / teardown'); + const code=scripts[1].slice(0,cut)+`\nglobalThis.internal={state,fastReferenceWorkerSource,referenceWorkerSource,precisionFallbackWorkerSource,fastReferenceBits,pixelPrecisionBits,referencePixelForSource,spanMantExp,adaptiveProbeSources,adaptiveProbeChoice,adaptiveIterationBonus,fixedSourceForPixel,log2FixedAt};})();`; + vm.runInNewContext(code,context);return context.internal; +} +const app=appContext(); +function runWorker(source,data){let result;const c={self:{},postMessage:d=>{result=d},performance,console,BigInt,Math,ArrayBuffer,DataView,Float32Array,Uint32Array,Uint8Array};vm.runInNewContext(source,c);c.self.onmessage({data});if(!result)throw new Error('no result');if(result.type==='error')throw new Error(result.error);return result;} +function maxAdaptive(snap,w){const spanLog2=app.log2FixedAt(snap.span,snap.bits),z=Math.max(0,Math.log10(3.4)-spanLog2/Math.log2(10)),screenBits=Math.max(0,Math.log2(w)+z*Math.LOG2E*Math.LN10),bonus=app.adaptiveIterationBonus(screenBits,1);return Math.min(16384,Math.max(350,350+bonus));} +function initialIter(snap,w,h){const adaptive=maxAdaptive(snap,w),sources=app.adaptiveProbeSources(snap,w,h),targetBits=Math.max(app.pixelPrecisionBits(snap,w,40)+32),scored=runWorker(app.referenceWorkerSource(),{type:'score',id:1,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter:adaptive,candidates:sources.map(c=>({re:c.re.toString(),im:c.im.toString()}))});return {adaptive,...app.adaptiveProbeChoice(scored.escapes||[],adaptive,350),escapes:scored.escapes};} +function buildFast(snap,iter,w,h){const targetBits=app.fastReferenceBits(snap,w),d=runWorker(app.fastReferenceWorkerSource(),{type:'build',id:2,key:'analysis',sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter});d.source={bits:snap.bits,re:BigInt(d.referenceRe),im:BigInt(d.referenceIm),viewRe:snap.re,viewIm:snap.im,span:snap.span,iter};return d;} +function oracle(snap,iter,w,h,indices){const targetBits=app.pixelPrecisionBits(snap,w,40);return runWorker(app.precisionFallbackWorkerSource(),{type:'solve',id:3,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter,terminalClass:-1,indices:Uint32Array.from(indices).buffer});} +const f=Math.fround;const add=(a,b)=>f(f(a)+f(b)),mul=(a,b)=>f(f(a)*f(b));const maxabs=v=>Math.max(Math.abs(v[0]),Math.abs(v[1])); +function cmul(a,b){return [add(mul(a[0],b[0]),-mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]} +function ld(v,e){return f(v*Math.pow(2,e))} +function refsFrom(buf,n){const dv=new DataView(buf),a=[];for(let i=0;i<=n;i++){const o=i*16;a.push([[dv.getFloat32(o,true),dv.getFloat32(o+4,true)],[dv.getFloat32(o+8,true),dv.getFloat32(o+12,true)]])}return a} +function simulate(snap,w,h,iter,ctx,index,{rebase=false,glitchG=0}={}){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=f(1/w),iwlo=f(1/w-iw),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy),dxh=mul(ox,iw),dxl=mul(ox,iwlo),dyh=mul(oy,iw),dyl=mul(oy,iwlo);let d=[add(mul(mh,dxh),add(mul(mh,dxl),mul(ml,dxh))),add(mul(mh,dyh),add(mul(mh,dyl),mul(ml,dyh)))];let ww=[0,0],scaleExp=se.exp,n=0,m=0,rebases=0,glitches=0,minRatio=Infinity;const refs=refsFrom(ctx.refs,ctx.refLen); + let rebaseRisk=false;while(true){const rp=refs[Math.min(m,ctx.refLen)],delta=[ld(ww[0],scaleExp),ld(ww[1],scaleExp)],rv=[add(rp[0][0],rp[1][0]),add(rp[0][1],rp[1][1])],z=[add(rp[0][0],add(rp[1][0],delta[0])),add(rp[0][1],add(rp[1][1],delta[1]))],mag=add(mul(z[0],z[0]),mul(z[1],z[1])),rmag=add(mul(rv[0],rv[0]),mul(rv[1],rv[1])),dmag=add(mul(delta[0],delta[0]),mul(delta[1],delta[1]));if(m>0&&Number.isFinite(mag)&&Number.isFinite(dmag)&&mag0)minRatio=Math.min(minRatio,mag/rmag); + if(m>0&&glitchG>0&&rmag>0&&mag4)return{kind:'escape',n,rebases,glitches,minRatio,rebaseRisk};if(n>=iter)return{kind:'op',n,rebases,glitches,minRatio,rebaseRisk};if(m>=ctx.refLen)return{kind:'refend',n,rebases,glitches,minRatio,rebaseRisk}; + if(rebase&&m>0&&mag=1e30)return{kind:'range',n,rebases,glitches,minRatio,rebaseRisk};if(mm>65536){ww=[mul(ww[0],1/65536),mul(ww[1],1/65536)];d=[mul(d[0],1/65536),mul(d[1],1/65536)];scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=[mul(ww[0],65536),mul(ww[1],65536)];d=[mul(d[0],65536),mul(d[1],65536)];scaleExp-=16}if(scaleExp>126)return{kind:'range',n,rebases,glitches,minRatio,rebaseRisk};}} +function grid(w,h,nx=16,ny=10){const out=[];for(let j=0;j]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]); +const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS; +function appContext(){ + const element={width:1365,height:768,clientWidth:1365,clientHeight:768,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:1365,height:768}}}; + 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:1365,innerHeight:768,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}}; + const cut=scripts[1].indexOf('// ── boot / teardown'); + const code=scripts[1].slice(0,cut)+`\nglobalThis.internal={state,fastReferenceWorkerSource,referenceWorkerSource,precisionFallbackWorkerSource,fastReferenceBits,pixelPrecisionBits,referencePixelForSource,spanMantExp,adaptiveProbeSources,adaptiveProbeChoice,adaptiveIterationBonus,fixedSourceForPixel,log2FixedAt};})();`; + vm.runInNewContext(code,context);return context.internal; +} +const app=appContext(); +function runWorker(source,data){let result;const c={self:{},postMessage:d=>{result=d},performance,console,BigInt,Math,ArrayBuffer,DataView,Float32Array,Uint32Array,Uint8Array};vm.runInNewContext(source,c);c.self.onmessage({data});if(!result)throw new Error('no result');if(result.type==='error')throw new Error(result.error);return result;} +function maxAdaptive(snap,w){const spanLog2=app.log2FixedAt(snap.span,snap.bits),z=Math.max(0,Math.log10(3.4)-spanLog2/Math.log2(10)),screenBits=Math.max(0,Math.log2(w)+z*Math.LOG2E*Math.LN10),bonus=app.adaptiveIterationBonus(screenBits,1);return Math.min(16384,Math.max(350,350+bonus));} +function initialIter(snap,w,h){const adaptive=maxAdaptive(snap,w),sources=app.adaptiveProbeSources(snap,w,h),targetBits=Math.max(app.pixelPrecisionBits(snap,w,40)+32),scored=runWorker(app.referenceWorkerSource(),{type:'score',id:1,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter:adaptive,candidates:sources.map(c=>({re:c.re.toString(),im:c.im.toString()}))});return {adaptive,...app.adaptiveProbeChoice(scored.escapes||[],adaptive,350),escapes:scored.escapes};} +function buildFast(snap,iter,w,h){const targetBits=app.fastReferenceBits(snap,w),d=runWorker(app.fastReferenceWorkerSource(),{type:'build',id:2,key:'analysis',sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter});d.source={bits:snap.bits,re:BigInt(d.referenceRe),im:BigInt(d.referenceIm),viewRe:snap.re,viewIm:snap.im,span:snap.span,iter};return d;} +function oracle(snap,iter,w,h,indices){const targetBits=app.pixelPrecisionBits(snap,w,40);return runWorker(app.precisionFallbackWorkerSource(),{type:'solve',id:3,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter,terminalClass:-1,indices:Uint32Array.from(indices).buffer});} +const f=Math.fround;const add=(a,b)=>f(f(a)+f(b)),mul=(a,b)=>f(f(a)*f(b));const maxabs=v=>Math.max(Math.abs(v[0]),Math.abs(v[1])); +function cmul(a,b){return [add(mul(a[0],b[0]),-mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]} +function ld(v,e){return f(v*Math.pow(2,e))} +function refsFrom(buf,n){const dv=new DataView(buf),a=[];for(let i=0;i<=n;i++){const o=i*16;a.push([[dv.getFloat32(o,true),dv.getFloat32(o+4,true)],[dv.getFloat32(o+8,true),dv.getFloat32(o+12,true)]])}return a} +function simulate(snap,w,h,iter,ctx,index,{rebase=false,glitchG=0}={}){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=f(1/w),iwlo=f(1/w-iw),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy),dxh=mul(ox,iw),dxl=mul(ox,iwlo),dyh=mul(oy,iw),dyl=mul(oy,iwlo);let d=[add(mul(mh,dxh),add(mul(mh,dxl),mul(ml,dxh))),add(mul(mh,dyh),add(mul(mh,dyl),mul(ml,dyh)))];let ww=[0,0],scaleExp=se.exp,n=0,m=0,rebases=0,glitches=0,minRatio=Infinity;const refs=refsFrom(ctx.refs,ctx.refLen); + let rebaseRisk=false;while(true){const rp=refs[Math.min(m,ctx.refLen)],delta=[ld(ww[0],scaleExp),ld(ww[1],scaleExp)],rv=[add(rp[0][0],rp[1][0]),add(rp[0][1],rp[1][1])],z=[add(rp[0][0],add(rp[1][0],delta[0])),add(rp[0][1],add(rp[1][1],delta[1]))],mag=add(mul(z[0],z[0]),mul(z[1],z[1])),rmag=add(mul(rv[0],rv[0]),mul(rv[1],rv[1])),dmag=add(mul(delta[0],delta[0]),mul(delta[1],delta[1]));if(m>0&&Number.isFinite(mag)&&Number.isFinite(dmag)&&mag0)minRatio=Math.min(minRatio,mag/rmag); + if(m>0&&glitchG>0&&rmag>0&&mag4)return{kind:'escape',n,rebases,glitches,minRatio,rebaseRisk};if(n>=iter)return{kind:'op',n,rebases,glitches,minRatio,rebaseRisk};if(m>=ctx.refLen)return{kind:'refend',n,rebases,glitches,minRatio,rebaseRisk}; + if(rebase&&m>0&&mag=1e30)return{kind:'range',n,rebases,glitches,minRatio,rebaseRisk};if(mm>65536){ww=[mul(ww[0],1/65536),mul(ww[1],1/65536)];d=[mul(d[0],1/65536),mul(d[1],1/65536)];scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=[mul(ww[0],65536),mul(ww[1],65536)];d=[mul(d[0],65536),mul(d[1],65536)];scaleExp-=16}if(scaleExp>126)return{kind:'range',n,rebases,glitches,minRatio,rebaseRisk};}} +function grid(w,h,nx=16,ny=10){const out=[];for(let j=0;j>>28)&3)===1,q=simulate(snap,1365,768,sel.iter,ctx,indices[k],{}),fast=q.kind==='escape'; + if(q.rebaseRisk)riskAll++; + if(fast!==exact){mismatch++;if(fast){falseEscape++;if(q.rebaseRisk)riskFalseEscape++;}details.push({index:indices[k],x:indices[k]%1365,y:Math.floor(indices[k]/1365),exactEscape:exact,fast:q});} + } + rows.push({case:c.name,initial:sel.iter,refLen:ctx.refLen,mismatch,falseEscape,riskFalseEscape,riskAll,details}); +} +console.log(JSON.stringify(rows,null,2)); diff --git a/experiments/root-cause/ref_coverage.mjs b/experiments/root-cause/ref_coverage.mjs new file mode 100644 index 0000000..0f463f6 --- /dev/null +++ b/experiments/root-cause/ref_coverage.mjs @@ -0,0 +1,56 @@ +import fs from 'node:fs'; +import vm from 'node:vm'; + +const prodHtml=fs.readFileSync(new URL('../../index.html',import.meta.url),'utf8'); +const scripts=[...prodHtml.matchAll(/]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]); +const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS; +function appContext(){ + const element={width:800,height:600,clientWidth:800,clientHeight:600,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:800,height:600}}}; + 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:800,innerHeight:600,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}}; + const cut=scripts[1].indexOf('// ── boot / teardown'); + const code=scripts[1].slice(0,cut)+`\nglobalThis.internal={state,fastReferenceWorkerSource,precisionFallbackWorkerSource,fastReferenceBits,pixelPrecisionBits,referencePixelForSource,spanMantExp,adaptiveProbeChoice};})();`; + vm.runInNewContext(code,context);return context.internal; +} +const app=appContext(); +function runWorker(source,data){let result;const c={self:{},postMessage:d=>{result=d},performance,console,BigInt,Math,ArrayBuffer,DataView,Float32Array,Uint32Array,Uint8Array};vm.runInNewContext(source,c);c.self.onmessage({data});if(!result)throw new Error('worker returned no result');if(result.type==='error')throw new Error(result.error);return result;} +function buildFast(snap,iter,w,h){const targetBits=app.fastReferenceBits(snap,w),d=runWorker(app.fastReferenceWorkerSource(),{type:'build',id:1,key:'analysis',sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter});d.source={bits:snap.bits,re:BigInt(d.referenceRe),im:BigInt(d.referenceIm),viewRe:snap.re,viewIm:snap.im,span:snap.span,iter};return d;} +function oracle(snap,iter,w,h,indices){const targetBits=app.pixelPrecisionBits(snap,w,40);return runWorker(app.precisionFallbackWorkerSource(),{type:'solve',id:2,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter,terminalClass:-1,indices:Uint32Array.from(indices).buffer});} +const f=Math.fround,U=5.960464477539063e-8; +const add=(a,b)=>f(f(a)+f(b)), mul=(a,b)=>f(f(a)*f(b)); +function cmul(a,b){return [add(mul(a[0],b[0]),-mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]} +const maxabs=v=>Math.max(Math.abs(v[0]),Math.abs(v[1])); +function ld(v,e){return f(v*Math.pow(2,e))} +function refsFrom(buf,n){const dv=new DataView(buf),a=[];for(let i=0;i<=n;i++){const o=i*16;a.push([[dv.getFloat32(o,true),dv.getFloat32(o+4,true)],[dv.getFloat32(o+8,true),dv.getFloat32(o+12,true)]])}return a} +function dsQuick(a,b){const q=f(a+b),e=f(b-f(q-a));return [q,e]} +function dsSum(a,b){const q=f(a+b),bb=f(q-a),e=f(f(a-f(q-bb))+f(b-bb));return [q,e]} +function dsProd(a,b){const q=f(a*b),ca=f(4097*a),ah=f(ca-f(ca-a)),al=f(a-ah),cb=f(4097*b),bh=f(cb-f(cb-b)),bl=f(b-bh);let e=f(f(ah*bh)-q);e=f(e+f(ah*bl));e=f(e+f(al*bh));e=f(e+f(al*bl));return [q,e]} +function dsAdd(a,b){const q=dsSum(a[0],b[0]);return dsQuick(q[0],f(q[1]+f(a[1]+b[1])))} +const dsNeg=a=>[-a[0],-a[1]],dsSub=(a,b)=>dsAdd(a,dsNeg(b)); +function dsMul(a,b){const q=dsProd(a[0],b[0]);let e=f(q[1]+f(a[0]*b[1]));e=f(e+f(a[1]*b[0]));e=f(e+f(a[1]*b[1]));return dsQuick(q[0],e)} +function dsScale(a,b){const q=dsProd(a[0],b);return dsQuick(q[0],f(q[1]+f(a[1]*b)))} +const dsPow2=(a,e)=>[ld(a[0],e),ld(a[1],e)],dsVal=a=>f(a[0]+a[1]); +const cdsAdd=(a,b)=>[dsAdd(a[0],b[0]),dsAdd(a[1],b[1])]; +const cdsMul=(a,b)=>[dsSub(dsMul(a[0],b[0]),dsMul(a[1],b[1])),dsAdd(dsMul(a[0],b[1]),dsMul(a[1],b[0]))]; +const cdsScale=(a,b)=>[dsScale(a[0],b),dsScale(a[1],b)],cdsPow2=(a,e)=>[dsPow2(a[0],e),dsPow2(a[1],e)]; +const cdsMag2=a=>dsAdd(dsMul(a[0],a[0]),dsMul(a[1],a[1])),cdsMax=a=>Math.max(Math.abs(dsVal(a[0])),Math.abs(dsVal(a[1]))); +function simulateDS(snap,w,h,iter,ctx,index){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=1/w,iwh=f(iw),iwl=f(iw-iwh),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy),inv=[iwh,iwl],sm=[mh,ml],dx=dsScale(inv,ox),dy=dsScale(inv,oy),d0=[dsMul(sm,dx),dsMul(sm,dy)];let d=d0,ww=[[0,0],[0,0]],scaleExp=se.exp,n=0,m=0;const refs=refsFrom(ctx.refs,ctx.refLen);while(true){const rp=refs[m],r=[[rp[0][0],rp[1][0]],[rp[0][1],rp[1][1]]],delta=cdsPow2(ww,scaleExp),z=cdsAdd(r,delta),mag=cdsMag2(z);if(mag[0]>4||(mag[0]===4&&mag[1]>0))return{kind:'escape',n};if(n>=iter)return{kind:'op',n};if(m>=ctx.refLen)return{kind:'refend',n};const linear=cdsScale(cdsMul(r,ww),2),sq=cdsPow2(cdsMul(ww,ww),scaleExp);ww=cdsAdd(cdsAdd(linear,sq),d);m++;n++;const mm=Math.max(cdsMax(ww),cdsMax(d));if(!Number.isFinite(mm)||mm>=1e30)return{kind:'range',n};if(mm>65536){ww=cdsScale(ww,1/65536);d=cdsScale(d,1/65536);scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=cdsScale(ww,65536);d=cdsScale(d,65536);scaleExp-=16}if(scaleExp>126)return{kind:'range',n};}} +function simulate(snap,w,h,iter,ctx,index,guarded){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=1/w,iwh=f(iw),iwl=f(iw-iwh),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy);let d; + if(guarded){const dxh=mul(ox,iwh),dxl=mul(ox,iwl),dyh=mul(oy,iwh),dyl=mul(oy,iwl);d=[add(mul(mh,dxh),add(mul(mh,dxl),mul(ml,dxh))),add(mul(mh,dyh),add(mul(mh,dyl),mul(ml,dyh)))];} + else {const dx=f(ox/f(w)),dy=f(oy/f(w));d=[mul(mh,dx),mul(mh,dy)];} + let ww=[0,0],scaleExp=se.exp,n=0,m=0,err=4*U*(maxabs(d)+1e-30);const refs=refsFrom(ctx.refs,ctx.refLen); + while(true){const rp=refs[Math.min(m,ctx.refLen)],delta=[ld(ww[0],scaleExp),ld(ww[1],scaleExp)],z=[add(rp[0][0],add(rp[1][0],delta[0])),add(rp[0][1],add(rp[1][1],delta[1]))],mag=add(mul(z[0],z[0]),mul(z[1],z[1])); + if(mag>4){if(!guarded)return{kind:'escape',n};const errAbs=Math.abs(ld(err,scaleExp))+U*(maxabs(z)+maxabs(delta)+1e-30);if(Math.hypot(z[0],z[1])-errAbs>2)return{kind:'escape',n};return{kind:'uncertain',n};} + if(n>=iter)return{kind:'op',n};if(m>=ctx.refLen)return{kind:'refend',n}; + const refAbs=maxabs(rp[0])+maxabs(rp[1]),wAbs=maxabs(ww),dAbs=maxabs(d),deltaAbs=maxabs(delta),ww2=cmul(ww,ww),sq=[ld(ww2[0],scaleExp),ld(ww2[1],scaleExp)],sqAbs=maxabs(sq),gain=2*refAbs+2*deltaAbs,roundErr=U*(2*refAbs*wAbs+sqAbs+dAbs+1e-30);if(guarded)err=f(gain*err+roundErr); + const l1=cmul(rp[0],ww),l2=cmul(rp[1],ww),linear=[f(2*f(l1[0]+l2[0])),f(2*f(l1[1]+l2[1]))];ww=[add(add(linear[0],sq[0]),d[0]),add(add(linear[1],sq[1]),d[1])];m++;n++;const mm=Math.max(maxabs(ww),maxabs(d));if(!Number.isFinite(mm)||mm>=1e30||guarded&&(!Number.isFinite(err)||err>1e35))return{kind:'range',n};if(mm>65536){ww=[mul(ww[0],1/65536),mul(ww[1],1/65536)];d=[mul(d[0],1/65536),mul(d[1],1/65536)];if(guarded)err=f(err/65536);scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=[mul(ww[0],65536),mul(ww[1],65536)];d=[mul(d[0],65536),mul(d[1],65536)];if(guarded)err=f(err*65536);scaleExp-=16}if(scaleExp>126)return{kind:'range',n}; + } +} +function grid(w,h,nx=8,ny=5){const out=[];for(let j=0;j>>28)&3)===1)exactEsc++;const q=simulate(snap,w,h,full,ctx,indices[k],false);fast[q.kind]=(fast[q.kind]||0)+1;}console.log(JSON.stringify({case:c.name,requested:iter,refLen:ctx.refLen,exactEsc,samples:indices.length,fast},null,2));} \ No newline at end of file diff --git a/experiments/root-cause/ref_margin.mjs b/experiments/root-cause/ref_margin.mjs new file mode 100644 index 0000000..330b5fb --- /dev/null +++ b/experiments/root-cause/ref_margin.mjs @@ -0,0 +1,56 @@ +import fs from 'node:fs'; +import vm from 'node:vm'; + +const prodHtml=fs.readFileSync(new URL('../../index.html',import.meta.url),'utf8'); +const scripts=[...prodHtml.matchAll(/]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]); +const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS; +function appContext(){ + const element={width:800,height:600,clientWidth:800,clientHeight:600,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:800,height:600}}}; + 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:800,innerHeight:600,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}}; + const cut=scripts[1].indexOf('// ── boot / teardown'); + const code=scripts[1].slice(0,cut)+`\nglobalThis.internal={state,fastReferenceWorkerSource,precisionFallbackWorkerSource,fastReferenceBits,pixelPrecisionBits,referencePixelForSource,spanMantExp,adaptiveProbeChoice};})();`; + vm.runInNewContext(code,context);return context.internal; +} +const app=appContext(); +function runWorker(source,data){let result;const c={self:{},postMessage:d=>{result=d},performance,console,BigInt,Math,ArrayBuffer,DataView,Float32Array,Uint32Array,Uint8Array};vm.runInNewContext(source,c);c.self.onmessage({data});if(!result)throw new Error('worker returned no result');if(result.type==='error')throw new Error(result.error);return result;} +function buildFast(snap,iter,w,h){const targetBits=app.fastReferenceBits(snap,w),d=runWorker(app.fastReferenceWorkerSource(),{type:'build',id:1,key:'analysis',sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter});d.source={bits:snap.bits,re:BigInt(d.referenceRe),im:BigInt(d.referenceIm),viewRe:snap.re,viewIm:snap.im,span:snap.span,iter};return d;} +function oracle(snap,iter,w,h,indices){const targetBits=app.pixelPrecisionBits(snap,w,40);return runWorker(app.precisionFallbackWorkerSource(),{type:'solve',id:2,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter,terminalClass:-1,indices:Uint32Array.from(indices).buffer});} +const f=Math.fround,U=5.960464477539063e-8; +const add=(a,b)=>f(f(a)+f(b)), mul=(a,b)=>f(f(a)*f(b)); +function cmul(a,b){return [add(mul(a[0],b[0]),-mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]} +const maxabs=v=>Math.max(Math.abs(v[0]),Math.abs(v[1])); +function ld(v,e){return f(v*Math.pow(2,e))} +function refsFrom(buf,n){const dv=new DataView(buf),a=[];for(let i=0;i<=n;i++){const o=i*16;a.push([[dv.getFloat32(o,true),dv.getFloat32(o+4,true)],[dv.getFloat32(o+8,true),dv.getFloat32(o+12,true)]])}return a} +function dsQuick(a,b){const q=f(a+b),e=f(b-f(q-a));return [q,e]} +function dsSum(a,b){const q=f(a+b),bb=f(q-a),e=f(f(a-f(q-bb))+f(b-bb));return [q,e]} +function dsProd(a,b){const q=f(a*b),ca=f(4097*a),ah=f(ca-f(ca-a)),al=f(a-ah),cb=f(4097*b),bh=f(cb-f(cb-b)),bl=f(b-bh);let e=f(f(ah*bh)-q);e=f(e+f(ah*bl));e=f(e+f(al*bh));e=f(e+f(al*bl));return [q,e]} +function dsAdd(a,b){const q=dsSum(a[0],b[0]);return dsQuick(q[0],f(q[1]+f(a[1]+b[1])))} +const dsNeg=a=>[-a[0],-a[1]],dsSub=(a,b)=>dsAdd(a,dsNeg(b)); +function dsMul(a,b){const q=dsProd(a[0],b[0]);let e=f(q[1]+f(a[0]*b[1]));e=f(e+f(a[1]*b[0]));e=f(e+f(a[1]*b[1]));return dsQuick(q[0],e)} +function dsScale(a,b){const q=dsProd(a[0],b);return dsQuick(q[0],f(q[1]+f(a[1]*b)))} +const dsPow2=(a,e)=>[ld(a[0],e),ld(a[1],e)],dsVal=a=>f(a[0]+a[1]); +const cdsAdd=(a,b)=>[dsAdd(a[0],b[0]),dsAdd(a[1],b[1])]; +const cdsMul=(a,b)=>[dsSub(dsMul(a[0],b[0]),dsMul(a[1],b[1])),dsAdd(dsMul(a[0],b[1]),dsMul(a[1],b[0]))]; +const cdsScale=(a,b)=>[dsScale(a[0],b),dsScale(a[1],b)],cdsPow2=(a,e)=>[dsPow2(a[0],e),dsPow2(a[1],e)]; +const cdsMag2=a=>dsAdd(dsMul(a[0],a[0]),dsMul(a[1],a[1])),cdsMax=a=>Math.max(Math.abs(dsVal(a[0])),Math.abs(dsVal(a[1]))); +function simulateDS(snap,w,h,iter,ctx,index){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=1/w,iwh=f(iw),iwl=f(iw-iwh),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy),inv=[iwh,iwl],sm=[mh,ml],dx=dsScale(inv,ox),dy=dsScale(inv,oy),d0=[dsMul(sm,dx),dsMul(sm,dy)];let d=d0,ww=[[0,0],[0,0]],scaleExp=se.exp,n=0,m=0;const refs=refsFrom(ctx.refs,ctx.refLen);while(true){const rp=refs[m],r=[[rp[0][0],rp[1][0]],[rp[0][1],rp[1][1]]],delta=cdsPow2(ww,scaleExp),z=cdsAdd(r,delta),mag=cdsMag2(z);if(mag[0]>4||(mag[0]===4&&mag[1]>0))return{kind:'escape',n};if(n>=iter)return{kind:'op',n};if(m>=ctx.refLen)return{kind:'refend',n};const linear=cdsScale(cdsMul(r,ww),2),sq=cdsPow2(cdsMul(ww,ww),scaleExp);ww=cdsAdd(cdsAdd(linear,sq),d);m++;n++;const mm=Math.max(cdsMax(ww),cdsMax(d));if(!Number.isFinite(mm)||mm>=1e30)return{kind:'range',n};if(mm>65536){ww=cdsScale(ww,1/65536);d=cdsScale(d,1/65536);scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=cdsScale(ww,65536);d=cdsScale(d,65536);scaleExp-=16}if(scaleExp>126)return{kind:'range',n};}} +function simulate(snap,w,h,iter,ctx,index,guarded){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=1/w,iwh=f(iw),iwl=f(iw-iwh),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy);let d; + if(guarded){const dxh=mul(ox,iwh),dxl=mul(ox,iwl),dyh=mul(oy,iwh),dyl=mul(oy,iwl);d=[add(mul(mh,dxh),add(mul(mh,dxl),mul(ml,dxh))),add(mul(mh,dyh),add(mul(mh,dyl),mul(ml,dyh)))];} + else {const dx=f(ox/f(w)),dy=f(oy/f(w));d=[mul(mh,dx),mul(mh,dy)];} + let ww=[0,0],scaleExp=se.exp,n=0,m=0,err=4*U*(maxabs(d)+1e-30);const refs=refsFrom(ctx.refs,ctx.refLen); + while(true){const rp=refs[Math.min(m,ctx.refLen)],delta=[ld(ww[0],scaleExp),ld(ww[1],scaleExp)],z=[add(rp[0][0],add(rp[1][0],delta[0])),add(rp[0][1],add(rp[1][1],delta[1]))],mag=add(mul(z[0],z[0]),mul(z[1],z[1])); + if(mag>4){if(!guarded)return{kind:'escape',n};const errAbs=Math.abs(ld(err,scaleExp))+U*(maxabs(z)+maxabs(delta)+1e-30);if(Math.hypot(z[0],z[1])-errAbs>2)return{kind:'escape',n};return{kind:'uncertain',n};} + if(n>=iter)return{kind:'op',n};if(m>=ctx.refLen)return{kind:'refend',n}; + const refAbs=maxabs(rp[0])+maxabs(rp[1]),wAbs=maxabs(ww),dAbs=maxabs(d),deltaAbs=maxabs(delta),ww2=cmul(ww,ww),sq=[ld(ww2[0],scaleExp),ld(ww2[1],scaleExp)],sqAbs=maxabs(sq),gain=2*refAbs+2*deltaAbs,roundErr=U*(2*refAbs*wAbs+sqAbs+dAbs+1e-30);if(guarded)err=f(gain*err+roundErr); + const l1=cmul(rp[0],ww),l2=cmul(rp[1],ww),linear=[f(2*f(l1[0]+l2[0])),f(2*f(l1[1]+l2[1]))];ww=[add(add(linear[0],sq[0]),d[0]),add(add(linear[1],sq[1]),d[1])];m++;n++;const mm=Math.max(maxabs(ww),maxabs(d));if(!Number.isFinite(mm)||mm>=1e30||guarded&&(!Number.isFinite(err)||err>1e35))return{kind:'range',n};if(mm>65536){ww=[mul(ww[0],1/65536),mul(ww[1],1/65536)];d=[mul(d[0],1/65536),mul(d[1],1/65536)];if(guarded)err=f(err/65536);scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=[mul(ww[0],65536),mul(ww[1],65536)];d=[mul(d[0],65536),mul(d[1],65536)];if(guarded)err=f(err*65536);scaleExp-=16}if(scaleExp>126)return{kind:'range',n}; + } +} +function grid(w,h,nx=8,ny=5){const out=[];for(let j=0;j>>28)&3)===1,q=simulate(snap,w,h,iter,ctx,indices[k],false),fast=q.kind==='escape';oe+=exact;fe+=fast;fp+=fast&&!exact;fn+=!fast&&exact;}rows.push({case:c.name,refLen:ctx.refLen,margin,iter,oracleEscape:oe,fastEscape:fe,fp,fn})}}console.log(JSON.stringify(rows,null,2)); \ No newline at end of file diff --git a/experiments/root-cause/ref_minima.mjs b/experiments/root-cause/ref_minima.mjs new file mode 100644 index 0000000..a2c98a1 --- /dev/null +++ b/experiments/root-cause/ref_minima.mjs @@ -0,0 +1,14 @@ +import fs from 'node:fs';import vm from 'node:vm'; +const html=fs.readFileSync(new URL('../../index.html',import.meta.url),'utf8'),scripts=[...html.matchAll(/]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]);const k={};vm.runInNewContext(scripts[0],k);const kernels=k.MANDEL_WEBGPU_KERNELS; +const e={width:1365,height:768,clientWidth:1365,clientHeight:768,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:1365,height:768}}},c={MANDEL_WEBGPU_KERNELS:kernels,document:{querySelector:()=>e,getElementById:()=>e,documentElement:{classList:{toggle(){}}},body:{classList:{toggle(){}},appendChild(){}}},navigator:{hardwareConcurrency:4,deviceMemory:8},location:{search:'',hash:''},history:{replaceState(){}},performance,URLSearchParams,URL:{createObjectURL:()=>'',revokeObjectURL(){}},TextEncoder,Blob:class{},console,addEventListener(){},requestAnimationFrame:()=>1,cancelAnimationFrame(){},setTimeout,clearTimeout,setInterval,clearInterval,innerWidth:1365,innerHeight:768,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}},cut=scripts[1].indexOf('// ── boot / teardown');vm.runInNewContext(scripts[1].slice(0,cut)+`\nglobalThis.I={fastReferenceWorkerSource,fastReferenceBits};})();`,c);const I=c.I; +function run(src,data){let r;const v={self:{},postMessage:d=>r=d,performance,console,BigInt,Math,ArrayBuffer,DataView,Float32Array,Uint32Array,Uint8Array};vm.runInNewContext(src,v);v.self.onmessage({data});return r} +function fixed(x,b=256){const s=1n<{const s=String(x),neg=s[0]=='-',t=neg?s.slice(1):s,[a,b='']=t.split('.'),den=10n**BigInt(b.length),v=(BigInt(a+b)*one+den/2n)/den;return (neg?-v:v).toString()}; +const cs=[ +{name:'c1',bits:256,re:'-148292766829529091721540000955948596563327036751005092894699321061162899553437',im:'7497924611962244671337883039784517355486082422571896698766224071500989488498',sp:'1051264689747601722664903477680432560343035141150552570739220680259730166'}, +{name:'c2',bits:256,re:'44279008708751415573784266681073018338222298986985889938567243759258661091705',im:'11537730301689015207261638283847108540669173974016900715268231497454915998519',sp:'136466963900827996566957710260403731102960944336062216372133428878224980'}, +{name:'c3',bits:256,re:'15717041212470055224256502413765101609014610520343947152977545154690386564664',im:'74092703473662244164463718953593537954306995881211045997604051198538528963626',sp:'7384741851176872716004457996517031059911173801417894624776297655376888'}, +{name:'seahorse-deep',bits:256,re:dec('-0.743643887037151'),im:dec('0.13182590420533'),sp:dec('0.000002')}, +{name:'period3-tiny',bits:256,re:dec('-0.1225611668766536'),im:dec('0.7448617666197442'),sp:dec('0.0000001')} +];console.log(JSON.stringify(cs.map(x=>build(x,1536)),null,2)); diff --git a/experiments/root-cause/ref_mismatch_detail.mjs b/experiments/root-cause/ref_mismatch_detail.mjs new file mode 100644 index 0000000..5f981ed --- /dev/null +++ b/experiments/root-cause/ref_mismatch_detail.mjs @@ -0,0 +1,56 @@ +import fs from 'node:fs'; +import vm from 'node:vm'; + +const prodHtml=fs.readFileSync(new URL('../../index.html',import.meta.url),'utf8'); +const scripts=[...prodHtml.matchAll(/]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]); +const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS; +function appContext(){ + const element={width:800,height:600,clientWidth:800,clientHeight:600,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:800,height:600}}}; + 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:800,innerHeight:600,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}}; + const cut=scripts[1].indexOf('// ── boot / teardown'); + const code=scripts[1].slice(0,cut)+`\nglobalThis.internal={state,fastReferenceWorkerSource,precisionFallbackWorkerSource,fastReferenceBits,pixelPrecisionBits,referencePixelForSource,spanMantExp,adaptiveProbeChoice};})();`; + vm.runInNewContext(code,context);return context.internal; +} +const app=appContext(); +function runWorker(source,data){let result;const c={self:{},postMessage:d=>{result=d},performance,console,BigInt,Math,ArrayBuffer,DataView,Float32Array,Uint32Array,Uint8Array};vm.runInNewContext(source,c);c.self.onmessage({data});if(!result)throw new Error('worker returned no result');if(result.type==='error')throw new Error(result.error);return result;} +function buildFast(snap,iter,w,h){const targetBits=app.fastReferenceBits(snap,w),d=runWorker(app.fastReferenceWorkerSource(),{type:'build',id:1,key:'analysis',sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter});d.source={bits:snap.bits,re:BigInt(d.referenceRe),im:BigInt(d.referenceIm),viewRe:snap.re,viewIm:snap.im,span:snap.span,iter};return d;} +function oracle(snap,iter,w,h,indices){const targetBits=app.pixelPrecisionBits(snap,w,40);return runWorker(app.precisionFallbackWorkerSource(),{type:'solve',id:2,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter,terminalClass:-1,indices:Uint32Array.from(indices).buffer});} +const f=Math.fround,U=5.960464477539063e-8; +const add=(a,b)=>f(f(a)+f(b)), mul=(a,b)=>f(f(a)*f(b)); +function cmul(a,b){return [add(mul(a[0],b[0]),-mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]} +const maxabs=v=>Math.max(Math.abs(v[0]),Math.abs(v[1])); +function ld(v,e){return f(v*Math.pow(2,e))} +function refsFrom(buf,n){const dv=new DataView(buf),a=[];for(let i=0;i<=n;i++){const o=i*16;a.push([[dv.getFloat32(o,true),dv.getFloat32(o+4,true)],[dv.getFloat32(o+8,true),dv.getFloat32(o+12,true)]])}return a} +function dsQuick(a,b){const q=f(a+b),e=f(b-f(q-a));return [q,e]} +function dsSum(a,b){const q=f(a+b),bb=f(q-a),e=f(f(a-f(q-bb))+f(b-bb));return [q,e]} +function dsProd(a,b){const q=f(a*b),ca=f(4097*a),ah=f(ca-f(ca-a)),al=f(a-ah),cb=f(4097*b),bh=f(cb-f(cb-b)),bl=f(b-bh);let e=f(f(ah*bh)-q);e=f(e+f(ah*bl));e=f(e+f(al*bh));e=f(e+f(al*bl));return [q,e]} +function dsAdd(a,b){const q=dsSum(a[0],b[0]);return dsQuick(q[0],f(q[1]+f(a[1]+b[1])))} +const dsNeg=a=>[-a[0],-a[1]],dsSub=(a,b)=>dsAdd(a,dsNeg(b)); +function dsMul(a,b){const q=dsProd(a[0],b[0]);let e=f(q[1]+f(a[0]*b[1]));e=f(e+f(a[1]*b[0]));e=f(e+f(a[1]*b[1]));return dsQuick(q[0],e)} +function dsScale(a,b){const q=dsProd(a[0],b);return dsQuick(q[0],f(q[1]+f(a[1]*b)))} +const dsPow2=(a,e)=>[ld(a[0],e),ld(a[1],e)],dsVal=a=>f(a[0]+a[1]); +const cdsAdd=(a,b)=>[dsAdd(a[0],b[0]),dsAdd(a[1],b[1])]; +const cdsMul=(a,b)=>[dsSub(dsMul(a[0],b[0]),dsMul(a[1],b[1])),dsAdd(dsMul(a[0],b[1]),dsMul(a[1],b[0]))]; +const cdsScale=(a,b)=>[dsScale(a[0],b),dsScale(a[1],b)],cdsPow2=(a,e)=>[dsPow2(a[0],e),dsPow2(a[1],e)]; +const cdsMag2=a=>dsAdd(dsMul(a[0],a[0]),dsMul(a[1],a[1])),cdsMax=a=>Math.max(Math.abs(dsVal(a[0])),Math.abs(dsVal(a[1]))); +function simulateDS(snap,w,h,iter,ctx,index){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=1/w,iwh=f(iw),iwl=f(iw-iwh),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy),inv=[iwh,iwl],sm=[mh,ml],dx=dsScale(inv,ox),dy=dsScale(inv,oy),d0=[dsMul(sm,dx),dsMul(sm,dy)];let d=d0,ww=[[0,0],[0,0]],scaleExp=se.exp,n=0,m=0;const refs=refsFrom(ctx.refs,ctx.refLen);while(true){const rp=refs[m],r=[[rp[0][0],rp[1][0]],[rp[0][1],rp[1][1]]],delta=cdsPow2(ww,scaleExp),z=cdsAdd(r,delta),mag=cdsMag2(z);if(mag[0]>4||(mag[0]===4&&mag[1]>0))return{kind:'escape',n};if(n>=iter)return{kind:'op',n};if(m>=ctx.refLen)return{kind:'refend',n};const linear=cdsScale(cdsMul(r,ww),2),sq=cdsPow2(cdsMul(ww,ww),scaleExp);ww=cdsAdd(cdsAdd(linear,sq),d);m++;n++;const mm=Math.max(cdsMax(ww),cdsMax(d));if(!Number.isFinite(mm)||mm>=1e30)return{kind:'range',n};if(mm>65536){ww=cdsScale(ww,1/65536);d=cdsScale(d,1/65536);scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=cdsScale(ww,65536);d=cdsScale(d,65536);scaleExp-=16}if(scaleExp>126)return{kind:'range',n};}} +function simulate(snap,w,h,iter,ctx,index,guarded){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=1/w,iwh=f(iw),iwl=f(iw-iwh),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy);let d; + if(guarded){const dxh=mul(ox,iwh),dxl=mul(ox,iwl),dyh=mul(oy,iwh),dyl=mul(oy,iwl);d=[add(mul(mh,dxh),add(mul(mh,dxl),mul(ml,dxh))),add(mul(mh,dyh),add(mul(mh,dyl),mul(ml,dyh)))];} + else {const dx=f(ox/f(w)),dy=f(oy/f(w));d=[mul(mh,dx),mul(mh,dy)];} + let ww=[0,0],scaleExp=se.exp,n=0,m=0,err=4*U*(maxabs(d)+1e-30);const refs=refsFrom(ctx.refs,ctx.refLen); + while(true){const rp=refs[Math.min(m,ctx.refLen)],delta=[ld(ww[0],scaleExp),ld(ww[1],scaleExp)],z=[add(rp[0][0],add(rp[1][0],delta[0])),add(rp[0][1],add(rp[1][1],delta[1]))],mag=add(mul(z[0],z[0]),mul(z[1],z[1])); + if(mag>4){if(!guarded)return{kind:'escape',n};const errAbs=Math.abs(ld(err,scaleExp))+U*(maxabs(z)+maxabs(delta)+1e-30);if(Math.hypot(z[0],z[1])-errAbs>2)return{kind:'escape',n};return{kind:'uncertain',n};} + if(n>=iter)return{kind:'op',n};if(m>=ctx.refLen)return{kind:'refend',n}; + const refAbs=maxabs(rp[0])+maxabs(rp[1]),wAbs=maxabs(ww),dAbs=maxabs(d),deltaAbs=maxabs(delta),ww2=cmul(ww,ww),sq=[ld(ww2[0],scaleExp),ld(ww2[1],scaleExp)],sqAbs=maxabs(sq),gain=2*refAbs+2*deltaAbs,roundErr=U*(2*refAbs*wAbs+sqAbs+dAbs+1e-30);if(guarded)err=f(gain*err+roundErr); + const l1=cmul(rp[0],ww),l2=cmul(rp[1],ww),linear=[f(2*f(l1[0]+l2[0])),f(2*f(l1[1]+l2[1]))];ww=[add(add(linear[0],sq[0]),d[0]),add(add(linear[1],sq[1]),d[1])];m++;n++;const mm=Math.max(maxabs(ww),maxabs(d));if(!Number.isFinite(mm)||mm>=1e30||guarded&&(!Number.isFinite(err)||err>1e35))return{kind:'range',n};if(mm>65536){ww=[mul(ww[0],1/65536),mul(ww[1],1/65536)];d=[mul(d[0],1/65536),mul(d[1],1/65536)];if(guarded)err=f(err/65536);scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=[mul(ww[0],65536),mul(ww[1],65536)];d=[mul(d[0],65536),mul(d[1],65536)];if(guarded)err=f(err*65536);scaleExp-=16}if(scaleExp>126)return{kind:'range',n}; + } +} +function grid(w,h,nx=8,ny=5){const out=[];for(let j=0;j>>28)&3)===1,q=simulate(snap,w,h,iter,ctx,indices[k],false),fast=q.kind==='escape';if(fast!==exact)console.log(JSON.stringify({case:c.name,refLen:ctx.refLen,index:indices[k],x:indices[k]%w,y:Math.floor(indices[k]/w),oracle:exact?'escape':'non',fast:q}));}} \ No newline at end of file diff --git a/experiments/root-cause/ref_snap_accuracy.mjs b/experiments/root-cause/ref_snap_accuracy.mjs new file mode 100644 index 0000000..07448d9 --- /dev/null +++ b/experiments/root-cause/ref_snap_accuracy.mjs @@ -0,0 +1,56 @@ +import fs from 'node:fs'; +import vm from 'node:vm'; + +const prodHtml=fs.readFileSync(new URL('../../index.html',import.meta.url),'utf8'); +const scripts=[...prodHtml.matchAll(/]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]); +const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS; +function appContext(){ + const element={width:800,height:600,clientWidth:800,clientHeight:600,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:800,height:600}}}; + 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:800,innerHeight:600,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}}; + const cut=scripts[1].indexOf('// ── boot / teardown'); + const code=scripts[1].slice(0,cut)+`\nglobalThis.internal={state,fastReferenceWorkerSource,precisionFallbackWorkerSource,fastReferenceBits,pixelPrecisionBits,referencePixelForSource,spanMantExp,adaptiveProbeChoice};})();`; + vm.runInNewContext(code,context);return context.internal; +} +const app=appContext(); +function runWorker(source,data){let result;const c={self:{},postMessage:d=>{result=d},performance,console,BigInt,Math,ArrayBuffer,DataView,Float32Array,Uint32Array,Uint8Array};vm.runInNewContext(source,c);c.self.onmessage({data});if(!result)throw new Error('worker returned no result');if(result.type==='error')throw new Error(result.error);return result;} +function buildFast(snap,iter,w,h){const targetBits=app.fastReferenceBits(snap,w),d=runWorker(app.fastReferenceWorkerSource(),{type:'build',id:1,key:'analysis',sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter});d.source={bits:snap.bits,re:BigInt(d.referenceRe),im:BigInt(d.referenceIm),viewRe:snap.re,viewIm:snap.im,span:snap.span,iter};return d;} +function oracle(snap,iter,w,h,indices){const targetBits=app.pixelPrecisionBits(snap,w,40);return runWorker(app.precisionFallbackWorkerSource(),{type:'solve',id:2,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter,terminalClass:-1,indices:Uint32Array.from(indices).buffer});} +const f=Math.fround,U=5.960464477539063e-8; +const add=(a,b)=>f(f(a)+f(b)), mul=(a,b)=>f(f(a)*f(b)); +function cmul(a,b){return [add(mul(a[0],b[0]),-mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]} +const maxabs=v=>Math.max(Math.abs(v[0]),Math.abs(v[1])); +function ld(v,e){return f(v*Math.pow(2,e))} +function refsFrom(buf,n){const dv=new DataView(buf),a=[];for(let i=0;i<=n;i++){const o=i*16;a.push([[dv.getFloat32(o,true),dv.getFloat32(o+4,true)],[dv.getFloat32(o+8,true),dv.getFloat32(o+12,true)]])}return a} +function dsQuick(a,b){const q=f(a+b),e=f(b-f(q-a));return [q,e]} +function dsSum(a,b){const q=f(a+b),bb=f(q-a),e=f(f(a-f(q-bb))+f(b-bb));return [q,e]} +function dsProd(a,b){const q=f(a*b),ca=f(4097*a),ah=f(ca-f(ca-a)),al=f(a-ah),cb=f(4097*b),bh=f(cb-f(cb-b)),bl=f(b-bh);let e=f(f(ah*bh)-q);e=f(e+f(ah*bl));e=f(e+f(al*bh));e=f(e+f(al*bl));return [q,e]} +function dsAdd(a,b){const q=dsSum(a[0],b[0]);return dsQuick(q[0],f(q[1]+f(a[1]+b[1])))} +const dsNeg=a=>[-a[0],-a[1]],dsSub=(a,b)=>dsAdd(a,dsNeg(b)); +function dsMul(a,b){const q=dsProd(a[0],b[0]);let e=f(q[1]+f(a[0]*b[1]));e=f(e+f(a[1]*b[0]));e=f(e+f(a[1]*b[1]));return dsQuick(q[0],e)} +function dsScale(a,b){const q=dsProd(a[0],b);return dsQuick(q[0],f(q[1]+f(a[1]*b)))} +const dsPow2=(a,e)=>[ld(a[0],e),ld(a[1],e)],dsVal=a=>f(a[0]+a[1]); +const cdsAdd=(a,b)=>[dsAdd(a[0],b[0]),dsAdd(a[1],b[1])]; +const cdsMul=(a,b)=>[dsSub(dsMul(a[0],b[0]),dsMul(a[1],b[1])),dsAdd(dsMul(a[0],b[1]),dsMul(a[1],b[0]))]; +const cdsScale=(a,b)=>[dsScale(a[0],b),dsScale(a[1],b)],cdsPow2=(a,e)=>[dsPow2(a[0],e),dsPow2(a[1],e)]; +const cdsMag2=a=>dsAdd(dsMul(a[0],a[0]),dsMul(a[1],a[1])),cdsMax=a=>Math.max(Math.abs(dsVal(a[0])),Math.abs(dsVal(a[1]))); +function simulateDS(snap,w,h,iter,ctx,index){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=1/w,iwh=f(iw),iwl=f(iw-iwh),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy),inv=[iwh,iwl],sm=[mh,ml],dx=dsScale(inv,ox),dy=dsScale(inv,oy),d0=[dsMul(sm,dx),dsMul(sm,dy)];let d=d0,ww=[[0,0],[0,0]],scaleExp=se.exp,n=0,m=0;const refs=refsFrom(ctx.refs,ctx.refLen);while(true){const rp=refs[m],r=[[rp[0][0],rp[1][0]],[rp[0][1],rp[1][1]]],delta=cdsPow2(ww,scaleExp),z=cdsAdd(r,delta),mag=cdsMag2(z);if(mag[0]>4||(mag[0]===4&&mag[1]>0))return{kind:'escape',n};if(n>=iter)return{kind:'op',n};if(m>=ctx.refLen)return{kind:'refend',n};const linear=cdsScale(cdsMul(r,ww),2),sq=cdsPow2(cdsMul(ww,ww),scaleExp);ww=cdsAdd(cdsAdd(linear,sq),d);m++;n++;const mm=Math.max(cdsMax(ww),cdsMax(d));if(!Number.isFinite(mm)||mm>=1e30)return{kind:'range',n};if(mm>65536){ww=cdsScale(ww,1/65536);d=cdsScale(d,1/65536);scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=cdsScale(ww,65536);d=cdsScale(d,65536);scaleExp-=16}if(scaleExp>126)return{kind:'range',n};}} +function simulate(snap,w,h,iter,ctx,index,guarded){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=1/w,iwh=f(iw),iwl=f(iw-iwh),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy);let d; + if(guarded){const dxh=mul(ox,iwh),dxl=mul(ox,iwl),dyh=mul(oy,iwh),dyl=mul(oy,iwl);d=[add(mul(mh,dxh),add(mul(mh,dxl),mul(ml,dxh))),add(mul(mh,dyh),add(mul(mh,dyl),mul(ml,dyh)))];} + else {const dx=f(ox/f(w)),dy=f(oy/f(w));d=[mul(mh,dx),mul(mh,dy)];} + let ww=[0,0],scaleExp=se.exp,n=0,m=0,err=4*U*(maxabs(d)+1e-30);const refs=refsFrom(ctx.refs,ctx.refLen); + while(true){const rp=refs[Math.min(m,ctx.refLen)],delta=[ld(ww[0],scaleExp),ld(ww[1],scaleExp)],z=[add(rp[0][0],add(rp[1][0],delta[0])),add(rp[0][1],add(rp[1][1],delta[1]))],mag=add(mul(z[0],z[0]),mul(z[1],z[1])); + if(mag>4){if(!guarded)return{kind:'escape',n};const errAbs=Math.abs(ld(err,scaleExp))+U*(maxabs(z)+maxabs(delta)+1e-30);if(Math.hypot(z[0],z[1])-errAbs>2)return{kind:'escape',n};return{kind:'uncertain',n};} + if(n>=iter)return{kind:'op',n};if(m>=ctx.refLen)return{kind:'refend',n}; + const refAbs=maxabs(rp[0])+maxabs(rp[1]),wAbs=maxabs(ww),dAbs=maxabs(d),deltaAbs=maxabs(delta),ww2=cmul(ww,ww),sq=[ld(ww2[0],scaleExp),ld(ww2[1],scaleExp)],sqAbs=maxabs(sq),gain=2*refAbs+2*deltaAbs,roundErr=U*(2*refAbs*wAbs+sqAbs+dAbs+1e-30);if(guarded)err=f(gain*err+roundErr); + const l1=cmul(rp[0],ww),l2=cmul(rp[1],ww),linear=[f(2*f(l1[0]+l2[0])),f(2*f(l1[1]+l2[1]))];ww=[add(add(linear[0],sq[0]),d[0]),add(add(linear[1],sq[1]),d[1])];m++;n++;const mm=Math.max(maxabs(ww),maxabs(d));if(!Number.isFinite(mm)||mm>=1e30||guarded&&(!Number.isFinite(err)||err>1e35))return{kind:'range',n};if(mm>65536){ww=[mul(ww[0],1/65536),mul(ww[1],1/65536)];d=[mul(d[0],1/65536),mul(d[1],1/65536)];if(guarded)err=f(err/65536);scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=[mul(ww[0],65536),mul(ww[1],65536)];d=[mul(d[0],65536),mul(d[1],65536)];if(guarded)err=f(err*65536);scaleExp-=16}if(scaleExp>126)return{kind:'range',n}; + } +} +function grid(w,h,nx=8,ny=5){const out=[];for(let j=0;jx<=refLen&&a.indexOf(x)===i);return choices.length?Math.max(...choices):Math.max(1,Math.min(iter,Math.max(64,Math.floor(refLen/64)*64)))}for(const c of cases){const snap={bits:c.bits,re:BigInt(c.re),im:BigInt(c.im),span:BigInt(c.sp)},w=1365,h=768,requested=its[c.name],ctx=buildFast(snap,requested,w,h),indices=gridN(w,h,16,10);for(const iter of [snapRef(ctx.refLen,requested),ctx.refLen]){const o=oracle(snap,iter,w,h,indices),meta=new Uint32Array(o.meta);let fp=0,fn=0,agree=0,oe=0,fe=0;for(let k=0;k>>28)&3)===1,q=simulate(snap,w,h,iter,ctx,indices[k],false),fast=q.kind==='escape';oe+=exact;fe+=fast;if(fast&&!exact)fp++;else if(!fast&&exact)fn++;else agree++;}rows.push({case:c.name,requested,refLen:ctx.refLen,iter,samples:indices.length,oracleEscape:oe,fastEscape:fe,falseEscape:fp,missedEscape:fn,agree})}}console.log(JSON.stringify(rows,null,2)); \ No newline at end of file diff --git a/experiments/root-cause/ref_snap_compare.mjs b/experiments/root-cause/ref_snap_compare.mjs new file mode 100644 index 0000000..d99274b --- /dev/null +++ b/experiments/root-cause/ref_snap_compare.mjs @@ -0,0 +1,56 @@ +import fs from 'node:fs'; +import vm from 'node:vm'; + +const prodHtml=fs.readFileSync(new URL('../../index.html',import.meta.url),'utf8'); +const scripts=[...prodHtml.matchAll(/]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]); +const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS; +function appContext(){ + const element={width:800,height:600,clientWidth:800,clientHeight:600,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:800,height:600}}}; + 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:800,innerHeight:600,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}}; + const cut=scripts[1].indexOf('// ── boot / teardown'); + const code=scripts[1].slice(0,cut)+`\nglobalThis.internal={state,fastReferenceWorkerSource,precisionFallbackWorkerSource,fastReferenceBits,pixelPrecisionBits,referencePixelForSource,spanMantExp,adaptiveProbeChoice};})();`; + vm.runInNewContext(code,context);return context.internal; +} +const app=appContext(); +function runWorker(source,data){let result;const c={self:{},postMessage:d=>{result=d},performance,console,BigInt,Math,ArrayBuffer,DataView,Float32Array,Uint32Array,Uint8Array};vm.runInNewContext(source,c);c.self.onmessage({data});if(!result)throw new Error('worker returned no result');if(result.type==='error')throw new Error(result.error);return result;} +function buildFast(snap,iter,w,h){const targetBits=app.fastReferenceBits(snap,w),d=runWorker(app.fastReferenceWorkerSource(),{type:'build',id:1,key:'analysis',sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter});d.source={bits:snap.bits,re:BigInt(d.referenceRe),im:BigInt(d.referenceIm),viewRe:snap.re,viewIm:snap.im,span:snap.span,iter};return d;} +function oracle(snap,iter,w,h,indices){const targetBits=app.pixelPrecisionBits(snap,w,40);return runWorker(app.precisionFallbackWorkerSource(),{type:'solve',id:2,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter,terminalClass:-1,indices:Uint32Array.from(indices).buffer});} +const f=Math.fround,U=5.960464477539063e-8; +const add=(a,b)=>f(f(a)+f(b)), mul=(a,b)=>f(f(a)*f(b)); +function cmul(a,b){return [add(mul(a[0],b[0]),-mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]} +const maxabs=v=>Math.max(Math.abs(v[0]),Math.abs(v[1])); +function ld(v,e){return f(v*Math.pow(2,e))} +function refsFrom(buf,n){const dv=new DataView(buf),a=[];for(let i=0;i<=n;i++){const o=i*16;a.push([[dv.getFloat32(o,true),dv.getFloat32(o+4,true)],[dv.getFloat32(o+8,true),dv.getFloat32(o+12,true)]])}return a} +function dsQuick(a,b){const q=f(a+b),e=f(b-f(q-a));return [q,e]} +function dsSum(a,b){const q=f(a+b),bb=f(q-a),e=f(f(a-f(q-bb))+f(b-bb));return [q,e]} +function dsProd(a,b){const q=f(a*b),ca=f(4097*a),ah=f(ca-f(ca-a)),al=f(a-ah),cb=f(4097*b),bh=f(cb-f(cb-b)),bl=f(b-bh);let e=f(f(ah*bh)-q);e=f(e+f(ah*bl));e=f(e+f(al*bh));e=f(e+f(al*bl));return [q,e]} +function dsAdd(a,b){const q=dsSum(a[0],b[0]);return dsQuick(q[0],f(q[1]+f(a[1]+b[1])))} +const dsNeg=a=>[-a[0],-a[1]],dsSub=(a,b)=>dsAdd(a,dsNeg(b)); +function dsMul(a,b){const q=dsProd(a[0],b[0]);let e=f(q[1]+f(a[0]*b[1]));e=f(e+f(a[1]*b[0]));e=f(e+f(a[1]*b[1]));return dsQuick(q[0],e)} +function dsScale(a,b){const q=dsProd(a[0],b);return dsQuick(q[0],f(q[1]+f(a[1]*b)))} +const dsPow2=(a,e)=>[ld(a[0],e),ld(a[1],e)],dsVal=a=>f(a[0]+a[1]); +const cdsAdd=(a,b)=>[dsAdd(a[0],b[0]),dsAdd(a[1],b[1])]; +const cdsMul=(a,b)=>[dsSub(dsMul(a[0],b[0]),dsMul(a[1],b[1])),dsAdd(dsMul(a[0],b[1]),dsMul(a[1],b[0]))]; +const cdsScale=(a,b)=>[dsScale(a[0],b),dsScale(a[1],b)],cdsPow2=(a,e)=>[dsPow2(a[0],e),dsPow2(a[1],e)]; +const cdsMag2=a=>dsAdd(dsMul(a[0],a[0]),dsMul(a[1],a[1])),cdsMax=a=>Math.max(Math.abs(dsVal(a[0])),Math.abs(dsVal(a[1]))); +function simulateDS(snap,w,h,iter,ctx,index){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=1/w,iwh=f(iw),iwl=f(iw-iwh),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy),inv=[iwh,iwl],sm=[mh,ml],dx=dsScale(inv,ox),dy=dsScale(inv,oy),d0=[dsMul(sm,dx),dsMul(sm,dy)];let d=d0,ww=[[0,0],[0,0]],scaleExp=se.exp,n=0,m=0;const refs=refsFrom(ctx.refs,ctx.refLen);while(true){const rp=refs[m],r=[[rp[0][0],rp[1][0]],[rp[0][1],rp[1][1]]],delta=cdsPow2(ww,scaleExp),z=cdsAdd(r,delta),mag=cdsMag2(z);if(mag[0]>4||(mag[0]===4&&mag[1]>0))return{kind:'escape',n};if(n>=iter)return{kind:'op',n};if(m>=ctx.refLen)return{kind:'refend',n};const linear=cdsScale(cdsMul(r,ww),2),sq=cdsPow2(cdsMul(ww,ww),scaleExp);ww=cdsAdd(cdsAdd(linear,sq),d);m++;n++;const mm=Math.max(cdsMax(ww),cdsMax(d));if(!Number.isFinite(mm)||mm>=1e30)return{kind:'range',n};if(mm>65536){ww=cdsScale(ww,1/65536);d=cdsScale(d,1/65536);scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=cdsScale(ww,65536);d=cdsScale(d,65536);scaleExp-=16}if(scaleExp>126)return{kind:'range',n};}} +function simulate(snap,w,h,iter,ctx,index,guarded){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=1/w,iwh=f(iw),iwl=f(iw-iwh),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy);let d; + if(guarded){const dxh=mul(ox,iwh),dxl=mul(ox,iwl),dyh=mul(oy,iwh),dyl=mul(oy,iwl);d=[add(mul(mh,dxh),add(mul(mh,dxl),mul(ml,dxh))),add(mul(mh,dyh),add(mul(mh,dyl),mul(ml,dyh)))];} + else {const dx=f(ox/f(w)),dy=f(oy/f(w));d=[mul(mh,dx),mul(mh,dy)];} + let ww=[0,0],scaleExp=se.exp,n=0,m=0,err=4*U*(maxabs(d)+1e-30);const refs=refsFrom(ctx.refs,ctx.refLen); + while(true){const rp=refs[Math.min(m,ctx.refLen)],delta=[ld(ww[0],scaleExp),ld(ww[1],scaleExp)],z=[add(rp[0][0],add(rp[1][0],delta[0])),add(rp[0][1],add(rp[1][1],delta[1]))],mag=add(mul(z[0],z[0]),mul(z[1],z[1])); + if(mag>4){if(!guarded)return{kind:'escape',n};const errAbs=Math.abs(ld(err,scaleExp))+U*(maxabs(z)+maxabs(delta)+1e-30);if(Math.hypot(z[0],z[1])-errAbs>2)return{kind:'escape',n};return{kind:'uncertain',n};} + if(n>=iter)return{kind:'op',n};if(m>=ctx.refLen)return{kind:'refend',n}; + const refAbs=maxabs(rp[0])+maxabs(rp[1]),wAbs=maxabs(ww),dAbs=maxabs(d),deltaAbs=maxabs(delta),ww2=cmul(ww,ww),sq=[ld(ww2[0],scaleExp),ld(ww2[1],scaleExp)],sqAbs=maxabs(sq),gain=2*refAbs+2*deltaAbs,roundErr=U*(2*refAbs*wAbs+sqAbs+dAbs+1e-30);if(guarded)err=f(gain*err+roundErr); + const l1=cmul(rp[0],ww),l2=cmul(rp[1],ww),linear=[f(2*f(l1[0]+l2[0])),f(2*f(l1[1]+l2[1]))];ww=[add(add(linear[0],sq[0]),d[0]),add(add(linear[1],sq[1]),d[1])];m++;n++;const mm=Math.max(maxabs(ww),maxabs(d));if(!Number.isFinite(mm)||mm>=1e30||guarded&&(!Number.isFinite(err)||err>1e35))return{kind:'range',n};if(mm>65536){ww=[mul(ww[0],1/65536),mul(ww[1],1/65536)];d=[mul(d[0],1/65536),mul(d[1],1/65536)];if(guarded)err=f(err/65536);scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=[mul(ww[0],65536),mul(ww[1],65536)];d=[mul(d[0],65536),mul(d[1],65536)];if(guarded)err=f(err*65536);scaleExp-=16}if(scaleExp>126)return{kind:'range',n}; + } +} +function grid(w,h,nx=8,ny=5){const out=[];for(let j=0;jx<=refLen&&a.indexOf(x)===i);return choices.length?Math.max(...choices):Math.max(1,Math.min(iter,Math.max(64,Math.floor(refLen/64)*64)))}for(const c of cases){const snapv={bits:c.bits,re:BigInt(c.re),im:BigInt(c.im),span:BigInt(c.sp)},w=1365,h=768,requested=its[c.name],ctx=buildFast(snapv,requested,w,h),cur=snap(ctx.refLen,requested),indices=gridN(w,h,8,6);for(const iter of [cur,ctx.refLen]){const o=oracle(snapv,iter,w,h,indices),meta=new Uint32Array(o.meta);let exactEsc=0,fast={};for(let k=0;k>>28)&3)===1)exactEsc++;const q=simulate(snapv,w,h,iter,ctx,indices[k],false);fast[q.kind]=(fast[q.kind]||0)+1;}rows.push({case:c.name,requested,refLen:ctx.refLen,iter,exactEsc,samples:indices.length,fast})}}console.log(JSON.stringify(rows,null,2)); \ No newline at end of file diff --git a/experiments/root-cause/reference_end_policy.mjs b/experiments/root-cause/reference_end_policy.mjs new file mode 100644 index 0000000..32c82e1 --- /dev/null +++ b/experiments/root-cause/reference_end_policy.mjs @@ -0,0 +1,56 @@ +import fs from 'node:fs'; +import vm from 'node:vm'; + +const prodHtml=fs.readFileSync(new URL('../../index.html',import.meta.url),'utf8'); +const scripts=[...prodHtml.matchAll(/]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]); +const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS; +function appContext(){ + const element={width:800,height:600,clientWidth:800,clientHeight:600,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:800,height:600}}}; + 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:800,innerHeight:600,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}}; + const cut=scripts[1].indexOf('// ── boot / teardown'); + const code=scripts[1].slice(0,cut)+`\nglobalThis.internal={state,fastReferenceWorkerSource,precisionFallbackWorkerSource,fastReferenceBits,pixelPrecisionBits,referencePixelForSource,spanMantExp,adaptiveProbeChoice};})();`; + vm.runInNewContext(code,context);return context.internal; +} +const app=appContext(); +function runWorker(source,data){let result;const c={self:{},postMessage:d=>{result=d},performance,console,BigInt,Math,ArrayBuffer,DataView,Float32Array,Uint32Array,Uint8Array};vm.runInNewContext(source,c);c.self.onmessage({data});if(!result)throw new Error('worker returned no result');if(result.type==='error')throw new Error(result.error);return result;} +function buildFast(snap,iter,w,h){const targetBits=app.fastReferenceBits(snap,w),d=runWorker(app.fastReferenceWorkerSource(),{type:'build',id:1,key:'analysis',sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter});d.source={bits:snap.bits,re:BigInt(d.referenceRe),im:BigInt(d.referenceIm),viewRe:snap.re,viewIm:snap.im,span:snap.span,iter};return d;} +function oracle(snap,iter,w,h,indices){const targetBits=app.pixelPrecisionBits(snap,w,40);return runWorker(app.precisionFallbackWorkerSource(),{type:'solve',id:2,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter,terminalClass:-1,indices:Uint32Array.from(indices).buffer});} +const f=Math.fround,U=5.960464477539063e-8; +const add=(a,b)=>f(f(a)+f(b)), mul=(a,b)=>f(f(a)*f(b)); +function cmul(a,b){return [add(mul(a[0],b[0]),-mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]} +const maxabs=v=>Math.max(Math.abs(v[0]),Math.abs(v[1])); +function ld(v,e){return f(v*Math.pow(2,e))} +function refsFrom(buf,n){const dv=new DataView(buf),a=[];for(let i=0;i<=n;i++){const o=i*16;a.push([[dv.getFloat32(o,true),dv.getFloat32(o+4,true)],[dv.getFloat32(o+8,true),dv.getFloat32(o+12,true)]])}return a} +function dsQuick(a,b){const q=f(a+b),e=f(b-f(q-a));return [q,e]} +function dsSum(a,b){const q=f(a+b),bb=f(q-a),e=f(f(a-f(q-bb))+f(b-bb));return [q,e]} +function dsProd(a,b){const q=f(a*b),ca=f(4097*a),ah=f(ca-f(ca-a)),al=f(a-ah),cb=f(4097*b),bh=f(cb-f(cb-b)),bl=f(b-bh);let e=f(f(ah*bh)-q);e=f(e+f(ah*bl));e=f(e+f(al*bh));e=f(e+f(al*bl));return [q,e]} +function dsAdd(a,b){const q=dsSum(a[0],b[0]);return dsQuick(q[0],f(q[1]+f(a[1]+b[1])))} +const dsNeg=a=>[-a[0],-a[1]],dsSub=(a,b)=>dsAdd(a,dsNeg(b)); +function dsMul(a,b){const q=dsProd(a[0],b[0]);let e=f(q[1]+f(a[0]*b[1]));e=f(e+f(a[1]*b[0]));e=f(e+f(a[1]*b[1]));return dsQuick(q[0],e)} +function dsScale(a,b){const q=dsProd(a[0],b);return dsQuick(q[0],f(q[1]+f(a[1]*b)))} +const dsPow2=(a,e)=>[ld(a[0],e),ld(a[1],e)],dsVal=a=>f(a[0]+a[1]); +const cdsAdd=(a,b)=>[dsAdd(a[0],b[0]),dsAdd(a[1],b[1])]; +const cdsMul=(a,b)=>[dsSub(dsMul(a[0],b[0]),dsMul(a[1],b[1])),dsAdd(dsMul(a[0],b[1]),dsMul(a[1],b[0]))]; +const cdsScale=(a,b)=>[dsScale(a[0],b),dsScale(a[1],b)],cdsPow2=(a,e)=>[dsPow2(a[0],e),dsPow2(a[1],e)]; +const cdsMag2=a=>dsAdd(dsMul(a[0],a[0]),dsMul(a[1],a[1])),cdsMax=a=>Math.max(Math.abs(dsVal(a[0])),Math.abs(dsVal(a[1]))); +function simulateDS(snap,w,h,iter,ctx,index){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=1/w,iwh=f(iw),iwl=f(iw-iwh),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy),inv=[iwh,iwl],sm=[mh,ml],dx=dsScale(inv,ox),dy=dsScale(inv,oy),d0=[dsMul(sm,dx),dsMul(sm,dy)];let d=d0,ww=[[0,0],[0,0]],scaleExp=se.exp,n=0,m=0;const refs=refsFrom(ctx.refs,ctx.refLen);while(true){const rp=refs[m],r=[[rp[0][0],rp[1][0]],[rp[0][1],rp[1][1]]],delta=cdsPow2(ww,scaleExp),z=cdsAdd(r,delta),mag=cdsMag2(z);if(mag[0]>4||(mag[0]===4&&mag[1]>0))return{kind:'escape',n};if(n>=iter)return{kind:'op',n};if(m>=ctx.refLen)return{kind:'refend',n};const linear=cdsScale(cdsMul(r,ww),2),sq=cdsPow2(cdsMul(ww,ww),scaleExp);ww=cdsAdd(cdsAdd(linear,sq),d);m++;n++;const mm=Math.max(cdsMax(ww),cdsMax(d));if(!Number.isFinite(mm)||mm>=1e30)return{kind:'range',n};if(mm>65536){ww=cdsScale(ww,1/65536);d=cdsScale(d,1/65536);scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=cdsScale(ww,65536);d=cdsScale(d,65536);scaleExp-=16}if(scaleExp>126)return{kind:'range',n};}} +function simulate(snap,w,h,iter,ctx,index,guarded){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=1/w,iwh=f(iw),iwl=f(iw-iwh),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy);let d; + if(guarded){const dxh=mul(ox,iwh),dxl=mul(ox,iwl),dyh=mul(oy,iwh),dyl=mul(oy,iwl);d=[add(mul(mh,dxh),add(mul(mh,dxl),mul(ml,dxh))),add(mul(mh,dyh),add(mul(mh,dyl),mul(ml,dyh)))];} + else {const dx=f(ox/f(w)),dy=f(oy/f(w));d=[mul(mh,dx),mul(mh,dy)];} + let ww=[0,0],scaleExp=se.exp,n=0,m=0,err=4*U*(maxabs(d)+1e-30);const refs=refsFrom(ctx.refs,ctx.refLen); + while(true){const rp=refs[Math.min(m,ctx.refLen)],delta=[ld(ww[0],scaleExp),ld(ww[1],scaleExp)],z=[add(rp[0][0],add(rp[1][0],delta[0])),add(rp[0][1],add(rp[1][1],delta[1]))],mag=add(mul(z[0],z[0]),mul(z[1],z[1])); + if(mag>4){if(!guarded)return{kind:'escape',n};const errAbs=Math.abs(ld(err,scaleExp))+U*(maxabs(z)+maxabs(delta)+1e-30);if(Math.hypot(z[0],z[1])-errAbs>2)return{kind:'escape',n};return{kind:'uncertain',n};} + if(n>=iter)return{kind:'op',n};if(m>=ctx.refLen)return{kind:'refend',n}; + const refAbs=maxabs(rp[0])+maxabs(rp[1]),wAbs=maxabs(ww),dAbs=maxabs(d),deltaAbs=maxabs(delta),ww2=cmul(ww,ww),sq=[ld(ww2[0],scaleExp),ld(ww2[1],scaleExp)],sqAbs=maxabs(sq),gain=2*refAbs+2*deltaAbs,roundErr=U*(2*refAbs*wAbs+sqAbs+dAbs+1e-30);if(guarded)err=f(gain*err+roundErr); + const l1=cmul(rp[0],ww),l2=cmul(rp[1],ww),linear=[f(2*f(l1[0]+l2[0])),f(2*f(l1[1]+l2[1]))];ww=[add(add(linear[0],sq[0]),d[0]),add(add(linear[1],sq[1]),d[1])];m++;n++;const mm=Math.max(maxabs(ww),maxabs(d));if(!Number.isFinite(mm)||mm>=1e30||guarded&&(!Number.isFinite(err)||err>1e35))return{kind:'range',n};if(mm>65536){ww=[mul(ww[0],1/65536),mul(ww[1],1/65536)];d=[mul(d[0],1/65536),mul(d[1],1/65536)];if(guarded)err=f(err/65536);scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=[mul(ww[0],65536),mul(ww[1],65536)];d=[mul(d[0],65536),mul(d[1],65536)];if(guarded)err=f(err*65536);scaleExp-=16}if(scaleExp>126)return{kind:'range',n}; + } +} +function grid(w,h,nx=8,ny=5){const out=[];for(let j=0;j=iter)return iter;const choices=[350,512,768,1024,1536,2048].filter((x,i,a)=>x<=refLen&&a.indexOf(x)===i);if(choices.length)return Math.max(...choices);return Math.max(1,Math.min(iter,Math.max(64,Math.floor(refLen/64)*64)))}const rows=[];for(const c of cases){const snap={bits:c.bits,re:BigInt(c.re),im:BigInt(c.im),span:BigInt(c.sp)},w=1365,h=768,requested=its[c.name],ctx=buildFast(snap,requested,w,h),indices=gridN(w,h,16,10);for(const mode of ['old','new']){const iter=mode==='old'?oldSnap(ctx.refLen,requested):requested,o=oracle(snap,iter,w,h,indices),meta=new Uint32Array(o.meta);let fast={},oracleEscape=0;for(let k=0;k>>28)&3)===1;oracleEscape+=exact;const q=simulate(snap,w,h,iter,ctx,indices[k],false);fast[q.kind]=(fast[q.kind]||0)+1;}rows.push({case:c.name,mode,requested,refLen:ctx.refLen,iter,samples:indices.length,oracleEscape,fast})}}console.log(JSON.stringify(rows,null,2)); \ No newline at end of file diff --git a/experiments/root-cause/reference_end_recovery.mjs b/experiments/root-cause/reference_end_recovery.mjs new file mode 100644 index 0000000..0522e82 --- /dev/null +++ b/experiments/root-cause/reference_end_recovery.mjs @@ -0,0 +1,56 @@ +import fs from 'node:fs'; +import vm from 'node:vm'; + +const prodHtml=fs.readFileSync(new URL('../../index.html',import.meta.url),'utf8'); +const scripts=[...prodHtml.matchAll(/]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]); +const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS; +function appContext(){ + const element={width:800,height:600,clientWidth:800,clientHeight:600,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:800,height:600}}}; + 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:800,innerHeight:600,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}}; + const cut=scripts[1].indexOf('// ── boot / teardown'); + const code=scripts[1].slice(0,cut)+`\nglobalThis.internal={state,fastReferenceWorkerSource,precisionFallbackWorkerSource,fastReferenceBits,pixelPrecisionBits,referencePixelForSource,spanMantExp,adaptiveProbeChoice};})();`; + vm.runInNewContext(code,context);return context.internal; +} +const app=appContext(); +function runWorker(source,data){let result;const c={self:{},postMessage:d=>{result=d},performance,console,BigInt,Math,ArrayBuffer,DataView,Float32Array,Uint32Array,Uint8Array};vm.runInNewContext(source,c);c.self.onmessage({data});if(!result)throw new Error('worker returned no result');if(result.type==='error')throw new Error(result.error);return result;} +function buildFast(snap,iter,w,h){const targetBits=app.fastReferenceBits(snap,w),d=runWorker(app.fastReferenceWorkerSource(),{type:'build',id:1,key:'analysis',sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter});d.source={bits:snap.bits,re:BigInt(d.referenceRe),im:BigInt(d.referenceIm),viewRe:snap.re,viewIm:snap.im,span:snap.span,iter};return d;} +function oracle(snap,iter,w,h,indices){const targetBits=app.pixelPrecisionBits(snap,w,40);return runWorker(app.precisionFallbackWorkerSource(),{type:'solve',id:2,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter,terminalClass:-1,indices:Uint32Array.from(indices).buffer});} +const f=Math.fround,U=5.960464477539063e-8; +const add=(a,b)=>f(f(a)+f(b)), mul=(a,b)=>f(f(a)*f(b)); +function cmul(a,b){return [add(mul(a[0],b[0]),-mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]} +const maxabs=v=>Math.max(Math.abs(v[0]),Math.abs(v[1])); +function ld(v,e){return f(v*Math.pow(2,e))} +function refsFrom(buf,n){const dv=new DataView(buf),a=[];for(let i=0;i<=n;i++){const o=i*16;a.push([[dv.getFloat32(o,true),dv.getFloat32(o+4,true)],[dv.getFloat32(o+8,true),dv.getFloat32(o+12,true)]])}return a} +function dsQuick(a,b){const q=f(a+b),e=f(b-f(q-a));return [q,e]} +function dsSum(a,b){const q=f(a+b),bb=f(q-a),e=f(f(a-f(q-bb))+f(b-bb));return [q,e]} +function dsProd(a,b){const q=f(a*b),ca=f(4097*a),ah=f(ca-f(ca-a)),al=f(a-ah),cb=f(4097*b),bh=f(cb-f(cb-b)),bl=f(b-bh);let e=f(f(ah*bh)-q);e=f(e+f(ah*bl));e=f(e+f(al*bh));e=f(e+f(al*bl));return [q,e]} +function dsAdd(a,b){const q=dsSum(a[0],b[0]);return dsQuick(q[0],f(q[1]+f(a[1]+b[1])))} +const dsNeg=a=>[-a[0],-a[1]],dsSub=(a,b)=>dsAdd(a,dsNeg(b)); +function dsMul(a,b){const q=dsProd(a[0],b[0]);let e=f(q[1]+f(a[0]*b[1]));e=f(e+f(a[1]*b[0]));e=f(e+f(a[1]*b[1]));return dsQuick(q[0],e)} +function dsScale(a,b){const q=dsProd(a[0],b);return dsQuick(q[0],f(q[1]+f(a[1]*b)))} +const dsPow2=(a,e)=>[ld(a[0],e),ld(a[1],e)],dsVal=a=>f(a[0]+a[1]); +const cdsAdd=(a,b)=>[dsAdd(a[0],b[0]),dsAdd(a[1],b[1])]; +const cdsMul=(a,b)=>[dsSub(dsMul(a[0],b[0]),dsMul(a[1],b[1])),dsAdd(dsMul(a[0],b[1]),dsMul(a[1],b[0]))]; +const cdsScale=(a,b)=>[dsScale(a[0],b),dsScale(a[1],b)],cdsPow2=(a,e)=>[dsPow2(a[0],e),dsPow2(a[1],e)]; +const cdsMag2=a=>dsAdd(dsMul(a[0],a[0]),dsMul(a[1],a[1])),cdsMax=a=>Math.max(Math.abs(dsVal(a[0])),Math.abs(dsVal(a[1]))); +function simulateDS(snap,w,h,iter,ctx,index){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=1/w,iwh=f(iw),iwl=f(iw-iwh),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy),inv=[iwh,iwl],sm=[mh,ml],dx=dsScale(inv,ox),dy=dsScale(inv,oy),d0=[dsMul(sm,dx),dsMul(sm,dy)];let d=d0,ww=[[0,0],[0,0]],scaleExp=se.exp,n=0,m=0;const refs=refsFrom(ctx.refs,ctx.refLen);while(true){const rp=refs[m],r=[[rp[0][0],rp[1][0]],[rp[0][1],rp[1][1]]],delta=cdsPow2(ww,scaleExp),z=cdsAdd(r,delta),mag=cdsMag2(z);if(mag[0]>4||(mag[0]===4&&mag[1]>0))return{kind:'escape',n};if(n>=iter)return{kind:'op',n};if(m>=ctx.refLen)return{kind:'refend',n};const linear=cdsScale(cdsMul(r,ww),2),sq=cdsPow2(cdsMul(ww,ww),scaleExp);ww=cdsAdd(cdsAdd(linear,sq),d);m++;n++;const mm=Math.max(cdsMax(ww),cdsMax(d));if(!Number.isFinite(mm)||mm>=1e30)return{kind:'range',n};if(mm>65536){ww=cdsScale(ww,1/65536);d=cdsScale(d,1/65536);scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=cdsScale(ww,65536);d=cdsScale(d,65536);scaleExp-=16}if(scaleExp>126)return{kind:'range',n};}} +function simulate(snap,w,h,iter,ctx,index,guarded){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=1/w,iwh=f(iw),iwl=f(iw-iwh),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy);let d; + if(guarded){const dxh=mul(ox,iwh),dxl=mul(ox,iwl),dyh=mul(oy,iwh),dyl=mul(oy,iwl);d=[add(mul(mh,dxh),add(mul(mh,dxl),mul(ml,dxh))),add(mul(mh,dyh),add(mul(mh,dyl),mul(ml,dyh)))];} + else {const dx=f(ox/f(w)),dy=f(oy/f(w));d=[mul(mh,dx),mul(mh,dy)];} + let ww=[0,0],scaleExp=se.exp,n=0,m=0,err=4*U*(maxabs(d)+1e-30);const refs=refsFrom(ctx.refs,ctx.refLen); + while(true){const rp=refs[Math.min(m,ctx.refLen)],delta=[ld(ww[0],scaleExp),ld(ww[1],scaleExp)],z=[add(rp[0][0],add(rp[1][0],delta[0])),add(rp[0][1],add(rp[1][1],delta[1]))],mag=add(mul(z[0],z[0]),mul(z[1],z[1])); + if(mag>4){if(!guarded)return{kind:'escape',n};const errAbs=Math.abs(ld(err,scaleExp))+U*(maxabs(z)+maxabs(delta)+1e-30);if(Math.hypot(z[0],z[1])-errAbs>2)return{kind:'escape',n};return{kind:'uncertain',n};} + if(n>=iter)return{kind:'op',n};if(m>=ctx.refLen)return{kind:'refend',n}; + const refAbs=maxabs(rp[0])+maxabs(rp[1]),wAbs=maxabs(ww),dAbs=maxabs(d),deltaAbs=maxabs(delta),ww2=cmul(ww,ww),sq=[ld(ww2[0],scaleExp),ld(ww2[1],scaleExp)],sqAbs=maxabs(sq),gain=2*refAbs+2*deltaAbs,roundErr=U*(2*refAbs*wAbs+sqAbs+dAbs+1e-30);if(guarded)err=f(gain*err+roundErr); + const l1=cmul(rp[0],ww),l2=cmul(rp[1],ww),linear=[f(2*f(l1[0]+l2[0])),f(2*f(l1[1]+l2[1]))];ww=[add(add(linear[0],sq[0]),d[0]),add(add(linear[1],sq[1]),d[1])];m++;n++;const mm=Math.max(maxabs(ww),maxabs(d));if(!Number.isFinite(mm)||mm>=1e30||guarded&&(!Number.isFinite(err)||err>1e35))return{kind:'range',n};if(mm>65536){ww=[mul(ww[0],1/65536),mul(ww[1],1/65536)];d=[mul(d[0],1/65536),mul(d[1],1/65536)];if(guarded)err=f(err/65536);scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=[mul(ww[0],65536),mul(ww[1],65536)];d=[mul(d[0],65536),mul(d[1],65536)];if(guarded)err=f(err*65536);scaleExp-=16}if(scaleExp>126)return{kind:'range',n}; + } +} +function grid(w,h,nx=8,ny=5){const out=[];for(let j=0;j>>28)&3,reason=(m[i]>>>20)&255;if(cls===1)escaped++;else if(reason===6)op++;}}rows.push({case:c.name,iter,refLen:ctx.refLen,samples:indices.length,referenceEnd:refend.length,precisionFallback:{accepted,escaped,operationLimit:op,rejected}})}console.log(JSON.stringify(rows,null,2)); \ No newline at end of file diff --git a/experiments/root-cause/risk_threshold_test.tmp.mjs b/experiments/root-cause/risk_threshold_test.tmp.mjs new file mode 100644 index 0000000..76370ef --- /dev/null +++ b/experiments/root-cause/risk_threshold_test.tmp.mjs @@ -0,0 +1,36 @@ +import fs from 'node:fs'; +import vm from 'node:vm'; +const prodHtml=fs.readFileSync(new URL('../../index.html',import.meta.url),'utf8'); +const scripts=[...prodHtml.matchAll(/]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]); +const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS; +function appContext(){ + const element={width:1365,height:768,clientWidth:1365,clientHeight:768,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:1365,height:768}}}; + 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:1365,innerHeight:768,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}}; + const cut=scripts[1].indexOf('// ── boot / teardown'); + const code=scripts[1].slice(0,cut)+`\nglobalThis.internal={state,fastReferenceWorkerSource,referenceWorkerSource,precisionFallbackWorkerSource,fastReferenceBits,pixelPrecisionBits,referencePixelForSource,spanMantExp,adaptiveProbeSources,adaptiveProbeChoice,adaptiveIterationBonus,fixedSourceForPixel,log2FixedAt};})();`; + vm.runInNewContext(code,context);return context.internal; +} +const app=appContext(); +function runWorker(source,data){let result;const c={self:{},postMessage:d=>{result=d},performance,console,BigInt,Math,ArrayBuffer,DataView,Float32Array,Uint32Array,Uint8Array};vm.runInNewContext(source,c);c.self.onmessage({data});if(!result)throw new Error('no result');if(result.type==='error')throw new Error(result.error);return result;} +function maxAdaptive(snap,w){const spanLog2=app.log2FixedAt(snap.span,snap.bits),z=Math.max(0,Math.log10(3.4)-spanLog2/Math.log2(10)),screenBits=Math.max(0,Math.log2(w)+z*Math.LOG2E*Math.LN10),bonus=app.adaptiveIterationBonus(screenBits,1);return Math.min(16384,Math.max(350,350+bonus));} +function initialIter(snap,w,h){const adaptive=maxAdaptive(snap,w),sources=app.adaptiveProbeSources(snap,w,h),targetBits=Math.max(app.pixelPrecisionBits(snap,w,40)+32),scored=runWorker(app.referenceWorkerSource(),{type:'score',id:1,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter:adaptive,candidates:sources.map(c=>({re:c.re.toString(),im:c.im.toString()}))});return {adaptive,...app.adaptiveProbeChoice(scored.escapes||[],adaptive,350),escapes:scored.escapes};} +function buildFast(snap,iter,w,h){const targetBits=app.fastReferenceBits(snap,w),d=runWorker(app.fastReferenceWorkerSource(),{type:'build',id:2,key:'analysis',sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter});d.source={bits:snap.bits,re:BigInt(d.referenceRe),im:BigInt(d.referenceIm),viewRe:snap.re,viewIm:snap.im,span:snap.span,iter};return d;} +function oracle(snap,iter,w,h,indices){const targetBits=app.pixelPrecisionBits(snap,w,40);return runWorker(app.precisionFallbackWorkerSource(),{type:'solve',id:3,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter,terminalClass:-1,indices:Uint32Array.from(indices).buffer});} +const f=Math.fround;const add=(a,b)=>f(f(a)+f(b)),mul=(a,b)=>f(f(a)*f(b));const maxabs=v=>Math.max(Math.abs(v[0]),Math.abs(v[1])); +function cmul(a,b){return [add(mul(a[0],b[0]),-mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]} +function ld(v,e){return f(v*Math.pow(2,e))} +function refsFrom(buf,n){const dv=new DataView(buf),a=[];for(let i=0;i<=n;i++){const o=i*16;a.push([[dv.getFloat32(o,true),dv.getFloat32(o+4,true)],[dv.getFloat32(o+8,true),dv.getFloat32(o+12,true)]])}return a} +function simulate(snap,w,h,iter,ctx,index,{rebase=false,glitchG=0}={}){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=f(1/w),iwlo=f(1/w-iw),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy),dxh=mul(ox,iw),dxl=mul(ox,iwlo),dyh=mul(oy,iw),dyl=mul(oy,iwlo);let d=[add(mul(mh,dxh),add(mul(mh,dxl),mul(ml,dxh))),add(mul(mh,dyh),add(mul(mh,dyl),mul(ml,dyh)))];let ww=[0,0],scaleExp=se.exp,n=0,m=0,rebases=0,glitches=0,minRatio=Infinity;const refs=refsFrom(ctx.refs,ctx.refLen); + let lastRiskN=-999999;let riskCount=0;while(true){const rp=refs[Math.min(m,ctx.refLen)],delta=[ld(ww[0],scaleExp),ld(ww[1],scaleExp)],rv=[add(rp[0][0],rp[1][0]),add(rp[0][1],rp[1][1])],z=[add(rp[0][0],add(rp[1][0],delta[0])),add(rp[0][1],add(rp[1][1],delta[1]))],mag=add(mul(z[0],z[0]),mul(z[1],z[1])),rmag=add(mul(rv[0],rv[0]),mul(rv[1],rv[1])),dmag=add(mul(delta[0],delta[0]),mul(delta[1],delta[1]));if(m>0&&Number.isFinite(mag)&&Number.isFinite(dmag)&&mag0)minRatio=Math.min(minRatio,mag/rmag); + if(m>0&&glitchG>0&&rmag>0&&mag4)return{kind:'escape',n,rebases,glitches,minRatio,lastRiskN,riskCount,riskAge:n-lastRiskN};if(n>=iter)return{kind:'op',n,rebases,glitches,minRatio};if(m>=ctx.refLen)return{kind:'refend',n,rebases,glitches,minRatio}; + if(rebase&&m>0&&mag=1e30)return{kind:'range',n,rebases,glitches,minRatio};if(mm>65536){ww=[mul(ww[0],1/65536),mul(ww[1],1/65536)];d=[mul(d[0],1/65536),mul(d[1],1/65536)];scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=[mul(ww[0],65536),mul(ww[1],65536)];d=[mul(d[0],65536),mul(d[1],65536)];scaleExp-=16}if(scaleExp>126)return{kind:'range',n,rebases,glitches,minRatio};}} +function grid(w,h,nx=16,ny=10){const out=[];for(let j=0;j>>28)&3)===1,q=simulate(snap,w,h,sel.iter,ctx,indices[k],{});if(q.kind==='escape')esc.push({...q,exact});}const thresholds=[];for(const count of [1,2,4,8,12,16,18,20,24,32,48,64])for(const age of [0,1,2,4,8,16]){const selected=esc.filter(x=>x.riskCount>=count&&x.riskAge<=age);thresholds.push({count,age,selected:selected.length,falseCaught:selected.filter(x=>!x.exact).length});}rows.push({case:c.name,initial:sel.iter,escapes:esc.length,falseEscapes:esc.filter(x=>!x.exact).length,riskCountStats:{min:Math.min(...esc.map(x=>x.riskCount)),max:Math.max(...esc.map(x=>x.riskCount)),median:[...esc.map(x=>x.riskCount)].sort((a,b)=>a-b)[Math.floor(esc.length/2)]},thresholds:thresholds.filter(x=>x.falseCaught>0||x.selected<20)});} +console.log(JSON.stringify(rows,null,2)); diff --git a/experiments/root-cause/riskall.tmp.mjs b/experiments/root-cause/riskall.tmp.mjs new file mode 100644 index 0000000..1910709 --- /dev/null +++ b/experiments/root-cause/riskall.tmp.mjs @@ -0,0 +1,36 @@ +import fs from 'node:fs'; +import vm from 'node:vm'; +const prodHtml=fs.readFileSync(new URL('../../index.html',import.meta.url),'utf8'); +const scripts=[...prodHtml.matchAll(/]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]); +const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS; +function appContext(){ + const element={width:1365,height:768,clientWidth:1365,clientHeight:768,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:1365,height:768}}}; + 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:1365,innerHeight:768,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}}; + const cut=scripts[1].indexOf('// ── boot / teardown'); + const code=scripts[1].slice(0,cut)+`\nglobalThis.internal={state,fastReferenceWorkerSource,referenceWorkerSource,precisionFallbackWorkerSource,fastReferenceBits,pixelPrecisionBits,referencePixelForSource,spanMantExp,adaptiveProbeSources,adaptiveProbeChoice,adaptiveIterationBonus,fixedSourceForPixel,log2FixedAt};})();`; + vm.runInNewContext(code,context);return context.internal; +} +const app=appContext(); +function runWorker(source,data){let result;const c={self:{},postMessage:d=>{result=d},performance,console,BigInt,Math,ArrayBuffer,DataView,Float32Array,Uint32Array,Uint8Array};vm.runInNewContext(source,c);c.self.onmessage({data});if(!result)throw new Error('no result');if(result.type==='error')throw new Error(result.error);return result;} +function maxAdaptive(snap,w){const spanLog2=app.log2FixedAt(snap.span,snap.bits),z=Math.max(0,Math.log10(3.4)-spanLog2/Math.log2(10)),screenBits=Math.max(0,Math.log2(w)+z*Math.LOG2E*Math.LN10),bonus=app.adaptiveIterationBonus(screenBits,1);return Math.min(16384,Math.max(350,350+bonus));} +function initialIter(snap,w,h){const adaptive=maxAdaptive(snap,w),sources=app.adaptiveProbeSources(snap,w,h),targetBits=Math.max(app.pixelPrecisionBits(snap,w,40)+32),scored=runWorker(app.referenceWorkerSource(),{type:'score',id:1,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter:adaptive,candidates:sources.map(c=>({re:c.re.toString(),im:c.im.toString()}))});return {adaptive,...app.adaptiveProbeChoice(scored.escapes||[],adaptive,350),escapes:scored.escapes};} +function buildFast(snap,iter,w,h){const targetBits=app.fastReferenceBits(snap,w),d=runWorker(app.fastReferenceWorkerSource(),{type:'build',id:2,key:'analysis',sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter});d.source={bits:snap.bits,re:BigInt(d.referenceRe),im:BigInt(d.referenceIm),viewRe:snap.re,viewIm:snap.im,span:snap.span,iter};return d;} +function oracle(snap,iter,w,h,indices){const targetBits=app.pixelPrecisionBits(snap,w,40);return runWorker(app.precisionFallbackWorkerSource(),{type:'solve',id:3,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter,terminalClass:-1,indices:Uint32Array.from(indices).buffer});} +const f=Math.fround;const add=(a,b)=>f(f(a)+f(b)),mul=(a,b)=>f(f(a)*f(b));const maxabs=v=>Math.max(Math.abs(v[0]),Math.abs(v[1])); +function cmul(a,b){return [add(mul(a[0],b[0]),-mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]} +function ld(v,e){return f(v*Math.pow(2,e))} +function refsFrom(buf,n){const dv=new DataView(buf),a=[];for(let i=0;i<=n;i++){const o=i*16;a.push([[dv.getFloat32(o,true),dv.getFloat32(o+4,true)],[dv.getFloat32(o+8,true),dv.getFloat32(o+12,true)]])}return a} +function simulate(snap,w,h,iter,ctx,index,{rebase=false,glitchG=0}={}){const px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap),mh=f(se.mant),ml=f(se.mant-mh),iw=f(1/w),iwlo=f(1/w-iw),gx=f(px+.5),gy=f(py+.5),ox=f(gx-f(rpix.x)),oy=f(f(rpix.y)-gy),dxh=mul(ox,iw),dxl=mul(ox,iwlo),dyh=mul(oy,iw),dyl=mul(oy,iwlo);let d=[add(mul(mh,dxh),add(mul(mh,dxl),mul(ml,dxh))),add(mul(mh,dyh),add(mul(mh,dyl),mul(ml,dyh)))];let ww=[0,0],scaleExp=se.exp,n=0,m=0,rebases=0,glitches=0,minRatio=Infinity;const refs=refsFrom(ctx.refs,ctx.refLen); + let lastRiskN=-999999;let riskCount=0;while(true){const rp=refs[Math.min(m,ctx.refLen)],delta=[ld(ww[0],scaleExp),ld(ww[1],scaleExp)],rv=[add(rp[0][0],rp[1][0]),add(rp[0][1],rp[1][1])],z=[add(rp[0][0],add(rp[1][0],delta[0])),add(rp[0][1],add(rp[1][1],delta[1]))],mag=add(mul(z[0],z[0]),mul(z[1],z[1])),rmag=add(mul(rv[0],rv[0]),mul(rv[1],rv[1])),dmag=add(mul(delta[0],delta[0]),mul(delta[1],delta[1]));if(m>0&&Number.isFinite(mag)&&Number.isFinite(dmag)&&mag0)minRatio=Math.min(minRatio,mag/rmag); + if(m>0&&glitchG>0&&rmag>0&&mag4)return{kind:'escape',n,rebases,glitches,minRatio,lastRiskN,riskCount,riskAge:n-lastRiskN};if(n>=iter)return{kind:'op',n,rebases,glitches,minRatio};if(m>=ctx.refLen)return{kind:'refend',n,rebases,glitches,minRatio}; + if(rebase&&m>0&&mag=1e30)return{kind:'range',n,rebases,glitches,minRatio};if(mm>65536){ww=[mul(ww[0],1/65536),mul(ww[1],1/65536)];d=[mul(d[0],1/65536),mul(d[1],1/65536)];scaleExp+=16}else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=[mul(ww[0],65536),mul(ww[1],65536)];d=[mul(d[0],65536),mul(d[1],65536)];scaleExp-=16}if(scaleExp>126)return{kind:'range',n,rebases,glitches,minRatio};}} +function grid(w,h,nx=16,ny=10){const out=[];for(let j=0;j>>28)&3)===1,q=simulate(snap,w,h,sel.iter,ctx,indices[k],{});if(q.kind==='escape')esc.push({...q,exact});}const thresholds=[];for(const count of [1,2,4,8,12,16,18,20,24,32,48,64])for(const age of [0,1,2,4,8,16]){const selected=esc.filter(x=>x.riskCount>=count&&x.riskAge<=age);thresholds.push({count,age,selected:selected.length,falseCaught:selected.filter(x=>!x.exact).length});}rows.push({case:c.name,initial:sel.iter,escapes:esc.length,falseEscapes:esc.filter(x=>!x.exact).length,riskCountStats:{min:Math.min(...esc.map(x=>x.riskCount)),max:Math.max(...esc.map(x=>x.riskCount)),median:[...esc.map(x=>x.riskCount)].sort((a,b)=>a-b)[Math.floor(esc.length/2)]},thresholds});} +console.log(JSON.stringify(rows,null,2)); diff --git a/index.html b/index.html index 6ece015..64ab0e4 100644 --- a/index.html +++ b/index.html @@ -194,9 +194,10 @@ fn main(@builtin(global_invocation_id) gid:vec3){ if(!claim(out)){return;} let q=atomicAdd(&count[0],1u);if(q>=p.unknownOnly){release(out);return;} let px=out%p.fullW;let py=out/p.fullW;let gx=f32(px)+p.sampleX;let gy=f32(py)+p.sampleY; - let dx=(gx-p.refPixelX)/f32(p.fullW);let dy=(p.refPixelY-gy)/f32(p.fullW); - let d=vec2(p.spanMantHi*dx,p.spanMantHi*dy); - states[q]=DeepState(d,vec2(0.0),p.spanExp,0u,0u,1.0*F32_U*maxabs(d));pixelMap[q]=out;queue[q]=q; + let ox=gx-p.refPixelX;let oy=p.refPixelY-gy; + let dxHi=ox*p.invFullWHi;let dxLo=ox*p.invFullWLo;let dyHi=oy*p.invFullWHi;let dyLo=oy*p.invFullWLo; + let d=vec2(p.spanMantHi*dxHi+(p.spanMantHi*dxLo+p.spanMantLo*dxHi),p.spanMantHi*dyHi+(p.spanMantHi*dyLo+p.spanMantLo*dyHi)); + states[q]=DeepState(d,vec2(0.0),p.spanExp,0u,0u,4.0*F32_U*(maxabs(d)+1.0e-30));pixelMap[q]=out;queue[q]=q; } `; const FAST_PERTURB_WGSL=COMMON+String.raw` @@ -943,6 +944,19 @@ function numericViewKey(){return[state.bits,state.re,state.im,state.span,state.b // ── iteration / quality policy ─────────────────────────────────────────── const ADAPTIVE_ITER_QUANTUM=64,ADAPTIVE_PROBE_COLS=4,ADAPTIVE_PROBE_ROWS=3; const ADAPTIVE_PROBE_CHOICES=[512,768,1024,1536,2048]; +// The initial-iteration choice is a pure function of the current view/probe. +// Never feed the previous frame's choice back into the numeric policy: doing +// so makes identical coordinates history-dependent and can hide real numeric +// discontinuities instead of fixing them. +function nextContinuationTarget(iter,maxIter,session){ + if(iter>=maxIter)return maxIter; + if(iter<1024)return Math.min(maxIter,Math.max(iter+1,iter*2)); + const initial=Math.max(1,session?.initial||session?.active||1),active=Math.max(0,session?.active||0),ratio=active/initial; + let factor=2; + if(active<=8192||ratio<=.02)factor=4/3;else if(ratio<=.10)factor=1.5; + const target=Math.ceil(iter*factor/64)*64; + return Math.min(maxIter,Math.max(iter+64,target)); +} function adaptiveIterationBonus(screenBits,q,quantum=ADAPTIVE_ITER_QUANTUM){const raw=48*Math.max(0,Number(q)||0)*Math.max(0,Number(screenBits)||0);return raw>0?Math.ceil(raw/quantum)*quantum:0} function maxIter(){if(!state.adaptive)return Math.min(state.baseIter,state.continuationBudget||state.baseIter);const z=zoomExp(),w=Math.max(1,canvas.width||innerWidth||1),screenBits=Math.max(0,Math.log2(w)+z*Math.LOG2E*Math.LN10),bonus=adaptiveIterationBonus(screenBits,1);return Math.min(state.continuationBudget||12000,12000,Math.max(state.baseIter,state.baseIter+bonus))} function canUseStableReprojection(){ @@ -1109,7 +1123,6 @@ async function selectAdaptiveInitialIter(snap,adaptiveIter,width,height,token=st try{ const scored=await referenceScorer.scoreFixedCandidates(snap,adaptiveIter,width,height,sources);if(token!==state.token)return null; const selected=adaptiveProbeChoice(scored.escapes||[],adaptiveIter,state.baseIter),probeMs=performance.now()-started; - return{...selected,adaptiveIter,probeMs,samples:sources.length,escapes:scored.escapes||[]}; }catch(error){ if(token!==state.token)return null; @@ -1124,14 +1137,17 @@ function roundShift(v,b){if(b<=0)return v;const neg=v<0n,a=neg?-v:v,half=1n<<(Bi function orbit(bits,re,im,iter){const B=BigInt(bits),half=1n<<(B-1n),round=v=>{const neg=v<0n,a=neg?-v:v,q=(a+half)>>B;return neg?-q:q},BAIL=16n*(1n<>B;zi2=(zi*zi+half)>>B;if(zr2+zi2>BAIL)escape=n+1}rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);return{rr,ri,refLen:escape||iter,escape}} function orbitEscape(bits,re,im,iter){const B=BigInt(bits),half=1n<<(B-1n),round=v=>{const neg=v<0n,a=neg?-v:v,q=(a+half)>>B;return neg?-q:q},BAIL=16n*(1n<>B;zi2=(zi*zi+half)>>B;if(zr2+zi2>BAIL)return n+1}return 0} function chooseReference(bits,re,im,span,w,h,iter,centerEscape=0,probeBits=bits){ - let best={re,im,escape:centerEscape,score:centerEscape||iter}; - if(centerEscape===0||span<=0n||w<=0||h<=0)return best; - // Score candidates at the packed-reference precision used by the orbit. - const pb=Math.max(64,Math.min(bits,probeBits|0)),bw=BigInt(w),seen=new Set([re+':'+im]),goal=Math.max(4096,Math.floor(iter/3)); - const probe=(sr,si)=>{const key=sr+':'+si;if(seen.has(key))return false;seen.add(key);const rr=requant(sr,bits,pb),ii=requant(si,bits,pb),escape=orbitEscape(pb,rr,ii,iter),score=escape||iter;if(score>best.score)best={re:sr,im:si,escape,score};return escape===0||score>=goal}; - // Sample the actual pixel-centre domain with an 8x6 checkerboard. - const div=(v,d)=>{const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q},den=2n*bw; - for(let jy=0;jy<6;jy++)for(let ix=0;ix<8;ix++){if(((ix+jy)&1)!==1)continue;const x=Math.floor((ix+.5)*w/8),y=Math.floor((jy+.5)*h/6),sr=re+div(span*(2n*BigInt(x)+1n-bw),den),si=im+div(span*(BigInt(h)-2n*BigInt(y)-1n),den);if(probe(sr,si))return best} + let best={re,im,escape:centerEscape,score:centerEscape||iter,dist2:0}; + if(span<=0n||w<=0||h<=0)return best; + // The primary FAST reference is chosen from a denser 12x8 checkerboard, + // ordered from the viewport centre outwards. A nearby survivor therefore + // wins before a distant one, avoiding large reference jumps after tiny pans. + const pb=Math.max(64,Math.min(bits,probeBits|0)),bw=BigInt(w),seen=new Set([re+':'+im]),goal=Math.max(4096,Math.floor(iter/3)),div=(v,d)=>{const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q},den=2n*bw,candidates=[]; + for(let jy=0;jy<8;jy++)for(let ix=0;ix<12;ix++){if(((ix+jy)&1)!==1)continue;const x=Math.floor((ix+.5)*w/12),y=Math.floor((jy+.5)*h/8),dx=x+.5-w*.5,dy=y+.5-h*.5;candidates.push({x,y,dist2:dx*dx+dy*dy})} + candidates.sort((a,b)=>a.dist2-b.dist2||a.y-b.y||a.x-b.x); + const probe=(sr,si,dist2)=>{const key=sr+':'+si;if(seen.has(key))return false;seen.add(key);const rr=requant(sr,bits,pb),ii=requant(si,bits,pb),escape=orbitEscape(pb,rr,ii,iter),score=escape||iter;if(score>best.score||score===best.score&&dist2=goal}; + if(centerEscape===0)return best; + for(const c of candidates){const sr=re+div(span*(2n*BigInt(c.x)+1n-bw),den),si=im+div(span*(BigInt(h)-2n*BigInt(c.y)-1n),den);if(probe(sr,si,c.dist2))return best} return best } function packRefs(rr,ri,refLen){const buf=new ArrayBuffer((refLen+1)*16),dv=new DataView(buf);for(let i=0;i<=refLen;i++){const hr=Math.fround(rr[i]),hi=Math.fround(ri[i]),lr=Math.fround(rr[i]-hr),li=Math.fround(ri[i]-hi),o=i*16;dv.setFloat32(o,hr,true);dv.setFloat32(o+4,hi,true);dv.setFloat32(o+8,lr,true);dv.setFloat32(o+12,li,true)}return buf} @@ -1656,12 +1672,12 @@ async function continuationReferenceForActive(r,session,snap,target,token,prefer const guided=await tryOperationLimitGuidedReference(r,session,snap,target,token,stats,best,tried);if(!guided||token!==state.token)return null;if(guided.used){best=guided.ctx;bestSource=guided.ctx.source;if(best.refLen>=target)return{ctx:best,source:bestSource}} return{ctx:best,source:bestSource}; } -async function refinePixelFrontier(r,snap,initialIter,token,initialStats,certified=0){ +async function refinePixelFrontier(r,snap,initialIter,token,initialStats,certified=0,preferredSource=null){ if(!state.continuationBudget)throw new Error('production quality requires a finite continuation budget'); const maxIter=Math.max(initialIter,state.continuationBudget),minimumIter=maxIter; - let session=await r.beginGpuOperationLimitContinuation(snap,token,{x:canvas.width*.5,y:canvas.height*.5},false,initialStats?.reasons?.operationLimit||0);if(!session||token!==state.token)return null;let iter=initialIter,stats=initialStats,rounds=0,processed=0,retired=0,ctx=null,fixedSource=null,converged=session.active===0||iter>=maxIter; + let session=await r.beginGpuOperationLimitContinuation(snap,token,{x:canvas.width*.5,y:canvas.height*.5},false,initialStats?.reasons?.operationLimit||0);if(!session||token!==state.token)return null;let iter=initialIter,stats=initialStats,rounds=0,processed=0,retired=0,ctx=null,fixedSource=preferredSource,converged=session.active===0||iter>=maxIter; while(!converged&&iter0||!!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,primaryIter:iter,requestedIter}} + // Do not shorten the requested primary iteration count to refLen. If the + // reference orbit ends first, FAST must emit REFERENCE_END so the existing + // numerical-recovery path repairs those pixels before publication. Turning + // reference exhaustion into an earlier OPERATION_LIMIT makes unresolved + // pixels transparent and caused large black regions at specific deep zooms. 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()),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} + const frontier=await refinePixelFrontier(r,snap,iter,token,{total:state.unresolved,reasons:{...(state.unknownReasons||{})}},certified,fastExtended?fastCtx?.source:null);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,primaryIter:iter,requestedIter} } function numericFrameComplete(){return state.fieldView?.complete===true&&numericalFailureCount({reasons:state.unknownReasons||{}})===0} async function renderFrame(){ diff --git a/tests/README.md b/tests/README.md index 98ce359..41a6a40 100644 --- a/tests/README.md +++ b/tests/README.md @@ -32,3 +32,27 @@ node tests/browser_smoke.mjs 9333 ``` `browser_smoke.mjs` はproduction `index.html`から一時的な診断HTMLを生成し、終了時に削除します。テスト用アプリ本体のコピーは保持しません。 + +## Navigation continuity regression (#8) + +Deep FAST reference reuse must be path-independent. Run: + +```bash +node tests/navigation_continuity.mjs +``` + +The test constructs the target view with a fresh FAST reference, then constructs references from nearby pan/zoom views and uses the production `reusableReferencePixel(..., allowNearby=true)` gate. For every reference that production would allow to be reused, it recomputes a deterministic 16×10 sample grid in the target view and checks: + +- field classification (`escape`, `operation-limit`, `reference-end`, `range`) +- escape iteration +- smooth escape value (diagnostic tolerance `1e-4`) + +Any difference is a navigation-continuity failure. The test reads production `index.html` directly and does not use a copied application HTML. + +The current build is known to fail this invariant. To collect the complete report without returning a failing exit status: + +```bash +node tests/navigation_continuity.mjs --report > navigation-continuity.json +``` + +This test is intentionally separate from `regression.mjs`: `regression.mjs` remains the fast structural/numerical sanity suite, while navigation continuity is the targeted defect detector for nearby-reference reuse. diff --git a/tests/kernel_hashes.json b/tests/kernel_hashes.json index 76f0969..564d8e4 100644 --- a/tests/kernel_hashes.json +++ b/tests/kernel_hashes.json @@ -4,7 +4,7 @@ "PRECISION_SCATTER_WGSL": "428ff66314ed0d69bc3bef83d5d0adc554bd89eb4a7137a4a8910f2d1016b03a", "DIRECT_F32_WGSL": "d695042d016fb7060923184db20df42e480af4b223714c5eaca195a8b9b2f18b", "ACTIVE_PREPARE_WGSL": "27bb3af8310bd0784888f2a648896e1fcf5872b4a9a3ade883ebe95a4868fa6e", - "OPERATION_LIMIT_QUEUE_WGSL": "ca83fa6e239a4b429431d65b79f3c2d1b7db840885dbf7c60aa8faa8f07c9026", + "OPERATION_LIMIT_QUEUE_WGSL": "2f11a0c0355a37219c51c91d905abb57922bbec6cf501e2df24d25484da5d0d4", "FAST_PERTURB_WGSL": "c0dcbc5139c2e57e5cee6340024c841d379ffe57d7035cb3dc2a89c8cac50f65", "FAST_PERTURB_POSTSTATS_WGSL": "92d99eaaa928da7920b60700c0ba5520fbf86bb783373ed7545e3d8c2dcffd45", "DEEP_ACTIVE_RESUME_INIT_WGSL": "fc50c00738705568a76bafc10e092e2500977387858839b5f46c83cb870822cf", diff --git a/tests/navigation_continuity.mjs b/tests/navigation_continuity.mjs new file mode 100644 index 0000000..689639c --- /dev/null +++ b/tests/navigation_continuity.mjs @@ -0,0 +1,186 @@ +// Navigation-continuity regression for deep FAST perturbation. +// +// Goal (#8 only): the same target view must not change numerically merely +// because its FAST reference came from a nearby pan/zoom view that production +// considers reusable. +// +// Default mode is a strict regression test: any classification/iteration +// discontinuity exits non-zero. Use --report to emit diagnostics without +// failing; this is useful while fixing a known continuity defect. + +import assert from 'node:assert/strict'; +import fs from 'node:fs/promises'; +import vm from 'node:vm'; +import {createHash} from 'node:crypto'; +import {performance} from 'node:perf_hooks'; + +const reportOnly=process.argv.includes('--report'); +const prodUrl=new URL('../index.html',import.meta.url); +const prodHtml=await fs.readFile(prodUrl,'utf8'); +const productionSha256=createHash('sha256').update(prodHtml).digest('hex'); +const scripts=[...prodHtml.matchAll(/]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]); +assert.equal(scripts.length,2,'expected the production kernel and app scripts'); +for(const script of scripts)new vm.Script(script); + +const kernelContext={}; +vm.runInNewContext(scripts[0],kernelContext); +const kernels=kernelContext.MANDEL_WEBGPU_KERNELS; + +function appContext(){ + const element={width:1365,height:768,clientWidth:1365,clientHeight:768,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:1365,height:768}}}; + 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:1365,innerHeight:768,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}} + }; + const cut=scripts[1].indexOf('// ── boot / teardown'); + assert.ok(cut>0,'boot marker missing'); + const code=scripts[1].slice(0,cut)+`\nglobalThis.internal={fastReferenceWorkerSource,referenceWorkerSource,fastReferenceBits,pixelPrecisionBits,referencePixelForSource,spanMantExp,adaptiveProbeSources,adaptiveProbeChoice,adaptiveIterationBonus,log2FixedAt,reusableReferencePixel};})();`; + vm.runInNewContext(code,context); + return context.internal; +} +const app=appContext(); + +function runWorker(source,data){ + let result; + const context={self:{},postMessage:d=>{result=d},performance,console,BigInt,Math,ArrayBuffer,DataView,Float32Array,Float64Array,Uint32Array,Uint8Array}; + vm.runInNewContext(source,context); + context.self.onmessage({data}); + if(!result)throw new Error('worker produced no result'); + if(result.type==='error')throw new Error(result.error); + return result; +} + +function maxAdaptive(snap,w){ + const spanLog2=app.log2FixedAt(snap.span,snap.bits); + const z=Math.max(0,Math.log10(3.4)-spanLog2/Math.log2(10)); + const screenBits=Math.max(0,Math.log2(w)+z*Math.LOG2E*Math.LN10); + const bonus=app.adaptiveIterationBonus(screenBits,1); + return Math.min(16384,Math.max(350,350+bonus)); +} + +function initialIter(snap,w,h){ + const adaptive=maxAdaptive(snap,w); + const sources=app.adaptiveProbeSources(snap,w,h); + const targetBits=Math.max(app.pixelPrecisionBits(snap,w,40)+32); + const scored=runWorker(app.referenceWorkerSource(),{ + type:'score',id:1,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter:adaptive, + candidates:sources.map(c=>({re:c.re.toString(),im:c.im.toString()})) + }); + return {adaptive,...app.adaptiveProbeChoice(scored.escapes||[],adaptive,350)}; +} + +function buildFast(snap,iter,w,h,id){ + const targetBits=app.fastReferenceBits(snap,w); + const d=runWorker(app.fastReferenceWorkerSource(),{ + type:'build',id,key:`nav-${id}`,sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter + }); + d.source={bits:snap.bits,re:BigInt(d.referenceRe),im:BigInt(d.referenceIm),viewRe:snap.re,viewIm:snap.im,span:snap.span,iter}; + return d; +} + +const f=Math.fround; +const add=(a,b)=>f(f(a)+f(b)); +const sub=(a,b)=>f(f(a)-f(b)); +const mul=(a,b)=>f(f(a)*f(b)); +const div=(a,b)=>f(f(a)/f(b)); +const maxabs=v=>Math.max(Math.abs(v[0]),Math.abs(v[1])); +function cmul(a,b){return[add(mul(a[0],b[0]),-mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]} +function ld(v,e){return f(v*Math.pow(2,e))} +function smoothEscape(n,mag2){const u=Math.log2(Math.max(4.0000005,mag2));return f(n+1-Math.log2(Math.max(1e-20,.5*u)))} +function refsFrom(buf,n){const dv=new DataView(buf),out=[];for(let i=0;i<=n;i++){const o=i*16;out.push([[dv.getFloat32(o,true),dv.getFloat32(o+4,true)],[dv.getFloat32(o+8,true),dv.getFloat32(o+12,true)]])}return out} + +function simulate(snap,w,h,iter,ctx,index){ + if(!ctx._navRefs)ctx._navRefs=refsFrom(ctx.refs,ctx.refLen); + const refs=ctx._navRefs,px=index%w,py=Math.floor(index/w),rpix=app.referencePixelForSource(ctx.source,snap,w,h),se=app.spanMantExp(snap); + const gx=f(px+.5),gy=f(py+.5),dx=div(sub(gx,f(rpix.x)),f(w)),dy=div(sub(f(rpix.y),gy),f(w)); + let d=[mul(f(se.mant),dx),mul(f(se.mant),dy)],ww=[0,0],scaleExp=se.exp,n=0,m=0; + for(;;){ + const rp=refs[Math.min(m,ctx.refLen)],delta=[ld(ww[0],scaleExp),ld(ww[1],scaleExp)],z=[add(rp[0][0],add(rp[1][0],delta[0])),add(rp[0][1],add(rp[1][1],delta[1]))],mag=add(mul(z[0],z[0]),mul(z[1],z[1])); + if(mag>4)return{kind:'escape',n,smooth:smoothEscape(n,mag)}; + if(n>=iter)return{kind:'operation-limit',n,smooth:0}; + if(m>=ctx.refLen)return{kind:'reference-end',n,smooth:0}; + const linearHi=cmul(rp[0],ww),linearLo=cmul(rp[1],ww),linear=[f(2*f(linearHi[0]+linearLo[0])),f(2*f(linearHi[1]+linearLo[1]))],sq0=cmul(ww,ww),sq=[ld(sq0[0],scaleExp),ld(sq0[1],scaleExp)]; + ww=[add(add(linear[0],sq[0]),d[0]),add(add(linear[1],sq[1]),d[1])];m++;n++; + const mm=Math.max(maxabs(ww),maxabs(d)); + if(!Number.isFinite(mm)||mm>=1e30)return{kind:'range',n,smooth:0}; + if(mm>65536){ww=[mul(ww[0],1/65536),mul(ww[1],1/65536)];d=[mul(d[0],1/65536),mul(d[1],1/65536)];scaleExp+=16} + else if(mm>0&&mm<1/65536&&scaleExp>se.exp){ww=[mul(ww[0],65536),mul(ww[1],65536)];d=[mul(d[0],65536),mul(d[1],65536)];scaleExp-=16} + if(scaleExp>126)return{kind:'range',n,smooth:0}; + } +} + +function grid(w,h,nx=16,ny=10){const out=[];for(let j=0;j0n?span:1n}} + +const cases=[ + {name:'mini-1',bits:256,re:'-148292766829529091721540000955948596563327036751005092894699321061162899553437',im:'7497924611962244671337883039784517355486082422571896698766224071500989488498',sp:'1051264689747601722664903477680432560343035141150552570739220680259730166'}, + {name:'mini-2',bits:256,re:'44279008708751415573784266681073018338222298986985889938567243759258661091705',im:'11537730301689015207261638283847108540669173974016900715268231497454915998519',sp:'136466963900827996566957710260403731102960944336062216372133428878224980'}, + {name:'mini-3-black',bits:256,re:'15717041212470055224256502413765101609014610520343947152977545154690386564664',im:'74092703473662244164463718953593537954306995881211045997604051198538528963626',sp:'7384741851176872716004457996517031059911173801417894624776297655376888'}, + {name:'refend-zoom-black',bits:273,re:'-20475306797689005889663451277157868621960315439319427815381950343245479294092514544',im:'966333796787038295532838729476816377447677486901484166221710883227030438393551731',sp:'14873556985132116323960339070653445992212160392759733161044631107644716'}, + {name:'refend-undrawn',bits:307,re:'46025019095185088783974324228976729149454131232585283954369753562823949096514860778965742615',im:'-151398041000872486580541963077907528105710741688397340075259729545288600585999778752201285140',sp:'1361049793375351627487861104794890130303800780964576514408724611446175'} +]; +const paths=[ + {name:'pan-right-0.10',fx:.1,fy:0,scale:1},{name:'pan-left-0.10',fx:-.1,fy:0,scale:1}, + {name:'pan-up-0.10',fx:0,fy:.1,scale:1},{name:'pan-down-0.10',fx:0,fy:-.1,scale:1}, + {name:'zoom-from-0.90',fx:0,fy:0,scale:.9},{name:'zoom-from-1.10',fx:0,fy:0,scale:1.1}, + {name:'pan-diagonal-a',fx:.2,fy:.15,scale:1},{name:'pan-diagonal-b',fx:-.2,fy:-.15,scale:1} +]; + +const w=1365,h=768,indices=grid(w,h),results=[]; +let referenceSerial=10,totalPaths=0,totalComparisons=0,classDifferences=0,iterationDifferences=0,smoothDifferences=0,maxSmoothDelta=0; +const SMOOTH_TOLERANCE=1e-4; + +for(const item of cases){ + const snap={bits:item.bits,re:BigInt(item.re),im:BigInt(item.im),span:BigInt(item.sp)}; + const selected=initialIter(snap,w,h),iter=selected.iter,fresh=buildFast(snap,iter,w,h,referenceSerial++); + const freshRows=indices.map(index=>({index,result:simulate(snap,w,h,iter,fresh,index)})); + const caseResult={case:item.name,iter,adaptive:selected.adaptive,freshRefLen:fresh.refLen,paths:[]}; + for(const path of paths){ + const sourceSnap=shiftSnap(snap,path.fx,path.fy,path.scale),candidate=buildFast(sourceSnap,iter,w,h,referenceSerial++); + const requiredBits=app.fastReferenceBits(snap,w),requiredLen=Math.min(iter,Math.max(4096,Math.floor(iter/3))); + const reusable=app.reusableReferencePixel(candidate,snap,requiredLen,w,h,requiredBits,true); + if(!reusable){caseResult.paths.push({...path,reusable:false,refLen:candidate.refLen});continue;} + totalPaths++; + let classDiff=0,iterDiff=0,smoothDiff=0,pathMaxSmooth=0; + const examples=[]; + for(const row of freshRows){ + const got=simulate(snap,w,h,iter,candidate,row.index),base=row.result; + totalComparisons++; + const cls=got.kind!==base.kind; + const it=!cls&&got.n!==base.n; + const sd=(!cls&&!it&&got.kind==='escape')?Math.abs(got.smooth-base.smooth):0; + const sm=sd>SMOOTH_TOLERANCE; + if(cls){classDiff++;classDifferences++} + if(it){iterDiff++;iterationDifferences++} + if(sm){smoothDiff++;smoothDifferences++} + if(sd>pathMaxSmooth)pathMaxSmooth=sd;if(sd>maxSmoothDelta)maxSmoothDelta=sd; + if((cls||it||sm)&&examples.length<8)examples.push({index:row.index,x:row.index%w,y:Math.floor(row.index/w),fresh:base,candidate:got,smoothDelta:sd}); + } + caseResult.paths.push({...path,reusable:true,refLen:candidate.refLen,referencePixel:reusable.pixel,classDifferences:classDiff,iterationDifferences:iterDiff,smoothDifferences:smoothDiff,maxSmoothDelta:pathMaxSmooth,examples}); + } + results.push(caseResult); +} + +const summary={ + generatedAt:new Date().toISOString(),productionSha256,width:w,height:h,samplesPerTarget:indices.length, + cases:results,totalReusablePaths:totalPaths,totalComparisons, + discontinuities:{classification:classDifferences,escapeIteration:iterationDifferences,smooth:smoothDifferences,maxSmoothDelta}, + smoothTolerance:SMOOTH_TOLERANCE, + invariant:'For any reference that production reusableReferencePixel(..., allowNearby=true) accepts, the target view must match a fresh target reference at sampled pixels. Classification and escape iteration are strict; smooth has a small diagnostic tolerance.' +}; + +console.log(JSON.stringify(summary,null,2)); + +if(totalPaths===0)throw new Error('navigation-continuity corpus produced no production-reusable nearby references'); +const hardDifferences=classDifferences+iterationDifferences; +if(!reportOnly){ + assert.equal(hardDifferences,0,`navigation continuity failed: ${classDifferences} classification + ${iterationDifferences} escape-iteration differences across ${totalComparisons} comparisons`); + assert.equal(smoothDifferences,0,`navigation smooth continuity failed: ${smoothDifferences} samples exceeded ${SMOOTH_TOLERANCE}`); + console.error(`PASS: navigation continuity (${totalPaths} reusable paths, ${totalComparisons} sampled comparisons)`); +}else{ + console.error(`REPORT: navigation continuity found ${classDifferences} classification, ${iterationDifferences} iteration, ${smoothDifferences} smooth differences across ${totalComparisons} comparisons`); +} diff --git a/tests/regression.mjs b/tests/regression.mjs index a7209d0..781bebb 100644 --- a/tests/regression.mjs +++ b/tests/regression.mjs @@ -45,6 +45,9 @@ assert.doesNotMatch(kernels.FAST_PERTURB_WGSL,/pack_meta\(n,FIELD_INTERIOR_HEURI assert.match(kernels.FAST_PERTURB_WGSL,/series_start/); assert.match(prodHtml,/SERIES_APPROXIMATION_ENABLED=false/); assert.match(prodHtml,/deferPrimaryColor=deferColor\|\|fastExtended/); +assert.doesNotMatch(prodHtml,/ADAPTIVE_PRIMARY_HYSTERESIS|stabilizeAdaptiveInitialChoice|lastPrimaryPolicy|nearbyAdaptiveViewSafe/); +assert.doesNotMatch(prodHtml,/primaryIterCoveredByReference|const covered=primaryIterCoveredByReference/); +assert.match(prodHtml,/reference orbit ends first, FAST must emit REFERENCE_END/); assert.doesNotMatch(kernels.FAST_PERTURB_WGSL,/unknownOnly==2u/); assert.match(prodHtml,/knownInteriorViewportMayOverlap/); assert.match(prodHtml,/numericHistoryScaleEligible/);