mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-24 23:54:41 +00:00
Gate users without GPU-accelerated WebGL2 instead of running at ~1fps
A small number of users run WebGL without GPU acceleration (hardware acceleration disabled, blocklisted driver, or a locked-down machine) and get a software (SwiftShader) context, which renders the game at ~1fps. Demand a GPU-accelerated WebGL2 context at game start; if we can't get one, show a full-screen gate with per-browser instructions for enabling hardware acceleration / WebGL instead of letting the game crawl. - initGL() demands failIfMajorPerformanceCaveat AND inspects the renderer string — Chrome still hands back SwiftShader when acceleration is turned off in settings (vs. a blocklisted driver). Classifies ok / software / unsupported. - GPURenderer throws GLUnavailableError on a non-accelerated context; the game-start catch shows the gate. The orphaned canvas is cleaned up. - <webgl-gate> Lit component renders the actionable gate (intentionally inlined/untranslated — rarely seen, browser-specific troubleshooting). - Log a gl_init analytics event (status + renderer) every session so we can size the affected %. - Unit tests for initGL's ok/software/unsupported branching. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
Evan Pelle
co-authored by
Claude Opus 4.8
parent
006f1690a5
commit
31833346ec
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* WebGL2 context acquisition that demands a GPU-accelerated context.
|
||||
*
|
||||
* Software-rendered WebGL (SwiftShader/llvmpipe — hardware acceleration off,
|
||||
* blocklisted driver, or a locked-down machine) runs the game at ~1fps, which
|
||||
* is unplayable. `failIfMajorPerformanceCaveat: true` forces a real GPU
|
||||
* context: Chrome returns null instead of silently handing back a software
|
||||
* context. We branch on that to gate the user with an actionable message
|
||||
* rather than running at 1fps.
|
||||
*/
|
||||
|
||||
import type { WebGLGateStatus } from "../../components/WebGLGate";
|
||||
|
||||
export type GLResult =
|
||||
| { gl: WebGL2RenderingContext; status: "ok" }
|
||||
| { gl: null; status: "software" | "unsupported"; renderer: string };
|
||||
|
||||
// Renderer strings reported by software WebGL backends. Mirrors the detection
|
||||
// in utilities/Diagnostic.ts.
|
||||
const SOFTWARE_RENDERER = /swiftshader|llvmpipe|software/i;
|
||||
|
||||
/** Read the unmasked GPU renderer string, or "unknown" if unavailable. */
|
||||
function readRenderer(gl: WebGL2RenderingContext): string {
|
||||
const dbg = gl.getExtension("WEBGL_debug_renderer_info");
|
||||
return dbg ? String(gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL)) : "unknown";
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquire a GPU-accelerated WebGL2 context on `canvas`.
|
||||
*
|
||||
* @param attrs Context attributes to merge with the mandatory
|
||||
* `failIfMajorPerformanceCaveat: true`. A second `getContext("webgl2")`
|
||||
* call on the same canvas returns the already-created context (ignoring
|
||||
* attrs), so these must be the attributes the renderer wants.
|
||||
*/
|
||||
export function initGL(
|
||||
canvas: HTMLCanvasElement,
|
||||
attrs: WebGLContextAttributes = {},
|
||||
): GLResult {
|
||||
// 1. demand a GPU-accelerated context
|
||||
const accel = canvas.getContext("webgl2", {
|
||||
...attrs,
|
||||
failIfMajorPerformanceCaveat: true,
|
||||
});
|
||||
if (accel) {
|
||||
// failIfMajorPerformanceCaveat does NOT reliably reject a software context
|
||||
// when hardware acceleration is turned off in browser settings (as opposed
|
||||
// to a blocklisted driver) — Chrome still hands back a SwiftShader context.
|
||||
// So inspect the renderer and gate if it's software; the game is unplayable
|
||||
// (~1fps) on it either way.
|
||||
const renderer = readRenderer(accel);
|
||||
if (SOFTWARE_RENDERER.test(renderer)) {
|
||||
return { gl: null, status: "software", renderer };
|
||||
}
|
||||
return { gl: accel, status: "ok" };
|
||||
}
|
||||
|
||||
// 2. probe what's actually available. A canvas locks to the first context
|
||||
// that *succeeds*; the failed (null) call above does NOT lock it, but we use
|
||||
// a throwaway canvas here regardless to avoid any chance of context lock-in.
|
||||
const probe = document.createElement("canvas").getContext("webgl2");
|
||||
if (!probe) return { gl: null, status: "unsupported", renderer: "" };
|
||||
|
||||
// WebGL2 exists but couldn't be obtained accelerated → treat as software.
|
||||
return { gl: null, status: "software", renderer: readRenderer(probe) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown by the renderer when a GPU-accelerated WebGL2 context can't be
|
||||
* obtained. Carries the detected status + renderer so the caller can gate the
|
||||
* user and log the outcome.
|
||||
*/
|
||||
export class GLUnavailableError extends Error {
|
||||
constructor(
|
||||
readonly glStatus: "software" | "unsupported",
|
||||
readonly renderer: string,
|
||||
) {
|
||||
super(`WebGL2 unavailable: ${glStatus}`);
|
||||
this.name = "GLUnavailableError";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Report the WebGL2 GPU-init outcome to analytics (Google Tag). Fires on every
|
||||
* session so we can size the share of users on a software or missing WebGL2
|
||||
* context. `renderer` is the unmasked GPU string for non-ok outcomes, empty
|
||||
* otherwise.
|
||||
*/
|
||||
export function trackGLInit(
|
||||
status: "ok" | "software" | "unsupported",
|
||||
renderer: string,
|
||||
): void {
|
||||
window.gtag?.("event", "gl_init", {
|
||||
status,
|
||||
renderer: status === "ok" ? "" : renderer,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the full-screen WebGL gate (no GPU-accelerated context). The markup and
|
||||
* per-browser fix steps live in the <webgl-gate> Lit component, which is loaded
|
||||
* on demand — it's only ever needed in this failure case.
|
||||
*/
|
||||
export function showGLGate(status: WebGLGateStatus): void {
|
||||
if (document.querySelector("webgl-gate")) {
|
||||
return;
|
||||
}
|
||||
void import("../../components/WebGLGate").then(({ WebGLGate }) => {
|
||||
if (document.querySelector("webgl-gate")) {
|
||||
return;
|
||||
}
|
||||
const gate = new WebGLGate();
|
||||
gate.status = status;
|
||||
document.body.appendChild(gate);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user