-
-
+
@@ -1006,7 +1005,14 @@ const UNKNOWN_REASON_NAMES=['none','error-bound','escape-uncertain','reference-e
const NUMERIC_PARAM_BYTES=96,UNRESOLVED_BYTES=32,SPARSE_QUEUE_STATS_BYTES=32,SPARSE_INDIRECT_BYTES=16,DEEP_BUCKET_STATE_BYTES=96;
const PIXEL_FRONTIER_PRODUCTION=true,PIXEL_FRONTIER_WORK=4000000,PIXEL_FRONTIER_MAX_ITER=150000;
const PIXEL_FRONTIER_ENABLED=PIXEL_FRONTIER_PRODUCTION;
-const state={bits:INITIAL_BITS,re:0n,im:0n,span:0n,baseIter:350,adaptive:true,continuationBudget:0,hq:false,processMode:'standard',renderMode:'fast',palette:0,cycle:.008,shift:.18,colorAuto:false,colorRevision:0,token:0,rendering:false,recoloring:false,recolorPending:false,dirty:true,lastRender:0,renderClock:null,lastEngine:'起動中',drawState:'REPROJECTED',frameView:null,fieldView:null,pointerActive:false,wheelActive:false,effectiveDpr:1,screenPixelBudget:0,unresolved:0,unknownReasons:null,correctionPasses:0,correctedPixels:0,multiRefPasses:0,multiRefRecovered:0,frontierRounds:0,frontierPixels:0,frontierEscaped:0,frontierIter:0,frontierMinimumIter:0,frontierConverged:false,backendDecision:null,gpuError:'',gpuInitFailed:false,gpuUnavailable:false,lastInteraction:performance.now(),focusX:.5,focusY:.5,uiHidden:false,qualityStage:'idle',qualityIter:0,qualityFinalIter:0,previewMs:0,refineMs:0,refinePending:false,temporalFill:false,numericRecoveryPending:false,numericRecoveryRunning:false,gpuStage:'',gpuTileRows:0};
+const QUALITY_PRESETS=Object.freeze({
+ fast:{scale:.75,pixels:524288,iterations:4096},
+ standard:{scale:1,pixels:1048576,iterations:16384},
+ high:{scale:1.5,pixels:2359296,iterations:32768}
+});
+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;state.hq=false}
+const state={bits:INITIAL_BITS,re:0n,im:0n,span:0n,baseIter:350,adaptive:true,continuationBudget:16384,hq:false,quality:'standard',renderMode:'fast',palette:0,cycle:.008,shift:.18,colorAuto:false,colorRevision:0,token:0,rendering:false,recoloring:false,recolorPending:false,dirty:true,lastRender:0,renderClock:null,lastEngine:'起動中',drawState:'REPROJECTED',frameView:null,fieldView:null,pointerActive:false,wheelActive:false,effectiveDpr:1,screenPixelBudget:0,unresolved:0,unknownReasons:null,correctionPasses:0,correctedPixels:0,multiRefPasses:0,multiRefRecovered:0,frontierRounds:0,frontierPixels:0,frontierEscaped:0,frontierIter:0,frontierMinimumIter:0,frontierConverged:false,backendDecision:null,gpuError:'',gpuInitFailed:false,gpuUnavailable:false,lastInteraction:performance.now(),focusX:.5,focusY:.5,uiHidden:false,qualityStage:'idle',qualityIter:0,qualityFinalIter:0,previewMs:0,refineMs:0,refinePending:false,temporalFill:false,numericRecoveryPending:false,numericRecoveryRunning:false,gpuStage:'',gpuTileRows:0};
let renderer=null,rendererInitPromise=null,raf=0,settleTimer=0,refineTimer=0,lastWrittenHash='',navigationHash='';
const viewHistory=[];let viewHistoryIndex=-1;
const TEST_MODE=new URLSearchParams(location.search).has('test');
@@ -1046,29 +1052,17 @@ function fastNeedsExtended(snap,w){return !(directPixelRatio(snap,w)>=4)}
function chooseBackend(snap=snapshot(),w=canvas.width){const fastExtended=fastNeedsExtended(snap,w);return{backend:fastExtended?'fast-extended':'direct',deep:false,fastExtended,probe:false,reason:fastExtended?'auto-fast-extended':'auto-direct'}}
function pixelPrecisionBits(snap=snapshot(),w=canvas.width,guard=32){const pixelLog2=log2FixedAt(snap.span,snap.bits)-Math.log2(Math.max(1,w)),need=Math.ceil(Math.max(0,-pixelLog2))+Math.max(16,guard|0);return Math.max(96,Math.min(snap.bits+64,need))}
function deepNeeded(){return false}
-function currentViewSpec(){return{bits:state.bits,re:state.re,im:state.im,span:state.span,palette:state.palette,cycle:state.cycle,shift:state.shift,baseIter:state.baseIter,adaptive:state.adaptive,continuationBudget:state.continuationBudget,renderMode:state.renderMode}}
-function viewSpecKey(v){return[v.bits,v.re,v.im,v.span,v.palette,v.cycle,v.shift,v.baseIter,v.adaptive,v.continuationBudget||0].join(':')}
+function currentViewSpec(){return{bits:state.bits,re:state.re,im:state.im,span:state.span,palette:state.palette,cycle:state.cycle,shift:state.shift,baseIter:state.baseIter,adaptive:state.adaptive,continuationBudget:state.continuationBudget,quality:state.quality,renderMode:state.renderMode}}
+function viewSpecKey(v){return[v.bits,v.re,v.im,v.span,v.palette,v.cycle,v.shift,v.baseIter,v.adaptive,v.continuationBudget||0,v.quality||'standard'].join(':')}
function recordView(){const v=currentViewSpec(),k=viewSpecKey(v);if(viewHistoryIndex>=0&&viewSpecKey(viewHistory[viewHistoryIndex])===k)return;viewHistory.splice(viewHistoryIndex+1);viewHistory.push(v);if(viewHistory.length>80)viewHistory.shift();viewHistoryIndex=viewHistory.length-1;syncHistoryButtons()}
-function numericViewKey(){return[state.bits,state.re,state.im,state.span,state.baseIter,state.adaptive,state.continuationBudget].join(':')}
-function restoreView(v){if(!v)return;const before=numericViewKey();Object.assign(state,{bits:v.bits,re:v.re,im:v.im,span:v.span,palette:VALID_PALETTE_IDS.has(v.palette)?v.palette:0,cycle:v.cycle,shift:v.shift,baseIter:v.baseIter,adaptive:v.adaptive,continuationBudget:v.continuationBudget||0,renderMode:'fast'});ensurePrecision();syncControls();saveHash(false);if(before===numericViewKey())requestRecolor();else markDirty()}
+function numericViewKey(){return[state.bits,state.re,state.im,state.span,state.baseIter,state.adaptive,state.continuationBudget,state.quality].join(':')}
+function restoreView(v){if(!v)return;const before=numericViewKey();Object.assign(state,{bits:v.bits,re:v.re,im:v.im,span:v.span,palette:VALID_PALETTE_IDS.has(v.palette)?v.palette:0,cycle:v.cycle,shift:v.shift,baseIter:v.baseIter,adaptive:v.adaptive,continuationBudget:v.continuationBudget||0,renderMode:'fast'});applyQuality(v.quality||'standard');ensurePrecision();resize();syncControls();saveHash(false);if(before===numericViewKey())requestRecolor();else markDirty()}
// ── iteration / quality policy ───────────────────────────────────────────
const ADAPTIVE_ITER_QUANTUM=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),q=state.processMode==='power'?.72:state.processMode==='fine'?1.25:state.processMode==='validate'?1.5:1,bonus=adaptiveIterationBonus(screenBits,q);return Math.min(state.continuationBudget||12000,12000,Math.max(state.baseIter,state.baseIter+bonus))}
-function previewIter(finalIter=maxIter()){
- const z=Math.max(0,zoomExp()),m=state.processMode==='power'?.72:state.processMode==='fine'?1.28:state.processMode==='validate'?1.55:1;
- // The old fixed 1100-iteration ceiling starved deep previews: around 10^12,
- // finalIter is ~2700 while many visible boundary pixels need >1100 steps.
- // Make the first pass track final quality as zoom deepens, with a mode-aware
- // ceiling so responsiveness is still bounded on extreme views.
- const legacy=Math.round(m*(420+24*z));
- const fractionBase=Math.min(.84,.48+.03*z);
- const fraction=state.processMode==='power'?Math.min(.68,fractionBase*.82):state.processMode==='fine'?Math.min(.90,fractionBase+.04):state.processMode==='validate'?Math.min(.94,fractionBase+.08):fractionBase;
- const deepFloor=Math.round(finalIter*fraction);
- const cap=state.processMode==='power'?1800:state.processMode==='fine'?4800:state.processMode==='validate'?6000:3600;
- return Math.min(finalIter,Math.max(state.baseIter,Math.min(cap,Math.max(legacy,deepFloor))));
-}
+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 previewIter(finalIter=maxIter()){return Math.min(finalIter,Math.max(state.baseIter,Math.min(1800,Math.round(finalIter*.6))))}
function canUseStableReprojection(){
if(!renderer?.historyReady||!state.frameView)return false;
// Covers normal wheel/pinch, one 2x toolbar zoom, and ordinary pans while
@@ -1086,12 +1080,7 @@ function qualityStages(finalIter=maxIter()){
const p=previewIter(finalIter);
return finalIter>p?[p,finalIter]:[p];
}
-function refinementDelay(stageIndex){
- // Full-quality latency matters more than leaving a long idle gap after the
- // first preview. Keep a short interaction settling window only.
- if(stageIndex<=1)return state.processMode==='validate'?300:state.processMode==='fine'?240:state.processMode==='power'?420:160;
- return state.processMode==='validate'?240:state.processMode==='fine'?200:360;
-}
+function refinementDelay(){return 100}
function sameViewSnapshot(a,b){return !!a&&!!b&&a.bits===b.bits&&a.re===b.re&&a.im===b.im&&a.span===b.span}
function canUseRealAxisSymmetry(snap){return !!snap&&snap.im===0n}
function reprojectionSafe(t,maxZoom=1.30,maxShift=.18){const s=Math.abs(Number(t?.scaleX));if(!Number.isFinite(s)||s<=0)return false;const z=Math.max(s,1/s),shift=Math.hypot(Number(t?.offsetX)||0,Number(t?.offsetY)||0);return z<=maxZoom&&shift<=maxShift}
@@ -1101,8 +1090,8 @@ function sliderToCycle(v){const t=Math.max(0,Math.min(CYCLE_SLIDER_MAX,Number(v)
function fract01(x){return x-Math.floor(x)}
function colorStyleSnapshot(){return{palette:state.palette,cycle:state.cycle,shift:state.shift,hq:state.hq}}
function colorStyleKey(style=colorStyleSnapshot()){return[style.palette,style.cycle,style.shift,style.hq?1:0].join(':')}
-function pixelBudget(){const low=Number(navigator.deviceMemory||8)<=4,small=matchMedia('(max-width:700px)').matches;if(!navigator.gpu||state.gpuUnavailable)return 262144;if(state.processMode==='power')return 524288;if(state.processMode==='fine')return(low||small?1572864:3145728);if(state.processMode==='validate')return(low||small?1048576:2097152);return(low||small?786432:1572864)}
-function resize(){const cssW=Math.max(1,innerWidth),cssH=Math.max(1,innerHeight),budget=pixelBudget(),native=Math.max(1,devicePixelRatio||1),bd=Math.sqrt(budget/(cssW*cssH));let dpr=Math.max(Math.min(1,64/Math.max(cssW,cssH)),Math.min(native,bd));if(renderer){const md=Math.max(2,renderer.adapterLimits.maxTextureDimension2D||8192);dpr=Math.min(dpr,md/cssW,md/cssH)}const w=Math.max(2,Math.round(cssW*dpr)),h=Math.max(2,Math.round(cssH*dpr));state.effectiveDpr=dpr;state.screenPixelBudget=budget;if(canvas.width!==w||canvas.height!==h){canvas.width=w;canvas.height=h;if(renderer)renderer.configure();markDirty(false)}}
+function pixelBudget(){const preset=QUALITY_PRESETS[state.quality],low=Number(navigator.deviceMemory||8)<=4,limit=renderer?.device?.limits?.maxStorageBufferBindingSize||134217728;return Math.max(4096,Math.min(Math.floor(preset.pixels*(low?.5:1)),Math.floor(limit/40)))}
+function resize(){const cssW=Math.max(1,innerWidth),cssH=Math.max(1,innerHeight),budget=pixelBudget(),native=Math.max(1,devicePixelRatio||1),bd=Math.sqrt(budget/(cssW*cssH));let dpr=Math.max(Math.min(1,64/Math.max(cssW,cssH)),Math.min(native*QUALITY_PRESETS[state.quality].scale,bd));if(renderer){const md=Math.max(2,renderer.adapterLimits.maxTextureDimension2D||8192);dpr=Math.min(dpr,md/cssW,md/cssH)}const w=Math.max(2,Math.round(cssW*dpr)),h=Math.max(2,Math.round(cssH*dpr));state.effectiveDpr=dpr;state.screenPixelBudget=budget;if(canvas.width!==w||canvas.height!==h){canvas.width=w;canvas.height=h;if(renderer)renderer.configure();markDirty(false)}}
// ── high precision reference worker ─────────────────────────────────────
function referenceWorkerSource(){return String.raw`
@@ -1685,7 +1674,7 @@ class WebGpuRenderer{
function numericalFailureCount(stats){const r=stats&&stats.reasons||{};return (r.errorBound||0)+(r.escapeUncertain||0)+(r.referenceEnd||0)+(r.rebaseGap||0)+(r.range||0)}
// A broad failure is cheaper to normalize once with the full-frame Deep pass;
// sparse/moderate failures go directly to local references.
-function tileRecoveryConfig(){const z=zoomExp(),deep=z>=32;if(state.processMode==='power')return{size:deep?224:320,maxRefs:4,minFailed:deep?4:48};if(state.processMode==='fine')return{size:deep?128:192,maxRefs:20,minFailed:deep?1:8};if(state.processMode==='validate')return{size:deep?112:160,maxRefs:32,minFailed:1};return{size:deep?160:224,maxRefs:12,minFailed:deep?1:12}}
+function tileRecoveryConfig(){const deep=zoomExp()>=32;return{size:deep?160:224,maxRefs:12,minFailed:deep?1:12}}
function isOperationLimitMeta(m){return ((m>>>28)&3)===FIELD_UNKNOWN&&((m>>>REASON_SHIFT)&255)===6}
function operationLimitIndices(meta){const out=[];for(let i=0;i
>>28)&3)!==FIELD_UNKNOWN)continue;const reason=(meta[i]>>>REASON_SHIFT)&255;if(reason>=1&&reason<=5)out.push(i)}return Uint32Array.from(out)}
@@ -1797,7 +1786,7 @@ async function refinePixelFrontier(r,snap,initialIter,token,initialStats){
// the candidate list from the already-published metadata and restart once
// with a newly selected long reference; no pixel is silently discarded.
if(ctx.refLen0){r.setDeepContext(ctx);const corrected=await r.correctUnknownFrame(snap,target,token,refPixel);if(!corrected||token!==state.token)return null;stats=await r.readUnresolvedStats();if(token!==state.token)return null}if(numericalFailureCount(stats)>512){const beforeLocal=numericalFailureCount(stats),mr=await recoverNumericalUnknownTiles(r,snap,target,token,stats);if(!mr||token!==state.token)return null;stats=mr.stats;if(numericalFailureCount(stats)>=beforeLocal)state.multiRefRecovered=0}if(numericalFailureCount(stats)>0){const pf=await recoverResidualPrecision(r,snap,target,token,stats);if(!pf||token!==state.token)return null;stats=pf.stats}const after=await r.readMetaAll();if(token!==state.token)return null;const advance=pixelEscapeAdvance(previousMeta,after,canvas.width,canvas.height);rounds++;escaped+=advance.escaped;iter=target;stableRounds=advance.pixelStable?stableRounds+1:0;previousMeta=after;state.frontierRounds=rounds;state.frontierEscaped=escaped;state.frontierIter=iter;
if(!await presentProvisional(r,snap,iter,token))return null;
// A quiet pair of doubling rounds is only meaningful after every remaining
@@ -1993,10 +1982,10 @@ canvas.addEventListener('pointermove',e=>{if(!pts.has(e.pointerId))return;update
function endPointer(e){pts.delete(e.pointerId);pinch=0;if(pts.size)return;clearTimeout(settleTimer);settleTimer=setTimeout(()=>{state.pointerActive=false;recordView();saveHash(false);markDirty()},90)}canvas.addEventListener('pointerup',endPointer);canvas.addEventListener('pointercancel',endPointer);
// ── URL / controls ───────────────────────────────────────────────────────
-function saveHash(push){const p=new URLSearchParams();p.set('v',String(VERSION));p.set('b',String(state.bits));p.set('re',state.re.toString());p.set('im',state.im.toString());p.set('sp',state.span.toString());p.set('pal',String(state.palette));p.set('cy',String(state.cycle));p.set('sh',String(state.shift));p.set('it',String(state.baseIter));p.set('ad',state.adaptive?'1':'0');if(state.continuationBudget)p.set('cb',String(state.continuationBudget));p.set('rm',state.renderMode);const h='#'+p.toString();lastWrittenHash=h;try{push?history.pushState(null,'',h):history.replaceState(null,'',h)}catch{location.hash=h}}
-function loadHash(){const p=new URLSearchParams(location.hash.slice(1));if(!p.has('b'))return false;try{const b=Number(p.get('b')),re=BigInt(p.get('re')),im=BigInt(p.get('im')),sp=BigInt(p.get('sp'));if(!Number.isInteger(b)||b<64||sp<=0n)return false;state.bits=b;state.re=re;state.im=im;state.span=sp;if(p.has('pal')){const pal=Math.max(0,Math.min(PALETTE_MAX,Number(p.get('pal'))|0));state.palette=VALID_PALETTE_IDS.has(pal)?pal:0;}if(p.has('cy'))state.cycle=Math.max(CYCLE_MIN,Math.min(CYCLE_MAX,Number(p.get('cy'))||.008));if(p.has('sh'))state.shift=Math.max(0,Math.min(1,Number(p.get('sh'))||0));if(p.has('it'))state.baseIter=Math.max(100,Math.min(2500,Number(p.get('it'))||350));if(p.has('ad'))state.adaptive=p.get('ad')!=='0';state.continuationBudget=Math.max(0,Math.min(150000,Math.floor(Number(p.get('cb'))||0)));state.renderMode='fast';ensurePrecision();return true}catch{return false}}
+function saveHash(push){const p=new URLSearchParams();p.set('v',String(VERSION));p.set('b',String(state.bits));p.set('re',state.re.toString());p.set('im',state.im.toString());p.set('sp',state.span.toString());p.set('pal',String(state.palette));p.set('cy',String(state.cycle));p.set('sh',String(state.shift));p.set('it',String(state.baseIter));p.set('ad',state.adaptive?'1':'0');p.set('q',state.quality);p.set('rm',state.renderMode);const h='#'+p.toString();lastWrittenHash=h;try{push?history.pushState(null,'',h):history.replaceState(null,'',h)}catch{location.hash=h}}
+function loadHash(){const p=new URLSearchParams(location.hash.slice(1));if(!p.has('b'))return false;try{const b=Number(p.get('b')),re=BigInt(p.get('re')),im=BigInt(p.get('im')),sp=BigInt(p.get('sp'));if(!Number.isInteger(b)||b<64||sp<=0n)return false;state.bits=b;state.re=re;state.im=im;state.span=sp;if(p.has('pal')){const pal=Math.max(0,Math.min(PALETTE_MAX,Number(p.get('pal'))|0));state.palette=VALID_PALETTE_IDS.has(pal)?pal:0;}if(p.has('cy'))state.cycle=Math.max(CYCLE_MIN,Math.min(CYCLE_MAX,Number(p.get('cy'))||.008));if(p.has('sh'))state.shift=Math.max(0,Math.min(1,Number(p.get('sh'))||0));if(p.has('it'))state.baseIter=Math.max(100,Math.min(2500,Number(p.get('it'))||350));if(p.has('ad'))state.adaptive=p.get('ad')!=='0';applyQuality(p.has('q')?p.get('q'):p.has('cb')&&Number(p.get('cb'))>0?(Number(p.get('cb'))<=4096?'fast':Number(p.get('cb'))<=16384?'standard':'high'):state.quality);state.renderMode='fast';ensurePrecision();return true}catch{return false}}
function syncHistoryButtons(){}
-function syncControls(){$('#continuationBudget').value=String(state.continuationBudget);$('#processMode').value=state.processMode;$('#palette').value=String(state.palette);$('#cycle').value=String(cycleToSlider(state.cycle));$('#cycleO').textContent=state.cycle.toFixed(4);$('#shift').value=String(state.shift);$('#shiftO').textContent=state.shift.toFixed(2);syncColorAutoButton()}
+function syncControls(){$('#quality').value=state.quality;$('#palette').value=String(state.palette);$('#cycle').value=String(cycleToSlider(state.cycle));$('#cycleO').textContent=state.cycle.toFixed(4);$('#shift').value=String(state.shift);$('#shiftO').textContent=state.shift.toFixed(2);syncColorAutoButton()}
function toast(s){const e=$('#toast');e.textContent=s;e.classList.add('show');setTimeout(()=>e.classList.remove('show'),1500)}
function applyUi(){document.body.classList.toggle('ui-hidden',state.uiHidden);$('#uiToggle').textContent=state.uiHidden?'UI+':'UI−';$('#uiToggle').setAttribute('aria-expanded',state.uiHidden?'false':'true')}
$('#uiToggle').onclick=()=>{state.uiHidden=!state.uiHidden;try{localStorage.setItem('mandelbrot.uiHidden',state.uiHidden?'1':'0')}catch{}applyUi()};
@@ -2020,10 +2009,10 @@ function applyCycleControl(e){state.cycle=sliderToCycle(e.target.value);$('#cycl
function applyShiftControl(e){if(state.colorAuto)stopColorAuto();state.shift=Math.max(0,Math.min(1,Number(e.target.value)||0));$('#shiftO').textContent=state.shift.toFixed(2);requestRecolor()}
$('#cycle').addEventListener('input',applyCycleControl);$('#cycle').addEventListener('change',()=>saveHash(false));
$('#shift').addEventListener('input',applyShiftControl);$('#shift').addEventListener('change',()=>saveHash(false));
-$('#continuationBudget').onchange=e=>{state.continuationBudget=Math.max(0,Math.min(150000,Number(e.target.value)||0));saveHash(false);recordView();markDirty()};
-$('#processMode').onchange=e=>{state.processMode=/^(power|standard|fine|validate)$/.test(e.target.value)?e.target.value:'standard';state.hq=state.processMode==='fine'||state.processMode==='validate';resize();markDirty();try{localStorage.setItem('mandelbrot.processMode',state.processMode)}catch{}};
+$('#quality').onchange=e=>{cancelRender();applyQuality(e.target.value);resize();saveHash(false);recordView();markDirty(false);try{localStorage.setItem('mandelbrot.quality',state.quality)}catch{}};
+
addEventListener('resize',()=>{const w=canvas.width,h=canvas.height;resize();if(w!==canvas.width||h!==canvas.height)markDirty()});addEventListener('keydown',e=>{if(/^(INPUT|SELECT|TEXTAREA|BUTTON)$/.test(e.target.tagName))return;let ok=true;if(e.key==='h'||e.key==='H')$('#uiToggle').click();else if(e.key==='r'||e.key==='R')$('#reset').click();else if(e.key==='+'||e.key==='='||e.key==='Enter'&&!e.shiftKey)$('#zin').click();else if(e.key==='-'||e.key==='Enter'&&e.shiftKey)$('#zout').click();else if(e.key==='ArrowLeft')pan(innerWidth*.08,0);else if(e.key==='ArrowRight')pan(-innerWidth*.08,0);else if(e.key==='ArrowUp')pan(0,innerHeight*.08);else if(e.key==='ArrowDown')pan(0,-innerHeight*.08);else ok=false;if(ok){e.preventDefault();recordView();saveHash(false);markDirty()}});
-addEventListener('hashchange',()=>{if(location.hash===lastWrittenHash){lastWrittenHash='';return}if(location.hash===navigationHash)return;navigationHash=location.hash;setTimeout(()=>navigationHash='',0);const before=numericViewKey();if(loadHash()){syncControls();recordView();if(before===numericViewKey())requestRecolor();else markDirty()}});
+addEventListener('hashchange',()=>{if(location.hash===lastWrittenHash){lastWrittenHash='';return}if(location.hash===navigationHash)return;navigationHash=location.hash;setTimeout(()=>navigationHash='',0);const before=numericViewKey();if(loadHash()){syncControls();recordView();if(before===numericViewKey())requestRecolor();else{resize();markDirty()}}});
// ── export: GPU tiled + streaming PNG, optional GPU 2x2 supersampling ──────
const exportJob={active:false,cancelled:false};
@@ -2118,7 +2107,7 @@ globalThis.__MANDEL_TEST__={
return Array.from(solved.indices,(index,j)=>({index,packed:field.meta[index],smoothed:smoothed[index],oracle:solved.meta[j],oracleSmooth:solved.smooth[j],accepted:solved.accepted[j]}));
},
async ensureGpuReady(){const r=renderer||await initRenderer();return{ready:!!r,navigatorGpu:!!navigator.gpu,renderer:!!renderer,gpuInitFailed:state.gpuInitFailed,gpuUnavailable:state.gpuUnavailable,gpuError:state.gpuError}},
- async setView({re,im,span,bits,baseIter=350,adaptive=false,continuationBudget=0,processMode='standard',renderMode='fast',fixed=false}){const ready=await this.ensureGpuReady();if(!ready.ready)throw new Error('WebGPU initialization failed: '+JSON.stringify(ready));cancelRender();if(bits){state.bits=bits}else{state.bits=Math.max(256,decimalRequiredBits(re),decimalRequiredBits(im),decimalRequiredBits(span))}state.re=fixed?BigInt(re):fromDec(re);state.im=fixed?BigInt(im):fromDec(im);state.span=fixed?BigInt(span):fromDec(span);state.baseIter=baseIter;state.adaptive=adaptive;state.continuationBudget=Math.max(0,Math.min(150000,Math.floor(continuationBudget)));state.processMode=processMode;state.renderMode='fast';state.hq=false;ensurePrecision();resize();markDirty(false);const start=performance.now();while((state.dirty||state.rendering)&&performance.now()-start<120000){schedule();await new Promise(r=>setTimeout(r,20))}if(state.dirty||state.rendering)throw new Error('test render timeout');if(!renderer?.frame||!state.fieldView)throw new Error('test render completed without a GPU frame: '+JSON.stringify({gpuInitFailed:state.gpuInitFailed,gpuUnavailable:state.gpuUnavailable,gpuError:state.gpuError,drawState:state.drawState,engine:state.lastEngine}));return{width:canvas.width,height:canvas.height,state:this.state()}},
+ async setView({re,im,span,bits,baseIter=350,adaptive=false,continuationBudget=0,processMode='standard',renderMode='fast',quality=null,fixed=false}){const ready=await this.ensureGpuReady();if(!ready.ready)throw new Error('WebGPU initialization failed: '+JSON.stringify(ready));cancelRender();if(bits){state.bits=bits}else{state.bits=Math.max(256,decimalRequiredBits(re),decimalRequiredBits(im),decimalRequiredBits(span))}state.re=fixed?BigInt(re):fromDec(re);state.im=fixed?BigInt(im):fromDec(im);state.span=fixed?BigInt(span):fromDec(span);state.baseIter=baseIter;state.adaptive=adaptive;state.continuationBudget=Math.max(0,Math.min(150000,Math.floor(continuationBudget)));state.quality='standard';if(quality)applyQuality(quality);state.renderMode='fast';state.hq=false;ensurePrecision();resize();markDirty(false);const start=performance.now();while((state.dirty||state.rendering)&&performance.now()-start<120000){schedule();await new Promise(r=>setTimeout(r,20))}if(state.dirty||state.rendering)throw new Error('test render timeout');if(!renderer?.frame||!state.fieldView)throw new Error('test render completed without a GPU frame: '+JSON.stringify({gpuInitFailed:state.gpuInitFailed,gpuUnavailable:state.gpuUnavailable,gpuError:state.gpuError,drawState:state.drawState,engine:state.lastEngine}));return{width:canvas.width,height:canvas.height,state:this.state()}},
async sampleMeta(points){if(!renderer||!renderer.frame)throw new Error('GPU field unavailable');const idx=points.map(([x,y])=>y*renderer.frame.w+x);const m=await renderer.readMeta(idx);return Array.from(m)},
async waitForPrimary({timeout=120000}={}){await waitForTestState(()=>!state.dirty&&!state.rendering&&!!state.fieldView,'primary',timeout);return this.state()},
async waitForFinal({timeout=120000}={}){await waitForTestState(()=>!state.dirty&&!state.rendering&&!state.refinePending&&state.qualityStage==='final'&&state.fieldView?.complete===true&&state.renderClock?.status==='complete','final',timeout);return this.state()},
@@ -2126,7 +2115,7 @@ globalThis.__MANDEL_TEST__={
async fieldHashes(){if(!renderer?.frame)throw new Error('WebGPU field unavailable');const field=await renderer.readFieldAll();return{pixels:field.meta.length,metaSha256:await sha256View(field.meta),smoothSha256:await sha256View(field.smoothBits)}},
gpuDiagnostics:()=>renderer?{adapterInfo:{...renderer.adapterInfo},adapterLimits:{...renderer.adapterLimits},compilation:renderer.compilation.map(x=>({...x})),uncapturedErrors:[...renderer.uncapturedErrors],lossReason:renderer.lossReason||'',activeGpuStage:renderer.activeGpuStage,lastCompletedGpuStage:renderer.lastCompletedGpuStage}:null,
stabilityPolicy:()=>({adaptiveIterationQuantum:ADAPTIVE_ITER_QUANTUM,pixelFrontierProduction:PIXEL_FRONTIER_PRODUCTION,pixelFrontierEnabled:PIXEL_FRONTIER_ENABLED,pixelFrontierWorkLimit:PIXEL_FRONTIER_WORK,pixelFrontierMaxIter:PIXEL_FRONTIER_MAX_ITER,pixelFrontierMinimumIter:pixelFrontierIterationFloor(snapshot(),canvas.width,maxIter()),stableHistoryReady:!!renderer?.historyReady,deepCorrectionDispatchWork:PIXEL_FRONTIER_WORK}),
- state:()=>{const snap=snapshot(),se=spanMantExp(snap),reasons=state.unknownReasons||{};return{bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),spanExp:se.exp,directPixelRatio:directPixelRatio(snap,canvas.width),width:canvas.width,height:canvas.height,iter:maxIter(),effectiveIter:state.fieldView?.iter??maxIter(),continuationBudget:state.continuationBudget,renderMode:state.renderMode,palette:state.palette,cycle:state.cycle,shift:state.shift,colorRevision:state.colorRevision,colorAuto:state.colorAuto,rendering:state.rendering,recoloring:state.recoloring,recolorPending:state.recolorPending,lastRender:state.lastRender,renderClock:state.renderClock?{...state.renderClock}:null,engine:state.lastEngine,backend:state.fieldView?.backend||null,fastExtended:!!state.fieldView?.fastExtended,referenceReused:!!state.fieldView?.referenceReused,provisional:state.fieldView?.complete===false,qualityStage:state.qualityStage,qualityIter:state.qualityIter,qualityFinalIter:state.qualityFinalIter,previewMs:state.previewMs,refineMs:state.refineMs,refinePending:state.refinePending,temporalFill:state.temporalFill,numericRecoveryPending:state.numericRecoveryPending,numericRecoveryRunning:state.numericRecoveryRunning,historyReady:!!renderer?.historyReady,multiRefPasses:state.multiRefPasses,multiRefRecovered:state.multiRefRecovered,frontierRounds:state.frontierRounds,frontierPixels:state.frontierPixels,frontierEscaped:state.frontierEscaped,frontierIter:state.frontierIter,frontierMinimumIter:state.frontierMinimumIter,frontierConverged:state.frontierConverged,frontierMaxDispatchWork:runtime.frontierMaxDispatchWork||0,numericalFailures:numericalFailureCount({reasons}),unknownReasons:state.unknownReasons,kernelVersion:G.version,gpuStage:state.gpuStage,gpuTileRows:state.gpuTileRows,numericSubmits:runtime.numericSubmits,correctionSubmits:runtime.correctionSubmits,gpuSyncWaits:runtime.gpuSyncWaits,stageCounts:{...runtime.stageCounts},stageMs:{...runtime.stageMs},generationMetrics:runtime.generationMetrics.slice(-16).map(x=>({...x}))}},
+ state:()=>{const snap=snapshot(),se=spanMantExp(snap),reasons=state.unknownReasons||{};return{bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),spanExp:se.exp,directPixelRatio:directPixelRatio(snap,canvas.width),width:canvas.width,height:canvas.height,iter:maxIter(),effectiveIter:state.fieldView?.iter??maxIter(),continuationBudget:state.continuationBudget,quality:state.quality,renderMode:state.renderMode,palette:state.palette,cycle:state.cycle,shift:state.shift,colorRevision:state.colorRevision,colorAuto:state.colorAuto,rendering:state.rendering,recoloring:state.recoloring,recolorPending:state.recolorPending,lastRender:state.lastRender,renderClock:state.renderClock?{...state.renderClock}:null,engine:state.lastEngine,backend:state.fieldView?.backend||null,fastExtended:!!state.fieldView?.fastExtended,referenceReused:!!state.fieldView?.referenceReused,provisional:state.fieldView?.complete===false,qualityStage:state.qualityStage,qualityIter:state.qualityIter,qualityFinalIter:state.qualityFinalIter,previewMs:state.previewMs,refineMs:state.refineMs,refinePending:state.refinePending,temporalFill:state.temporalFill,numericRecoveryPending:state.numericRecoveryPending,numericRecoveryRunning:state.numericRecoveryRunning,historyReady:!!renderer?.historyReady,multiRefPasses:state.multiRefPasses,multiRefRecovered:state.multiRefRecovered,frontierRounds:state.frontierRounds,frontierPixels:state.frontierPixels,frontierEscaped:state.frontierEscaped,frontierIter:state.frontierIter,frontierMinimumIter:state.frontierMinimumIter,frontierConverged:state.frontierConverged,frontierMaxDispatchWork:runtime.frontierMaxDispatchWork||0,numericalFailures:numericalFailureCount({reasons}),unknownReasons:state.unknownReasons,kernelVersion:G.version,gpuStage:state.gpuStage,gpuTileRows:state.gpuTileRows,numericSubmits:runtime.numericSubmits,correctionSubmits:runtime.correctionSubmits,gpuSyncWaits:runtime.gpuSyncWaits,stageCounts:{...runtime.stageCounts},stageMs:{...runtime.stageMs},generationMetrics:runtime.generationMetrics.slice(-16).map(x=>({...x}))}},
async panPixels(dx,dy){pan(dx,dy);const start=performance.now();while((state.dirty||state.rendering)&&performance.now()-start<120000){schedule();await new Promise(r=>setTimeout(r,20))}if(state.dirty||state.rendering)throw new Error('test pan timeout');return this.state()},
async compareCurrentToFrontierBaseline({strict=true}={}){if(!renderer?.frame)throw new Error('WebGPU frame unavailable');const snap=snapshot(),iter=state.fieldView?.iter??maxIter(),w=canvas.width,h=canvas.height,view=state.fieldView||{},deep=true,exactRequired=!!view.deep,ctx=await refs.request(snap,iter,w,h);if(ctx.checkpointMismatch)throw new Error('frontier baseline reference guard checkpoint mismatch');const rows=Math.max(1,Math.floor(PIXEL_FRONTIER_WORK/Math.max(1,w*iter))),baseline=new Uint32Array(w*h);let maxDispatchWork=0;for(let y=0;y>>0,b=baseline[i]>>>0,ac=(a>>>28)&3,bc=(b>>>28)&3;if(ac===FIELD_ESCAPED)escaped++;if(bc===FIELD_ESCAPED)baselineEscaped++;const sameClass=(ac===FIELD_ESCAPED)===(bc===FIELD_ESCAPED),sameIter=ac!==FIELD_ESCAPED||(a&ITER_MASK)===(b&ITER_MASK);if(!sameClass)classMismatch++;else if(!sameIter)escapeIterationMismatch++;if(!sameClass||(exactRequired&&!sameIter)){mismatch++;disputes.push(i);if(first<0)first=i}}let oracleChecked=0,oracleInconclusive=0,oracleCurrentMismatch=0,oracleBaselineMismatch=0;if(disputes.length){const solved=await precisionFallback.request(snap,iter,w,h,disputes,-1);for(let j=0;j>>0,a=current[i]>>>0,b=baseline[i]>>>0,oc=(o>>>28)&3,ac=(a>>>28)&3,bc=(b>>>28)&3,currentSame=(oc===FIELD_ESCAPED)===(ac===FIELD_ESCAPED)&&(!exactRequired||oc!==FIELD_ESCAPED||(o&ITER_MASK)===(a&ITER_MASK)),baselineSame=(oc===FIELD_ESCAPED)===(bc===FIELD_ESCAPED)&&(!exactRequired||oc!==FIELD_ESCAPED||(o&ITER_MASK)===(b&ITER_MASK));if(!currentSame)oracleCurrentMismatch++;if(!baselineSame)oracleBaselineMismatch++}}const oracleGate=oracleCurrentMismatch===0&&oracleInconclusive===0;return{pixels:current.length,mismatch,classMismatch,escapeIterationMismatch,exactRequired,first,escaped,baselineEscaped,iter,baseline:'deep-gold',oracleChecked,oracleInconclusive,oracleCurrentMismatch,oracleBaselineMismatch,oracleGate,maxDispatchWork,workLimit:PIXEL_FRONTIER_WORK,gatePassed:oracleGate&&maxDispatchWork<=PIXEL_FRONTIER_WORK}},
async probeMeta({w,h,strict=true,forceDeep=null,correctUnknown=true}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=forceDeep===null?deepNeeded(snap,w):!!forceDeep,fastExtended=!deep&&fastNeedsExtended(snap,w);let ctx=null,fastCtx=null;if(deep)ctx=await refs.request(snap,iter,w,h);else if(fastExtended)fastCtx=await fastRefs.request(snap,iter,w,h);return Array.from(await renderer.renderTileMeta({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,w,h,forceStrict:strict,correctUnknown:deep&&correctUnknown}))},
@@ -2137,7 +2126,7 @@ globalThis.__MANDEL_TEST__={
// ── boot / teardown ──────────────────────────────────────────────────────
addEventListener('visibilitychange',()=>{if(document.hidden){cancelRender();exportJob.cancelled=true}else markDirty(false)});addEventListener('pagehide',()=>{stopColorAuto(false);cancelRender();refs.destroy();fastRefs.destroy();precisionFallback.destroy();for(const service of precisionFallbackPool)service.destroy();if(renderer)renderer.destroy()},{once:true});
-try{state.uiHidden=localStorage.getItem('mandelbrot.uiHidden')==='1';const m=localStorage.getItem('mandelbrot.processMode');if(/^(power|standard|fine|validate)$/.test(m)){state.processMode=m;state.hq=m==='fine'||m==='validate'}localStorage.removeItem('mandelbrot.renderMode')}catch{}applyUi();resize();if(!loadHash())reset();recordView();syncControls();updateStats();initRenderer().then(()=>{resize();if(!state.rendering&&!state.frameView)markDirty(false)});schedule();
+try{state.uiHidden=localStorage.getItem('mandelbrot.uiHidden')==='1';applyQuality(localStorage.getItem('mandelbrot.quality')||localStorage.getItem('mandelbrot.processMode'));localStorage.removeItem('mandelbrot.processMode');localStorage.removeItem('mandelbrot.renderMode')}catch{}applyUi();resize();if(!loadHash())reset();recordView();syncControls();updateStats();initRenderer().then(()=>{resize();if(!state.rendering&&!state.frameView)markDirty(false)});schedule();
})();
diff --git a/tests/browser_smoke.mjs b/tests/browser_smoke.mjs
index 7120fc9..bee73be 100644
--- a/tests/browser_smoke.mjs
+++ b/tests/browser_smoke.mjs
@@ -35,7 +35,7 @@ const cases=[
{id:'axis',re:'-0.75',im:'0',span:'0.02',renderMode:'fast',backend:'direct'},
{id:'black',re:'-29466147000382485924219538765424656674342940730553342389512209954887705938978413587',im:'5179018789925933872641942859702187326563010594882200888329237785122225251258352',span:'250637324516887125119843167990565159991251418774166207381437546036409',bits:273,baseIter:350,adaptive:true,fixed:true,renderMode:'accurate',backend:'fast-extended'}
];
-assert.ok(['all','kernels','cancel','bench','budget','ui','palette','pan'].includes(selection)||cases.some(c=>c.id===selection),'Unknown case');
+assert.ok(['all','kernels','cancel','bench','budget','ui','palette','pan','quality'].includes(selection)||cases.some(c=>c.id===selection),'Unknown case');
// Execute the real production kernels on small, fully specified states.
// This isolates terminal and queue behavior from render convergence heuristics.
@@ -137,6 +137,38 @@ try{
for(let i=0;i<100;i++){if(await evaluate('!!globalThis.__MANDEL_TEST__'))break;await sleep(100)}
const ready=await evaluate('globalThis.__MANDEL_TEST__.ensureGpuReady()');
assert.equal(ready.ready,true,JSON.stringify(ready));
+ if(selection==='quality'||selection==='all'){
+ await evaluate('__MANDEL_TEST__.waitForNumericComplete()');
+ assert.deepEqual(await evaluate('Array.from(document.querySelector("#quality").options,o=>o.textContent)'),['高速','標準','高画質']);
+ assert.equal(await evaluate('!!document.querySelector("#processMode, #continuationBudget")'),false);
+ const reports=[];
+ for(const scenario of [cases[0],cases[1],cases[2]]){
+ // The mid view contains many surviving pixels; this exercised unfinished
+ // high-quality frames in the old automatic/Strict policies.
+ await evaluate(`__MANDEL_TEST__.setView(${JSON.stringify({...scenario,bits:448,baseIter:512,adaptive:false,quality:'fast'})})`);
+ for(const [quality,width,height,budget] of [['fast',240,180,4096],['standard',320,240,16384],['high',480,360,32768]]){
+ const result=await evaluate(`(async()=>{const e=document.querySelector('#quality');e.value='${quality}';e.dispatchEvent(new Event('change'));const s=await __MANDEL_TEST__.waitForNumericComplete({timeout:180000});const r=__MANDEL_TEST__.kernelAccess().renderer,field=await r.readFieldAll();let invalid=0;for(const p of field.meta){if((p&0xfffff)>${budget})invalid++;if((p>>>28)===0&&((p>>>20)!==6||(p&0xfffff)!==${budget}))invalid++}return{s,invalid,front:r.frontColorKey,png:await __MANDEL_TEST__.pngRoundTrip()}})()`);
+ assert.equal(result.s.width,width);assert.equal(result.s.height,height);assert.equal(result.s.continuationBudget,budget);
+ assert.equal(result.s.numericalFailures,0);assert.equal(result.s.provisional,false);assert.equal(result.s.renderClock.status,'complete');assert.equal(result.invalid,0);
+ assert.equal(result.front.split(':')[3],'0','no neighborhood blur');assert.equal(result.png.mismatches,0);
+ reports.push({case:scenario.id,quality,width,height,budget,ms:result.s.lastRender});
+ console.log(JSON.stringify({quality:reports.at(-1)}));
+ }
+ }
+ // New URL/local preferences restore the quality and its resolution.
+ await command('Page.reload');await sleep(150);await evaluate('__MANDEL_TEST__.waitForNumericComplete({timeout:180000})');
+ assert.equal(await evaluate('__MANDEL_TEST__.state().quality'),'high');
+ assert.equal(await evaluate('document.querySelector("#quality").value'),'high');
+ assert.equal(await evaluate('new URLSearchParams(location.hash.slice(1)).get("q")'),'high');
+ // Cancel a high-resolution calculation via the actual selector.
+ await evaluate(`__MANDEL_TEST__.setView(${JSON.stringify({...cases[0],bits:448,baseIter:512,adaptive:false,quality:'fast'})})`);
+ const work=evaluate(`__MANDEL_TEST__.setView(${JSON.stringify({...cases[1],bits:448,baseIter:512,adaptive:false,quality:'high'})})`).then(()=>true,()=>false);
+ for(let i=0;i<500;i++){if(await evaluate('__MANDEL_TEST__.state().rendering'))break;await sleep(10)}
+ await evaluate('const e=document.querySelector("#quality");e.value="fast";e.dispatchEvent(new Event("change"))');
+ await work;const final=await evaluate('__MANDEL_TEST__.waitForNumericComplete()');
+ assert.equal(final.quality,'fast');assert.equal(final.width,240);assert.equal(final.continuationBudget,4096);
+ console.log('PASS: three quality presets, real supersampling, finite completion, PNG, URL reload, cancellation');
+ }
if(selection==='pan'||selection==='all'){
await evaluate('__MANDEL_TEST__.waitForNumericComplete()');
await evaluate('if(!document.body.classList.contains("ui-hidden"))document.querySelector("#uiToggle").click()');
diff --git a/tests/regression.mjs b/tests/regression.mjs
index 92dd62b..ead5db4 100644
--- a/tests/regression.mjs
+++ b/tests/regression.mjs
@@ -19,7 +19,7 @@ function appContext(search=''){
const element={width:800,height:600,clientWidth:800,clientHeight:600,style:{},addEventListener(){},classList:{toggle(){}},setAttribute(){}};
const context={MANDEL_WEBGPU_KERNELS:kernels,document:{querySelector:()=>element},navigator:{hardwareConcurrency:4},location:{search},performance,URLSearchParams,TextEncoder,console,addEventListener(){}};
const code=scripts[1].slice(0,scripts[1].indexOf('// ── boot / teardown'))+
- 'globalThis.internal={state,runtime,refs,refinePixelFrontier,setTestPresentation:fn=>{presentProvisional=fn},recolor,setRecolorRenderer:r=>{renderer=r;updateStats=()=>{}},WebGpuRenderer,chooseBackend,startRenderClock,finishRenderClock,renderTimeLabel,fromDec,operationLimitIndices,numericalFailureIndices,pixelPrecisionBits,precisionBatchSize,certifiedInteriorTiles,referenceWorkerSource,fastReferenceWorkerSource,precisionFallbackWorkerSource};})();';
+ 'globalThis.internal={state,runtime,refs,refinePixelFrontier,setTestPresentation:fn=>{presentProvisional=fn},applyQuality,pixelBudget,resize,recolor,setRecolorRenderer:r=>{renderer=r;updateStats=()=>{}},WebGpuRenderer,chooseBackend,startRenderClock,finishRenderClock,renderTimeLabel,fromDec,operationLimitIndices,numericalFailureIndices,pixelPrecisionBits,precisionBatchSize,certifiedInteriorTiles,referenceWorkerSource,fastReferenceWorkerSource,precisionFallbackWorkerSource};})();';
vm.runInNewContext(code,context);
return context;
}
@@ -29,6 +29,25 @@ assert.equal(typeof diagnostic.__MANDEL_TEST__.state,'function');
const a=normal.internal;
assert.equal(a.runtime.generationMetrics.length,0);
+// Quality presets increase real raster dimensions, not a blur radius. Bound
+// both work and memory at large windows, including low-memory devices.
+{
+ const c=appContext(),i=c.internal;Object.assign(c,{innerWidth:800,innerHeight:600,devicePixelRatio:1,requestAnimationFrame:()=>1});
+ const dimensions=[];
+ for(const [quality,budget] of [['fast',4096],['standard',16384],['high',32768]]){
+ i.applyQuality(quality);i.resize();
+ const canvas=c.document.querySelector('#view');dimensions.push([canvas.width,canvas.height]);
+ assert.equal(i.state.continuationBudget,budget);assert.equal(i.state.hq,false);assert.ok(canvas.width*canvas.height<=i.pixelBudget()+canvas.width+canvas.height);
+ }
+ assert.deepEqual(dimensions,[[600,450],[800,600],[1200,900]]);
+ Object.assign(c,{innerWidth:7680,innerHeight:4320,devicePixelRatio:2});
+ for(const memory of [4,8])for(const quality of ['fast','standard','high']){
+ c.navigator.deviceMemory=memory;i.applyQuality(quality);i.resize();const canvas=c.document.querySelector('#view');
+ assert.ok(canvas.width*canvas.height<=i.pixelBudget()+canvas.width+canvas.height);
+ }
+ for(const old of ['fine','validate']){i.applyQuality(old);assert.equal(i.state.quality,'high');assert.equal(i.state.continuationBudget,32768)}
+}
+
// A faster animation producer must not hold recoloring=true indefinitely.
// Release after one GPU pass, and do not take ownership from a pending pan.
{
@@ -155,7 +174,7 @@ assert.equal(await renderer.applyPrecisionFallback({indices:Uint32Array.of(0)},a
// Exercise the actual frontier controller with one repaired pixel and one
// healthy survivor. GPU state/queue correctness is tested in browser_smoke.
{
- const snap={bits:256,re:0n,im:0n,span:a.fromDec('3.4')};Object.assign(a.state,snap);
+ const snap={bits:256,re:0n,im:0n,span:a.fromDec('3.4')};Object.assign(a.state,snap,{continuationBudget:0});
const ctx={referenceId:'fixture',key:'fixture',source:snap,refLen:4096};
a.refs.request=async()=>ctx;a.refs.requestFixed=async()=>ctx;a.setTestPresentation(async()=>true);
const field=Uint32Array.of(6<<20|512,6<<20|512),replayed=[];let initializations=0,advances=0;