diff --git a/emscripten/pages-index.html b/emscripten/pages-index.html index e02d3a6728..42ac633144 100644 --- a/emscripten/pages-index.html +++ b/emscripten/pages-index.html @@ -56,6 +56,10 @@ /* The old direct launch and the ZIP path stay wired for the script but out of sight. */ #launch,#reload,label[for=ownershipFiles],#clearOwnership{display:none} + .quality{display:grid;grid-template-columns:repeat(3,1fr);gap:4px;margin-top:14px;padding:4px;border-radius:14px;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.08)} + .quality button{padding:9px 6px;border-radius:11px;font-size:14px;font-weight:600;color:var(--ink);background:transparent;box-shadow:none} + .quality button[aria-pressed=true]{color:#06121b;background:linear-gradient(180deg,#6fd0ff,var(--blue));box-shadow:0 4px 14px rgba(47,180,255,.3)} + #qualityHint{margin-top:6px;font-size:12.5px;color:var(--dim)} details{width:100%;color:var(--dim);font-size:13px} summary{list-style:none;cursor:pointer;text-align:center;padding:6px;font-size:13px;color:var(--dim)} summary::-webkit-details-marker{display:none} @@ -103,6 +107,12 @@

Choose your Portal folder

Play

Turn your iPhone sideways. The game fills the screen.

+
+ + + +
+

@@ -231,15 +241,38 @@

Diagnostics

await navigator.serviceWorker.ready; if(registration.waiting)registration.waiting.postMessage({type:'SKIP_WAITING'}); set('sw',true,navigator.serviceWorker.controller?'controlling':'registered'); - if(!navigator.serviceWorker.controller&&!sessionStorage.getItem('render360PagesReloaded')){ - sessionStorage.setItem('render360PagesReloaded','1'); - log('Service worker installed; reloading once for isolation.'); - location.reload(); - return new Promise(()=>{}); + // The isolation headers only apply to pages the worker controls. After + // a deploy Safari can load the page before the new worker takes over, + // leaving it without SharedArrayBuffer, so reload whenever isolation is + // missing. The time guard stops a loop on browsers that can't isolate. + if(!navigator.serviceWorker.controller||!window.crossOriginIsolated){ + let last=0;try{last=Number(sessionStorage.getItem('render360PagesReloadAt'))||0}catch(_){} + if(Date.now()-last>20000){ + try{sessionStorage.setItem('render360PagesReloadAt',String(Date.now()))}catch(_){} + log('Service worker not isolating this page yet; reloading.'); + location.reload(); + return new Promise(()=>{}); + } } }catch(error){set('sw',false,'registration failed');log('Service worker error:',error?.stack||String(error))} } + const QUALITY_HINTS={ + standard:'540p, lighter textures. Smoothest, and safest on older iPhones.', + hd:'720p, sharper textures. Uses more memory and battery.', + max:'Full screen resolution and full textures. May run out of memory on older iPhones.' + }; + function setupQuality(){ + let current='standard'; + try{current=localStorage.getItem('render360Quality')||'standard'}catch(_){} + if(!QUALITY_HINTS[current])current='standard'; + const buttons=[...document.querySelectorAll('.quality button')]; + const show=()=>{for(const b of buttons)b.setAttribute('aria-pressed',String(b.dataset.quality===current));$('qualityHint').textContent=QUALITY_HINTS[current]}; + for(const b of buttons)b.addEventListener('click',()=>{current=b.dataset.quality;try{localStorage.setItem('render360Quality',current)}catch(_){}show()}); + show(); + } + setupQuality(); + function testSharedMemory(){ try{const memory=new WebAssembly.Memory({initial:1,maximum:1,shared:true});return !!memory.buffer&&Object.prototype.toString.call(memory.buffer).includes('SharedArrayBuffer')} catch(error){log('Wasm shared-memory test:',String(error));return false} diff --git a/emscripten/pre.js b/emscripten/pre.js index 7d28bd51e0..957846bc62 100644 --- a/emscripten/pre.js +++ b/emscripten/pre.js @@ -307,6 +307,24 @@ Module['arguments'].push( // engine's own on-screen touch controls are the only way to move or look. // They default to off everywhere except Android. Window-only: this file also // runs inside every pthread worker. +// Graphics quality, picked on the start page or in the in-game menu. +// standard: 540p, quarter-size textures (the safe default for older phones) +// hd: 720p, half-size textures +// max: the screen's full resolution, full-size textures (uses the most memory) +const RENDER360_QUALITY = { + standard: { phoneHeight: 540, desktopHeight: 720, picmip: 2 }, + hd: { phoneHeight: 720, desktopHeight: 1080, picmip: 1 }, + max: { phoneHeight: 4096, desktopHeight: 4096, picmip: 0 } +} +let render360Quality = 'standard' +if(render360IsWindow) { + try { + const saved = localStorage.getItem('render360Quality') + if(saved && RENDER360_QUALITY[saved]) render360Quality = saved + } catch(_) {} + Module.render360Quality = render360Quality +} + if(render360IsWindow) { let coarsePointer = false try { coarsePointer = !!(window.matchMedia && window.matchMedia('(pointer: coarse)').matches) } catch(_) {} @@ -320,7 +338,7 @@ if(render360IsWindow) { // Quarter-size textures. On a 6 inch screen the difference is hard to // see, and textures are the largest thing a map load adds to both the // Wasm heap and GPU memory, which is where iOS kills the page. - Module['arguments'].push('+mat_picmip', '2') + Module['arguments'].push('+mat_picmip', String(RENDER360_QUALITY[render360Quality].picmip)) // Cheaper rendering features for a phone GPU. Bump maps and specular // stay on: turning them off makes Portal's materials ask for a // $bumpmap texture that is not loaded (console errors). @@ -348,7 +366,8 @@ if(render360IsWindow) { } catch(_) {} if(longSide > 0 && shortSide > 0) { const dpr = Math.max(1, Number(window.devicePixelRatio || 1)) - const height = Math.max(360, Math.min(phone ? 540 : 720, Math.round(shortSide * dpr))) + const quality = RENDER360_QUALITY[render360Quality] + const height = Math.max(360, Math.min(phone ? quality.phoneHeight : quality.desktopHeight, Math.round(shortSide * dpr))) const width = Math.round(height * longSide / shortSide / 2) * 2 Module.render360GameSize = { width, height } Module['arguments'].push('-w', String(width), '-h', String(height)) diff --git a/emscripten/shell.html b/emscripten/shell.html index 0736faef4c..e26baab0a0 100644 --- a/emscripten/shell.html +++ b/emscripten/shell.html @@ -227,6 +227,7 @@

Portal stopped

+
Shares the engine log with the developer
@@ -344,7 +345,30 @@

Portal stopped

} // ---------------------------------------------------------- menu + var RENDER360_QUALITY_NAMES = { standard: 'Standard', hd: 'HD', max: 'Max' }; + var RENDER360_QUALITY_ORDER = ['standard', 'hd', 'max']; + function render360SavedQuality() { + var q = 'standard'; + try { q = localStorage.getItem('render360Quality') || 'standard'; } catch (_) {} + return RENDER360_QUALITY_NAMES[q] ? q : 'standard'; + } + function render360ShowQuality() { + var row = document.getElementById('r360-quality-row'); + if (!row) return; + var saved = render360SavedQuality(); + var running = (window.Module && Module.render360Quality) || saved; + row.textContent = 'Graphics: ' + RENDER360_QUALITY_NAMES[saved] + + (saved !== running ? ' (next launch)' : ''); + } + // Resolution and texture size are fixed when the engine starts, so a new + // choice applies the next time Portal is opened. + function render360CycleQuality() { + var next = RENDER360_QUALITY_ORDER[(RENDER360_QUALITY_ORDER.indexOf(render360SavedQuality()) + 1) % RENDER360_QUALITY_ORDER.length]; + try { localStorage.setItem('render360Quality', next); } catch (_) {} + render360ShowQuality(); + } function render360OpenSheet(open) { + if (open) render360ShowQuality(); document.getElementById('r360-sheet').classList.toggle('r360-open', !!open); } @@ -410,7 +434,8 @@

