Files

966 lines
35 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GPU Diagnostics</title>
<style>
:root {
--bg: #0c0c0c;
--bg2: #111111;
--bg3: #161616;
--border: #2a2a2a;
--border2: #333333;
--text: #cccccc;
--dim: #666666;
--ok: #33cc55;
--warn: #ddaa22;
--err: #cc3333;
--accent: #5588cc;
--head: #aaaaaa;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
height: 100%;
background: var(--bg);
color: var(--text);
font-family: "Consolas", "Cascadia Code", "Lucida Console", "Courier New", monospace;
font-size: 12px;
line-height: 1.45;
}
/* ── title bar ── */
#titlebar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 12px;
background: var(--bg3);
border-bottom: 1px solid var(--border2);
user-select: none;
}
#titlebar .left {
display: flex;
align-items: center;
gap: 12px;
}
#titlebar h1 {
font-size: 12px;
font-weight: normal;
color: var(--head);
letter-spacing: 0.06em;
text-transform: uppercase;
}
#titlebar .sep { color: var(--border2); }
#run-ts {
font-size: 11px;
color: var(--dim);
}
#titlebar .right {
display: flex;
gap: 6px;
}
button {
background: var(--bg3);
border: 1px solid var(--border2);
color: var(--head);
font: inherit;
font-size: 11px;
padding: 3px 10px;
cursor: pointer;
text-transform: uppercase;
letter-spacing: 0.04em;
}
button:hover { background: var(--bg2); border-color: var(--accent); color: var(--accent); }
button:active { background: #1a2030; }
/* ── body layout ── */
#layout {
display: grid;
grid-template-columns: 360px 1fr;
grid-template-rows: auto 1fr;
height: calc(100vh - 33px);
overflow: hidden;
}
/* ── left column: test panels ── */
#left-col {
grid-row: 1 / 3;
display: flex;
flex-direction: column;
border-right: 1px solid var(--border);
overflow-y: auto;
}
/* ── right top: capability table ── */
#cap-panel {
overflow-y: auto;
border-bottom: 1px solid var(--border);
}
/* ── right bottom: run log ── */
#log-panel {
overflow-y: auto;
}
/* ── panel structure ── */
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 10px;
background: var(--bg3);
border-bottom: 1px solid var(--border);
color: var(--head);
text-transform: uppercase;
letter-spacing: 0.07em;
font-size: 11px;
position: sticky;
top: 0;
z-index: 1;
}
.panel-header span { color: var(--dim); font-size: 10px; text-transform: none; letter-spacing: 0; }
.panel-body { padding: 8px 10px; }
/* ── test rows ── */
.test-block {
border-bottom: 1px solid var(--border);
}
.test-block:last-child { border-bottom: none; }
.test-header {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
background: var(--bg2);
border-bottom: 1px solid var(--border);
}
.test-name {
flex: 1;
font-weight: bold;
color: var(--head);
text-transform: uppercase;
letter-spacing: 0.05em;
font-size: 11px;
}
.badge {
font-size: 10px;
font-weight: bold;
padding: 1px 6px;
border: 1px solid;
letter-spacing: 0.08em;
}
.badge-ok { color: var(--ok); border-color: var(--ok); }
.badge-warn { color: var(--warn); border-color: var(--warn); }
.badge-err { color: var(--err); border-color: var(--err); }
.badge-wait { color: var(--dim); border-color: var(--dim); }
.test-body {
padding: 6px 10px 8px;
}
canvas {
display: block;
width: 100%;
height: 80px;
border: 1px solid var(--border);
background: #080808;
margin-bottom: 6px;
}
.kv-grid {
display: grid;
grid-template-columns: minmax(130px, auto) 1fr;
gap: 2px 8px;
}
.kv-key {
color: var(--dim);
white-space: nowrap;
}
.kv-val {
color: var(--text);
overflow-wrap: anywhere;
}
.kv-val.ok { color: var(--ok); }
.kv-val.warn { color: var(--warn); }
.kv-val.err { color: var(--err); }
/* ── capability table ── */
.cap-table {
width: 100%;
border-collapse: collapse;
}
.cap-table th {
text-align: left;
color: var(--dim);
font-weight: normal;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.06em;
padding: 3px 6px;
border-bottom: 1px solid var(--border);
background: var(--bg3);
position: sticky;
top: 37px;
}
.cap-table td {
padding: 2px 6px;
border-bottom: 1px solid var(--border);
vertical-align: top;
}
.cap-table tr:last-child td { border-bottom: none; }
.cap-table .section-row td {
padding: 5px 6px 2px;
color: var(--accent);
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.1em;
background: var(--bg2);
border-top: 1px solid var(--border);
}
.param-name { color: var(--dim); width: 220px; }
.param-val { color: var(--text); word-break: break-all; }
/* ── extensions list ── */
.ext-list {
display: flex;
flex-wrap: wrap;
gap: 3px;
padding: 6px 10px 10px;
}
.ext-tag {
font-size: 10px;
padding: 1px 5px;
border: 1px solid var(--border2);
color: var(--dim);
background: var(--bg2);
}
.ext-tag.tier1 { border-color: #334455; color: #5588aa; }
.ext-tag.tier2 { border-color: var(--border2); color: #888; }
/* ── log ── */
#log-lines {
padding: 6px 10px;
font-size: 11px;
line-height: 1.6;
}
.log-line { display: flex; gap: 8px; }
.log-ts { color: var(--dim); flex-shrink: 0; }
.log-ok { color: var(--ok); }
.log-warn { color: var(--warn); }
.log-err { color: var(--err); }
.log-info { color: var(--text); }
.log-dim { color: var(--dim); }
/* ── scrollbars ── */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: var(--bg); }
::-webkit-scrollbar-thumb { background: var(--border2); }
::-webkit-scrollbar-thumb:hover { background: #444; }
/* ── copy flash ── */
@keyframes flash { 0%,100% { opacity:1; } 50% { opacity:0.4; } }
.flash { animation: flash 0.25s ease; }
</style>
</head>
<body>
<div id="titlebar">
<div class="left">
<h1>GPU Diagnostics</h1>
<span class="sep">/</span>
<span id="run-ts">not run</span>
</div>
<div class="right">
<button id="btn-copy">Copy Report</button>
<button id="btn-run">Run Tests</button>
</div>
</div>
<div id="layout">
<!-- ── LEFT: test panels ── -->
<div id="left-col">
<!-- Overall -->
<div class="test-block">
<div class="test-header">
<span class="test-name">Overall</span>
<span class="badge badge-wait" id="badge-overall">WAITING</span>
</div>
<div class="test-body">
<div class="kv-grid" id="kv-overall"></div>
</div>
</div>
<!-- WebGL 1 -->
<div class="test-block">
<div class="test-header">
<span class="test-name">WebGL 1</span>
<span class="badge badge-wait" id="badge-webgl">WAITING</span>
</div>
<div class="test-body">
<canvas id="webgl-canvas" width="480" height="160"></canvas>
<div class="kv-grid" id="kv-webgl"></div>
</div>
</div>
<!-- WebGL 2 -->
<div class="test-block">
<div class="test-header">
<span class="test-name">WebGL 2</span>
<span class="badge badge-wait" id="badge-webgl2">WAITING</span>
</div>
<div class="test-body">
<canvas id="webgl2-canvas" width="480" height="160"></canvas>
<div class="kv-grid" id="kv-webgl2"></div>
</div>
</div>
<!-- Canvas 2D -->
<div class="test-block">
<div class="test-header">
<span class="test-name">Canvas 2D</span>
<span class="badge badge-wait" id="badge-canvas2d">WAITING</span>
</div>
<div class="test-body">
<canvas id="canvas2d" width="480" height="160"></canvas>
<div class="kv-grid" id="kv-canvas2d"></div>
</div>
</div>
<!-- WebGPU -->
<div class="test-block">
<div class="test-header">
<span class="test-name">WebGPU</span>
<span class="badge badge-wait" id="badge-webgpu">WAITING</span>
</div>
<div class="test-body">
<div class="kv-grid" id="kv-webgpu"></div>
</div>
</div>
<!-- System -->
<div class="test-block">
<div class="test-header">
<span class="test-name">System</span>
</div>
<div class="test-body">
<div class="kv-grid" id="kv-system"></div>
</div>
</div>
</div><!-- /left-col -->
<!-- ── RIGHT TOP: capability table ── -->
<div id="cap-panel">
<div class="panel-header">
GPU Capability Parameters
<span id="cap-count"></span>
</div>
<table class="cap-table" id="cap-table">
<thead><tr><th>Parameter</th><th>Value</th></tr></thead>
<tbody id="cap-body">
<tr><td colspan="2" style="padding:8px 6px;color:var(--dim)">Run tests to populate…</td></tr>
</tbody>
</table>
<div class="panel-header" style="margin-top:0;border-top:1px solid var(--border)">
Extensions
<span id="ext-count"></span>
</div>
<div class="ext-list" id="ext-list">
<span style="color:var(--dim)">Run tests to populate…</span>
</div>
</div>
<!-- ── RIGHT BOTTOM: run log ── -->
<div id="log-panel">
<div class="panel-header">Run Log</div>
<div id="log-lines"><span class="log-dim">Waiting for first run…</span></div>
</div>
</div><!-- /layout -->
<script>
/* ── helpers ── */
const $ = id => document.getElementById(id);
function fmt(v) {
if (v === undefined || v === null || v === '') return 'n/a';
if (Array.isArray(v)) return v.length ? v.join(', ') : 'n/a';
return String(v);
}
function setBadge(id, level, text) {
const el = $(id);
el.className = `badge badge-${level}`;
el.textContent = text;
}
function kvFill(containerId, rows) {
const el = $(containerId);
el.replaceChildren();
rows.forEach(([k, v, cls]) => {
const key = document.createElement('span');
key.className = 'kv-key';
key.textContent = k;
const val = document.createElement('span');
val.className = 'kv-val' + (cls ? ` ${cls}` : '');
val.textContent = fmt(v);
el.append(key, val);
});
}
/* ── log ── */
let logLines = [];
const t0 = { v: 0 };
function log(msg, cls = 'info') {
const elapsed = ((performance.now() - t0.v) / 1000).toFixed(3);
logLines.push({ ts: elapsed, msg, cls });
renderLog();
}
function renderLog() {
const el = $('log-lines');
el.replaceChildren();
logLines.forEach(({ ts, msg, cls }) => {
const line = document.createElement('div');
line.className = 'log-line';
const tsSpan = document.createElement('span');
tsSpan.className = 'log-ts';
tsSpan.textContent = `+${ts}s`;
const msgSpan = document.createElement('span');
msgSpan.className = `log-${cls}`;
msgSpan.textContent = msg;
line.append(tsSpan, msgSpan);
el.append(line);
});
el.scrollTop = el.scrollHeight;
}
/* ── WebGL helpers ── */
function createShader(gl, type, src) {
const s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
const msg = gl.getShaderInfoLog(s) || 'compile error';
gl.deleteShader(s);
throw new Error(msg);
}
return s;
}
function getShaderPrecision(gl, shaderType, precType) {
try {
const f = gl.getShaderPrecisionFormat(shaderType, precType);
if (!f) return 'n/a';
return `range:[${f.rangeMin},${f.rangeMax}] precision:${f.precision}`;
} catch { return 'n/a'; }
}
function glParam(gl, name) {
try {
const v = gl.getParameter(gl[name]);
if (v === null || v === undefined) return 'null';
if (v instanceof Float32Array || v instanceof Int32Array) return Array.from(v).join(', ');
return String(v);
} catch { return 'error'; }
}
function collectRendererInfo(gl) {
const dbg = gl.getExtension('WEBGL_debug_renderer_info');
return {
version: gl.getParameter(gl.VERSION),
glslVersion: gl.getParameter(gl.SHADING_LANGUAGE_VERSION),
vendor: gl.getParameter(gl.VENDOR),
renderer: gl.getParameter(gl.RENDERER),
unmaskedVendor: dbg ? gl.getParameter(dbg.UNMASKED_VENDOR_WEBGL) : null,
unmaskedRenderer: dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : null,
};
}
function collectCaps(gl, isGL2) {
const params = [
// Texture
['_TEX', 'Textures'],
['MAX_TEXTURE_SIZE', ''],
['MAX_CUBE_MAP_TEXTURE_SIZE', ''],
['MAX_TEXTURE_IMAGE_UNITS', ''],
['MAX_VERTEX_TEXTURE_IMAGE_UNITS', ''],
['MAX_COMBINED_TEXTURE_IMAGE_UNITS', ''],
// Geometry
['_GEO', 'Geometry'],
['MAX_VERTEX_ATTRIBS', ''],
['MAX_VERTEX_UNIFORM_VECTORS', ''],
['MAX_VARYING_VECTORS', ''],
['MAX_FRAGMENT_UNIFORM_VECTORS', ''],
['MAX_RENDERBUFFER_SIZE', ''],
['MAX_VIEWPORT_DIMS', ''],
// Framebuffer
['_FB', 'Framebuffer'],
['MAX_COLOR_ATTACHMENTS', isGL2 ? '' : null],
['MAX_DRAW_BUFFERS', isGL2 ? '' : null],
['MAX_SAMPLES', isGL2 ? '' : null],
// Uniform buffers (GL2)
['_UBO', 'Uniform Buffers'],
['MAX_UNIFORM_BUFFER_BINDINGS', isGL2 ? '' : null],
['MAX_UNIFORM_BLOCK_SIZE', isGL2 ? '' : null],
['MAX_VERTEX_UNIFORM_BLOCKS', isGL2 ? '' : null],
['MAX_FRAGMENT_UNIFORM_BLOCKS', isGL2 ? '' : null],
['MAX_COMBINED_UNIFORM_BLOCKS', isGL2 ? '' : null],
// Transform feedback (GL2)
['_TF', 'Transform Feedback'],
['MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS', isGL2 ? '' : null],
['MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS', isGL2 ? '' : null],
// 3D / Array textures (GL2)
['_3D', '3D & Array Textures'],
['MAX_3D_TEXTURE_SIZE', isGL2 ? '' : null],
['MAX_ARRAY_TEXTURE_LAYERS', isGL2 ? '' : null],
// Aliasing
['_ALIAS', 'Aliasing'],
['ALIASED_LINE_WIDTH_RANGE', ''],
['ALIASED_POINT_SIZE_RANGE', ''],
];
const rows = [];
params.forEach(([name, label]) => {
if (name.startsWith('_')) {
rows.push({ section: label });
return;
}
if (label === null) return; // skip if not available in this version
const val = gl[name] !== undefined ? glParam(gl, name) : 'n/a';
rows.push({ name, val });
});
return rows;
}
function collectExtensions(gl) {
return (gl.getSupportedExtensions() || []).sort();
}
function collectPrecisions(gl) {
const types = [
[gl.VERTEX_SHADER, 'vert'],
[gl.FRAGMENT_SHADER, 'frag'],
];
const precs = ['LOW_FLOAT','MEDIUM_FLOAT','HIGH_FLOAT','LOW_INT','MEDIUM_INT','HIGH_INT'];
const out = [];
types.forEach(([stype, label]) => {
precs.forEach(p => {
out.push([`${label} ${p.toLowerCase().replace('_', '.')}`, getShaderPrecision(gl, stype, gl[p])]);
});
});
return out;
}
/* ── WebGL draw test ── */
function drawWebGL(canvasId, version) {
const canvas = $(canvasId);
const names = version === 2 ? ['webgl2'] : ['webgl', 'experimental-webgl'];
const gl = names.map(n => canvas.getContext(n)).find(Boolean);
if (!gl) return { ok: false, error: `WebGL${version === 2 ? '2' : ''} context unavailable` };
const vs = version === 2
? `#version 300 es\nin vec2 p;\nvoid main(){gl_Position=vec4(p,0,1);}`
: `attribute vec2 p;\nvoid main(){gl_Position=vec4(p,0,1);}`;
const fs = version === 2
? `#version 300 es\nprecision mediump float;\nout vec4 c;\nvoid main(){c=vec4(0.2,0.55,0.85,1.0);}`
: `precision mediump float;\nvoid main(){gl_FragColor=vec4(0.2,0.55,0.85,1.0);}`;
const vert = createShader(gl, gl.VERTEX_SHADER, vs);
const frag = createShader(gl, gl.FRAGMENT_SHADER, fs);
const prog = gl.createProgram();
gl.attachShader(prog, vert);
gl.attachShader(prog, frag);
gl.linkProgram(prog);
if (!gl.getProgramParameter(prog, gl.LINK_STATUS))
throw new Error(gl.getProgramInfoLog(prog) || 'link error');
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-0.8,-0.7, 0.8,-0.7, 0.0,0.8]), gl.STATIC_DRAW);
const loc = gl.getAttribLocation(prog, 'p');
gl.viewport(0, 0, canvas.width, canvas.height);
gl.clearColor(0.05, 0.05, 0.05, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(prog);
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.flush();
return {
ok: true,
renderer: collectRendererInfo(gl),
caps: collectCaps(gl, version === 2),
extensions: collectExtensions(gl),
precisions: collectPrecisions(gl),
contextAttrs: (() => {
const a = gl.getContextAttributes();
return a ? Object.entries(a).map(([k,v]) => [k, String(v)]) : [];
})(),
};
}
/* ── Canvas 2D test ── */
function testCanvas2D() {
const canvas = $('canvas2d');
const ctx = canvas.getContext('2d');
if (!ctx) return { ok: false, error: 'Canvas 2D context unavailable' };
const t = performance.now();
ctx.fillStyle = '#101010';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// grid lines
ctx.strokeStyle = '#222';
ctx.lineWidth = 1;
for (let x = 0; x < canvas.width; x += 20) {
ctx.beginPath(); ctx.moveTo(x,0); ctx.lineTo(x,canvas.height); ctx.stroke();
}
for (let y = 0; y < canvas.height; y += 20) {
ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(canvas.width,y); ctx.stroke();
}
// 900 blobs stress test
for (let i = 0; i < 900; i++) {
const x = (i * 29) % canvas.width;
const y = (i * 47) % canvas.height;
ctx.fillStyle = `rgba(${(i*3)%255},${(i*7)%255},200,0.12)`;
ctx.beginPath();
ctx.arc(x, y, 4 + (i % 8), 0, Math.PI * 2);
ctx.fill();
}
// diagonal bar
ctx.fillStyle = 'rgba(85,136,204,0.55)';
ctx.beginPath();
ctx.moveTo(0,0); ctx.lineTo(canvas.width,0);
ctx.lineTo(canvas.width - 40, canvas.height);
ctx.lineTo(-40, canvas.height);
ctx.closePath();
ctx.fill();
ctx.fillStyle = '#cccccc';
ctx.font = 'bold 11px Consolas, monospace';
ctx.fillText('CANVAS 2D OK', 8, 18);
const drawTime = Math.round((performance.now() - t) * 100) / 100;
return {
ok: true,
drawTimeMs: drawTime,
ops: 900,
compositing: ctx.globalCompositeOperation,
smoothingEnabled: ctx.imageSmoothingEnabled,
};
}
/* ── WebGPU probe ── */
async function probeWebGPU() {
if (!navigator.gpu) return { ok: false, reason: 'navigator.gpu unavailable' };
try {
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) return { ok: false, reason: 'No adapter returned' };
const info = adapter.info || {};
return {
ok: true,
vendor: info.vendor || 'n/a',
architecture: info.architecture || 'n/a',
device: info.device || 'n/a',
description: info.description || 'n/a',
};
} catch (e) {
return { ok: false, reason: e.message };
}
}
/* ── capability table renderer ── */
function renderCapTable(capRows, extensions) {
const tbody = $('cap-body');
tbody.replaceChildren();
capRows.forEach(row => {
const tr = document.createElement('tr');
if (row.section !== undefined) {
tr.className = 'section-row';
const td = document.createElement('td');
td.colSpan = 2;
td.textContent = row.section;
tr.append(td);
} else {
const tdName = document.createElement('td');
tdName.className = 'param-name';
tdName.textContent = row.name;
const tdVal = document.createElement('td');
tdVal.className = 'param-val';
tdVal.textContent = row.val;
tr.append(tdName, tdVal);
}
tbody.append(tr);
});
$('cap-count').textContent = `${capRows.filter(r => !r.section).length} params`;
// Extensions
const extEl = $('ext-list');
extEl.replaceChildren();
$('ext-count').textContent = `${extensions.length} extensions`;
const tier1 = new Set([
'WEBGL_debug_renderer_info','OES_texture_float','OES_texture_half_float',
'WEBGL_lose_context','OES_vertex_array_object','ANGLE_instanced_arrays',
'EXT_color_buffer_float','EXT_texture_filter_anisotropic',
'OES_element_index_uint','WEBGL_depth_texture','WEBGL_draw_buffers',
'EXT_disjoint_timer_query','EXT_disjoint_timer_query_webgl2',
'KHR_parallel_shader_compile',
]);
extensions.forEach(ext => {
const tag = document.createElement('span');
tag.className = 'ext-tag ' + (tier1.has(ext) ? 'tier1' : 'tier2');
tag.textContent = ext;
extEl.append(tag);
});
}
/* ── full report ── */
let reportData = null;
/* ── run ── */
async function runDiagnostics() {
logLines = [];
t0.v = performance.now();
$('run-ts').textContent = new Date().toISOString().replace('T', ' ').replace('Z', ' UTC');
log('Starting GPU diagnostic run…', 'info');
// Reset badges
['overall','webgl','webgl2','canvas2d','webgpu'].forEach(id => setBadge(`badge-${id}`, 'wait', 'RUNNING'));
const report = { ts: new Date().toISOString(), webgl: {}, webgl2: {}, canvas2d: {}, webgpu: {}, system: {} };
/* system */
report.system = {
userAgent: navigator.userAgent,
platform: navigator.platform,
language: navigator.language,
hardwareConcurrency: navigator.hardwareConcurrency,
deviceMemoryGb: navigator.deviceMemory ?? 'n/a',
maxTouchPoints: navigator.maxTouchPoints,
webdriver: navigator.webdriver,
screenWidth: screen.width,
screenHeight: screen.height,
colorDepth: screen.colorDepth,
devicePixelRatio: window.devicePixelRatio,
};
kvFill('kv-system', [
['User Agent', report.system.userAgent],
['Platform', report.system.platform],
['Language', report.system.language],
['CPU Threads', report.system.hardwareConcurrency],
['Device RAM', report.system.deviceMemoryGb !== 'n/a' ? `${report.system.deviceMemoryGb} GB` : 'n/a'],
['Screen', `${report.system.screenWidth}×${report.system.screenHeight} @ ${report.system.devicePixelRatio}x`],
['Color Depth', `${report.system.colorDepth}-bit`],
['Webdriver', String(report.system.webdriver)],
]);
/* webgl 1 */
log('[WebGL1] Initialising context…', 'dim');
try {
const t1 = performance.now();
report.webgl = drawWebGL('webgl-canvas', 1);
const elapsed = ((performance.now() - t1) / 1).toFixed(1);
if (report.webgl.ok) {
log(`[WebGL1] OK — ${report.webgl.renderer.unmaskedRenderer || report.webgl.renderer.renderer} (${elapsed} ms)`, 'ok');
setBadge('badge-webgl', 'ok', 'OK');
kvFill('kv-webgl', [
['Status', 'Available', 'ok'],
['Renderer', report.webgl.renderer.unmaskedRenderer || report.webgl.renderer.renderer],
['Vendor', report.webgl.renderer.unmaskedVendor || report.webgl.renderer.vendor],
['Version', report.webgl.renderer.version],
['GLSL', report.webgl.renderer.glslVersion],
['Extensions', `${report.webgl.extensions.length} supported`],
['Init time', `${elapsed} ms`],
]);
} else {
log(`[WebGL1] FAIL — ${report.webgl.error}`, 'err');
setBadge('badge-webgl', 'err', 'FAIL');
kvFill('kv-webgl', [['Status', 'Unavailable', 'err'], ['Error', report.webgl.error]]);
}
} catch (e) {
report.webgl = { ok: false, error: e.message };
log(`[WebGL1] ERROR — ${e.message}`, 'err');
setBadge('badge-webgl', 'err', 'ERROR');
kvFill('kv-webgl', [['Status', 'Error', 'err'], ['Error', e.message]]);
}
/* webgl 2 */
log('[WebGL2] Initialising context…', 'dim');
try {
const t1 = performance.now();
report.webgl2 = drawWebGL('webgl2-canvas', 2);
const elapsed = ((performance.now() - t1) / 1).toFixed(1);
if (report.webgl2.ok) {
log(`[WebGL2] OK — ${report.webgl2.renderer.unmaskedRenderer || report.webgl2.renderer.renderer} (${elapsed} ms)`, 'ok');
setBadge('badge-webgl2', 'ok', 'OK');
kvFill('kv-webgl2', [
['Status', 'Available', 'ok'],
['Renderer', report.webgl2.renderer.unmaskedRenderer || report.webgl2.renderer.renderer],
['Vendor', report.webgl2.renderer.unmaskedVendor || report.webgl2.renderer.vendor],
['Version', report.webgl2.renderer.version],
['GLSL', report.webgl2.renderer.glslVersion],
['Extensions', `${report.webgl2.extensions.length} supported`],
['Init time', `${elapsed} ms`],
]);
} else {
log(`[WebGL2] UNAVAIL — ${report.webgl2.error}`, 'warn');
setBadge('badge-webgl2', 'warn', 'N/A');
kvFill('kv-webgl2', [['Status', 'Unavailable', 'warn'], ['Reason', report.webgl2.error]]);
}
} catch (e) {
report.webgl2 = { ok: false, error: e.message };
log(`[WebGL2] ERROR — ${e.message}`, 'err');
setBadge('badge-webgl2', 'err', 'ERROR');
kvFill('kv-webgl2', [['Status', 'Error', 'err'], ['Error', e.message]]);
}
/* canvas 2d */
log('[Canvas2D] Running draw stress test (900 ops)…', 'dim');
try {
const t1 = performance.now();
report.canvas2d = testCanvas2D();
const elapsed = ((performance.now() - t1) / 1).toFixed(1);
if (report.canvas2d.ok) {
log(`[Canvas2D] OK — ${report.canvas2d.drawTimeMs} ms draw time`, 'ok');
setBadge('badge-canvas2d', 'ok', 'OK');
kvFill('kv-canvas2d', [
['Status', 'Working', 'ok'],
['Draw time', `${report.canvas2d.drawTimeMs} ms`],
['Ops', `${report.canvas2d.ops} arcs`],
['Compositing', report.canvas2d.compositing],
['Smoothing', String(report.canvas2d.smoothingEnabled)],
]);
} else {
log(`[Canvas2D] FAIL — ${report.canvas2d.error}`, 'err');
setBadge('badge-canvas2d', 'err', 'FAIL');
kvFill('kv-canvas2d', [['Status', 'Unavailable', 'err'], ['Error', report.canvas2d.error]]);
}
} catch (e) {
report.canvas2d = { ok: false, error: e.message };
log(`[Canvas2D] ERROR — ${e.message}`, 'err');
setBadge('badge-canvas2d', 'err', 'ERROR');
kvFill('kv-canvas2d', [['Status', 'Error', 'err'], ['Error', e.message]]);
}
/* webgpu */
log('[WebGPU] Probing adapter…', 'dim');
report.webgpu = await probeWebGPU();
if (report.webgpu.ok) {
log(`[WebGPU] Adapter found — ${report.webgpu.vendor} / ${report.webgpu.architecture}`, 'ok');
setBadge('badge-webgpu', 'ok', 'OK');
kvFill('kv-webgpu', [
['Status', 'Available', 'ok'],
['Vendor', report.webgpu.vendor],
['Architecture', report.webgpu.architecture],
['Device', report.webgpu.device],
['Description', report.webgpu.description],
]);
} else {
log(`[WebGPU] UNAVAIL — ${report.webgpu.reason}`, 'warn');
setBadge('badge-webgpu', 'warn', 'N/A');
kvFill('kv-webgpu', [['Status', 'Unavailable', 'warn'], ['Reason', report.webgpu.reason]]);
}
/* capability table — prefer WebGL2, fall back to WebGL1 */
const glForCaps = report.webgl2.ok ? report.webgl2 : report.webgl.ok ? report.webgl : null;
const extensions = glForCaps ? glForCaps.extensions : [];
if (glForCaps) {
const capRows = [...glForCaps.caps];
// Shader precision section
capRows.push({ section: 'Shader Precision' });
glForCaps.precisions.forEach(([k, v]) => capRows.push({ name: k, val: v }));
// Context attributes
capRows.push({ section: 'Context Attributes' });
glForCaps.contextAttrs.forEach(([k, v]) => capRows.push({ name: k, val: v }));
renderCapTable(capRows, extensions);
}
/* overall */
const healthy = report.webgl.ok && report.canvas2d.ok;
const gl2ok = report.webgl2.ok;
const overallLevel = healthy ? 'ok' : 'err';
const overallText = healthy ? 'PASS' : 'DEGRADED';
setBadge('badge-overall', overallLevel, overallText);
kvFill('kv-overall', [
['Status', healthy ? 'All critical tests passed' : 'One or more critical tests failed', healthy ? 'ok' : 'err'],
['WebGL 1', report.webgl.ok ? 'OK' : 'FAIL', report.webgl.ok ? 'ok' : 'err'],
['WebGL 2', gl2ok ? 'OK' : 'N/A', gl2ok ? 'ok' : 'warn'],
['Canvas 2D', report.canvas2d.ok ? 'OK' : 'FAIL', report.canvas2d.ok ? 'ok' : 'err'],
['WebGPU', report.webgpu.ok ? 'OK' : 'N/A', report.webgpu.ok ? 'ok' : 'warn'],
]);
const totalMs = (performance.now() - t0.v).toFixed(1);
log(`Run complete in ${totalMs} ms`, healthy ? 'ok' : 'warn');
reportData = report;
}
/* ── copy report ── */
$('btn-copy').addEventListener('click', () => {
if (!reportData) return;
navigator.clipboard.writeText(JSON.stringify(reportData, null, 2)).then(() => {
const btn = $('btn-copy');
btn.textContent = 'Copied!';
btn.classList.add('flash');
setTimeout(() => { btn.textContent = 'Copy Report'; btn.classList.remove('flash'); }, 1200);
});
});
$('btn-run').addEventListener('click', runDiagnostics);
window.addEventListener('DOMContentLoaded', runDiagnostics);
</script>
</body>
</html>