mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-24 11:36:52 +00:00
privacy.resistFingerprinting (default-on in LibreWolf and Mullvad Browser, opt-in in Firefox) caps MAX_TEXTURE_SIZE at 2048 on an otherwise hardware-accelerated context. The renderer unconditionally allocates a 4096-wide palette texture, so the oversized texImage2D calls fail silently, framebuffers never complete, and the whole map renders black (#4357). Extend initGL with a third failure class: after the software check, read MAX_TEXTURE_SIZE and classify the context as "limited" when it's below the palette size. The gate shows fingerprinting-specific instructions for this case (add the site to privacy.resistFingerprinting.exemptedDomains) instead of the hardware-acceleration steps, and gl_init reports the capped size. Fixes #4357 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
138 lines
5.3 KiB
TypeScript
138 lines
5.3 KiB
TypeScript
/**
|
|
* 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";
|
|
import { getPaletteSize } from "./utils/ColorUtils";
|
|
|
|
export type GLResult =
|
|
| { gl: WebGL2RenderingContext; status: "ok" }
|
|
| { gl: null; status: "software" | "unsupported"; renderer: string }
|
|
| { gl: null; status: "limited"; renderer: string; maxTextureSize: number };
|
|
|
|
// The renderer unconditionally allocates a PALETTE_SIZE-wide (4096) palette
|
|
// texture, so a context whose MAX_TEXTURE_SIZE is below that can't run the
|
|
// game: the oversized texImage2D calls fail silently and everything renders
|
|
// black (#4357). In practice this means fingerprinting protection —
|
|
// privacy.resistFingerprinting (on by default in LibreWolf, opt-in in
|
|
// Firefox) caps MAX_TEXTURE_SIZE at 2048.
|
|
const REQUIRED_TEXTURE_SIZE = getPaletteSize();
|
|
|
|
// 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 };
|
|
}
|
|
// Fingerprinting protection caps texture sizes on an otherwise
|
|
// hardware-accelerated context; the game would render black (#4357).
|
|
const maxTextureSize = Number(accel.getParameter(accel.MAX_TEXTURE_SIZE));
|
|
if (maxTextureSize < REQUIRED_TEXTURE_SIZE) {
|
|
return { gl: null, status: "limited", renderer, maxTextureSize };
|
|
}
|
|
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" | "limited",
|
|
readonly renderer: string,
|
|
readonly maxTextureSize?: number,
|
|
) {
|
|
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" | "limited",
|
|
renderer: string,
|
|
maxTextureSize?: number,
|
|
): void {
|
|
window.gtag?.("event", "gl_init", {
|
|
status,
|
|
renderer: status === "ok" ? "" : renderer,
|
|
...(maxTextureSize !== undefined && {
|
|
max_texture_size: maxTextureSize,
|
|
}),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
});
|
|
}
|