forked from Rockachopa/the-matrix
Fixes: - #22 OrbitControls damping: call updateControls() in animate loop - #23 Empty catch blocks: add console.warn + error surfacing to chat panel - #24 escapeHtml: add quote escaping (" '), use in renderAgentList - #25 WS reconnect: check close code (1000/1001) before reconnecting, add exponential backoff + heartbeat zombie detection - #26 IDLE state visibility: brighten from near-invisible to #005500 - #5 PWA: manifest.json, service worker (network-first), theme-color, favicon, loading screen, safe-area-inset padding, apple-mobile-web-app - #14 Adaptive render quality: new quality.js hardware detection (low/ medium/high tiers), tiered particle counts, grid density, antialias, pixel ratio caps New files: - js/quality.js — hardware detection + quality tier logic - manifest.json — PWA manifest - public/sw.js — service worker (network-first with offline cache) - public/favicon.svg — SVG favicon - icons/icon-192.svg, icons/icon-512.svg — PWA icons
91 lines
3.0 KiB
JavaScript
91 lines
3.0 KiB
JavaScript
/**
|
|
* quality.js — Detect hardware capability and return a quality tier.
|
|
*
|
|
* Tiers:
|
|
* 'low' — older iPads, phones, low-end GPUs (reduce particles, simpler effects)
|
|
* 'medium' — mid-range (moderate particle count)
|
|
* 'high' — desktop, modern iPad Pro (full quality)
|
|
*
|
|
* Detection uses a combination of:
|
|
* - Device pixel ratio (low DPR = likely low-end)
|
|
* - Logical core count (navigator.hardwareConcurrency)
|
|
* - Device memory (navigator.deviceMemory, Chrome/Edge only)
|
|
* - Screen size (small viewport = likely mobile)
|
|
* - Touch capability (touch + small screen = phone/tablet)
|
|
* - WebGL renderer string (if available)
|
|
*/
|
|
|
|
let cachedTier = null;
|
|
|
|
export function getQualityTier() {
|
|
if (cachedTier) return cachedTier;
|
|
|
|
let score = 0;
|
|
|
|
// Core count: 1-2 = low, 4 = mid, 8+ = high
|
|
const cores = navigator.hardwareConcurrency || 2;
|
|
if (cores >= 8) score += 3;
|
|
else if (cores >= 4) score += 2;
|
|
else score += 0;
|
|
|
|
// Device memory (Chrome/Edge): < 4GB = low, 4-8 = mid, 8+ = high
|
|
const mem = navigator.deviceMemory || 4;
|
|
if (mem >= 8) score += 3;
|
|
else if (mem >= 4) score += 2;
|
|
else score += 0;
|
|
|
|
// Screen dimensions (logical pixels)
|
|
const maxDim = Math.max(window.screen.width, window.screen.height);
|
|
if (maxDim < 768) score -= 1; // phone
|
|
else if (maxDim >= 1920) score += 1; // large desktop
|
|
|
|
// DPR: high DPR on small screens = more GPU work
|
|
const dpr = window.devicePixelRatio || 1;
|
|
if (dpr > 2 && maxDim < 1024) score -= 1; // retina phone
|
|
|
|
// Touch-only device heuristic
|
|
const touchOnly = 'ontouchstart' in window && !window.matchMedia('(pointer: fine)').matches;
|
|
if (touchOnly) score -= 1;
|
|
|
|
// Try reading WebGL renderer for GPU hints
|
|
try {
|
|
const canvas = document.createElement('canvas');
|
|
const gl = canvas.getContext('webgl') || canvas.getContext('webgl2');
|
|
if (gl) {
|
|
const debugExt = gl.getExtension('WEBGL_debug_renderer_info');
|
|
if (debugExt) {
|
|
const renderer = gl.getParameter(debugExt.UNMASKED_RENDERER_WEBGL).toLowerCase();
|
|
// Known low-end GPU strings
|
|
if (renderer.includes('swiftshader') || renderer.includes('llvmpipe') || renderer.includes('software')) {
|
|
score -= 3; // software renderer
|
|
}
|
|
if (renderer.includes('apple gpu') || renderer.includes('apple m')) {
|
|
score += 2; // Apple Silicon is good
|
|
}
|
|
}
|
|
gl.getExtension('WEBGL_lose_context')?.loseContext();
|
|
}
|
|
} catch {
|
|
// Can't probe GPU, use other signals
|
|
}
|
|
|
|
// Map score to tier
|
|
if (score <= 1) cachedTier = 'low';
|
|
else if (score <= 4) cachedTier = 'medium';
|
|
else cachedTier = 'high';
|
|
|
|
console.info(`[Matrix Quality] Tier: ${cachedTier} (score: ${score}, cores: ${cores}, mem: ${mem}GB, dpr: ${dpr}, touch: ${touchOnly})`);
|
|
|
|
return cachedTier;
|
|
}
|
|
|
|
/**
|
|
* Get the recommended pixel ratio cap for the renderer.
|
|
*/
|
|
export function getMaxPixelRatio() {
|
|
const tier = getQualityTier();
|
|
if (tier === 'low') return 1;
|
|
if (tier === 'medium') return 1.5;
|
|
return 2;
|
|
}
|