mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-24 02:43:00 +00:00
Make the fingerprint-capped (limited) case a dismissible warning
A capped context still runs the game — only textures wider than the cap render wrong — so don't hard-block. initGL now returns the context for "limited"; GPURenderer proceeds and exposes the capped renderer/size via glLimited (surfaced through MapRenderer), and ClientGameRunner shows the gate as a warning with a "Continue anyway" button instead of aborting game creation. GLUnavailableError is back to software/unsupported only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
906831d803
commit
ab10d32f43
@@ -191,9 +191,8 @@ export function joinLobby(
|
||||
if (startingModal) {
|
||||
startingModal.classList.add("hidden");
|
||||
}
|
||||
// No usable WebGL2 (software-rendered, fingerprint-capped, or
|
||||
// missing): gate with an actionable message rather than the generic
|
||||
// crash modal (the game would crawl at ~1fps or render black).
|
||||
// No GPU-accelerated WebGL2: gate with an actionable message rather
|
||||
// than the generic crash modal (the game would crawl at ~1fps).
|
||||
if (e instanceof GLUnavailableError) {
|
||||
showGLGate(e.glStatus);
|
||||
return;
|
||||
@@ -334,14 +333,25 @@ function createWebGLView(
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof GLUnavailableError) {
|
||||
trackGLInit(e.glStatus, e.renderer, e.maxTextureSize);
|
||||
trackGLInit(e.glStatus, e.renderer);
|
||||
}
|
||||
// The renderer never took ownership of the canvas, so remove it here —
|
||||
// otherwise it lingers in the DOM holding a (possibly software) GL context.
|
||||
glCanvas.remove();
|
||||
throw e;
|
||||
}
|
||||
trackGLInit("ok", "");
|
||||
// Fingerprint-capped context (#4357): the game runs, but the map may render
|
||||
// with black areas. Warn with fix instructions; the player can continue.
|
||||
if (view.glLimited) {
|
||||
trackGLInit(
|
||||
"limited",
|
||||
view.glLimited.renderer,
|
||||
view.glLimited.maxTextureSize,
|
||||
);
|
||||
showGLGate("limited");
|
||||
} else {
|
||||
trackGLInit("ok", "");
|
||||
}
|
||||
|
||||
(window as unknown as { __webglView?: unknown }).__webglView = view;
|
||||
|
||||
|
||||
@@ -44,9 +44,11 @@ const STEP_SECTIONS: ReadonlyArray<{ title: string; steps: string[] }> = [
|
||||
];
|
||||
|
||||
// Shown for the "limited" status: WebGL works but texture sizes are capped
|
||||
// below what the game needs, so the map renders black (#4357). The only known
|
||||
// cause is fingerprinting protection (privacy.resistFingerprinting — on by
|
||||
// default in LibreWolf and Mullvad Browser, opt-in in Firefox).
|
||||
// below what the game needs, so the map may render with black areas (#4357).
|
||||
// The only known cause is fingerprinting protection
|
||||
// (privacy.resistFingerprinting — on by default in LibreWolf and Mullvad
|
||||
// Browser, opt-in in Firefox). Unlike the other statuses this is a warning:
|
||||
// the player may dismiss it and play anyway.
|
||||
const LIMITED_SECTIONS: ReadonlyArray<{ title: string; steps: string[] }> = [
|
||||
{
|
||||
title: "Firefox / LibreWolf / Mullvad Browser",
|
||||
@@ -70,12 +72,12 @@ const SAFARI_NOTES: string[] = [
|
||||
];
|
||||
|
||||
/**
|
||||
* Full-screen blocking gate shown when a usable WebGL2 context can't be
|
||||
* obtained — software rendering (~1fps), no WebGL2 at all, or texture sizes
|
||||
* capped by fingerprinting protection. Shows how to turn hardware
|
||||
* acceleration / WebGL back on (or exempt the site from fingerprinting
|
||||
* protection) across the most popular browsers. Shown imperatively from the
|
||||
* game-start path.
|
||||
* Full-screen gate shown when the WebGL2 context is unusable ("software",
|
||||
* "unsupported" — hard block) or degraded ("limited" — texture sizes capped
|
||||
* by fingerprinting protection; dismissible via "Continue anyway"). Shows how
|
||||
* to turn hardware acceleration / WebGL back on, or exempt the site from
|
||||
* fingerprinting protection, across the most popular browsers. Shown
|
||||
* imperatively from the game-start path.
|
||||
*/
|
||||
@customElement("webgl-gate")
|
||||
export class WebGLGate extends LitElement {
|
||||
@@ -95,7 +97,7 @@ export class WebGLGate extends LitElement {
|
||||
? "Hardware acceleration is off"
|
||||
: "WebGL2 not supported";
|
||||
const intro = limited
|
||||
? 'A privacy setting is capping WebGL texture sizes below what the game needs, so the map would render black. This is usually "resist fingerprinting" protection, which is on by default in some Firefox-based browsers. Here is how to exempt this site:'
|
||||
? 'A privacy setting is capping WebGL texture sizes below what the game needs, so the map may render with black areas. This is usually "resist fingerprinting" protection, which is on by default in some Firefox-based browsers. Here is how to exempt this site:'
|
||||
: software
|
||||
? "Your browser is rendering without GPU acceleration, so the game can't run smoothly. Here is how to activate it across the most popular web browsers:"
|
||||
: "Your browser doesn't support WebGL2, which this game requires. Here is how to enable it across the most popular web browsers:";
|
||||
@@ -134,6 +136,16 @@ export class WebGLGate extends LitElement {
|
||||
${notes.map((note) => html`<li>${note}</li>`)}
|
||||
</ul>
|
||||
</section>
|
||||
${limited
|
||||
? html`
|
||||
<button
|
||||
class="mt-5 w-full py-2.5 rounded-lg bg-white/10 hover:bg-white/20 text-sm font-bold text-white transition-colors"
|
||||
@click=${() => this.remove()}
|
||||
>
|
||||
Continue anyway
|
||||
</button>
|
||||
`
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -104,6 +104,15 @@ export class MapRenderer {
|
||||
this.onContextRestored?.();
|
||||
};
|
||||
|
||||
/**
|
||||
* Set when the context is hardware-accelerated but its MAX_TEXTURE_SIZE is
|
||||
* below what the game needs (fingerprinting protection, #4357). The game
|
||||
* runs, but the map may render with black areas — the owner should warn.
|
||||
*/
|
||||
get glLimited(): { renderer: string; maxTextureSize: number } | null {
|
||||
return this.renderer?.glLimited ?? null;
|
||||
}
|
||||
|
||||
// ---- Camera ----
|
||||
|
||||
setCameraState(x: number, y: number, z: number): void {
|
||||
|
||||
@@ -103,6 +103,13 @@ export class GPURenderer {
|
||||
private camera: Camera;
|
||||
private res: GPUResources;
|
||||
|
||||
/**
|
||||
* Set when the context is hardware-accelerated but its MAX_TEXTURE_SIZE is
|
||||
* below what the game needs (fingerprinting protection, #4357). The game
|
||||
* runs, but the map may render with black areas — the owner should warn.
|
||||
*/
|
||||
readonly glLimited: { renderer: string; maxTextureSize: number } | null;
|
||||
|
||||
// Passes
|
||||
private terrainPass: TerrainPass;
|
||||
private territoryPass: TerritoryPass;
|
||||
@@ -205,22 +212,24 @@ export class GPURenderer {
|
||||
this.raf = raf;
|
||||
this.caf = caf;
|
||||
|
||||
// Demand a GPU-accelerated context with usable texture limits. A software
|
||||
// (SwiftShader), fingerprinting-capped, or missing WebGL2 context throws
|
||||
// GLUnavailableError, which the game-start path turns into an actionable
|
||||
// gate instead of letting the game crawl at ~1fps or render black.
|
||||
// Demand a GPU-accelerated context. A software (SwiftShader) or missing
|
||||
// WebGL2 context throws GLUnavailableError, which the game-start path
|
||||
// turns into an actionable gate instead of letting the game crawl at
|
||||
// ~1fps. A fingerprint-capped context ("limited" — MAX_TEXTURE_SIZE below
|
||||
// the palette width, #4357) proceeds anyway; glLimited lets the owner
|
||||
// warn the player that the map may render with black areas.
|
||||
const res = initGL(canvas, {
|
||||
alpha: false,
|
||||
antialias: false,
|
||||
powerPreference: "high-performance",
|
||||
});
|
||||
if (res.status !== "ok") {
|
||||
throw new GLUnavailableError(
|
||||
res.status,
|
||||
res.renderer,
|
||||
res.status === "limited" ? res.maxTextureSize : undefined,
|
||||
);
|
||||
if (res.gl === null) {
|
||||
throw new GLUnavailableError(res.status, res.renderer);
|
||||
}
|
||||
this.glLimited =
|
||||
res.status === "limited"
|
||||
? { renderer: res.renderer, maxTextureSize: res.maxTextureSize }
|
||||
: null;
|
||||
const gl = res.gl;
|
||||
this.gl = gl;
|
||||
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
||||
|
||||
@@ -14,15 +14,21 @@ 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 };
|
||||
| {
|
||||
gl: WebGL2RenderingContext;
|
||||
status: "limited";
|
||||
renderer: string;
|
||||
maxTextureSize: number;
|
||||
}
|
||||
| { gl: null; status: "software" | "unsupported"; renderer: string };
|
||||
|
||||
// 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 —
|
||||
// texture, so a context whose MAX_TEXTURE_SIZE is below that renders wrong:
|
||||
// the oversized texImage2D calls fail silently and territory/map areas come
|
||||
// out 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.
|
||||
// Firefox) caps MAX_TEXTURE_SIZE at 2048. "limited" still returns the
|
||||
// context: the player is warned with fix instructions but may continue.
|
||||
const REQUIRED_TEXTURE_SIZE = getPaletteSize();
|
||||
|
||||
// Renderer strings reported by software WebGL backends. Mirrors the detection
|
||||
@@ -63,10 +69,10 @@ export function initGL(
|
||||
return { gl: null, status: "software", renderer };
|
||||
}
|
||||
// Fingerprinting protection caps texture sizes on an otherwise
|
||||
// hardware-accelerated context; the game would render black (#4357).
|
||||
// hardware-accelerated context; the map renders with black areas (#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: "limited", renderer, maxTextureSize };
|
||||
}
|
||||
return { gl: accel, status: "ok" };
|
||||
}
|
||||
@@ -88,9 +94,8 @@ export function initGL(
|
||||
*/
|
||||
export class GLUnavailableError extends Error {
|
||||
constructor(
|
||||
readonly glStatus: "software" | "unsupported" | "limited",
|
||||
readonly glStatus: "software" | "unsupported",
|
||||
readonly renderer: string,
|
||||
readonly maxTextureSize?: number,
|
||||
) {
|
||||
super(`WebGL2 unavailable: ${glStatus}`);
|
||||
this.name = "GLUnavailableError";
|
||||
|
||||
@@ -111,20 +111,18 @@ describe("initGL", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("reports limited when MAX_TEXTURE_SIZE is below the palette size (fingerprinting cap)", () => {
|
||||
it("reports limited (but still returns the context) when MAX_TEXTURE_SIZE is below the palette size", () => {
|
||||
// privacy.resistFingerprinting (LibreWolf default, Firefox opt-in) caps
|
||||
// MAX_TEXTURE_SIZE at 2048 on an otherwise hardware-accelerated context;
|
||||
// the 4096-wide palette texture then fails silently and the map renders
|
||||
// black (#4357).
|
||||
stubGetContext({
|
||||
accelerated: fakeContext("AMD Radeon", 2048),
|
||||
probe: null,
|
||||
});
|
||||
// with black areas (#4357). The player is warned but may continue.
|
||||
const accel = fakeContext("AMD Radeon", 2048);
|
||||
stubGetContext({ accelerated: accel, probe: null });
|
||||
|
||||
const res = initGL(document.createElement("canvas"));
|
||||
|
||||
expect(res.status).toBe("limited");
|
||||
expect(res.gl).toBeNull();
|
||||
expect(res.gl).toBe(accel);
|
||||
if (res.status === "limited") {
|
||||
expect(res.renderer).toBe("AMD Radeon");
|
||||
expect(res.maxTextureSize).toBe(2048);
|
||||
@@ -171,11 +169,4 @@ describe("GLUnavailableError", () => {
|
||||
expect(err.glStatus).toBe("software");
|
||||
expect(err.renderer).toBe("SwiftShader");
|
||||
});
|
||||
|
||||
it("carries the capped texture size for the limited status", () => {
|
||||
const err = new GLUnavailableError("limited", "AMD Radeon", 2048);
|
||||
|
||||
expect(err.glStatus).toBe("limited");
|
||||
expect(err.maxTextureSize).toBe(2048);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user