massfront-playtest / src /glrecover.js
CREATORJD's picture
Publish MASSFRONT web 1.33.33 (part 3)
3597f5a verified
Raw
History Blame Contribute Delete
9.83 kB
;
;
/* ============================================================================
GRAPHICS RECOVERY — wait, rebuild, step down. Do not throw the match away.
----------------------------------------------------------------------------
What used to happen when Android reclaimed the GPU: the game stopped, showed
an overlay, and reloaded the whole page 1.7 seconds later. Two things were
wrong with that, and the second is the worse one.
1.7 SECONDS IS NOT LONG ENOUGH TO ASK. A lost WebGL context is very often
given back — the browser fires `webglcontextrestored` once the driver has
room again, and on a phone that routinely takes several seconds. Reloading
after 1.7 s guaranteed we never found out.
AND THE RESTORE PATH ALSO RELOADED. Even when the browser DID hand the
context back, the handler called location.replace(). So the one event that
means "you can carry on" was treated identically to the failure, and the
player lost their match either way.
The sim does not live on the GPU. Units, structures, resources and the clock
are plain JavaScript arrays and are completely untouched by a context loss.
Only GL OBJECTS die — programs, textures, buffers, vertex arrays — and every
one of those is created by a short, known sequence at boot. So the honest
response is to pause, wait properly, re-run that sequence, and carry on in
place. No reload, no lost match.
And because a context loss means the device ran out of graphics memory, the
quality preset steps DOWN one notch on recovery and stays there. Restoring at
exactly the settings that just exhausted the GPU invites the same crash
ninety seconds later, and a player who has to find the graphics menu to stop
a crash loop will just stop playing.
============================================================================ */
/* How long to wait before conceding. Generous on purpose: the alternative to
waiting is losing the match, so a player would rather look at a "recovering"
card for twenty seconds than be thrown back to the menu in two. */
/* Eight seconds is long enough for Android to return a backgrounded context,
but short enough that a genuinely dead driver does not strand the player on
a countdown. The session snapshot makes the automatic reload recoverable. */
const GLR_WAIT_MS = 8000;
let glrLost = false, glrDeadline = 0, glrTick = 0, glrWasRunning = false;
function glrQualityDown(){
try{
const order=['cinematic','high','medium','low'];
const cur=(typeof qualityKey==='function')?qualityKey():'high';
const i=order.indexOf(cur);
if(i<0||i>=order.length-1) return null; // already at the floor
META.settings.quality=order[i+1];
if(typeof applySettings==='function') applySettings();
if(typeof metaSave==='function') metaSave(); // survive the next launch too
return META.settings.quality;
}catch(e){ return null; }
}
/* Re-run ONLY the GL-object creators from the boot sequence in main.js.
Deliberately NOT setupDeposits/setupDoodads/camera — those mutate world
state, and re-running them would regenerate the map underneath a live match
or stack a second set of deposits on top of the first. */
function glrRebuildResources(){
const step=(name,fn)=>{ try{ fn(); }catch(e){ console.warn('glrecover: '+name+' failed',e); throw e; } };
/* FORGET THE DEAD HANDLES FIRST. Builders that cache their VAO/texture in a
module variable branch on if(!handle) - after a loss the handle is truthy
but dead, so without this the terrain rebuild poured vertices into dead
buffers and the ground stayed a fog-coloured void for the rest of the
match (seen on device at 11:37: units and structures back, terrain gone). */
step('terrainGLReset',()=>{ if(typeof terrainGLReset==='function') terrainGLReset(); });
step('worldKitGLReset',()=>{ if(typeof worldKitGLReset==='function') worldKitGLReset(); });
step('fogGLReset', ()=>{ if(typeof fogGLReset==='function') fogGLReset(); });
step('initGL3D', ()=>initGL3D());
step('buildMatAtlas', ()=>buildMatAtlas());
step('initMaterialV2',()=>{ if(typeof initMaterialV2==='function') initMaterialV2(); });
step('initBillboards',()=>{ if(typeof initBillboards==='function') initBillboards(); });
step('initModels', ()=>initModels());
step('buildAtlas', ()=>{ if(typeof buildAtlas==='function') atlasTex=buildAtlas(); });
step('mfIconInitGL', ()=>{ if(typeof mfIconInitGL==='function') mfIconInitGL(); });
step('buildDetailTex',()=>{ if(typeof buildDetailTex==='function') buildDetailTex(); });
step('terrainTextures',()=>{ terrGroundTex=terrSoilTex=terrPaveTex=terrGrassTex=null;
terrGroundNrm=terrSoilNrm=terrPaveNrm=terrGrassNrm=null;
if(typeof loadTerrainTextures==='function') loadTerrainTextures(); });
step('initFloatText', ()=>{ if(typeof initFloatText==='function') initFloatText(); });
/* Terrain is deterministic from the map seed, so rebuilding its texture
reproduces the same ground rather than a new map. */
step('buildTerrain', ()=>{ terrainTex=buildTerrain(); });
step('resize', ()=>resize());
/* The fog texture is GPU-side and would otherwise come back blank, hiding
everything the player had already explored. */
try{ if(typeof updateFog==='function'&&typeof fogOn!=='undefined'&&fogOn) updateFog(); }catch(e){}
return true;
}
function glrCard(title,body,action){
let o=document.getElementById('glrCard');
if(!o){
o=document.createElement('div'); o.id='glrCard';
o.style.cssText='position:fixed;inset:0;z-index:2147483000;display:flex;flex-direction:column;'
+'align-items:center;justify-content:center;gap:14px;background:rgba(2,6,12,.94);color:#cfe6ff;'
+'text-align:center;padding:26px;font-family:var(--fT,system-ui,sans-serif)';
(document.body||document.documentElement).appendChild(o);
}
o.innerHTML='<div style="font:900 14px/1.4 var(--fT,sans-serif);letter-spacing:.1em;color:#8fd0ff">'+title+'</div>'
+'<div id="glrBody" style="font-size:12px;max-width:300px;line-height:1.55;color:#9fb8cc">'+body+'</div>'
+(action?'<button id="glrAct" type="button" style="margin-top:6px;padding:13px 30px;border-radius:12px;'
+'border:1px solid #48cfff;background:linear-gradient(180deg,#2a6f96,#0d2a42);color:#eaf7ff;'
+'font:900 12px var(--fT,sans-serif);letter-spacing:.12em">'+action.label+'</button>':'');
if(action){
const b=document.getElementById('glrAct');
if(b){ if(typeof mfBindTap==='function') mfBindTap(b,action.fn); else b.addEventListener('click',action.fn); }
}
return o;
}
function glrCardBody(html){ const b=document.getElementById('glrBody'); if(b) b.innerHTML=html; }
function glrHide(){ const o=document.getElementById('glrCard'); if(o) o.remove(); }
function glrOnLost(e){
if(e&&e.preventDefault) e.preventDefault(); // required, or the context is never restorable
if(glrLost) return;
glrLost=true;
glrWasRunning=(typeof running!=='undefined')&&running&&!(typeof gameEnded!=='undefined'&&gameEnded);
/* PAUSE, do not stop. `running=false` tears down the match; the sim state is
fine and we intend to carry on with it. */
try{ if(typeof paused!=='undefined') paused=true; }catch(_){}
/* Belt and braces: if recovery fails and we do end up reloading, the match is
already on disk. */
try{ if(typeof sessSnapshot==='function') sessSnapshot('contextlost'); }catch(_){}
glrDeadline=Date.now()+GLR_WAIT_MS;
glrCard('GRAPHICS PAUSED',
'The device reclaimed graphics memory. Waiting for it to come back — your match is paused, not lost.',
{label:'RELOAD NOW', fn:glrGiveUp});
clearInterval(glrTick);
glrTick=setInterval(function(){
const left=Math.max(0,Math.ceil((glrDeadline-Date.now())/1000));
glrCardBody('The device reclaimed graphics memory. Waiting for it to come back — '
+'your match is paused, not lost.<br><span style="color:#7f9db4">'+left+'s</span>');
if(left<=0) glrGiveUp();
},500);
}
function glrOnRestored(){
if(!glrLost) return;
clearInterval(glrTick);
glrCardBody('Graphics restored. Rebuilding…');
let ok=false;
try{ ok=glrRebuildResources(); }catch(e){ ok=false; }
if(!ok){ glrGiveUp(); return; }
/* Step down so the same fight does not exhaust the same GPU again in ninety
seconds. Told plainly, because a silent quality drop reads as the game
getting worse for no reason. */
const now=glrQualityDown();
glrLost=false; glrHide();
try{ if(typeof paused!=='undefined') paused=false; }catch(_){}
try{
if(typeof toast==='function')
toast(now ? '◈ GRAPHICS RESTORED — quality lowered to '+String(now).toUpperCase()+' to keep it stable'
: '◈ GRAPHICS RESTORED');
}catch(_){}
}
/* Last resort only. The session was snapshotted at the moment of loss, and
sessRenderResume() offers it the moment the menu comes back. */
function glrGiveUp(){
clearInterval(glrTick);
glrQualityDown();
try{ if(typeof sessSnapshot==='function') sessSnapshot('contextlost'); }catch(_){}
glrCardBody('Graphics did not come back. Reloading — your match is saved, and RESUME DROPPED SESSION will pick it up.');
setTimeout(function(){
try{ const u=new URL(location.href); u.searchParams.set('mf_glreset',Date.now().toString(36)); location.replace(u.href); }
catch(e){ location.reload(); }
},900);
}
(function glrInstall(){
try{
const c=document.getElementById('gl');
if(!c) return;
/* Capture phase, so this runs before the legacy handler in gl.js and can
stop it reloading out from under a recovery that is already working. */
c.addEventListener('webglcontextlost', glrOnLost, true);
c.addEventListener('webglcontextrestored', glrOnRestored, true);
}catch(e){}
})();