UNPKG

@tensorflow/tfjs-core

Version:

Hardware-accelerated JavaScript library for machine intelligence

699 lines (609 loc) 23.9 kB
/** * @license * Copyright 2017 Google Inc. All Rights Reserved. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ============================================================================= */ import {env} from '../../environment'; import * as util from '../../util'; import {getWebGLContext} from './canvas_util'; import {getTextureConfig} from './tex_util'; export function callAndCheck<T>( gl: WebGLRenderingContext, debugMode: boolean, func: () => T): T { const returnValue = func(); if (debugMode) { checkWebGLError(gl); } return returnValue; } function checkWebGLError(gl: WebGLRenderingContext) { const error = gl.getError(); if (error !== gl.NO_ERROR) { throw new Error('WebGL Error: ' + getWebGLErrorMessage(gl, error)); } } // https://en.wikipedia.org/wiki/Half-precision_floating-point_format const MIN_FLOAT16 = 5.96e-8; const MAX_FLOAT16 = 65504; export function canBeRepresented(num: number): boolean { if (env().getBool('WEBGL_RENDER_FLOAT32_ENABLED') || num === 0 || (MIN_FLOAT16 < Math.abs(num) && Math.abs(num) < MAX_FLOAT16)) { return true; } return false; } export function getWebGLErrorMessage( gl: WebGLRenderingContext, status: number): string { switch (status) { case gl.NO_ERROR: return 'NO_ERROR'; case gl.INVALID_ENUM: return 'INVALID_ENUM'; case gl.INVALID_VALUE: return 'INVALID_VALUE'; case gl.INVALID_OPERATION: return 'INVALID_OPERATION'; case gl.INVALID_FRAMEBUFFER_OPERATION: return 'INVALID_FRAMEBUFFER_OPERATION'; case gl.OUT_OF_MEMORY: return 'OUT_OF_MEMORY'; case gl.CONTEXT_LOST_WEBGL: return 'CONTEXT_LOST_WEBGL'; default: return `Unknown error code ${status}`; } } export function getExtensionOrThrow( gl: WebGLRenderingContext, debug: boolean, extensionName: string): {} { return throwIfNull<{}>( gl, debug, () => gl.getExtension(extensionName), 'Extension "' + extensionName + '" not supported on this browser.'); } export function createVertexShader( gl: WebGLRenderingContext, debug: boolean, vertexShaderSource: string): WebGLShader { const vertexShader: WebGLShader = throwIfNull<WebGLShader>( gl, debug, () => gl.createShader(gl.VERTEX_SHADER), 'Unable to create vertex WebGLShader.'); callAndCheck( gl, debug, () => gl.shaderSource(vertexShader, vertexShaderSource)); callAndCheck(gl, debug, () => gl.compileShader(vertexShader)); if (gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS) === false) { console.log(gl.getShaderInfoLog(vertexShader)); throw new Error('Failed to compile vertex shader.'); } return vertexShader; } export function createFragmentShader( gl: WebGLRenderingContext, debug: boolean, fragmentShaderSource: string): WebGLShader { const fragmentShader: WebGLShader = throwIfNull<WebGLShader>( gl, debug, () => gl.createShader(gl.FRAGMENT_SHADER), 'Unable to create fragment WebGLShader.'); callAndCheck( gl, debug, () => gl.shaderSource(fragmentShader, fragmentShaderSource)); callAndCheck(gl, debug, () => gl.compileShader(fragmentShader)); if (gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS) === false) { logShaderSourceAndInfoLog( fragmentShaderSource, gl.getShaderInfoLog(fragmentShader)); throw new Error('Failed to compile fragment shader.'); } return fragmentShader; } const lineNumberRegex = /ERROR: [0-9]+:([0-9]+):/g; function logShaderSourceAndInfoLog( shaderSource: string, shaderInfoLog: string) { const lineNumberRegexResult = lineNumberRegex.exec(shaderInfoLog); if (lineNumberRegexResult == null) { console.log(`Couldn't parse line number in error: ${shaderInfoLog}`); console.log(shaderSource); return; } const lineNumber = +lineNumberRegexResult[1]; const shaderLines = shaderSource.split('\n'); const pad = shaderLines.length.toString().length + 2; const linesWithLineNumbers = shaderLines.map( (line, lineNumber) => util.rightPad((lineNumber + 1).toString(), pad) + line); let maxLineLength = 0; for (let i = 0; i < linesWithLineNumbers.length; i++) { maxLineLength = Math.max(linesWithLineNumbers[i].length, maxLineLength); } const beforeErrorLines = linesWithLineNumbers.slice(0, lineNumber - 1); const errorLine = linesWithLineNumbers.slice(lineNumber - 1, lineNumber); const afterErrorLines = linesWithLineNumbers.slice(lineNumber); console.log(beforeErrorLines.join('\n')); console.log(shaderInfoLog.split('\n')[0]); console.log( `%c ${util.rightPad(errorLine[0], maxLineLength)}`, 'border:1px solid red; background-color:#e3d2d2; color:#a61717'); console.log(afterErrorLines.join('\n')); } export function createProgram( gl: WebGLRenderingContext, debug: boolean): WebGLProgram { return throwIfNull<WebGLProgram>( gl, debug, () => gl.createProgram(), 'Unable to create WebGLProgram.'); } export function linkProgram( gl: WebGLRenderingContext, debug: boolean, program: WebGLProgram) { callAndCheck(gl, debug, () => gl.linkProgram(program)); if (gl.getProgramParameter(program, gl.LINK_STATUS) === false) { console.log(gl.getProgramInfoLog(program)); throw new Error('Failed to link vertex and fragment shaders.'); } } export function validateProgram( gl: WebGLRenderingContext, debug: boolean, program: WebGLProgram) { callAndCheck(gl, debug, () => gl.validateProgram(program)); if (gl.getProgramParameter(program, gl.VALIDATE_STATUS) === false) { console.log(gl.getProgramInfoLog(program)); throw new Error('Shader program validation failed.'); } } export function createStaticVertexBuffer( gl: WebGLRenderingContext, debug: boolean, data: Float32Array): WebGLBuffer { const buffer: WebGLBuffer = throwIfNull<WebGLBuffer>( gl, debug, () => gl.createBuffer(), 'Unable to create WebGLBuffer'); callAndCheck(gl, debug, () => gl.bindBuffer(gl.ARRAY_BUFFER, buffer)); callAndCheck( gl, debug, () => gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW)); return buffer; } export function createStaticIndexBuffer( gl: WebGLRenderingContext, debug: boolean, data: Uint16Array): WebGLBuffer { const buffer: WebGLBuffer = throwIfNull<WebGLBuffer>( gl, debug, () => gl.createBuffer(), 'Unable to create WebGLBuffer'); callAndCheck(gl, debug, () => gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer)); callAndCheck( gl, debug, () => gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, data, gl.STATIC_DRAW)); return buffer; } export function getNumChannels(): number { if (env().getNumber('WEBGL_VERSION') === 2) { return 1; } return 4; } export function createTexture( gl: WebGLRenderingContext, debug: boolean): WebGLTexture { return throwIfNull<WebGLTexture>( gl, debug, () => gl.createTexture(), 'Unable to create WebGLTexture.'); } export function validateTextureSize(width: number, height: number) { const maxTextureSize = env().getNumber('WEBGL_MAX_TEXTURE_SIZE'); if ((width <= 0) || (height <= 0)) { const requested = `[${width}x${height}]`; throw new Error('Requested texture size ' + requested + ' is invalid.'); } if ((width > maxTextureSize) || (height > maxTextureSize)) { const requested = `[${width}x${height}]`; const max = `[${maxTextureSize}x${maxTextureSize}]`; throw new Error( 'Requested texture size ' + requested + ' greater than WebGL maximum on this browser / GPU ' + max + '.'); } } export function createFramebuffer( gl: WebGLRenderingContext, debug: boolean): WebGLFramebuffer { return throwIfNull<WebGLFramebuffer>( gl, debug, () => gl.createFramebuffer(), 'Unable to create WebGLFramebuffer.'); } export function bindVertexBufferToProgramAttribute( gl: WebGLRenderingContext, debug: boolean, program: WebGLProgram, attribute: string, buffer: WebGLBuffer, arrayEntriesPerItem: number, itemStrideInBytes: number, itemOffsetInBytes: number): boolean { const loc = gl.getAttribLocation(program, attribute); if (loc === -1) { // The GPU compiler decided to strip out this attribute because it's unused, // thus no need to bind. return false; } callAndCheck(gl, debug, () => gl.bindBuffer(gl.ARRAY_BUFFER, buffer)); callAndCheck( gl, debug, () => gl.vertexAttribPointer( loc, arrayEntriesPerItem, gl.FLOAT, false, itemStrideInBytes, itemOffsetInBytes)); callAndCheck(gl, debug, () => gl.enableVertexAttribArray(loc)); return true; } export function bindTextureUnit( gl: WebGLRenderingContext, debug: boolean, texture: WebGLTexture, textureUnit: number) { validateTextureUnit(gl, textureUnit); callAndCheck(gl, debug, () => gl.activeTexture(gl.TEXTURE0 + textureUnit)); callAndCheck(gl, debug, () => gl.bindTexture(gl.TEXTURE_2D, texture)); } export function unbindTextureUnit( gl: WebGLRenderingContext, debug: boolean, textureUnit: number) { validateTextureUnit(gl, textureUnit); callAndCheck(gl, debug, () => gl.activeTexture(gl.TEXTURE0 + textureUnit)); callAndCheck(gl, debug, () => gl.bindTexture(gl.TEXTURE_2D, null)); } export function getProgramUniformLocationOrThrow( gl: WebGLRenderingContext, debug: boolean, program: WebGLProgram, uniformName: string): WebGLUniformLocation { return throwIfNull<WebGLUniformLocation>( gl, debug, () => gl.getUniformLocation(program, uniformName), 'uniform "' + uniformName + '" not present in program.'); } export function getProgramUniformLocation( gl: WebGLRenderingContext, program: WebGLProgram, uniformName: string): WebGLUniformLocation { return gl.getUniformLocation(program, uniformName); } export function bindTextureToProgramUniformSampler( gl: WebGLRenderingContext, debug: boolean, program: WebGLProgram, texture: WebGLTexture, uniformSamplerLocation: WebGLUniformLocation, textureUnit: number) { callAndCheck( gl, debug, () => bindTextureUnit(gl, debug, texture, textureUnit)); callAndCheck( gl, debug, () => gl.uniform1i(uniformSamplerLocation, textureUnit)); } export function bindCanvasToFramebuffer( gl: WebGLRenderingContext, debug: boolean) { callAndCheck(gl, debug, () => gl.bindFramebuffer(gl.FRAMEBUFFER, null)); callAndCheck( gl, debug, () => gl.viewport(0, 0, gl.canvas.width, gl.canvas.height)); callAndCheck( gl, debug, () => gl.scissor(0, 0, gl.canvas.width, gl.canvas.height)); } export function bindColorTextureToFramebuffer( gl: WebGLRenderingContext, debug: boolean, texture: WebGLTexture, framebuffer: WebGLFramebuffer) { callAndCheck( gl, debug, () => gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer)); callAndCheck( gl, debug, () => gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0)); } export function unbindColorTextureFromFramebuffer( gl: WebGLRenderingContext, debug: boolean, framebuffer: WebGLFramebuffer) { callAndCheck( gl, debug, () => gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer)); callAndCheck( gl, debug, () => gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, null, 0)); } export function validateFramebuffer(gl: WebGLRenderingContext) { const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); if (status !== gl.FRAMEBUFFER_COMPLETE) { throw new Error( 'Error binding framebuffer: ' + getFramebufferErrorMessage(gl, status)); } } export function getFramebufferErrorMessage( gl: WebGLRenderingContext, status: number): string { switch (status) { case gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT: return 'FRAMEBUFFER_INCOMPLETE_ATTACHMENT'; case gl.FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: return 'FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT'; case gl.FRAMEBUFFER_INCOMPLETE_DIMENSIONS: return 'FRAMEBUFFER_INCOMPLETE_DIMENSIONS'; case gl.FRAMEBUFFER_UNSUPPORTED: return 'FRAMEBUFFER_UNSUPPORTED'; default: return `unknown error ${status}`; } } function throwIfNull<T>( gl: WebGLRenderingContext, debug: boolean, returnTOrNull: () => T | null, failureMessage: string): T { const tOrNull: T|null = callAndCheck(gl, debug, () => returnTOrNull()); if (tOrNull == null) { throw new Error(failureMessage); } return tOrNull; } function validateTextureUnit(gl: WebGLRenderingContext, textureUnit: number) { const maxTextureUnit = gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS - 1; const glTextureUnit = textureUnit + gl.TEXTURE0; if (glTextureUnit < gl.TEXTURE0 || glTextureUnit > maxTextureUnit) { const textureUnitRange = `[gl.TEXTURE0, gl.TEXTURE${maxTextureUnit}]`; throw new Error(`textureUnit must be in ${textureUnitRange}.`); } } export function getBatchDim(shape: number[], dimsToSkip = 2): number { return util.sizeFromShape(shape.slice(0, shape.length - dimsToSkip)); } export function getRowsCols(shape: number[]): [number, number] { if (shape.length === 0) { throw Error('Cannot get rows and columns of an empty shape array.'); } return [ shape.length > 1 ? shape[shape.length - 2] : 1, shape[shape.length - 1] ]; } export function getShapeAs3D(shape: number[]): [number, number, number] { let shapeAs3D: [number, number, number] = [1, 1, 1]; const isScalar = shape.length === 0 || (shape.length === 1 && shape[0] === 1); if (!isScalar) { shapeAs3D = [getBatchDim(shape), ...getRowsCols(shape)] as [number, number, number]; } return shapeAs3D; } export function getTextureShapeFromLogicalShape( logShape: number[], isPacked = false): [number, number] { let maxTexSize = env().getNumber('WEBGL_MAX_TEXTURE_SIZE'); if (isPacked) { maxTexSize = maxTexSize * 2; // This logic ensures we accurately count the number of packed texels needed // to accommodate the tensor. We can only pack values in the same texel if // they are from adjacent pairs of rows/cols within the same batch. So if a // tensor has 3 rows, we pretend it has 4 rows in order to account for the // fact that the texels containing the third row are half empty. logShape = logShape.map( (d, i) => i >= logShape.length - 2 ? util.nearestLargerEven(logShape[i]) : logShape[i]); // Packed texture height is at least 2 (the channel height of a single // texel). if (logShape.length === 1) { logShape = [2, logShape[0]]; } } // If logical shape is 2, we don't squeeze, since we want to match physical. if (logShape.length !== 2) { const squeezeResult = util.squeezeShape(logShape); logShape = squeezeResult.newShape; } let size = util.sizeFromShape(logShape); if (logShape.length <= 1 && size <= maxTexSize) { return [1, size]; } else if ( logShape.length === 2 && logShape[0] <= maxTexSize && logShape[1] <= maxTexSize) { return logShape as [number, number]; } else if ( logShape.length === 3 && logShape[0] * logShape[1] <= maxTexSize && logShape[2] <= maxTexSize) { return [logShape[0] * logShape[1], logShape[2]]; } else if ( logShape.length === 3 && logShape[0] <= maxTexSize && logShape[1] * logShape[2] <= maxTexSize) { return [logShape[0], logShape[1] * logShape[2]]; } else if ( logShape.length === 4 && logShape[0] * logShape[1] * logShape[2] <= maxTexSize && logShape[3] <= maxTexSize) { return [logShape[0] * logShape[1] * logShape[2], logShape[3]]; } else if ( logShape.length === 4 && logShape[0] <= maxTexSize && logShape[1] * logShape[2] * logShape[3] <= maxTexSize) { return [logShape[0], logShape[1] * logShape[2] * logShape[3]]; } else { if (isPacked) { // For packed textures size equals the number of channels required to // accommodate the texture data. However in order to squarify such that // inner dimensions stay even, we rewrite size to equal the number of // texels. Then in the return statement we rehydrate the squarified // dimensions to channel units. const batchDim = getBatchDim(logShape); let rows = 2, cols = 2; if (logShape.length) { [rows, cols] = getRowsCols(logShape); } size = batchDim * (rows / 2) * (cols / 2); return util.sizeToSquarishShape(size).map(d => d * 2) as [number, number]; } return util.sizeToSquarishShape(size); } } function isEven(n: number): boolean { return n % 2 === 0; } /** * This determines whether reshaping a packed texture requires rearranging * the data within the texture, assuming 2x2 packing. */ export function isReshapeFree(shape1: number[], shape2: number[]): boolean { shape1 = shape1.slice(-2); shape2 = shape2.slice(-2); if (util.arraysEqual(shape1, shape2)) { return true; } if (!shape1.length || !shape2.length) { // One of the shapes is a scalar. return true; } if (shape1[0] === 0 || shape1[1] === 0 || shape2[0] === 0 || shape2[1] === 0) { return true; } if (shape1.length !== shape2.length) { // One of the shapes is a vector. const shape1Cols = shape1.slice(-1)[0]; const shape2Cols = shape2.slice(-1)[0]; if (shape1Cols === shape2Cols) { return true; } if (isEven(shape1Cols) && isEven(shape2Cols) && (shape1[0] === 1 || shape2[0] === 1)) { return true; } } return shape1[1] === shape2[1] && isEven(shape1[0]) && isEven(shape2[0]); } // We cache webgl params because the environment gets reset between // unit tests and we don't want to constantly query the WebGLContext for // MAX_TEXTURE_SIZE. let MAX_TEXTURE_SIZE: number; let MAX_TEXTURES_IN_SHADER: number; export function getWebGLMaxTextureSize(webGLVersion: number): number { if (MAX_TEXTURE_SIZE == null) { const gl = getWebGLContext(webGLVersion); MAX_TEXTURE_SIZE = gl.getParameter(gl.MAX_TEXTURE_SIZE); } return MAX_TEXTURE_SIZE; } export function resetMaxTextureSize() { MAX_TEXTURE_SIZE = null; } export function resetMaxTexturesInShader() { MAX_TEXTURES_IN_SHADER = null; } export function getMaxTexturesInShader(webGLVersion: number): number { if (MAX_TEXTURES_IN_SHADER == null) { const gl = getWebGLContext(webGLVersion); MAX_TEXTURES_IN_SHADER = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS); } // We cap at 16 to avoid spurious runtime "memory exhausted" error. return Math.min(16, MAX_TEXTURES_IN_SHADER); } export function getWebGLDisjointQueryTimerVersion(webGLVersion: number): number { if (webGLVersion === 0) { return 0; } let queryTimerVersion: number; const gl = getWebGLContext(webGLVersion); if (hasExtension(gl, 'EXT_disjoint_timer_query_webgl2') && webGLVersion === 2) { queryTimerVersion = 2; } else if (hasExtension(gl, 'EXT_disjoint_timer_query')) { queryTimerVersion = 1; } else { queryTimerVersion = 0; } return queryTimerVersion; } export function hasExtension(gl: WebGLRenderingContext, extensionName: string) { const ext = gl.getExtension(extensionName); return ext != null; } export function isWebGLVersionEnabled(webGLVersion: 1|2) { try { const gl = getWebGLContext(webGLVersion); if (gl != null) { return true; } } catch (e) { return false; } return false; } export function isCapableOfRenderingToFloatTexture(webGLVersion: number): boolean { if (webGLVersion === 0) { return false; } const gl = getWebGLContext(webGLVersion); if (webGLVersion === 1) { if (!hasExtension(gl, 'OES_texture_float')) { return false; } } else { if (!hasExtension(gl, 'EXT_color_buffer_float')) { return false; } } const isFrameBufferComplete = createFloatTextureAndBindToFramebuffer(gl); return isFrameBufferComplete; } /** * Check if we can download values from a float/half-float texture. * * Note that for performance reasons we use binding a texture to a framebuffer * as a proxy for ability to download float values later using readPixels. The * texture params of this texture will not match those in readPixels exactly * but if we are unable to bind some kind of float texture to the frameBuffer * then we definitely will not be able to read float values from it. */ export function isDownloadFloatTextureEnabled(webGLVersion: number): boolean { if (webGLVersion === 0) { return false; } const gl = getWebGLContext(webGLVersion); if (webGLVersion === 1) { if (!hasExtension(gl, 'OES_texture_float')) { return false; } if (!hasExtension(gl, 'WEBGL_color_buffer_float')) { return false; } } else { if (hasExtension(gl, 'EXT_color_buffer_float')) { return createFloatTextureAndBindToFramebuffer(gl); } const COLOR_BUFFER_HALF_FLOAT = 'EXT_color_buffer_half_float'; if (hasExtension(gl, COLOR_BUFFER_HALF_FLOAT)) { const textureHalfFloatExtension = gl.getExtension(COLOR_BUFFER_HALF_FLOAT); return createHalfFloatTextureAndBindToFramebuffer( gl, textureHalfFloatExtension); } return false; } const isFrameBufferComplete = createFloatTextureAndBindToFramebuffer(gl); return isFrameBufferComplete; } function createFloatTextureAndBindToFramebuffer(gl: WebGLRenderingContext): boolean { const texConfig = getTextureConfig(gl); const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); const width = 1; const height = 1; gl.texImage2D( gl.TEXTURE_2D, 0, texConfig.internalFormatFloat, width, height, 0, texConfig.textureFormatFloat, texConfig.textureTypeFloat, null); const frameBuffer = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer); gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); const isFrameBufferComplete = gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE; gl.bindTexture(gl.TEXTURE_2D, null); gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.deleteTexture(texture); gl.deleteFramebuffer(frameBuffer); return isFrameBufferComplete; } function createHalfFloatTextureAndBindToFramebuffer( // tslint:disable-next-line:no-any gl: WebGLRenderingContext, textureHalfFloatExtension: any): boolean { const texConfig = getTextureConfig(gl, textureHalfFloatExtension); const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); const width = 1; const height = 1; gl.texImage2D( gl.TEXTURE_2D, 0, texConfig.internalFormatHalfFloat, width, height, 0, texConfig.textureFormatFloat, texConfig.textureTypeHalfFloat, null); const frameBuffer = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer); gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); const isFrameBufferComplete = gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE; gl.bindTexture(gl.TEXTURE_2D, null); gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.deleteTexture(texture); gl.deleteFramebuffer(frameBuffer); return isFrameBufferComplete; } export function isWebGLFenceEnabled(webGLVersion: number) { if (webGLVersion !== 2) { return false; } const gl = getWebGLContext(webGLVersion); // tslint:disable-next-line:no-any const isEnabled = (gl as any).fenceSync != null; return isEnabled; }