// WebGL renderer policy for attached terminals. Split out of attach.ts so it // can be tested without a DOM or an xterm instance (same reasoning as // reconnect.ts). // // xterm's default renderer builds DOM nodes per cell, which is CPU-bound — // exactly the wrong bottleneck on a phone, where a full-screen repaint from an // agent dumping output visibly stutters. The WebGL addon moves glyph drawing to // a GPU texture atlas instead. // // The cost is that a WebGL context is a scarce, revocable resource: // // * Browsers cap live contexts (~16 on desktop Chrome, fewer on Android) and // silently kill the oldest past the cap. `ConnectionPool` keeps MAX_PARKED // parked connections alive on top of the mounted ones, so handing every // connection its own context would blow that budget on its own and bring a // parked terminal back blank. // * Android drops contexts as routine housekeeping — backgrounding the tab, // locking the screen, memory pressure. // // So the context follows the *view*, not the connection: acquired on mount, // released on park. Losses count against a shared budget, because a device that // keeps dropping contexts is telling us it cannot sustain one. /** Context losses tolerated before the page gives up and stays on the DOM * renderer. One loss is ordinary (a tab switch); repeated losses mean the * device is thrashing, and re-acquiring only makes it worse. */ export const MAX_CONTEXT_LOSSES = 2; /** Can this browser hand out a WebGL2 context at all? */ export function probeWebgl2(): boolean { return document.createElement("canvas").getContext("webgl2") !== null; } /** * Whether an attached terminal should try the WebGL renderer right now. * * Shared by every connection on the page: the context budget and the "this * device cannot cope" signal are both global, not per-terminal. */ export class WebglPolicy { private supported: boolean | null = null; private losses = 0; constructor(private readonly probe: () => boolean = probeWebgl2) {} /** True while WebGL is both available and still worth attempting. The probe * runs at most once — support does not change under us, and creating * throwaway contexts to re-ask would itself consume the budget. */ enabled(): boolean { if (this.losses >= MAX_CONTEXT_LOSSES) return false; if (this.supported === null) { try { this.supported = this.probe(); } catch { // A browser that throws rather than returning null has no usable // context either way. this.supported = false; } } return this.supported; } /** Record a lost context. Past the budget, `enabled()` stays false for the * life of the page. */ noteContextLoss(): void { this.losses += 1; } /** Losses recorded so far — for the debug HUD and tests. */ contextLosses(): number { return this.losses; } }