FERS 1.0.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
webglSupport.ts
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2026-present FERS Contributors (see AUTHORS.md).
3
4export type WebGLContextMode = 'webgl2' | 'webgl' | 'experimental-webgl';
5
6interface NavigatorWithUserAgentData extends Navigator {
7 userAgentData?: {
8 platform?: string;
9 };
10}
11
12export interface WebGLProbeResult {
13 mode: WebGLContextMode;
14 available: boolean;
15 usable: boolean;
16 lostAtCreation: boolean;
17 lostAfterExercise: boolean;
18 clearError: number | null;
19 version: string | null;
20 renderer: string | null;
21 vendor: string | null;
22 shaderCreated: boolean;
23 reason: string;
24}
25
26export interface WebGLPlatformInfo {
27 platform: string;
28 userAgent: string;
29 isMac: boolean;
30 isIntelMac: boolean;
31}
32
33export interface WebGLSupportReport {
34 rendererSupported: boolean;
35 supportedMode: WebGLContextMode | null;
36 summary: string;
37 platform: WebGLPlatformInfo;
38 probes: WebGLProbeResult[];
39}
40
41type WebGLProbeContext = WebGLRenderingContext | WebGL2RenderingContext;
42
43const CONTEXT_MODES: readonly WebGLContextMode[] = [
44 'webgl2',
45 'webgl',
46 'experimental-webgl',
47];
48
49let cachedReportPromise: Promise<WebGLSupportReport> | null = null;
50
51function getPlatformInfo(): WebGLPlatformInfo {
52 if (typeof navigator === 'undefined') {
53 return {
54 platform: 'unknown',
55 userAgent: 'unknown',
56 isMac: false,
57 isIntelMac: false,
58 };
59 }
60
61 const nav = navigator as NavigatorWithUserAgentData;
62 const platform =
63 nav.userAgentData?.platform ?? navigator.platform ?? 'unknown';
64 const userAgent = navigator.userAgent ?? 'unknown';
65 const platformText = `${platform} ${userAgent}`;
66 const isMac = /mac/i.test(platformText);
67 const isIntelMac = isMac && /(intel|x86_64|macintel)/i.test(platformText);
68
69 return {
70 platform,
71 userAgent,
72 isMac,
73 isIntelMac,
74 };
75}
76
77function safeIsContextLost(gl: WebGLProbeContext): boolean {
78 try {
79 return typeof gl.isContextLost === 'function'
80 ? gl.isContextLost()
81 : false;
82 } catch {
83 return true;
84 }
85}
86
87function safeGetParameter(
88 gl: WebGLProbeContext,
89 parameter: number
90): string | null {
91 try {
92 const value = gl.getParameter(parameter);
93
94 if (value === null || value === undefined) {
95 return null;
96 }
97
98 return String(value);
99 } catch {
100 return null;
101 }
102}
103
104function releaseContext(gl: WebGLProbeContext): void {
105 try {
106 gl.getExtension('WEBGL_lose_context')?.loseContext();
107 } catch {
108 // Best-effort cleanup only.
109 }
110}
111
112function buildFailureReason(result: {
113 available: boolean;
114 lostAtCreation: boolean;
115 lostAfterExercise: boolean;
116 clearError: number | null;
117 shaderCreated: boolean;
118 version: string | null;
119}): string {
120 if (!result.available) {
121 return 'Context acquisition returned null.';
122 }
123
124 if (result.lostAtCreation) {
125 return 'Context was already lost at creation time.';
126 }
127
128 if (result.clearError === 37442) {
129 return 'Basic GL commands returned CONTEXT_LOST_WEBGL.';
130 }
131
132 if (result.lostAfterExercise) {
133 return 'Context was lost during the startup probe.';
134 }
135
136 if (!result.shaderCreated) {
137 return 'Shader creation failed during the startup probe.';
138 }
139
140 if (result.version === null) {
141 return 'Capability queries returned null.';
142 }
143
144 return 'Renderer startup checks failed.';
145}
146
147function probeContext(mode: WebGLContextMode): WebGLProbeResult {
148 if (typeof document === 'undefined') {
149 return {
150 mode,
151 available: false,
152 usable: false,
153 lostAtCreation: false,
154 lostAfterExercise: false,
155 clearError: null,
156 version: null,
157 renderer: null,
158 vendor: null,
159 shaderCreated: false,
160 reason: 'Document is unavailable in the current runtime.',
161 };
162 }
163
164 const canvas = document.createElement('canvas');
165 canvas.width = 1;
166 canvas.height = 1;
167
168 let gl: WebGLProbeContext | null = null;
169
170 try {
171 gl = canvas.getContext(mode, {
172 alpha: true,
173 antialias: false,
174 depth: true,
175 stencil: false,
176 powerPreference: 'default',
177 }) as WebGLProbeContext | null;
178
179 if (!gl) {
180 return {
181 mode,
182 available: false,
183 usable: false,
184 lostAtCreation: false,
185 lostAfterExercise: false,
186 clearError: null,
187 version: null,
188 renderer: null,
189 vendor: null,
190 shaderCreated: false,
191 reason: 'Context acquisition returned null.',
192 };
193 }
194
195 const lostAtCreation = safeIsContextLost(gl);
196 const version = safeGetParameter(gl, gl.VERSION);
197 const renderer = safeGetParameter(gl, gl.RENDERER);
198 const vendor = safeGetParameter(gl, gl.VENDOR);
199
200 let clearError: number | null = null;
201 let lostAfterExercise = lostAtCreation;
202 let shaderCreated = false;
203
204 try {
205 gl.viewport(0, 0, 1, 1);
206 gl.clearColor(0, 0, 0, 1);
207 gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
208 clearError = gl.getError();
209 lostAfterExercise = safeIsContextLost(gl);
210
211 const shader = gl.createShader(gl.VERTEX_SHADER);
212 shaderCreated = shader !== null;
213
214 if (shader) {
215 gl.deleteShader(shader);
216 }
217 } catch {
218 lostAfterExercise = safeIsContextLost(gl);
219 }
220
221 const usable =
222 !lostAtCreation &&
223 !lostAfterExercise &&
224 clearError !== gl.CONTEXT_LOST_WEBGL &&
225 shaderCreated &&
226 version !== null;
227
228 return {
229 mode,
230 available: true,
231 usable,
232 lostAtCreation,
233 lostAfterExercise,
234 clearError,
235 version,
236 renderer,
237 vendor,
238 shaderCreated,
239 reason: usable
240 ? 'Context passed startup checks.'
241 : buildFailureReason({
242 available: true,
243 lostAtCreation,
244 lostAfterExercise,
245 clearError,
246 shaderCreated,
247 version,
248 }),
249 };
250 } catch (error) {
251 return {
252 mode,
253 available: gl !== null,
254 usable: false,
255 lostAtCreation: gl ? safeIsContextLost(gl) : false,
256 lostAfterExercise: gl ? safeIsContextLost(gl) : false,
257 clearError: null,
258 version: null,
259 renderer: null,
260 vendor: null,
261 shaderCreated: false,
262 reason:
263 error instanceof Error
264 ? error.message
265 : 'Unknown WebGL startup error.',
266 };
267 } finally {
268 if (gl) {
269 releaseContext(gl);
270 }
271
272 canvas.width = 0;
273 canvas.height = 0;
274 }
275}
276
277function buildSummary(
278 webgl2Probe: WebGLProbeResult | undefined,
279 fallbackProbe: WebGLProbeResult | undefined,
280 platform: WebGLPlatformInfo
281): string {
282 if (!webgl2Probe) {
283 return 'WebGL2 startup probe did not run.';
284 }
285
286 if (!webgl2Probe.available) {
287 return fallbackProbe?.usable
288 ? 'Only legacy WebGL is usable on this system. The current 3D renderer requires a working WebGL2 context.'
289 : 'No usable WebGL context was detected during startup.';
290 }
291
292 if (webgl2Probe.lostAtCreation || webgl2Probe.clearError === 37442) {
293 return platform.isIntelMac
294 ? 'The system WebKit/WebGL stack reported a lost WebGL context during startup on this Intel Mac.'
295 : 'The system WebGL stack reported a lost WebGL context during startup.';
296 }
297
298 return webgl2Probe.reason;
299}
300
301function evaluateWebGLSupport(): WebGLSupportReport {
302 const platform = getPlatformInfo();
303 const probes = CONTEXT_MODES.map(probeContext);
304 const webgl2Probe = probes.find((probe) => probe.mode === 'webgl2');
305 const fallbackProbe = probes.find(
306 (probe) => probe.mode !== 'webgl2' && probe.usable
307 );
308
309 if (webgl2Probe?.usable) {
310 return {
311 rendererSupported: true,
312 supportedMode: 'webgl2',
313 summary: 'WebGL2 startup probe succeeded.',
314 platform,
315 probes,
316 };
317 }
318
319 return {
320 rendererSupported: false,
321 supportedMode: null,
322 summary: buildSummary(webgl2Probe, fallbackProbe, platform),
323 platform,
324 probes,
325 };
326}
327
328export function getWebGLSupportReport(): Promise<WebGLSupportReport> {
329 if (!cachedReportPromise) {
330 cachedReportPromise = Promise.resolve().then(evaluateWebGLSupport);
331 }
332
333 return cachedReportPromise;
334}
335
336export function resetWebGLSupportReportCache(): void {
337 cachedReportPromise = null;
338}