UNPKG

@itwin/webgl-compatibility

Version:

APIs for determining the level of compatibility of a browser+device with the iTwin.js rendering system.

337 lines • 19.4 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ /** @packageDocumentation * @module Compatibility */ import { ProcessDetector } from "@itwin/core-bentley"; import { WebGLFeature, WebGLRenderCompatibilityStatus, } from "./RenderCompatibility"; const knownExtensions = [ "WEBGL_draw_buffers", "OES_element_index_uint", "OES_texture_float", "OES_texture_float_linear", "OES_texture_half_float", "OES_texture_half_float_linear", "EXT_texture_filter_anisotropic", "WEBGL_depth_texture", "EXT_color_buffer_float", "EXT_shader_texture_lod", "EXT_frag_depth", "ANGLE_instanced_arrays", "OES_vertex_array_object", "WEBGL_lose_context", "EXT_disjoint_timer_query", "EXT_disjoint_timer_query_webgl2", "OES_standard_derivatives", "EXT_float_blend", ]; /** Describes the type of a render target. Used by Capabilities to represent maximum precision render target available on host system. * @internal */ export var RenderType; (function (RenderType) { RenderType[RenderType["TextureUnsignedByte"] = 0] = "TextureUnsignedByte"; RenderType[RenderType["TextureHalfFloat"] = 1] = "TextureHalfFloat"; RenderType[RenderType["TextureFloat"] = 2] = "TextureFloat"; })(RenderType || (RenderType = {})); /** * Describes the type of a depth buffer. Used by Capabilities to represent maximum depth buffer precision available on host system. * Note: the commented-out values are unimplemented but left in place for reference, in case desired for future implementation. * @internal */ export var DepthType; (function (DepthType) { DepthType[DepthType["RenderBufferUnsignedShort16"] = 0] = "RenderBufferUnsignedShort16"; // TextureUnsignedShort16, // core to WebGL2; available to WebGL1 via WEBGL_depth_texture // TextureUnsignedInt24, // core to WebGL2 DepthType[DepthType["TextureUnsignedInt24Stencil8"] = 1] = "TextureUnsignedInt24Stencil8"; DepthType[DepthType["TextureUnsignedInt32"] = 2] = "TextureUnsignedInt32"; // TextureFloat32, // core to WebGL2 // TextureFloat32Stencil8, // core to WeBGL2 })(DepthType || (DepthType = {})); const maxTexSizeAllowed = 4096; // many devices and browsers have issues with source textures larger than this // Regexes to match Intel UHD/HD 620/630 integrated GPUS that suffer from GraphicsDriverBugs.fragDepthDoesNotDisableEarlyZ. const buggyIntelMatchers = [ // Original unmasked renderer string when workaround we implemented. /ANGLE \(Intel\(R\) (U)?HD Graphics 6(2|3)0 Direct3D11/, // New unmasked renderer string circa October 2021. /ANGLE \(Intel, Intel\(R\) (U)?HD Graphics 6(2|3)0 Direct3D11/, ]; // Regexes to match Mali GPUs known to suffer from GraphicsDriverBugs.msaaWillHang. const buggyMaliMatchers = [ /Mali-G71/, /Mali-G72/, /Mali-G76/, ]; // Regexes to match as many Intel integrated GPUs as possible. // https://en.wikipedia.org/wiki/List_of_Intel_graphics_processing_units const integratedIntelGpuMatchers = [ /(U)?HD Graphics/, /Iris/, ]; function isIntegratedGraphics(args) { if (args.unmaskedRenderer && args.unmaskedRenderer.includes("Intel") && integratedIntelGpuMatchers.some((x) => x.test(args.unmaskedRenderer))) return true; // NB: For now, we do not attempt to detect AMD integrated graphics. // It appears that AMD integrated graphics are not usually paired with a graphics card so detecting integrated usage there is less important than Intel. return false; } /** Describes the rendering capabilities of the host system. * @internal */ export class Capabilities { _maxRenderType = RenderType.TextureUnsignedByte; _maxDepthType = DepthType.RenderBufferUnsignedShort16; _maxTextureSize = 0; _maxColorAttachments = 0; _maxDrawBuffers = 0; _maxFragTextureUnits = 0; _maxVertTextureUnits = 0; _maxVertAttribs = 0; _maxVertUniformVectors = 0; _maxVaryingVectors = 0; _maxFragUniformVectors = 0; _maxAnisotropy; _maxAntialiasSamples = 1; _supportsCreateImageBitmap = false; _maxTexSizeAllow = maxTexSizeAllowed; _extensionMap = {}; // Use this map to store actual extension objects retrieved from GL. _presentFeatures = []; // List of features the system can support (not necessarily dependent on extensions) _isWebGL2 = false; _isMobile = false; _driverBugs = {}; get maxRenderType() { return this._maxRenderType; } get maxDepthType() { return this._maxDepthType; } get maxTextureSize() { return this._maxTextureSize; } get maxTexSizeAllow() { return this._maxTexSizeAllow; } get supportsCreateImageBitmap() { return this._supportsCreateImageBitmap; } get maxColorAttachments() { return this._maxColorAttachments; } get maxDrawBuffers() { return this._maxDrawBuffers; } get maxFragTextureUnits() { return this._maxFragTextureUnits; } get maxVertTextureUnits() { return this._maxVertTextureUnits; } get maxVertAttribs() { return this._maxVertAttribs; } get maxVertUniformVectors() { return this._maxVertUniformVectors; } get maxVaryingVectors() { return this._maxVaryingVectors; } get maxFragUniformVectors() { return this._maxFragUniformVectors; } get maxAntialiasSamples() { return this._maxAntialiasSamples; } get isWebGL2() { return this._isWebGL2; } get driverBugs() { return this._driverBugs; } /** These getters check for existence of extension objects to determine availability of features. In WebGL2, could just return true for some. */ get supportsNonPowerOf2Textures() { return false; } get supportsDrawBuffers() { return this._isWebGL2 || this.queryExtensionObject("WEBGL_draw_buffers") !== undefined; } get supportsInstancing() { return this._isWebGL2 || this.queryExtensionObject("ANGLE_instanced_arrays") !== undefined; } get supports32BitElementIndex() { return this._isWebGL2 || this.queryExtensionObject("OES_element_index_uint") !== undefined; } get supportsTextureFloat() { return this._isWebGL2 || this.queryExtensionObject("OES_texture_float") !== undefined; } get supportsTextureFloatLinear() { return this._isWebGL2 || this.queryExtensionObject("OES_texture_float_linear") !== undefined; } get supportsTextureHalfFloat() { return this._isWebGL2 || this.queryExtensionObject("OES_texture_half_float") !== undefined; } get supportsTextureHalfFloatLinear() { return this._isWebGL2 || this.queryExtensionObject("OES_texture_half_float_linear") !== undefined; } get supportsTextureFilterAnisotropic() { return this.queryExtensionObject("EXT_texture_filter_anisotropic") !== undefined; } get supportsShaderTextureLOD() { return this._isWebGL2 || this.queryExtensionObject("EXT_shader_texture_lod") !== undefined; } get supportsVertexArrayObjects() { return this._isWebGL2 || this.queryExtensionObject("OES_vertex_array_object") !== undefined; } get supportsFragDepth() { return this._isWebGL2 || this.queryExtensionObject("EXT_frag_depth") !== undefined; } get supportsDisjointTimerQuery() { return (this._isWebGL2 && this.queryExtensionObject("EXT_disjoint_timer_query_webgl2") !== undefined) || this.queryExtensionObject("EXT_disjoint_timer_query") !== undefined; } get supportsStandardDerivatives() { return this._isWebGL2 || this.queryExtensionObject("OES_standard_derivatives") !== undefined; } get supportsMRTTransparency() { return this.maxColorAttachments >= 2; } get supportsMRTPickShaders() { return this.maxColorAttachments >= 3; } get supportsShadowMaps() { return this.supportsTextureFloat || this.supportsTextureHalfFloat; } get supportsAntiAliasing() { return this._isWebGL2 && this.maxAntialiasSamples > 1; } get isMobile() { return this._isMobile; } findExtension(name) { const ext = this._extensionMap[name]; return null !== ext ? ext : undefined; } /** Queries an extension object if available. This is necessary for other parts of the system to access some constants within extensions. */ queryExtensionObject(ext) { return this.findExtension(ext); } static optionalFeatures = [ WebGLFeature.MrtTransparency, WebGLFeature.MrtPick, WebGLFeature.DepthTexture, WebGLFeature.FloatRendering, WebGLFeature.Instancing, WebGLFeature.ShadowMaps, WebGLFeature.FragDepth, WebGLFeature.StandardDerivatives, WebGLFeature.AntiAliasing, ]; static requiredFeatures = [ WebGLFeature.UintElementIndex, WebGLFeature.MinimalTextureUnits, ]; get _hasRequiredTextureUnits() { return this.maxFragTextureUnits >= 4 && this.maxVertTextureUnits >= 5; } /** Return an array containing any features not supported by the system as compared to the input array. */ _findMissingFeatures(featuresToSeek) { const missingFeatures = []; for (const featureName of featuresToSeek) { if (-1 === this._presentFeatures.indexOf(featureName)) missingFeatures.push(featureName); } return missingFeatures; } /** Populate and return an array containing features that this system supports. */ _gatherFeatures() { const features = []; // simply check for presence of various extensions if that gives enough information if (this._isWebGL2 || this._extensionMap["OES_element_index_uint"] !== undefined) features.push(WebGLFeature.UintElementIndex); if (this._isWebGL2 || this._extensionMap["ANGLE_instanced_arrays"] !== undefined) features.push(WebGLFeature.Instancing); if (this.supportsMRTTransparency) features.push(WebGLFeature.MrtTransparency); if (this.supportsMRTPickShaders) features.push(WebGLFeature.MrtPick); if (this.supportsShadowMaps) features.push(WebGLFeature.ShadowMaps); if (this._hasRequiredTextureUnits) features.push(WebGLFeature.MinimalTextureUnits); if (this.supportsFragDepth) features.push(WebGLFeature.FragDepth); if (this.supportsStandardDerivatives) features.push(WebGLFeature.StandardDerivatives); if (this.supportsAntiAliasing) features.push(WebGLFeature.AntiAliasing); if (DepthType.TextureUnsignedInt24Stencil8 === this._maxDepthType) features.push(WebGLFeature.DepthTexture); // check if at least half-float rendering is available based on maximum discovered renderable target if (RenderType.TextureUnsignedByte !== this._maxRenderType) features.push(WebGLFeature.FloatRendering); return features; } /** Retrieve compatibility status based on presence of various features. */ _getCompatibilityStatus(missingRequiredFeatures, missingOptionalFeatures) { let status = WebGLRenderCompatibilityStatus.AllOkay; if (missingOptionalFeatures.length > 0) status = WebGLRenderCompatibilityStatus.MissingOptionalFeatures; if (missingRequiredFeatures.length > 0) status = WebGLRenderCompatibilityStatus.MissingRequiredFeatures; return status; } /** Initializes the capabilities based on a GL context. Must be called first. */ init(gl, disabledExtensions) { const gl2 = !(gl instanceof WebGLRenderingContext) ? gl : undefined; this._isWebGL2 = undefined !== gl2; this._isMobile = ProcessDetector.isMobileBrowser; const debugInfo = gl.getExtension("WEBGL_debug_renderer_info"); const unmaskedRenderer = debugInfo !== null ? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) : undefined; const unmaskedVendor = debugInfo !== null ? gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) : undefined; this._driverBugs = {}; if (unmaskedRenderer && buggyIntelMatchers.some((x) => x.test(unmaskedRenderer))) this._driverBugs.fragDepthDoesNotDisableEarlyZ = true; if (unmaskedRenderer && buggyMaliMatchers.some((x) => x.test(unmaskedRenderer))) this._driverBugs.msaaWillHang = true; this._maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE); this._supportsCreateImageBitmap = typeof createImageBitmap === "function" && ProcessDetector.isChromium && !ProcessDetector.isIOSBrowser; this._maxTexSizeAllow = Math.min(this._maxTextureSize, maxTexSizeAllowed); this._maxFragTextureUnits = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS); this._maxVertTextureUnits = gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS); this._maxVertAttribs = gl.getParameter(gl.MAX_VERTEX_ATTRIBS); this._maxVertUniformVectors = gl.getParameter(gl.MAX_VERTEX_UNIFORM_VECTORS); this._maxVaryingVectors = gl.getParameter(gl.MAX_VARYING_VECTORS); this._maxFragUniformVectors = gl.getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS); this._maxAntialiasSamples = this._driverBugs.msaaWillHang ? 1 : (this._isWebGL2 && undefined !== gl2 ? gl.getParameter(gl2.MAX_SAMPLES) : 1); const extensions = gl.getSupportedExtensions(); // This just retrieves a list of available extensions (not necessarily enabled). if (extensions) { for (const extStr of extensions) { const ext = extStr; if (-1 === knownExtensions.indexOf(ext)) continue; else if (undefined !== disabledExtensions && -1 !== disabledExtensions.indexOf(ext)) continue; const extObj = gl.getExtension(ext); // This call enables the extension and returns a WebGLObject containing extension instance. if (null !== extObj) this._extensionMap[ext] = extObj; } } if (this._isWebGL2 && undefined !== gl2) { this._maxColorAttachments = gl.getParameter(gl2.MAX_COLOR_ATTACHMENTS); this._maxDrawBuffers = gl.getParameter(gl2.MAX_DRAW_BUFFERS); } else { const dbExt = this.queryExtensionObject("WEBGL_draw_buffers"); this._maxColorAttachments = dbExt !== undefined ? gl.getParameter(dbExt.MAX_COLOR_ATTACHMENTS_WEBGL) : 1; this._maxDrawBuffers = dbExt !== undefined ? gl.getParameter(dbExt.MAX_DRAW_BUFFERS_WEBGL) : 1; } // Determine the maximum color-renderable attachment type. const allowFloatRender = (undefined === disabledExtensions || -1 === disabledExtensions.indexOf("OES_texture_float")) // iOS>=15 allows full-float rendering. However, it does not actually work on non-M1 devices. // Because of this, for now we disallow full float rendering on iOS devices. // ###TODO: Re-assess this after future iOS updates. && !ProcessDetector.isIOSBrowser // Samsung Galaxy Note 8 exhibits same issue as described above for iOS >= 15. // It uses specifically Mali-G71 MP20 but reports its renderer as follows. // Samsung Galaxy A50 and S9 exhibits same issue; they use Mali-G72. // HUAWEI P30 exhibits same issue; it uses Mali-G76. && unmaskedRenderer !== "Mali-G71" && unmaskedRenderer !== "Mali-G72" && unmaskedRenderer !== "Mali-G76"; if (allowFloatRender && undefined !== this.queryExtensionObject("EXT_float_blend") && this.isTextureRenderable(gl, gl.FLOAT)) { this._maxRenderType = RenderType.TextureFloat; } else if (this.isWebGL2) { this._maxRenderType = (this.isTextureRenderable(gl, gl.HALF_FLOAT)) ? RenderType.TextureHalfFloat : RenderType.TextureUnsignedByte; } else { const hfExt = this.queryExtensionObject("OES_texture_half_float"); this._maxRenderType = (hfExt !== undefined && this.isTextureRenderable(gl, hfExt.HALF_FLOAT_OES)) ? RenderType.TextureHalfFloat : RenderType.TextureUnsignedByte; } // Determine the maximum depth attachment type. // this._maxDepthType = this.queryExtensionObject("WEBGL_depth_texture") !== undefined ? DepthType.TextureUnsignedInt32 : DepthType.RenderBufferUnsignedShort16; this._maxDepthType = this._isWebGL2 || this.queryExtensionObject("WEBGL_depth_texture") !== undefined ? DepthType.TextureUnsignedInt24Stencil8 : DepthType.RenderBufferUnsignedShort16; this._presentFeatures = this._gatherFeatures(); const missingRequiredFeatures = this._findMissingFeatures(Capabilities.requiredFeatures); const missingOptionalFeatures = this._findMissingFeatures(Capabilities.optionalFeatures); return { status: this._getCompatibilityStatus(missingRequiredFeatures, missingOptionalFeatures), missingRequiredFeatures, missingOptionalFeatures, unmaskedRenderer, unmaskedVendor, usingIntegratedGraphics: isIntegratedGraphics({ unmaskedVendor, unmaskedRenderer }), driverBugs: { ...this._driverBugs }, userAgent: navigator.userAgent, createdContext: gl, }; } static create(gl, disabledExtensions) { const caps = new Capabilities(); const compatibility = caps.init(gl, disabledExtensions); if (WebGLRenderCompatibilityStatus.CannotCreateContext === compatibility.status || WebGLRenderCompatibilityStatus.MissingRequiredFeatures === compatibility.status) return undefined; return caps; } /** Determines if a particular texture type is color-renderable on the host system. */ isTextureRenderable(gl, texType) { const tex = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, tex); if (this.isWebGL2) { if (gl.FLOAT === texType) gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, 1, 1, 0, gl.RGBA, texType, null); else gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, 1, 1, 0, gl.RGBA, texType, null); } else gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, texType, null); const fb = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, fb); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0); const fbStatus = gl.checkFramebufferStatus(gl.FRAMEBUFFER); gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.deleteFramebuffer(fb); gl.deleteTexture(tex); gl.getError(); // clear any errors return fbStatus === gl.FRAMEBUFFER_COMPLETE; } setMaxAnisotropy(desiredMax, gl) { const ext = this.queryExtensionObject("EXT_texture_filter_anisotropic"); if (undefined === ext) return; if (undefined === this._maxAnisotropy) this._maxAnisotropy = gl.getParameter(ext.MAX_TEXTURE_MAX_ANISOTROPY_EXT); const max = (undefined !== desiredMax) ? Math.min(desiredMax, this._maxAnisotropy) : this._maxAnisotropy; gl.texParameterf(gl.TEXTURE_2D, ext.TEXTURE_MAX_ANISOTROPY_EXT, max); } } //# sourceMappingURL=Capabilities.js.map