Portal stopped

'OffscreenCanvas=' + typeof OffscreenCanvas, 'WebGL2=' + String(!!document.createElement('canvas').getContext('webgl2')), 'viewport=' + window.innerWidth + 'x' + window.innerHeight + '@' + (window.devicePixelRatio || 1) + - ' game=' + JSON.stringify((typeof Module !== 'undefined' && Module.render360GameSize) || null), + ' game=' + JSON.stringify((typeof Module !== 'undefined' && Module.render360GameSize) || null) + + ' quality=' + ((typeof Module !== 'undefined' && Module.render360Quality) || 'unknown'), '', '--- latest runtime event only ---', render360LatestOutput || '' @@ -555,6 +580,17 @@

Portal stopped

' OffscreenCanvas=' + typeof OffscreenCanvas); if (!window.crossOriginIsolated || typeof SharedArrayBuffer === 'undefined') { + // Usually the start page's service worker wasn't in control yet (Safari + // right after a deploy). Reload the whole page once so it is; the time + // guard stops a loop where isolation is impossible (Private tabs). + var render360LastIsolationReload = 0; + try { render360LastIsolationReload = Number(sessionStorage.getItem('render360PagesReloadAt')) || 0; } catch (_) {} + if (Date.now() - render360LastIsolationReload > 20000) { + try { sessionStorage.setItem('render360PagesReloadAt', String(Date.now())); } catch (_) {} + render360AppendOutput('[Render360 baseline] not isolated; reloading the start page'); + try { (window.top || window).location.reload(); } catch (_) { window.location.reload(); } + throw new Error('Render360: reloading for cross-origin isolation'); + } render360ShowFailure('Portal can’t start here', 'This page is not cross-origin isolated, so threads are unavailable. Open it from the Portal start page, not a Private tab.'); } diff --git a/materialsystem/shaderapidx9/shaderdevicedx8.cpp b/materialsystem/shaderapidx9/shaderdevicedx8.cpp index d44060917b..4bec1f201b 100644 --- a/materialsystem/shaderapidx9/shaderdevicedx8.cpp +++ b/materialsystem/shaderapidx9/shaderdevicedx8.cpp @@ -1462,10 +1462,15 @@ bool CShaderDeviceMgrDx8::ValidateMode( int nAdapter, const ShaderDeviceInfo_t & // settings GetCurrentModeInfo( &displayMode, 0 ); +#ifndef __EMSCRIPTEN__ // make sure the window fits within the current video mode + // (In the browser the GL layer reports a fixed 640x480 display, while + // the canvas can be any size the page asks for; a page-sized window + // was rejected here and the engine quit before showing anything.) if ( ( info.m_DisplayMode.m_nWidth > displayMode.m_nWidth ) || ( info.m_DisplayMode.m_nHeight > displayMode.m_nHeight ) ) return false; +#endif } else {