export type WebGLRendererKind = 'webgl2' | 'webgl' | 'experimental-webgl' | 'none'; export type WebGLFailureReason = | 'no-document' | 'canvas-unavailable' | 'context-unavailable' | 'context-creation-error' | 'context-lost'; export interface WebGLSupportDetails { version?: string; vendor?: string; renderer?: string; unmaskedVendor?: string; unmaskedRenderer?: string; maxTextureSize?: number; maxVertexUniformVectors?: number; devicePixelRatio?: number; userAgent?: string; errorMessage?: string; } export interface WebGLSupportResult { supported: boolean; renderer: WebGLRendererKind; reason?: WebGLFailureReason; details: WebGLSupportDetails; } export interface DetectWebGLOptions { canvas?: HTMLCanvasElement; preferWebGL2?: boolean; failIfMajorPerformanceCaveat?: boolean; powerPreference?: WebGLPowerPreference; forceContextLossAfterCheck?: boolean; } type WebGLContextName = 'webgl2' | 'webgl' | 'experimental-webgl'; type WebGLAnyContext = WebGLRenderingContext & { getExtension(name: 'WEBGL_lose_context'): | { loseContext: () => void; } | null; }; export function detectWebGLSupport(options: DetectWebGLOptions = {}): WebGLSupportResult { const details: WebGLSupportDetails = { devicePixelRatio: typeof window !== 'undefined' ? window.devicePixelRatio : undefined, userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : undefined, }; const canvas = options.canvas ?? createCanvas(); if (!canvas) { return { supported: false, renderer: 'none', reason: 'no-document', details, }; } if (typeof canvas.getContext !== 'function') { return { supported: false, renderer: 'none', reason: 'canvas-unavailable', details, }; } const contextNames: WebGLContextName[] = options.preferWebGL2 === false ? ['webgl', 'experimental-webgl'] : ['webgl2', 'webgl', 'experimental-webgl']; let lastError: unknown; for (const contextName of contextNames) { try { const context = canvas.getContext(contextName, { alpha: true, antialias: true, depth: true, failIfMajorPerformanceCaveat: options.failIfMajorPerformanceCaveat ?? false, powerPreference: options.powerPreference ?? 'high-performance', premultipliedAlpha: true, preserveDrawingBuffer: false, stencil: false, }) as WebGLAnyContext | null; if (!context) { continue; } const supportDetails = readWebGLDetails(context, details); if (options.forceContextLossAfterCheck) { context.getExtension('WEBGL_lose_context')?.loseContext(); } return { supported: true, renderer: contextName, details: supportDetails, }; } catch (error) { lastError = error; } } return { supported: false, renderer: 'none', reason: lastError ? 'context-creation-error' : 'context-unavailable', details: { ...details, errorMessage: lastError instanceof Error ? lastError.message : undefined, }, }; } export function getWebGLFailureMessage(result: WebGLSupportResult): string { if (result.supported) { return 'WebGL is available.'; } switch (result.reason) { case 'no-document': return 'This environment cannot create a canvas. Server rendering will defer the 3D viewer until the browser takes over.'; case 'canvas-unavailable': return 'Your browser does not expose the HTML canvas APIs required by the 3D viewer.'; case 'context-creation-error': return 'The browser raised an error while creating the WebGL context. Updating graphics drivers or disabling aggressive privacy extensions may help.'; case 'context-lost': return 'The graphics context was lost. This can happen after GPU resets, memory pressure, or driver interruptions.'; case 'context-unavailable': default: return 'WebGL is unavailable in this browser or has been disabled by system graphics settings.'; } } function createCanvas(): HTMLCanvasElement | undefined { if (typeof document === 'undefined') { return undefined; } return document.createElement('canvas'); } function readWebGLDetails( context: WebGLAnyContext, baseDetails: WebGLSupportDetails, ): WebGLSupportDetails { const debugInfo = context.getExtension('WEBGL_debug_renderer_info') as | { UNMASKED_VENDOR_WEBGL: number; UNMASKED_RENDERER_WEBGL: number; } | null; return { ...baseDetails, version: readParameter(context, 'VERSION'), vendor: readParameter(context, 'VENDOR'), renderer: readParameter(context, 'RENDERER'), unmaskedVendor: debugInfo ? safeGetParameter(context, debugInfo.UNMASKED_VENDOR_WEBGL) : undefined, unmaskedRenderer: debugInfo ? safeGetParameter(context, debugInfo.UNMASKED_RENDERER_WEBGL) : undefined, maxTextureSize: readParameter(context, 'MAX_TEXTURE_SIZE'), maxVertexUniformVectors: readParameter(context, 'MAX_VERTEX_UNIFORM_VECTORS'), }; } function readParameter( context: WebGLRenderingContext, constantName: keyof WebGLRenderingContext, ): TValue | undefined { const constant = context[constantName]; if (typeof constant !== 'number') { return undefined; } return safeGetParameter(context, constant); } function safeGetParameter( context: WebGLRenderingContext, parameter: number, ): TValue | undefined { try { return context.getParameter(parameter) as TValue; } catch { return undefined; } }