@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
JavaScript
/*---------------------------------------------------------------------------------------------
* 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