diff --git a/modules/constants/src/webgl-types.ts b/modules/constants/src/webgl-types.ts index 59941df254..8d29cdbf2d 100644 --- a/modules/constants/src/webgl-types.ts +++ b/modules/constants/src/webgl-types.ts @@ -345,37 +345,59 @@ export type GLParameters = GLValueParameters & GLFunctionParameters; /** WebGL2 Extensions */ export type GLExtensions = { + /** https://registry.khronos.org/webgl/extensions/EXT_color_buffer_float */ EXT_color_buffer_float?: EXT_color_buffer_float | null; + /** https://registry.khronos.org/webgl/extensions/EXT_color_buffer_half_float */ EXT_color_buffer_half_float?: EXT_color_buffer_half_float | null; + /** https://registry.khronos.org/webgl/extensions/EXT_texture_compression_bptc */ EXT_texture_compression_bptc?: EXT_texture_compression_bptc | null; + /** https://registry.khronos.org/webgl/extensions/EXT_texture_compression_rgtc */ EXT_texture_compression_rgtc?: EXT_texture_compression_rgtc | null; /** https://registry.khronos.org/webgl/extensions/EXT_texture_filter_anisotropic */ EXT_texture_filter_anisotropic?: EXT_texture_filter_anisotropic | null; /** https://registry.khronos.org/webgl/extensions/KHR_parallel_shader_compile */ KHR_parallel_shader_compile?: KHR_parallel_shader_compile | null; + /** https://registry.khronos.org/webgl/extensions/OES_fbo_render_mipmap */ OES_fbo_render_mipmap?: OES_fbo_render_mipmap | null; + /** https://registry.khronos.org/webgl/extensions/OES_texture_float */ OES_texture_float?: OES_texture_float | null; + /** https://registry.khronos.org/webgl/extensions/OES_texture_float_linear */ OES_texture_float_linear?: OES_texture_float_linear | null; + /** https://registry.khronos.org/webgl/extensions/OES_texture_half_float */ OES_texture_half_float?: OES_texture_half_float | null; + /** https://registry.khronos.org/webgl/extensions/OES_texture_half_float_linear */ OES_texture_half_float_linear?: OES_texture_half_float_linear | null; + /** https://registry.khronos.org/webgl/extensions/OES_vertex_array_object */ OES_vertex_array_object?: OES_vertex_array_object | null; + /** https://registry.khronos.org/webgl/extensions/EXT_float_blend */ + EXT_float_blend?: EXT_float_blend | null; + /** https://registry.khronos.org/webgl/extensions/OVR_multiview2 */ OVR_multiview2?: OVR_multiview2 | null; + /** https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_astc */ WEBGL_compressed_texture_astc?: WEBGL_compressed_texture_astc | null; + /** https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_etc */ WEBGL_compressed_texture_etc?: WEBGL_compressed_texture_etc | null; + /** https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_etc1 */ WEBGL_compressed_texture_etc1?: WEBGL_compressed_texture_etc1 | null; + /** https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_pvrtc */ WEBGL_compressed_texture_pvrtc?: WEBGL_compressed_texture_pvrtc | null; + /** https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_s3tc */ WEBGL_compressed_texture_s3tc?: WEBGL_compressed_texture_s3tc | null; + /** https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_s3tc_srgb */ WEBGL_compressed_texture_s3tc_srgb?: WEBGL_compressed_texture_s3tc_srgb | null; + /** https://registry.khronos.org/webgl/extensions/WEBGL_debug_renderer_info */ WEBGL_debug_renderer_info?: WEBGL_debug_renderer_info | null; + /** https://registry.khronos.org/webgl/extensions/WEBGL_debug_shaders */ WEBGL_debug_shaders?: WEBGL_debug_shaders | null; + /** https://registry.khronos.org/webgl/extensions/WEBGL_lose_context */ WEBGL_lose_context?: WEBGL_lose_context | null; + // Predefined typescript types not available for the following extensions + /** https://registry.khronos.org/webgl/extensions/EXT_depth_clamp/ */ EXT_depth_clamp?: EXT_depth_clamp | null; - /** https://registry.khronos.org/webgl/extensions/WEBGL_provoking_vertex/ */ WEBGL_provoking_vertex?: WEBGL_provoking_vertex | null; - /** https://registry.khronos.org/webgl/extensions/WEBGL_polygon_mode/ */ WEBGL_polygon_mode?: WEBGL_polygon_mode | null; @@ -404,7 +426,6 @@ export type GLExtensions = { // ANGLE_instanced_arrays?: ANGLE_instanced_arrays | null; // EXT_blend_minmax?: EXT_blend_minmax | null; - // EXT_float_blend?: EXT_float_blend | null; // EXT_frag_depth?: EXT_frag_depth | null; // EXT_sRGB?: EXT_sRGB | null; // EXT_shader_texture_lod?: EXT_shader_texture_lod | null; diff --git a/modules/core/src/adapter/device.ts b/modules/core/src/adapter/device.ts index 7c48f3793a..6f84f2b594 100644 --- a/modules/core/src/adapter/device.ts +++ b/modules/core/src/adapter/device.ts @@ -20,36 +20,6 @@ import type {CommandEncoder, CommandEncoderProps} from './resources/command-enco import type {VertexArray, VertexArrayProps} from './resources/vertex-array'; import type {TransformFeedback, TransformFeedbackProps} from './resources/transform-feedback'; -/** Device properties */ -export type DeviceProps = { - id?: string; - - type?: 'webgl' | 'webgpu' | 'best-available'; - - // Common parameters - canvas?: HTMLCanvasElement | OffscreenCanvas | string | null; // A canvas element or a canvas string id - container?: HTMLElement | string | null; - width?: number /** width is only used when creating a new canvas */; - height?: number /** height is only used when creating a new canvas */; - - // WebGLContext PARAMETERS - Can only be set on context creation... - // alpha?: boolean; // Default render target has an alpha buffer. - // depth?: boolean; // Default render target has a depth buffer of at least 16 bits. - // stencil?: boolean; // Default render target has a stencil buffer of at least 8 bits. - // antialias?: boolean; // Boolean that indicates whether or not to perform anti-aliasing. - // premultipliedAlpha?: boolean; // Boolean that indicates that the page compositor will assume the drawing buffer contains colors with pre-multiplied alpha. - // preserveDrawingBuffer?: boolean; // Default render target buffers will not be automatically cleared and will preserve their values until cleared or overwritten - // failIfMajorPerformanceCaveat?: boolean; // Do not create if the system performance is low. - - // Unclear if these are still supported - debug?: boolean; // Instrument context (at the expense of performance) - manageState?: boolean; // Set to false to disable WebGL state management instrumentation - break?: string[]; // TODO: types - - // @deprecated Attach to existing context - gl?: WebGL2RenderingContext | null; -}; - /** * Identifies the GPU vendor and driver. * @note Chrome WebGPU does not provide much information, though more can be enabled with @@ -125,7 +95,7 @@ export type WebGPUDeviceFeature = | 'texture-compression-etc2' | 'texture-compression-astc'; -// Removed WebGPU features... +// WebGPU features that have been removed from the WebGPU spec... // 'depth-clamping' | // 'pipeline-statistics-query' | @@ -170,6 +140,57 @@ export type DeviceFeature = | WebGLDeviceFeature | WebGLCompressedTextureFeatures; +/** Set-like class for features (lets apps check for WebGL / WebGPU extensions) */ +export class DeviceFeatures { + protected features: Set; + + constructor(features: DeviceFeature[] = []) { + this.features = new Set(features); + } + + *[Symbol.iterator](): IterableIterator { + yield* this.features; + } + + has(feature: DeviceFeature): boolean { + return this.features.has(feature); + } +} + +/** Device properties */ +export type DeviceProps = { + id?: string; + + type?: 'webgl' | 'webgpu' | 'best-available'; + + // Common parameters + canvas?: HTMLCanvasElement | OffscreenCanvas | string | null; // A canvas element or a canvas string id + container?: HTMLElement | string | null; + width?: number /** width is only used when creating a new canvas */; + height?: number /** height is only used when creating a new canvas */; + + // WebGLContext PARAMETERS - Can only be set on context creation... + // alpha?: boolean; // Default render target has an alpha buffer. + // depth?: boolean; // Default render target has a depth buffer of at least 16 bits. + // stencil?: boolean; // Default render target has a stencil buffer of at least 8 bits. + // antialias?: boolean; // Boolean that indicates whether or not to perform anti-aliasing. + // premultipliedAlpha?: boolean; // Boolean that indicates that the page compositor will assume the drawing buffer contains colors with pre-multiplied alpha. + // preserveDrawingBuffer?: boolean; // Default render target buffers will not be automatically cleared and will preserve their values until cleared or overwritten + // failIfMajorPerformanceCaveat?: boolean; // Do not create if the system performance is low. + + /** Instrument context (at the expense of performance) */ + debug?: boolean; + /** Initialize the SpectorJS WebGL debugger */ + spector?: boolean; + + // Unclear if these are still supported + manageState?: boolean; // Set to false to disable WebGL state management instrumentation + break?: string[]; // TODO: types + + // @deprecated Attach to existing context + gl?: WebGL2RenderingContext | null; +}; + /** * WebGPU Device/WebGL context abstraction */ @@ -182,7 +203,8 @@ export abstract class Device { manageState: true, width: 800, // width are height are only used by headless gl height: 600, - debug: Boolean(log.get('debug')), // Instrument context (at the expense of performance) + debug: false, // Instrument context (at the expense of performance) + spector: false, // Initialize the SpectorJS WebGL debugger break: [], // alpha: undefined, @@ -226,7 +248,7 @@ export abstract class Device { abstract info: DeviceInfo; /** Optional capability discovery */ - abstract get features(): Set; + abstract features: DeviceFeatures; /** WebGPU style device limits */ abstract get limits(): DeviceLimits; diff --git a/modules/core/src/index.ts b/modules/core/src/index.ts index 2890890e18..c954b8504c 100644 --- a/modules/core/src/index.ts +++ b/modules/core/src/index.ts @@ -15,7 +15,7 @@ export {isTypedArray, isNumberArray} from './utils/is-array'; export {luma} from './lib/luma'; export type {DeviceProps, DeviceLimits, DeviceInfo, DeviceFeature} from './adapter/device'; -export {Device} from './adapter/device'; +export {Device, DeviceFeatures} from './adapter/device'; export type {CanvasContextProps} from './adapter/canvas-context'; export {CanvasContext} from './adapter/canvas-context'; diff --git a/modules/engine/src/model/model.ts b/modules/engine/src/model/model.ts index 84dfb342f4..3bf605b8ee 100644 --- a/modules/engine/src/model/model.ts +++ b/modules/engine/src/model/model.ts @@ -751,7 +751,7 @@ export function getPlatformInfo(device: Device): PlatformInfo { shaderLanguage: device.info.shadingLanguage, shaderLanguageVersion: device.info.shadingLanguageVersion as 100 | 300, gpu: device.info.gpu, - features: device.features + features: new Set(device.features) }; } diff --git a/modules/shadertools/test/lib/shader-assembly/assemble-shaders.spec.ts b/modules/shadertools/test/lib/shader-assembly/assemble-shaders.spec.ts index fea4b0a758..ac0efe4e45 100644 --- a/modules/shadertools/test/lib/shader-assembly/assemble-shaders.spec.ts +++ b/modules/shadertools/test/lib/shader-assembly/assemble-shaders.spec.ts @@ -11,7 +11,7 @@ function getInfo(device: Device): PlatformInfo { gpu: device.info.gpu, shaderLanguage: device.info.shadingLanguage, shaderLanguageVersion: device.info.shadingLanguageVersion as 100 | 300, - features: device.features + features: new Set(device.features) }; } diff --git a/modules/shadertools/test/lib/shader-assembly/inject-shader.spec.ts b/modules/shadertools/test/lib/shader-assembly/inject-shader.spec.ts index afb0dbc535..bd4636c7f8 100644 --- a/modules/shadertools/test/lib/shader-assembly/inject-shader.spec.ts +++ b/modules/shadertools/test/lib/shader-assembly/inject-shader.spec.ts @@ -15,7 +15,7 @@ function getInfo(device: Device): PlatformInfo { gpu: device.info.gpu, shaderLanguage: device.info.shadingLanguage, shaderLanguageVersion: device.info.shadingLanguageVersion as 100 | 300, - features: device.features + features: new Set(device.features) }; } diff --git a/modules/webgl/src/adapter/converters/texture-formats.ts b/modules/webgl/src/adapter/converters/texture-formats.ts index cb1a36591a..23226eb835 100644 --- a/modules/webgl/src/adapter/converters/texture-formats.ts +++ b/modules/webgl/src/adapter/converters/texture-formats.ts @@ -3,7 +3,8 @@ import type {TextureFormat, DeviceFeature} from '@luma.gl/core'; import {decodeTextureFormat} from '@luma.gl/core'; -import {GL} from '@luma.gl/constants'; +import {GL, GLExtensions} from '@luma.gl/constants'; +import {getWebGLExtension} from '../../context/helpers/webgl-extensions'; import {getGLFromVertexType} from './vertex-formats'; /* eslint-disable camelcase */ @@ -38,7 +39,7 @@ const EXT_color_buffer_float = 'EXT_color_buffer_float'; // const EXT_HALF_FLOAT_WEBGL1 = 'EXT_color_buffer_half_float'; // prettier-ignore -const TEXTURE_FEATURE_CHECKS: Partial> = { +export const TEXTURE_FEATURES: Partial> = { 'float32-renderable-webgl': ['EXT_color_buffer_float'], // [false, 'EXT_color_buffer_float'], 'float16-renderable-webgl': ['EXT_color_buffer_half_float'], 'norm16-renderable-webgl': [EXT_texture_norm16], @@ -61,19 +62,27 @@ const TEXTURE_FEATURE_CHECKS: Partial> = { 'texture-compression-atc-webgl': [X_ATC] }; -function checkTextureFeature(gl: WebGL2RenderingContext, feature: DeviceFeature): boolean { - const extensions = TEXTURE_FEATURE_CHECKS[feature] || []; - return extensions.every(extension => gl.getExtension(extension)); -} - -function checkTextureFeatures(gl: WebGL2RenderingContext, features: DeviceFeature[]): boolean { - return features.every(feature => checkTextureFeature(gl, feature)); +/** Return a list of texture feature strings (for Device.features). Mainly compressed texture support */ +// export function getTextureFeatures( +// gl: WebGL2RenderingContext, +// extensions: GLExtensions +// ): DeviceFeature[] { +// const textureFeatures = Object.keys(TEXTURE_FEATURES) as DeviceFeature[]; +// return textureFeatures.filter(feature => checkTextureFeature(gl, feature, extensions)); +// } + +export function isTextureFeature(feature: DeviceFeature): boolean { + return feature in TEXTURE_FEATURES; } -/** Return a list of texture feature strings (for Device.features). Mainly compressed texture support */ -export function getTextureFeatures(gl: WebGL2RenderingContext): DeviceFeature[] { - const textureFeatures = Object.keys(TEXTURE_FEATURE_CHECKS) as DeviceFeature[]; - return textureFeatures.filter(feature => checkTextureFeature(gl, feature)); +/** Checks a texture feature (for Device.features). Mainly compressed texture support */ +export function checkTextureFeature( + gl: WebGL2RenderingContext, + feature: DeviceFeature, + extensions: GLExtensions +): boolean { + const textureExtensions = TEXTURE_FEATURES[feature] || []; + return textureExtensions.every(extension => getWebGLExtension(gl, extension, extensions)); } // TEXTURE FORMATS @@ -435,7 +444,8 @@ const TYPE_SIZES = { /** Checks if a texture format is supported */ export function isTextureFormatSupported( gl: WebGL2RenderingContext, - formatOrGL: TextureFormat | GL + formatOrGL: TextureFormat | GL, + extensions: GLExtensions ): boolean { const format = convertGLToTextureFormat(formatOrGL); const info = TEXTURE_FORMATS[format]; @@ -449,17 +459,18 @@ export function isTextureFormatSupported( // Check extensions const extension = info.x || info.gl2ext; if (extension) { - return Boolean(gl.getExtension(extension)); + return Boolean(getWebGLExtension(gl, extension, extensions)); } return true; } export function isRenderbufferFormatSupported( gl: WebGL2RenderingContext, - format: TextureFormat + format: TextureFormat, + extensions: GLExtensions ): boolean { // Note: Order is important since the function call initializes extensions. - return isTextureFormatSupported(gl, format) && TEXTURE_FORMATS[format]?.renderbuffer; + return isTextureFormatSupported(gl, format, extensions) && TEXTURE_FORMATS[format]?.renderbuffer; } /** @@ -491,7 +502,8 @@ export function convertTextureFormatToGL(format: TextureFormat): GL | undefined /** Checks if a texture format is supported */ export function getTextureFormatSupport( gl: WebGL2RenderingContext, - formatOrGL: TextureFormat | GL + formatOrGL: TextureFormat | GL, + extensions: GLExtensions ): { supported: boolean; filterable?: boolean; @@ -511,20 +523,20 @@ export function getTextureFormatSupport( // Support Check that we have a GL constant let supported = info.gl === undefined; - supported = supported && checkTextureFeatures(gl, [info.f]); + supported = supported && checkTextureFeature(gl, info.f, extensions); // Filtering // const filterable = info.filter - // ? checkTextureFeatures(gl, [info.filter]) + // ? checkTextureFeature(gl, infofilter]) // : decoded && !decoded.signed; // const renderable = info.filter - // ? checkTextureFeatures(gl, [info.render]) + // ? checkTextureFeature(gl, inforender]) // : decoded && !decoded.signed; return { supported, - renderable: supported && checkTextureFeatures(gl, [info.render]), - filterable: supported && checkTextureFeatures(gl, [info.filter]), + renderable: supported && checkTextureFeature(gl, info.render, extensions), + filterable: supported && checkTextureFeature(gl, info.filter, extensions), blendable: false, // tod, storable: false }; @@ -533,10 +545,11 @@ export function getTextureFormatSupport( /** Checks whether linear filtering (interpolated sampling) is available for floating point textures */ export function isTextureFormatFilterable( gl: WebGL2RenderingContext, - formatOrGL: TextureFormat | GL + formatOrGL: TextureFormat | GL, + extensions: GLExtensions ): boolean { const format = convertGLToTextureFormat(formatOrGL); - if (!isTextureFormatSupported(gl, format)) { + if (!isTextureFormatSupported(gl, format, extensions)) { return false; } try { @@ -548,20 +561,21 @@ export function isTextureFormatFilterable( return false; } if (format.endsWith('32float')) { - return Boolean(gl.getExtension('OES_texture_float_linear')); + return Boolean(getWebGLExtension(gl, 'OES_texture_float_linear, extensions', extensions)); } if (format.endsWith('16float')) { - return Boolean(gl.getExtension('OES_texture_half_float_linear')); + return Boolean(getWebGLExtension(gl, 'OES_texture_half_float_linear, extensions', extensions)); } return true; } export function isTextureFormatRenderable( gl: WebGL2RenderingContext, - formatOrGL: TextureFormat | GL + formatOrGL: TextureFormat | GL, + extensions: GLExtensions ): boolean { const format = convertGLToTextureFormat(formatOrGL); - if (!isTextureFormatSupported(gl, format)) { + if (!isTextureFormatSupported(gl, format, extensions)) { return false; } if (typeof format === 'number') { diff --git a/modules/webgl/src/adapter/device-helpers/webgl-device-features.ts b/modules/webgl/src/adapter/device-helpers/webgl-device-features.ts index afeef48304..d178c93a3b 100644 --- a/modules/webgl/src/adapter/device-helpers/webgl-device-features.ts +++ b/modules/webgl/src/adapter/device-helpers/webgl-device-features.ts @@ -4,51 +4,11 @@ // Feature detection for WebGL // Provides a function that enables simple checking of which WebGL features are -import {DeviceFeature} from '@luma.gl/core'; -import {getTextureFeatures} from '../converters/texture-formats'; - -/** Get WebGPU style feature strings */ -export function getDeviceFeatures(gl: WebGL2RenderingContext): Set { - const features = getWebGLFeatures(gl); - - // texture features - // features.add('texture-compression-bc'); - for (const textureFeature of getTextureFeatures(gl)) { - features.add(textureFeature); - } - - // TODO - // features.add('depth-clip-control'); // GPUPrimitiveState.clampDepth - // features.add('depth24unorm-stencil8'); // GPUTextureFormat 'depth24unorm-stencil8'. - // features.add('depth32float-stencil8'); // GPUTextureFormat 'depth32float-stencil8'. - // features.add('timestamp-query'); // GPUQueryType "timestamp-query" - // "indirect-first-instance" - - return features; -} - -/** Extract all WebGL features */ -export function getWebGLFeatures(gl: WebGL2RenderingContext): Set { - // Enable EXT_float_blend first: https://developer.mozilla.org/en-US/docs/Web/API/EXT_float_blend - gl.getExtension('EXT_color_buffer_float'); - - const features = new Set(); - for (const feature of Object.keys(WEBGL_FEATURES) as DeviceFeature[]) { - if (isFeatureSupported(gl, feature)) { - features.add(feature); - } - } - return features; -} - -function isFeatureSupported(gl: WebGL2RenderingContext, feature: DeviceFeature): boolean { - const featureInfo = WEBGL_FEATURES[feature]; - - // string value requires checking the corresponding WebGL extension - return typeof featureInfo === 'string' - ? Boolean(gl.getExtension(featureInfo)) - : Boolean(featureInfo); -} +import {DeviceFeature, DeviceFeatures} from '@luma.gl/core'; +import {GLExtensions} from '@luma.gl/constants'; +import {getWebGLExtension} from '../../context/helpers/webgl-extensions'; +import {isTextureFeature, checkTextureFeature} from '../converters/texture-formats'; +import {TEXTURE_FEATURES} from '../converters/texture-formats'; /** * Defines luma.gl "feature" names and semantics @@ -56,16 +16,89 @@ function isFeatureSupported(gl: WebGL2RenderingContext, feature: DeviceFeature): */ const WEBGL_FEATURES: Partial> = { // optional WebGPU features - 'depth-clip-control': 'EXT_depth_clamp', + 'depth-clip-control': 'EXT_depth_clamp', // TODO these seem subtly different + // 'timestamp-query' // GPUQueryType "timestamp-query" + // "indirect-first-instance" + // Textures are handled by getTextureFeatures() + // 'depth24unorm-stencil8' // GPUTextureFormat 'depth24unorm-stencil8' + // 'depth32float-stencil8' // GPUTextureFormat 'depth32float-stencil8' // optional WebGL features 'timer-query-webgl': 'EXT_disjoint_timer_query_webgl2', 'compilation-status-async-webgl': 'KHR_parallel_shader_compile', - 'provoking-vertex-webgl': 'WEBGL_provoking_vertex', 'polygon-mode-webgl': 'WEBGL_polygon_mode', + 'provoking-vertex-webgl': 'WEBGL_provoking_vertex', 'shader-clip-cull-distance-webgl': 'WEBGL_clip_cull_distance', 'shader-noperspective-interpolation-webgl': 'NV_shader_noperspective_interpolation', 'shader-conservative-depth-webgl': 'EXT_conservative_depth' // Textures are handled by getTextureFeatures() }; + +/** + * WebGL extensions exposed as luma.gl features + * To minimize GL log noise and improve performance, this class ensures that + * - WebGL extensions are not queried until the corresponding feature is checked. + * - WebGL extensions are only queried once. + */ +export class WebGLDeviceFeatures extends DeviceFeatures { + protected gl: WebGL2RenderingContext; + protected extensions: GLExtensions; + protected testedFeatures = new Set(); + + constructor(gl: WebGL2RenderingContext, extensions: GLExtensions) { + super(); + this.gl = gl; + this.extensions = extensions; + // TODO - is this really needed? + // Enable EXT_float_blend first: https://developer.mozilla.org/en-US/docs/Web/API/EXT_float_blend + getWebGLExtension(gl, 'EXT_color_buffer_float', extensions); + } + + *[Symbol.iterator](): IterableIterator { + for (const feature of Object.keys(WEBGL_FEATURES) as DeviceFeature[]) { + if (this.has(feature)) { + yield feature; + } + } + for (const feature of Object.keys(TEXTURE_FEATURES) as DeviceFeature[]) { + if (this.has(feature)) { + yield feature; + } + } + return []; + } + + override has(feature: DeviceFeature): boolean { + // We have already tested this feature + if (this.testedFeatures.has(feature)) { + return this.features.has(feature); + } + + // Check the feature once + this.testedFeatures.add(feature); + if (isTextureFeature(feature) && checkTextureFeature(this.gl, feature, this.extensions)) { + this.features.add(feature); + return true; + } + + if (this.getWebGLFeature(feature)) { + this.features.add(feature); + return true; + } + + return false; + } + + /** Extract all WebGL features */ + protected getWebGLFeature(feature: DeviceFeature): boolean { + const featureInfo = WEBGL_FEATURES[feature]; + // string value requires checking the corresponding WebGL extension + const isSupported = + typeof featureInfo === 'string' + ? Boolean(getWebGLExtension(this.gl, featureInfo, this.extensions)) + : Boolean(featureInfo); + + return isSupported; + } +} diff --git a/modules/webgl/src/adapter/device-helpers/webgl-device-info.ts b/modules/webgl/src/adapter/device-helpers/webgl-device-info.ts index 8ae59d56f7..8c4b13df70 100644 --- a/modules/webgl/src/adapter/device-helpers/webgl-device-info.ts +++ b/modules/webgl/src/adapter/device-helpers/webgl-device-info.ts @@ -2,17 +2,19 @@ // Copyright (c) vis.gl contributors import {DeviceInfo} from '@luma.gl/core'; -import {GL} from '@luma.gl/constants'; +import {GL, GLExtensions} from '@luma.gl/constants'; +import {getWebGLExtension} from '../../context/helpers/webgl-extensions'; /** @returns strings identifying the GPU vendor and driver. */ -export function getDeviceInfo(gl: WebGL2RenderingContext): DeviceInfo { +export function getDeviceInfo(gl: WebGL2RenderingContext, extensions: GLExtensions): DeviceInfo { // "Masked" info is always available, but don't contain much useful information const vendorMasked = gl.getParameter(GL.VENDOR); const rendererMasked = gl.getParameter(GL.RENDERER); // If we are lucky, unmasked info is available // https://www.khronos.org/registry/webgl/extensions/WEBGL_debug_renderer_info/ - const ext = gl.getExtension('WEBGL_debug_renderer_info'); + getWebGLExtension(gl, 'WEBGL_debug_renderer_info', extensions); + const ext = extensions.WEBGL_debug_renderer_info; const vendorUnmasked = gl.getParameter(ext ? ext.UNMASKED_VENDOR_WEBGL : GL.VENDOR); const rendererUnmasked = gl.getParameter(ext ? ext.UNMASKED_RENDERER_WEBGL : GL.RENDERER); const vendor = vendorUnmasked || vendorMasked; @@ -47,44 +49,50 @@ export function getDeviceInfo(gl: WebGL2RenderingContext): DeviceInfo { } /** "Sniff" the GPU type from the info. This works best if unmasked info is available. */ -function identifyGPUVendor(vendor: string, renderer: string): 'nvidia' | 'intel' | 'apple' | 'amd' | 'software' | 'unknown' { - if ((/NVIDIA/i.exec(vendor)) || (/NVIDIA/i.exec(renderer))) { +function identifyGPUVendor( + vendor: string, + renderer: string +): 'nvidia' | 'intel' | 'apple' | 'amd' | 'software' | 'unknown' { + if (/NVIDIA/i.exec(vendor) || /NVIDIA/i.exec(renderer)) { return 'nvidia'; } - if ((/INTEL/i.exec(vendor)) || (/INTEL/i.exec(renderer))) { + if (/INTEL/i.exec(vendor) || /INTEL/i.exec(renderer)) { return 'intel'; } - if ((/Apple/i.exec(vendor)) || (/Apple/i.exec(renderer))) { + if (/Apple/i.exec(vendor) || /Apple/i.exec(renderer)) { return 'apple'; } if ( - (/AMD/i.exec(vendor)) || - (/AMD/i.exec(renderer)) || - (/ATI/i.exec(vendor)) || - (/ATI/i.exec(renderer)) + /AMD/i.exec(vendor) || + /AMD/i.exec(renderer) || + /ATI/i.exec(vendor) || + /ATI/i.exec(renderer) ) { return 'amd'; } - if ((/SwiftShader/i.exec(vendor)) || (/SwiftShader/i.exec(renderer))) { + if (/SwiftShader/i.exec(vendor) || /SwiftShader/i.exec(renderer)) { return 'software'; } - + return 'unknown'; } /** "Sniff" the GPU backend from the info. This works best if unmasked info is available. */ function identifyGPUBackend(vendor: string, renderer: string): 'opengl' | 'metal' | 'unknown' { - if ((/Metal/i.exec(vendor)) || (/Metal/i.exec(renderer))) { + if (/Metal/i.exec(vendor) || /Metal/i.exec(renderer)) { return 'metal'; } - if ((/ANGLE/i.exec(vendor)) || (/ANGLE/i.exec(renderer))) { + if (/ANGLE/i.exec(vendor) || /ANGLE/i.exec(renderer)) { return 'opengl'; - } + } return 'unknown'; } -function identifyGPUType(vendor: string, renderer: string): 'discrete' | 'integrated' | 'cpu' | 'unknown' { - if ((/SwiftShader/i.exec(vendor)) || (/SwiftShader/i.exec(renderer))) { +function identifyGPUType( + vendor: string, + renderer: string +): 'discrete' | 'integrated' | 'cpu' | 'unknown' { + if (/SwiftShader/i.exec(vendor) || /SwiftShader/i.exec(renderer)) { return 'cpu'; } diff --git a/modules/webgl/src/adapter/device-helpers/webgl-device-limits.ts b/modules/webgl/src/adapter/device-helpers/webgl-device-limits.ts index 89ecee9bd9..f06bcda58a 100644 --- a/modules/webgl/src/adapter/device-helpers/webgl-device-limits.ts +++ b/modules/webgl/src/adapter/device-helpers/webgl-device-limits.ts @@ -97,8 +97,9 @@ export function getWebGLLimits(gl: WebGL2RenderingContext): WebGLLimits { return (gl.getParameter(pname) as T) || defaultValue; } // function getMaxAnistropy() { - // const extension = gl.getExtension('EXT_texture_filter_anisotropic'); + // const extension = getWebGLExtension(gl, 'EXT_texture_filter_anisotropic'); // } + return { [GL.ALIASED_LINE_WIDTH_RANGE]: get(GL.ALIASED_LINE_WIDTH_RANGE), [GL.ALIASED_POINT_SIZE_RANGE]: get(GL.ALIASED_POINT_SIZE_RANGE), diff --git a/modules/webgl/src/adapter/objects/webgl-renderbuffer.ts b/modules/webgl/src/adapter/objects/webgl-renderbuffer.ts index 0de14c807b..ad9b492c47 100644 --- a/modules/webgl/src/adapter/objects/webgl-renderbuffer.ts +++ b/modules/webgl/src/adapter/objects/webgl-renderbuffer.ts @@ -48,7 +48,7 @@ export class WEBGLRenderbuffer extends WebGLResource { glFormat: GL; static isTextureFormatSupported(device: WebGLDevice, format: TextureFormat): boolean { - return isRenderbufferFormatSupported(device.gl, format); + return isRenderbufferFormatSupported(device.gl, format, device._extensions); } constructor(device: WebGLDevice, props: RenderbufferProps) { diff --git a/modules/webgl/src/adapter/objects/webgl-resource.ts b/modules/webgl/src/adapter/objects/webgl-resource.ts index 106fd5c835..23c33ae4b3 100644 --- a/modules/webgl/src/adapter/objects/webgl-resource.ts +++ b/modules/webgl/src/adapter/objects/webgl-resource.ts @@ -1,14 +1,11 @@ // luma.gl, MIT license // Copyright (c) vis.gl contributors -import {Resource, assert, uid, stubRemovedMethods} from '@luma.gl/core'; import type {Device, ResourceProps} from '@luma.gl/core'; +import {Resource, uid, stubRemovedMethods} from '@luma.gl/core'; import {GL} from '@luma.gl/constants'; import {WebGLDevice} from '../webgl-device'; -// Requires full GL enum to be bundled... Make these bindings dependent on dynamic import (debug)? -import {getKeyValue} from './constants-to-keys'; - const ERR_RESOURCE_METHOD_UNDEFINED = 'Resource subclass must define virtual methods'; /** @@ -114,122 +111,6 @@ export abstract class WebGLResource extends Resourc this.bind(null); } - /** - * Query a Resource parameter - * - * @param name - * @return param - */ - getParameter(pname: number, props: any = {}): any { - pname = getKeyValue(this.gl, pname); - assert(pname); - - // @ts-expect-error - const parameters = this.constructor.PARAMETERS || {}; - - // Use parameter definitions to handle unsupported parameters - const parameter = parameters[pname]; - if (parameter) { - // Check if we can query for this parameter - const parameterAvailable = - (!('extension' in parameter) || this.gl.getExtension(parameter.extension)); - - if (!parameterAvailable) { - return parameter.webgl2; - } - } - - // If unknown parameter - Could be a valid parameter not covered by PARAMS - // Attempt to query for it and let WebGL report errors - return this._getParameter(pname, props); - } - - // Many resources support a getParameter call - - // getParameters will get all parameters - slow but useful for debugging - // eslint-disable-next-line complexity - getParameters(options: {parameters?: any, keys?: any} = {}) { - const {parameters, keys} = options; - - // Get parameter definitions for this Resource - // @ts-expect-error - const PARAMETERS = this.constructor.PARAMETERS || {}; - - const values: Record = {}; - - // Query all parameters if no list provided - const parameterKeys = parameters || Object.keys(PARAMETERS); - - // WEBGL limits - for (const pname of parameterKeys) { - const parameter = PARAMETERS[pname]; - - // Check if this parameter is available on this platform - const parameterAvailable = - parameter && - (!('extension' in parameter) || this.gl.getExtension(parameter.extension)); - - if (parameterAvailable) { - const key = keys ? this.device.getGLKey(pname) : pname; - values[key] = this.getParameter(pname, options); - if (keys && parameter.type === 'GLenum') { - values[key] = this.device.getGLKey(values[key]); - } - } - } - - return values; - } - - /** - * Update a Resource setting - * - * @todo - cache parameter to avoid issuing WebGL calls? - * - * @param pname - parameter (GL constant, value or key) - * @param value {GLint|GLfloat|GLenum} - * @return returns self to enable chaining - */ - setParameter(pname: GL | string, value: any): this { - pname = getKeyValue(this.gl, pname); - assert(pname); - - // @ts-expect-error - const parameters = this.constructor.PARAMETERS || {}; - - const parameter = parameters[pname]; - if (parameter) { - // Check if this parameter is available on this platform - const parameterAvailable = - (!('extension' in parameter) || this.gl.getExtension(parameter.extension)); - - if (!parameterAvailable) { - throw new Error('Parameter not available on this platform'); - } - - // Handle string keys - if (parameter.type === 'GLenum') { - // @ts-expect-error - value = getKeyValue(value); - } - } - - // If unknown parameter - Could be a valid parameter not covered by PARAMS - // attempt to set it and let WebGL report errors - this._setParameter(pname, value); - return this; - } - - /* - * Batch update resource parameters - * Assumes the subclass supports a setParameter call - */ - setParameters(parameters: Record) { - for (const pname in parameters) { - this.setParameter(pname, parameters[pname]); - } - return this; - } - // Install stubs for removed methods stubRemovedMethods(className: string, version: string, methodNames: string[]) { return stubRemovedMethods(this, className, version, methodNames); diff --git a/modules/webgl/src/adapter/resources/webgl-framebuffer.ts b/modules/webgl/src/adapter/resources/webgl-framebuffer.ts index 32cede2c0a..565d62ac4b 100644 --- a/modules/webgl/src/adapter/resources/webgl-framebuffer.ts +++ b/modules/webgl/src/adapter/resources/webgl-framebuffer.ts @@ -59,12 +59,16 @@ export class WEBGLFramebuffer extends Framebuffer { ); } - this.gl.bindFramebuffer(GL.FRAMEBUFFER, null); - } + /** Check the status */ + // @ts-expect-error + if (props.check !== false) { + const status = this.gl.checkFramebufferStatus(GL.FRAMEBUFFER); + if (status !== GL.FRAMEBUFFER_COMPLETE) { + throw new Error(`Framebuffer ${_getFrameBufferStatus(status)}`); + } + } - // @ts-expect-error - if (props.check !== false) { - this._checkStatus(); + this.gl.bindFramebuffer(GL.FRAMEBUFFER, null); } } @@ -79,18 +83,6 @@ export class WEBGLFramebuffer extends Framebuffer { // PRIVATE - /** Check the status */ - protected _checkStatus(): void { - const {gl} = this; - // TODO - should we really rely on this trick? - const prevHandle = gl.bindFramebuffer(GL.FRAMEBUFFER, this.handle) as unknown as WebGLFramebuffer; - const status = gl.checkFramebufferStatus(GL.FRAMEBUFFER); - gl.bindFramebuffer(GL.FRAMEBUFFER, prevHandle || null); - if (status !== gl.FRAMEBUFFER_COMPLETE) { - throw new Error(`Framebuffer ${_getFrameBufferStatus(status)}`); - } - } - /** In WebGL we must use renderbuffers for depth/stencil attachments (unless we have extensions) */ protected override createDepthStencilTexture(format: TextureFormat): Texture { return new WEBGLRenderbuffer(this.device, { @@ -103,8 +95,8 @@ export class WEBGLFramebuffer extends Framebuffer { }) as unknown as WEBGLTexture; } - /** - * Attachment resize is expected to be a noop if size is same + /** + * Attachment resize is expected to be a noop if size is same */ protected override resizeAttachments(width: number, height: number): this { // for default framebuffer, just update the stored size @@ -206,7 +198,9 @@ export class WEBGLFramebuffer extends Framebuffer { function mapIndexToCubeMapFace(layer: number | GL): GL { // TEXTURE_CUBE_MAP_POSITIVE_X is a big value (0x8515) // if smaller assume layer is index, otherwise assume it is already a cube map face constant - return layer < (GL.TEXTURE_CUBE_MAP_POSITIVE_X as number) ? layer + GL.TEXTURE_CUBE_MAP_POSITIVE_X : layer; + return layer < (GL.TEXTURE_CUBE_MAP_POSITIVE_X as number) + ? layer + GL.TEXTURE_CUBE_MAP_POSITIVE_X + : layer; } // Helper METHODS diff --git a/modules/webgl/src/adapter/resources/webgl-shader.ts b/modules/webgl/src/adapter/resources/webgl-shader.ts index cffb14361c..158fe2361f 100644 --- a/modules/webgl/src/adapter/resources/webgl-shader.ts +++ b/modules/webgl/src/adapter/resources/webgl-shader.ts @@ -49,7 +49,8 @@ export class WEBGLShader extends Shader { } override getTranslatedSource(): string | null { - const ext = this.device.gl.getExtension('WEBGL_debug_shaders'); + const extensions = this.device.getExtension('WEBGL_debug_shaders'); + const ext = extensions.WEBGL_debug_shaders; return ext?.getTranslatedShaderSource(this.handle); } diff --git a/modules/webgl/src/adapter/webgl-device.ts b/modules/webgl/src/adapter/webgl-device.ts index b0b2646c78..9f93b5ceab 100644 --- a/modules/webgl/src/adapter/webgl-device.ts +++ b/modules/webgl/src/adapter/webgl-device.ts @@ -5,7 +5,6 @@ import type { DeviceProps, DeviceInfo, DeviceLimits, - DeviceFeature, CanvasContextProps, TextureFormat, VertexArray, @@ -17,15 +16,14 @@ import type { } from '@luma.gl/core'; import {Device, CanvasContext, log, uid, assert} from '@luma.gl/core'; import type {GLExtensions} from '@luma.gl/constants'; -import {isBrowser} from '@probe.gl/env'; import { popContextState, pushContextState, trackContextState } from '../context/state-tracker/track-context-state'; -import {createBrowserContext} from '../context/context/create-browser-context'; +import {createBrowserContext} from '../context/helpers/create-browser-context'; import {getDeviceInfo} from './device-helpers/webgl-device-info'; -import {getDeviceFeatures} from './device-helpers/webgl-device-features'; +import {WebGLDeviceFeatures} from './device-helpers/webgl-device-features'; import {getDeviceLimits, getWebGLLimits, WebGLLimits} from './device-helpers/webgl-device-limits'; import {WebGLCanvasContext} from './webgl-canvas-context'; import {loadSpectorJS, initializeSpectorJS} from '../context/debug/spector'; @@ -74,6 +72,7 @@ import {readPixelsToArray, readPixelsToBuffer} from '../classic/copy-and-blit'; import {setGLParameters, getGLParameters} from '../context/parameters/unified-parameter-api'; import {withGLParameters} from '../context/state-tracker/with-parameters'; import {clear} from '../classic/clear'; +import {getWebGLExtension} from '../context/helpers/webgl-extensions'; const LOG_LEVEL = 1; @@ -89,15 +88,12 @@ export class WebGLDevice extends Device { return typeof WebGL2RenderingContext !== 'undefined'; } - readonly info: DeviceInfo; - readonly canvasContext: WebGLCanvasContext; - + /** The underlying WebGL context */ readonly handle: WebGL2RenderingContext; + features: WebGLDeviceFeatures; - get features(): Set { - this._features = this._features || getDeviceFeatures(this.gl); - return this._features; - } + readonly info: DeviceInfo; + readonly canvasContext: WebGLCanvasContext; get limits(): DeviceLimits { this._limits = this._limits || getDeviceLimits(this.gl); @@ -107,7 +103,6 @@ export class WebGLDevice extends Device { readonly lost: Promise<{reason: 'destroyed'; message: string}>; private _resolveContextLost?: (value: {reason: 'destroyed'; message: string}) => void; - private _features?: Set; private _limits?: DeviceLimits; // @@ -138,27 +133,31 @@ export class WebGLDevice extends Device { static async create(props: DeviceProps = {}): Promise { log.groupCollapsed(LOG_LEVEL, 'WebGLDevice created')(); - // Wait for page to load. Only wait when props. canvas is string - // to avoid setting page onload callback unless necessary - if (typeof props.canvas === 'string') { - await CanvasContext.pageLoaded; - } + const promises: Promise[] = []; // Load webgl and spector debug scripts from CDN if requested if (log.get('debug') || props.debug) { - await loadWebGLDeveloperTools(); + promises.push(loadWebGLDeveloperTools()) ; + } + + if (log.get('spector') || props.spector) { + promises.push(loadSpectorJS()); } - // @ts-expect-error spector not on props - const {spector} = props; - if (log.get('spector') || spector) { - await loadSpectorJS(); + // Wait for page to load: if canvas is a string we need to query the DOM for the canvas element. + // We only wait when props.canvas is string to avoids setting the global page onload callback unless necessary. + if (typeof props.canvas === 'string') { + promises.push(CanvasContext.pageLoaded); } + // Wait for all the loads to settle before creating the context. + // The Device.create() functions are async, so in contrast to the constructor, we can `await` here. + await Promise.all(promises); + log.probe(LOG_LEVEL + 1, 'DOM is loaded')(); // @ts-expect-error - if (props.gl && props.gl.device) { + if (props.gl?.device) { return WebGLDevice.attach(props.gl); } @@ -191,41 +190,40 @@ ${device.info.vendor}, ${device.info.renderer} for canvas: ${device.canvasContex } // Create and instrument context - const canvas = props.canvas || props.gl?.canvas; + const canvas = props.gl?.canvas || props.canvas; this.canvasContext = new WebGLCanvasContext(this, {...props, canvas}); this.lost = new Promise<{reason: 'destroyed'; message: string}>(resolve => { this._resolveContextLost = resolve; }); - const onContextLost = (event: Event) => - this._resolveContextLost?.({ - reason: 'destroyed', - message: 'Computer entered sleep mode, or too many apps or browser tabs are using the GPU.' - }); - let gl: WebGL2RenderingContext | null = props.gl || null; - gl = - gl || - (isBrowser() - ? createBrowserContext(this.canvasContext.canvas, {...props, onContextLost}) - : null); - + gl ||= createBrowserContext(this.canvasContext.canvas, { + ...props, + onContextLost: (event: Event) => + this._resolveContextLost?.({ + reason: 'destroyed', + message: 'Entered sleep mode, or too many apps or browser tabs are using the GPU.' + }) + }); + if (!gl) { throw new Error('WebGL context creation failed'); } this.handle = gl; - this.gl = this.handle; + this.gl = gl; this.canvasContext.resize(); // luma Device fields - this.info = getDeviceInfo(this.gl); + this.info = getDeviceInfo(this.gl, this._extensions); + this.features = new WebGLDeviceFeatures(this.gl, this._extensions); + // Update context // @ts-expect-error Link webgl context back to device this.gl.device = this; - // @ts-expect-error Annotate webgl context to handle - this.gl._version = this.isWebGL2 ? 2 : 1; + // @ts-expect-error Store WebGL version field on gl context (HACK to identify debug contexts) + this.gl._version = 2; // Install context state tracking // @ts-expect-error - hidden parameters @@ -237,17 +235,15 @@ ${device.info.vendor}, ${device.info.renderer} for canvas: ${device.canvasContex }); // DEBUG contexts: Add debug instrumentation to the context, force log level to at least 1 - if (isBrowser() && props.debug) { + if (props.debug) { this.gl = makeDebugContext(this.gl, {...props, throwOnError: true}); this.debug = true; log.level = Math.max(log.level, 1); log.warn('WebGL debug mode activated. Performance reduced.')(); } - // @ts-expect-error spector not on props - if (isBrowser() && props.spector) { - const canvas = this.handle.canvas || (props.canvas as HTMLCanvasElement); - this.spector = initializeSpectorJS({...this.props, canvas}); + if (props.spector) { + this.spector = initializeSpectorJS({...this.props, canvas: this.handle.canvas}); } } @@ -255,8 +251,7 @@ ${device.info.vendor}, ${device.info.renderer} for canvas: ${device.canvasContex * Destroys the context * @note Has no effect for browser contexts, there is no browser API for destroying contexts */ - destroy(): void { - } + destroy(): void {} get isLost(): boolean { return this.gl.isContextLost(); @@ -267,15 +262,15 @@ ${device.info.vendor}, ${device.info.renderer} for canvas: ${device.canvasContex } isTextureFormatSupported(format: TextureFormat): boolean { - return isTextureFormatSupported(this.gl, format); + return isTextureFormatSupported(this.gl, format, this._extensions); } isTextureFormatFilterable(format: TextureFormat): boolean { - return isTextureFormatFilterable(this.gl, format); + return isTextureFormatFilterable(this.gl, format, this._extensions); } isTextureFormatRenderable(format: TextureFormat): boolean { - return isTextureFormatRenderable(this.gl, format); + return isTextureFormatRenderable(this.gl, format, this._extensions); } // IMPLEMENTATION OF ABSTRACT DEVICE @@ -361,7 +356,7 @@ ${device.info.vendor}, ${device.info.renderer} for canvas: ${device.canvasContex // // TEMPORARY HACKS - will be removed in v9.1 - // + // /** @deprecated - should use command encoder */ override readPixelsToArrayWebGL( @@ -411,10 +406,15 @@ ${device.info.vendor}, ${device.info.renderer} for canvas: ${device.canvasContex withGLParameters(this, parameters, func); } - override clearWebGL(options?: {framebuffer?: Framebuffer; color?: any; depth?: any; stencil?: any}): void { + override clearWebGL(options?: { + framebuffer?: Framebuffer; + color?: any; + depth?: any; + stencil?: any; + }): void { clear(this, options); } - + // // WebGL-only API (not part of `Device` API) // @@ -513,7 +513,10 @@ ${device.info.vendor}, ${device.info.renderer} for canvas: ${device.canvasContex this._constants = this._constants || new Array(this.limits.maxVertexAttributes).fill(null); const currentConstant = this._constants[location]; if (currentConstant && compareConstantArrayValues(currentConstant, constant)) { - log.info(1, `setConstantAttributeWebGL(${location}) could have been skipped, value unchanged`)(); + log.info( + 1, + `setConstantAttributeWebGL(${location}) could have been skipped, value unchanged` + )(); } this._constants[location] = constant; @@ -533,10 +536,8 @@ ${device.info.vendor}, ${device.info.renderer} for canvas: ${device.canvasContex } /** Ensure extensions are only requested once */ - getExtension(name: string): GLExtensions { - if (this._extensions[name] === undefined) { - this._extensions[name] = this.gl.getExtension(name) || null; - } + getExtension(name: keyof GLExtensions): GLExtensions { + getWebGLExtension(this.gl, name, this._extensions); return this._extensions; } } diff --git a/modules/webgl/src/context/context/context-data.ts b/modules/webgl/src/context/context/context-data.ts deleted file mode 100644 index 438b2f2e17..0000000000 --- a/modules/webgl/src/context/context/context-data.ts +++ /dev/null @@ -1,44 +0,0 @@ -// luma.gl, MIT license -// Copyright (c) vis.gl contributors - -/** - * Stores luma.gl specific state associated with a context - */ -export interface WebGLContextData { - _polyfilled: boolean; - _extensions: Record; -} - -/** - * Gets luma.gl specific state from a context - * @returns context state - */ -export function getContextData(gl: WebGL2RenderingContext): WebGLContextData { - // @ts-expect-error - const luma = gl.luma as WebGLContextData | null; - if (!luma) { - const contextState: WebGLContextData = { - _polyfilled: false, - _extensions: {} - }; - // @ts-expect-error - gl.luma = contextState; - } - - // @ts-expect-error - return gl.luma; -} - -export function initializeExtensions(gl: WebGL2RenderingContext): void { - const contextState = getContextData(gl); - // `getSupportedExtensions` can return null when context is lost. - const EXTENSIONS = gl.getSupportedExtensions() || []; - // Generates warnings in Chrome - const IGNORE_EXTENSIONS = ['WEBGL_polygon_mode']; - for (const extensionName of EXTENSIONS) { - if (!IGNORE_EXTENSIONS.includes(extensionName)) { - const extension = gl.getExtension(extensionName); - contextState._extensions[extensionName] = extension; - } - } -} diff --git a/modules/webgl/src/context/debug/webgl-developer-tools.ts b/modules/webgl/src/context/debug/webgl-developer-tools.ts index 51afcc7e9c..61f5d638e3 100644 --- a/modules/webgl/src/context/debug/webgl-developer-tools.ts +++ b/modules/webgl/src/context/debug/webgl-developer-tools.ts @@ -27,7 +27,7 @@ type ContextData = { } // Helper to get shared context data -function getContextData(gl: any): ContextData { +function getWebGLContextData(gl: any): ContextData { gl.luma = gl.luma || {}; return gl.luma; } @@ -65,7 +65,7 @@ export function makeDebugContext(gl: WebGL2RenderingContext, props: DebugContext // Returns the real context from either of the real/debug contexts function getRealContext(gl: WebGL2RenderingContext): WebGL2RenderingContext { - const data = getContextData(gl); + const data = getWebGLContextData(gl); // If the context has a realContext member, it is a debug context so return the realContext return data.realContext ? data.realContext : gl; } @@ -77,7 +77,7 @@ function getDebugContext(gl: WebGL2RenderingContext, props: DebugContextProps): return gl; } - const data = getContextData(gl); + const data = getWebGLContextData(gl); // If this already has a debug context, return it. if (data.debugContext) { diff --git a/modules/webgl/src/context/context/create-browser-context.ts b/modules/webgl/src/context/helpers/create-browser-context.ts similarity index 100% rename from modules/webgl/src/context/context/create-browser-context.ts rename to modules/webgl/src/context/helpers/create-browser-context.ts diff --git a/modules/webgl/src/context/helpers/webgl-context-data.ts b/modules/webgl/src/context/helpers/webgl-context-data.ts new file mode 100644 index 0000000000..8f86cb2caf --- /dev/null +++ b/modules/webgl/src/context/helpers/webgl-context-data.ts @@ -0,0 +1,30 @@ +// luma.gl, MIT license +// Copyright (c) vis.gl contributors + +/** + * Stores luma.gl specific state associated with a context + */ +export interface WebGLContextData { + _polyfilled: boolean; + _extensions: Record; +} + +/** + * Gets luma.gl specific state from a context + * @returns context state + */ +export function getWebGLContextData(gl: WebGL2RenderingContext): WebGLContextData { + // @ts-expect-error + const luma = gl.luma as WebGLContextData | null; + if (!luma) { + const contextState: WebGLContextData = { + _polyfilled: false, + _extensions: {} + }; + // @ts-expect-error + gl.luma = contextState; + } + + // @ts-expect-error + return gl.luma; +} diff --git a/modules/webgl/src/context/helpers/webgl-extensions.ts b/modules/webgl/src/context/helpers/webgl-extensions.ts new file mode 100644 index 0000000000..4170360c07 --- /dev/null +++ b/modules/webgl/src/context/helpers/webgl-extensions.ts @@ -0,0 +1,15 @@ +// luma.gl, MIT license +// Copyright (c) vis.gl contributors + +import {GLExtensions} from '@luma.gl/constants'; + +/** Ensure extensions are only requested once */ +export function getWebGLExtension( + gl: WebGL2RenderingContext, + name: string, + extensions: GLExtensions +): void { + if (extensions[name] === undefined) { + extensions[name] = gl.getExtension(name) || null; + } +} diff --git a/modules/webgpu/src/adapter/webgpu-device.ts b/modules/webgpu/src/adapter/webgpu-device.ts index fffca1e639..620fd8e49e 100644 --- a/modules/webgpu/src/adapter/webgpu-device.ts +++ b/modules/webgpu/src/adapter/webgpu-device.ts @@ -24,7 +24,7 @@ import type { TransformFeedback, TransformFeedbackProps } from '@luma.gl/core'; -import {Device, CanvasContext, log, uid} from '@luma.gl/core'; +import {Device, DeviceFeatures, CanvasContext, log, uid} from '@luma.gl/core'; import {WebGPUBuffer} from './resources/webgpu-buffer'; import {WebGPUTexture} from './resources/webgpu-texture'; import {WebGPUExternalTexture} from './resources/webgpu-external-texture'; @@ -43,18 +43,21 @@ import {WebGPUCanvasContext} from './webgpu-canvas-context'; /** WebGPU Device implementation */ export class WebGPUDevice extends Device { + static type: string = 'webgpu'; + + /** The underlying WebGPU device */ readonly handle: GPUDevice; + /* The underlying WebGPU adapter */ readonly adapter: GPUAdapter; + readonly lost: Promise<{reason: 'destroyed'; message: string}>; + readonly features: DeviceFeatures; canvasContext: WebGPUCanvasContext | null = null; - commandEncoder: GPUCommandEncoder | null = null; - renderPass: WebGPURenderPass | null = null; - private _info: DeviceInfo; private _isLost: boolean = false; - - static type: string = 'webgpu'; + commandEncoder: GPUCommandEncoder | null = null; + renderPass: WebGPURenderPass | null = null; /** Check if WebGPU is available */ static isSupported(): boolean { @@ -162,8 +165,6 @@ export class WebGPUDevice extends Device { return this._info; } - features: Set; - get limits(): DeviceLimits { return this.handle.limits; } @@ -277,10 +278,9 @@ export class WebGPUDevice extends Device { // this.renderPass = null; } - _getFeatures() { + _getFeatures(): DeviceFeatures { // Initialize with actual WebGPU Features (note that unknown features may not be in DeviceFeature type) const features = new Set(this.handle.features as Set); - // Fixups for pre-standard names: https://github.com/webgpu-native/webgpu-headers/issues/133 // @ts-expect-error Chrome Canary v99 if (features.has('depth-clamping')) { @@ -308,7 +308,7 @@ export class WebGPUDevice extends Device { features.add(feature); } - return features; + return new DeviceFeatures(Array.from(features)); } copyExternalImageToTexture(options: {