File size: 4,030 Bytes
750ca83
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7b49a92
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
750ca83
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
// 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
}