tiny-army / web /deviceInfo.js
polats's picture
Settings: "Use recommended for this device" button (GPU/device-tier heuristic)
7b49a92
// Best-effort device/GPU/RAM probe for the Settings "Your device" panel — handy for
// debugging and benchmarking which quality preset a machine can handle. Everything is
// guarded: unsupported APIs just read "—" rather than throwing.
import { storageEstimate } from '/web/storage.js'
import { fmtBytes } from '/web/modelCatalog.js'
function webglRenderer() {
try {
const c = document.createElement('canvas')
const gl = c.getContext('webgl') || c.getContext('experimental-webgl')
if (!gl) return '—'
const dbg = gl.getExtension('WEBGL_debug_renderer_info')
return dbg ? String(gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL)) : (gl.getParameter(gl.RENDERER) || '—')
} catch { return '—' }
}
async function webgpuInfo() {
try {
if (!navigator.gpu) return 'unavailable'
const ad = await navigator.gpu.requestAdapter()
if (!ad) return 'no adapter'
const info = ad.info || (ad.requestAdapterInfo ? await ad.requestAdapterInfo() : null)
const desc = info ? [info.vendor, info.architecture, info.description].filter(Boolean).join(' ').trim() : ''
return desc || 'available'
} catch { return 'error' }
}
export function isMobile() {
try { if (navigator.userAgentData && typeof navigator.userAgentData.mobile === 'boolean') return navigator.userAgentData.mobile } catch { /* ignore */ }
return /Mobi|Android|iPhone|iPad|iPod|IEMobile|Opera Mini/i.test(navigator.userAgent || '')
}
// A combined, lowercased GPU descriptor (WebGL renderer + WebGPU adapter info) for
// heuristic matching. WebGL's ANGLE string usually carries the real chip name, e.g.
// "…nvidia geforce rtx 3090…" on desktop or "adreno (tm) 730" on Android.
async function gpuDescriptor() {
let s = webglRenderer()
try {
if (navigator.gpu) {
const ad = await navigator.gpu.requestAdapter()
const info = ad && (ad.info || (ad.requestAdapterInfo ? await ad.requestAdapterInfo() : null))
if (info) s += ' ' + [info.vendor, info.architecture, info.description].filter(Boolean).join(' ')
}
} catch { /* ignore */ }
return s.toLowerCase()
}
// Recommend a quality preset for this device:
// • Mobile → Low, but Medium if a Qualcomm Adreno 700-series (or higher) is detected.
// • Desktop → Medium, but High if an NVIDIA RTX 3090 (or higher) is detected.
// "or higher" for RTX = a newer generation, or the same 30-gen at ≥90 (so 3090/3090 Ti
// and all 40xx/50xx qualify; 3080 and below don't).
export async function recommendPreset() {
const gpu = await gpuDescriptor()
if (isMobile()) {
const a = gpu.match(/adreno[^\d]*(\d{3,4})/)
if (a && parseInt(a[1], 10) >= 700) return { id: 'medium', reason: `mobile · Adreno ${a[1]} (≥700)` }
return { id: 'low', reason: 'mobile device' }
}
const r = gpu.match(/rtx\s*0*(\d{3,4})/)
if (r) {
const n = parseInt(r[1], 10), gen = Math.floor(n / 100), model = n % 100
if (gen > 30 || (gen === 30 && model >= 90)) return { id: 'high', reason: `desktop · RTX ${r[1]} (≥3090)` }
}
return { id: 'medium', reason: 'desktop' }
}
// Returns an ordered array of [label, value] rows.
export async function gatherDeviceInfo() {
const rows = []
const add = (k, v) => rows.push([k, v == null || v === '' ? '—' : String(v)])
add('CPU threads', navigator.hardwareConcurrency)
add('Device memory', navigator.deviceMemory ? `${navigator.deviceMemory} GB (approx)` : '— (not reported)')
add('WebGPU', await webgpuInfo())
add('GPU (WebGL)', webglRenderer())
try {
const { usage, quota } = await storageEstimate()
add('Storage cache', quota ? `${fmtBytes(usage || 0)} / ${fmtBytes(quota)}` : '—')
} catch { add('Storage cache', '—') }
try { add('Screen', `${screen.width}×${screen.height} @ ${window.devicePixelRatio || 1}×`) } catch { add('Screen', '—') }
add('Platform', (navigator.userAgentData && navigator.userAgentData.platform) || navigator.platform)
add('Language', navigator.language)
add('User agent', navigator.userAgent)
return rows
}