@ 261106.15
current
/**
 * @license
 * Copyright 2010 The Emscripten Authors
 * SPDX-License-Identifier: MIT
 */

{{{
  const isCurrentContextWebGL2 = () => {
    // This function should only be called inside of `#if MAX_WEBGL_VERSION >= 2` blocks
    assert(MAX_WEBGL_VERSION >= 2, 'isCurrentContextWebGL2 called without webgl2 support');
    if (MIN_WEBGL_VERSION >= 2) return 'true';
    return 'GL.currentContext.version >= 2';
  }
}}}

var LibraryGL = {
  // Specifies the size of the GL temp buffer pool, in bytes. Must be a multiple
  // of 9 and 16.
  ...defineInternalConsts({
    $GL_POOL_TEMP_BUFFERS_SIZE: 2 * 9 * 16, // = 288
    $GL_MAX_TEMP_BUFFER_SIZE: GL_MAX_TEMP_BUFFER_SIZE,
    // Maximum number of temp VBOs of one size to maintain, after that we start
    // reusing old ones, which is safe but can give a performance impact. If
    // CPU-GPU stalls are a problem, increasing this might help.
    $GL_NUM_TEMP_VERTEX_BUFFERS_PER_SIZE: 64,
  }),

  // For functions such as glDrawBuffers, glInvalidateFramebuffer and
  // glInvalidateSubFramebuffer that need to pass a short array to the WebGL
  // API, create a set of short fixed-length arrays to avoid having to generate
  // any garbage when calling those functions.
  $tempFixedLengthArray__postset: 'for (let i = 0; i < 32; ++i) tempFixedLengthArray.push(new Array(i));',
  $tempFixedLengthArray: [],

  $miniTempWebGLFloatBuffers: [],
  $miniTempWebGLFloatBuffers__postset: `var miniTempWebGLFloatBuffersStorage = new Float32Array(GL_POOL_TEMP_BUFFERS_SIZE);
// Create GL_POOL_TEMP_BUFFERS_SIZE+1 temporary buffers, for uploads of size 0 through GL_POOL_TEMP_BUFFERS_SIZE inclusive
for (let i = 0; i <= GL_POOL_TEMP_BUFFERS_SIZE; ++i) {
  miniTempWebGLFloatBuffers[i] = miniTempWebGLFloatBuffersStorage.subarray(0, i);
}`,

  $miniTempWebGLIntBuffers: [],
  $miniTempWebGLIntBuffers__postset: `var miniTempWebGLIntBuffersStorage = new Int32Array(GL_POOL_TEMP_BUFFERS_SIZE);
// Create GL_POOL_TEMP_BUFFERS_SIZE+1 temporary buffers, for uploads of size 0 through GL_POOL_TEMP_BUFFERS_SIZE inclusive
for (let i = 0; i <= GL_POOL_TEMP_BUFFERS_SIZE; ++i) {
  miniTempWebGLIntBuffers[i] = miniTempWebGLIntBuffersStorage.subarray(0, i);
}`,

  $heapObjectForWebGLType: (type) => {
    // Micro-optimization for size: Subtract lowest GL enum number (0x1400/* GL_BYTE */) from type to compare
    // smaller values for the heap, for shorter generated code size.
    // Also the type HEAPU16 is not tested for explicitly, but any unrecognized type will return out HEAPU16.
    // (since most types are HEAPU16)
    type -= 0x1400;
#if MAX_WEBGL_VERSION >= 2
    if (type == {{{ 0x1400 - 0x1400/* GL_BYTE */ }}}) return HEAP8;
#endif

    if (type == {{{ 0x1401 - 0x1400/* GL_UNSIGNED_BYTE */ }}}) return HEAPU8;

#if MAX_WEBGL_VERSION >= 2
    if (type == {{{ 0x1402 - 0x1400/* GL_SHORT */ }}}) return HEAP16;
#endif

    if (type == {{{ 0x1404 - 0x1400/* GL_INT */ }}}) return HEAP32;

    if (type == {{{ 0x1406 - 0x1400/* GL_FLOAT */ }}}) return HEAPF32;

    if (type == {{{ 0x1405 - 0x1400 /* GL_UNSIGNED_INT */ }}}
      || type == {{{ 0x84FA - 0x1400 /* GL_UNSIGNED_INT_24_8_WEBGL/GL_UNSIGNED_INT_24_8 */ }}}
#if MAX_WEBGL_VERSION >= 2
      || type == {{{ 0x8368 - 0x1400 /* GL_UNSIGNED_INT_2_10_10_10_REV */ }}}
      || type == {{{ 0x8C3B - 0x1400 /* GL_UNSIGNED_INT_10F_11F_11F_REV */ }}}
      || type == {{{ 0x8C3E - 0x1400 /* GL_UNSIGNED_INT_5_9_9_9_REV */ }}}
#endif
      )
      return HEAPU32;

#if GL_ASSERTIONS
      if (type != {{{ 0x1403 - 0x1400 /* GL_UNSIGNED_SHORT */ }}}
#if MAX_WEBGL_VERSION >= 2
        && type != {{{ 0x140B - 0x1400 /* GL_HALF_FLOAT */ }}}
#endif
        && type != {{{ 0x8033 - 0x1400 /* GL_UNSIGNED_SHORT_4_4_4_4 */ }}}
        && type != {{{ 0x8034 - 0x1400 /* GL_UNSIGNED_SHORT_5_5_5_1 */ }}}
        && type != {{{ 0x8363 - 0x1400 /* GL_UNSIGNED_SHORT_5_6_5 */ }}}
        && type != {{{ 0x8D61 - 0x1400 /* GL_HALF_FLOAT_OES */ }}}) {
        err(`Invalid WebGL type 0x${(type+0x1400).toString()} passed to $heapObjectForWebGLType!`);
      }
#endif
    return HEAPU16;
  },

  $toTypedArrayIndex: (pointer, heap) =>
#if MEMORY64
    pointer / heap.BYTES_PER_ELEMENT,
#else
    pointer >>> (31 - Math.clz32(heap.BYTES_PER_ELEMENT)),
#endif

#if MIN_WEBGL_VERSION == 1
  $webgl_enable_ANGLE_instanced_arrays: (ctx) => {
    // Extension available in WebGL 1 from Firefox 26 and Google Chrome 30 onwards. Core feature in WebGL 2.
    var ext = ctx.getExtension('ANGLE_instanced_arrays');
    // Because this extension is a core function in WebGL 2, assign the extension entry points in place of
    // where the core functions will reside in WebGL 2. This way the calling code can call these without
    // having to dynamically branch depending if running against WebGL 1 or WebGL 2.
    if (ext) {
      ctx['vertexAttribDivisor'] = (index, divisor) => ext['vertexAttribDivisorANGLE'](index, divisor);
      ctx['drawArraysInstanced'] = (mode, first, count, primcount) => ext['drawArraysInstancedANGLE'](mode, first, count, primcount);
      ctx['drawElementsInstanced'] = (mode, count, type, indices, primcount) => ext['drawElementsInstancedANGLE'](mode, count, type, indices, primcount);
      return 1;
    }
  },

  emscripten_webgl_enable_ANGLE_instanced_arrays: (ctx) => webgl_enable_ANGLE_instanced_arrays(GL.contexts[ctx].GLctx),

  $webgl_enable_OES_vertex_array_object: (ctx) => {
    // Extension available in WebGL 1 from Firefox 25 and WebKit 536.28/desktop Safari 6.0.3 onwards. Core feature in WebGL 2.
    var ext = ctx.getExtension('OES_vertex_array_object');
    if (ext) {
      ctx['createVertexArray'] = () => ext['createVertexArrayOES']();
      ctx['deleteVertexArray'] = (vao) => ext['deleteVertexArrayOES'](vao);
      ctx['bindVertexArray'] = (vao) => ext['bindVertexArrayOES'](vao);
      ctx['isVertexArray'] = (vao) => ext['isVertexArrayOES'](vao);
      return 1;
    }
  },

  emscripten_webgl_enable_OES_vertex_array_object: (ctx) => webgl_enable_OES_vertex_array_object(GL.contexts[ctx].GLctx),

  $webgl_enable_WEBGL_draw_buffers: (ctx) => {
    // Extension available in WebGL 1 from Firefox 28 onwards. Core feature in WebGL 2.
    var ext = ctx.getExtension('WEBGL_draw_buffers');
    if (ext) {
      ctx['drawBuffers'] = (n, bufs) => ext['drawBuffersWEBGL'](n, bufs);
      return 1;
    }
  },

  emscripten_webgl_enable_WEBGL_draw_buffers: (ctx) => webgl_enable_WEBGL_draw_buffers(GL.contexts[ctx].GLctx),
#endif

  $webgl_enable_WEBGL_multi_draw: (ctx) =>
    // Closure is expected to be allowed to minify the '.multiDrawWebgl' property, so not accessing it quoted.
    !!(ctx.multiDrawWebgl = ctx.getExtension('WEBGL_multi_draw')),

  emscripten_webgl_enable_WEBGL_multi_draw: (ctx) => webgl_enable_WEBGL_multi_draw(GL.contexts[ctx].GLctx),

  $webgl_enable_EXT_polygon_offset_clamp: (ctx) =>
    !!(ctx.extPolygonOffsetClamp = ctx.getExtension('EXT_polygon_offset_clamp')),

  emscripten_webgl_enable_EXT_polygon_offset_clamp: (ctx) => webgl_enable_EXT_polygon_offset_clamp(GL.contexts[ctx].GLctx),

  $webgl_enable_EXT_clip_control: (ctx) =>
    !!(ctx.extClipControl = ctx.getExtension('EXT_clip_control')),

  emscripten_webgl_enable_EXT_clip_control: (ctx) => webgl_enable_EXT_clip_control(GL.contexts[ctx].GLctx),

  $webgl_enable_WEBGL_polygon_mode: (ctx) =>
    !!(ctx.webglPolygonMode = ctx.getExtension('WEBGL_polygon_mode')),

  emscripten_webgl_enable_WEBGL_polygon_mode: (ctx) => webgl_enable_WEBGL_polygon_mode(GL.contexts[ctx].GLctx),

  $getEmscriptenSupportedExtensions__internal: true,
  $getEmscriptenSupportedExtensions: (ctx) => {
    // Restrict the list of advertised extensions to those that we actually
    // support.
    var supportedExtensions = [
#if MIN_WEBGL_VERSION == 1
      // WebGL 1 extensions
      'ANGLE_instanced_arrays',
      'EXT_blend_minmax',
      'EXT_disjoint_timer_query',
      'EXT_frag_depth',
      'EXT_shader_texture_lod',
      'EXT_sRGB',
      'OES_element_index_uint',
      'OES_fbo_render_mipmap',
      'OES_standard_derivatives',
      'OES_texture_float',
      'OES_texture_half_float',
      'OES_texture_half_float_linear',
      'OES_vertex_array_object',
      'WEBGL_color_buffer_float',
      'WEBGL_depth_texture',
      'WEBGL_draw_buffers',
#endif
#if MAX_WEBGL_VERSION >= 2
      // WebGL 2 extensions
      'EXT_color_buffer_float',
      'EXT_conservative_depth',
      'EXT_disjoint_timer_query_webgl2',
      'EXT_texture_norm16',
      'NV_shader_noperspective_interpolation',
      'WEBGL_clip_cull_distance',
#endif
      // WebGL 1 and WebGL 2 extensions
      'EXT_clip_control',
      'EXT_color_buffer_half_float',
      'EXT_depth_clamp',
      'EXT_float_blend',
      'EXT_polygon_offset_clamp',
      'EXT_texture_compression_bptc',
      'EXT_texture_compression_rgtc',
      'EXT_texture_filter_anisotropic',
      'KHR_parallel_shader_compile',
      'OES_texture_float_linear',
      'WEBGL_blend_func_extended',
      'WEBGL_compressed_texture_astc',
      'WEBGL_compressed_texture_etc',
      'WEBGL_compressed_texture_etc1',
      'WEBGL_compressed_texture_s3tc',
      'WEBGL_compressed_texture_s3tc_srgb',
      'WEBGL_debug_renderer_info',
      'WEBGL_debug_shaders',
      'WEBGL_lose_context',
      'WEBGL_multi_draw',
      'WEBGL_polygon_mode'
    ];
    // .getSupportedExtensions() can return null if context is lost, so coerce to empty array.
    return ctx.getSupportedExtensions()?.filter(ext => supportedExtensions.includes(ext)) ?? [];
  },

  $GLctx__internal: true,
  $GLctx__deps: ['$GL'],
  $GLctx: undefined,
  $GL__deps: [
#if GL_SUPPORT_AUTOMATIC_ENABLE_EXTENSIONS
  // If GL_SUPPORT_AUTOMATIC_ENABLE_EXTENSIONS is enabled, GL.initExtensions() will call to initialize these.
#if PTHREADS
    'malloc', // Needed by registerContext
    'free', // Needed by deleteContext
#endif
#endif // GL_SUPPORT_AUTOMATIC_ENABLE_EXTENSIONS
  ],
#if FULL_ES2 || LEGACY_GL_EMULATION
  $GL__postset: `
    // Signal GL rendering layer that processing of a new frame is about to
    // start. This helps it optimize VBO double-buffering and reduce GPU stalls.
    registerPreMainLoop(() => GL.newRenderingFrameStarted());
  `,
#endif
  $GL: {
#if GL_DEBUG
    debug: true,
#endif

/* We do not depend on the exact initial values of falsey member fields - these
   fields can be populated on-demand to save code size.
   (but still documented here to keep track of what is supposed to be present)
#if GL_TRACK_ERRORS
    lastError: 0,
#endif
    currentContext: null,

#if FULL_ES2 || LEGACY_GL_EMULATION
    currArrayBuffer: 0,
    currElementArrayBuffer: 0,
#endif
*/

    counter: 1, // 0 is reserved as 'null' in gl
    buffers: [],
#if FULL_ES3
    mappedBuffers: {},
#endif
    programs: [],
    framebuffers: [],
    renderbuffers: [],
    textures: [],
    shaders: [],
    vaos: [],
#if PTHREADS // with pthreads a context is a location in memory with some synchronized data between threads
    contexts: {},
#else            // without pthreads, it's just an integer ID
    contexts: [],
#endif
    // DOM ID -> OffscreenCanvas mappings of <canvas> elements that have their
    // rendering control transferred to offscreen.
    offscreenCanvases: {},
    // on WebGL1 stores WebGLTimerQueryEXT, on WebGL2 WebGLQuery
    queries: [],
#if MAX_WEBGL_VERSION >= 2
    samplers: [],
    transformFeedbacks: [],
    syncs: [],
#endif

#if FULL_ES2 || LEGACY_GL_EMULATION
    byteSizeByTypeRoot: 0x1400, // GL_BYTE
    byteSizeByType: [
      1, // GL_BYTE
      1, // GL_UNSIGNED_BYTE
      2, // GL_SHORT
      2, // GL_UNSIGNED_SHORT
      4, // GL_INT
      4, // GL_UNSIGNED_INT
      4, // GL_FLOAT
      2, // GL_2_BYTES
      3, // GL_3_BYTES
      4, // GL_4_BYTES
      8  // GL_DOUBLE
    ],
#endif

    stringCache: {},
#if MAX_WEBGL_VERSION >= 2
    stringiCache: {},
#endif

    unpackAlignment: 4, // default alignment is 4 bytes
    unpackRowLength: 0,

    // Records a GL error condition that occurred, stored until user calls
    // glGetError() to fetch it. As per GLES2 spec, only the first error is
    // remembered, and subsequent errors are discarded until the user has
    // cleared the stored error by a call to glGetError().
    recordError: (errorCode) => {
#if GL_TRACK_ERRORS
      if (!GL.lastError) {
        GL.lastError = errorCode;
      }
#endif
    },
    // Get a new ID for a texture/buffer/etc., while keeping the table dense and
    // fast. Creation is fairly rare so it is worth optimizing lookups later.
    getNewId: (table) => {
      var ret = GL.counter++;
      for (var i = table.length; i < ret; i++) {
        table[i] = null;
      }
#if FULL_ES2
      // Skip over any non-null elements that might have been created by
      // glBindBuffer.
      while (table[ret]) {
        ret = GL.counter++;
      }
#endif
      return ret;
    },

    // The code path for creating textures, buffers, framebuffers and other
    // objects is the same (and not in fast path), so we merge the functions
    // together.
    // 'createFunction' refers to the WebGL context function name to do the actual
    // creation, 'objectTable' points to the GL object table where to populate the
    // created objects, and 'functionName' carries the name of the caller for
    // debug information.
    genObject: (n, buffers, createFunction, objectTable
#if GL_ASSERTIONS
      , functionName
#endif
      ) => {
      for (var i = 0; i < n; i++) {
        var buffer = GLctx[createFunction]();
        var id = buffer && GL.getNewId(objectTable);
        if (buffer) {
          buffer.name = id;
          objectTable[id] = buffer;
        } else {
          GL.recordError(0x502 /* GL_INVALID_OPERATION */);
#if GL_ASSERTIONS
          err(`GL_INVALID_OPERATION in ${functionName}: GLctx.${createFunction} returned null - most likely GL context is lost!`);
#endif
        }
        {{{ makeSetValue('buffers', 'i*4', 'id', 'i32') }}};
      }
    },

#if FULL_ES2 || LEGACY_GL_EMULATION
    // When user GL code wants to render from client-side memory, we need to
    // upload the vertex data to a temp VBO for rendering. Maintain a set of
    // temp VBOs that are created-on-demand to appropriate sizes, and never
    // destroyed.  Also, for best performance the VBOs are double-buffered, i.e.
    // every second frame we switch the set of VBOs we upload to, so that
    // rendering from the previous frame is not disturbed by uploading from new
    // data to it, which could cause a GPU-CPU pipeline stall.
    // Note that index buffers are not double-buffered (at the moment) in this
    // manner.
    // Precompute a lookup table for the function ceil(log2(x)), i.e. how many
    // bits are needed to represent x, or, if x was rounded up to next pow2,
    // which index is the single '1' bit at?
    // Then log2ceilLookup[x] returns ceil(log2(x)).
    log2ceilLookup: (i) => 32 - Math.clz32(i ? i - 1 : 0),

    generateTempBuffers: (quads, context) => {
      var largestIndex = GL.log2ceilLookup(GL_MAX_TEMP_BUFFER_SIZE);
      context.tempVertexBufferCounters1 = [];
      context.tempVertexBufferCounters2 = [];
      context.tempVertexBufferCounters1.length = context.tempVertexBufferCounters2.length = largestIndex+1;
      context.tempVertexBuffers1 = [];
      context.tempVertexBuffers2 = [];
      context.tempVertexBuffers1.length = context.tempVertexBuffers2.length = largestIndex+1;
      context.tempIndexBuffers = [];
      context.tempIndexBuffers.length = largestIndex+1;
      for (var i = 0; i <= largestIndex; ++i) {
        context.tempIndexBuffers[i] = null; // Created on-demand
        context.tempVertexBufferCounters1[i] = context.tempVertexBufferCounters2[i] = 0;
        var ringbufferLength = GL_NUM_TEMP_VERTEX_BUFFERS_PER_SIZE;
        context.tempVertexBuffers1[i] = [];
        context.tempVertexBuffers2[i] = [];
        var ringbuffer1 = context.tempVertexBuffers1[i];
        var ringbuffer2 = context.tempVertexBuffers2[i];
        ringbuffer1.length = ringbuffer2.length = ringbufferLength;
        for (var j = 0; j < ringbufferLength; ++j) {
          ringbuffer1[j] = ringbuffer2[j] = null; // Created on-demand
        }
      }

      if (quads) {
        // GL_QUAD indexes can be precalculated
        context.tempQuadIndexBuffer = GLctx.createBuffer();
        context.GLctx.bindBuffer(0x8893 /*GL_ELEMENT_ARRAY_BUFFER*/, context.tempQuadIndexBuffer);
        var numIndexes = GL_MAX_TEMP_BUFFER_SIZE >> 1;
        var quadIndexes = new Uint16Array(numIndexes);
        var i = 0, v = 0;
        while (1) {
          quadIndexes[i++] = v;
          if (i >= numIndexes) break;
          quadIndexes[i++] = v+1;
          if (i >= numIndexes) break;
          quadIndexes[i++] = v+2;
          if (i >= numIndexes) break;
          quadIndexes[i++] = v;
          if (i >= numIndexes) break;
          quadIndexes[i++] = v+2;
          if (i >= numIndexes) break;
          quadIndexes[i++] = v+3;
          if (i >= numIndexes) break;
          v += 4;
        }
        context.GLctx.bufferData(0x8893 /*GL_ELEMENT_ARRAY_BUFFER*/, quadIndexes, 0x88E4 /*GL_STATIC_DRAW*/);
        context.GLctx.bindBuffer(0x8893 /*GL_ELEMENT_ARRAY_BUFFER*/, null);
      }
    },

    getTempVertexBuffer: (sizeBytes) => {
      var idx = GL.log2ceilLookup(sizeBytes);
      var ringbuffer = GL.currentContext.tempVertexBuffers1[idx];
#if GL_ASSERTIONS
      assert(ringbuffer, `MAX_TEMP_BUFFER_SIZE is not large enough to store a buffer of size ${sizeBytes}`);
#endif
      var nextFreeBufferIndex = GL.currentContext.tempVertexBufferCounters1[idx];
      GL.currentContext.tempVertexBufferCounters1[idx] = (GL.currentContext.tempVertexBufferCounters1[idx]+1) & (GL_NUM_TEMP_VERTEX_BUFFERS_PER_SIZE-1);
      var vbo = ringbuffer[nextFreeBufferIndex];
      if (vbo) {
        return vbo;
      }
      var prevVBO = GLctx.getParameter(0x8894 /*GL_ARRAY_BUFFER_BINDING*/);
      ringbuffer[nextFreeBufferIndex] = GLctx.createBuffer();
      GLctx.bindBuffer(0x8892 /*GL_ARRAY_BUFFER*/, ringbuffer[nextFreeBufferIndex]);
      GLctx.bufferData(0x8892 /*GL_ARRAY_BUFFER*/, 1 << idx, 0x88E8 /*GL_DYNAMIC_DRAW*/);
      GLctx.bindBuffer(0x8892 /*GL_ARRAY_BUFFER*/, prevVBO);
      return ringbuffer[nextFreeBufferIndex];
    },

    getTempIndexBuffer: (sizeBytes) => {
      var idx = GL.log2ceilLookup(sizeBytes);
      var ibo = GL.currentContext.tempIndexBuffers[idx];
      if (ibo) {
        return ibo;
      }
      var prevIBO = GLctx.getParameter(0x8895 /*ELEMENT_ARRAY_BUFFER_BINDING*/);
      GL.currentContext.tempIndexBuffers[idx] = GLctx.createBuffer();
      GLctx.bindBuffer(0x8893 /*GL_ELEMENT_ARRAY_BUFFER*/, GL.currentContext.tempIndexBuffers[idx]);
      GLctx.bufferData(0x8893 /*GL_ELEMENT_ARRAY_BUFFER*/, 1 << idx, 0x88E8 /*GL_DYNAMIC_DRAW*/);
      GLctx.bindBuffer(0x8893 /*GL_ELEMENT_ARRAY_BUFFER*/, prevIBO);
      return GL.currentContext.tempIndexBuffers[idx];
    },

    // Called at start of each new WebGL rendering frame. This swaps the
    // double-buffered temp VB memory pointers, so that every second frame
    // utilizes different set of temp buffers. The aim is to keep the set of
    // buffers being rendered, and the set of buffers being updated disjoint.
    newRenderingFrameStarted: () => {
      if (!GL.currentContext) {
        return;
      }
      var vb = GL.currentContext.tempVertexBuffers1;
      GL.currentContext.tempVertexBuffers1 = GL.currentContext.tempVertexBuffers2;
      GL.currentContext.tempVertexBuffers2 = vb;
      vb = GL.currentContext.tempVertexBufferCounters1;
      GL.currentContext.tempVertexBufferCounters1 = GL.currentContext.tempVertexBufferCounters2;
      GL.currentContext.tempVertexBufferCounters2 = vb;
      var largestIndex = GL.log2ceilLookup(GL_MAX_TEMP_BUFFER_SIZE);
      for (var i = 0; i <= largestIndex; ++i) {
        GL.currentContext.tempVertexBufferCounters1[i] = 0;
      }
    },
#endif

    getSource: (shader, count, string, length) => {
      var source = '';
      for (var i = 0; i < count; ++i) {
        var len = length ? {{{ makeGetValue('length', 'i*' + POINTER_SIZE, '*') }}} : undefined;
        source += UTF8ToString({{{ makeGetValue('string', 'i*' + POINTER_SIZE, '*') }}}, len);
      }
#if LEGACY_GL_EMULATION
      // Let's see if we need to enable the standard derivatives extension
      var type = GLctx.getShaderParameter(GL.shaders[shader], 0x8B4F /* GL_SHADER_TYPE */);
      if (type == 0x8B30 /* GL_FRAGMENT_SHADER */) {
        if (GLEmulation.findToken(source, 'dFdx') ||
            GLEmulation.findToken(source, 'dFdy') ||
            GLEmulation.findToken(source, 'fwidth')) {
          source = '#extension GL_OES_standard_derivatives : enable\n' + source;
          var extension = GLctx.getExtension('OES_standard_derivatives');
#if GL_DEBUG
          if (!extension) {
            dbg('Shader attempts to use the standard derivatives extension which is not available.');
          }
#endif
        }
      }
#endif
      return source;
    },

#if GL_FFP_ONLY
    enabledClientAttribIndices: [],
    enableVertexAttribArray: (index) => {
      if (!GL.enabledClientAttribIndices[index]) {
        GL.enabledClientAttribIndices[index] = true;
        GLctx.enableVertexAttribArray(index);
      }
    },
    disableVertexAttribArray: (index) => {
      if (GL.enabledClientAttribIndices[index]) {
        GL.enabledClientAttribIndices[index] = false;
        GLctx.disableVertexAttribArray(index);
      }
    },
#endif

#if FULL_ES2
    calcBufLength: (size, type, stride, count) => {
      if (stride > 0) {
        return count * stride;  // XXXvlad this is not exactly correct I don't think
      }
      var typeSize = GL.byteSizeByType[type - GL.byteSizeByTypeRoot];
      return size * typeSize * count;
    },

    preDrawHandleClientVertexAttribBindings: (count) => {
      GL.resetBufferBinding = false;

      // TODO: initial pass to detect ranges we need to upload, might not need
      // an upload per attrib
      for (var i = 0; i < GL.currentContext.maxVertexAttribs; ++i) {
        var cb = GL.currentContext.clientBuffers[i];
        if (!cb.clientside || !cb.enabled) continue;

#if ASSERTIONS
        assert(count || !GLctx.currentElementArrayBufferBinding, 'must use array buffers when using element buffer');
#endif

        GL.resetBufferBinding = true;

        var size = GL.calcBufLength(cb.size, cb.type, cb.stride, count);
        var buf = GL.getTempVertexBuffer(size);
        GLctx.bindBuffer(0x8892 /*GL_ARRAY_BUFFER*/, buf);
        webglBufferSubData(0x8892 /*GL_ARRAY_BUFFER*/, 0, size, cb.ptr);
#if GL_ASSERTIONS
        GL.validateVertexAttribPointer(cb.size, cb.type, cb.stride, 0);
#endif
        cb.vertexAttribPointerAdaptor.call(GLctx, i, cb.size, cb.type, cb.normalized, cb.stride, 0);
      }
    },

    postDrawHandleClientVertexAttribBindings: () => {
      if (GL.resetBufferBinding) {
        GLctx.bindBuffer(0x8892 /*GL_ARRAY_BUFFER*/, GL.buffers[GLctx.currentArrayBufferBinding]);
      }
    },
#endif

#if GL_ASSERTIONS
    validateGLObjectID: (objectHandleArray, objectID, callerFunctionName, objectReadableType) => {
      // `objectHandleArray` may be uninitialized when GL uniforms are lazily initialized, and `glUniform*` is called
      // for the first time before uniforms have been populated. So ignore this validation if the handle array is not present.
      if (objectID != 0 && objectHandleArray) {
        if (objectHandleArray[objectID] === null) {
          err(`${callerFunctionName} called with an already deleted ${objectReadableType} ID ${objectID}!`);
        } else if (!(objectID in objectHandleArray)) {
          err(`${callerFunctionName} called with a nonexisting ${objectReadableType} ID ${objectID}!`);
        }
      }
    },
    // Validates that user obeys GL spec #6.4: http://www.khronos.org/registry/webgl/specs/latest/1.0/#6.4
    validateVertexAttribPointer: (dimension, dataType, stride, offset) => {
      var sizeBytes = 1;
      switch (dataType) {
        case 0x1400 /* GL_BYTE */:
        case 0x1401 /* GL_UNSIGNED_BYTE */:
          sizeBytes = 1;
          break;
        case 0x1402 /* GL_SHORT */:
        case 0x1403 /* GL_UNSIGNED_SHORT */:
          sizeBytes = 2;
          break;
        case 0x1404 /* GL_INT */:
        case 0x1405 /* GL_UNSIGNED_INT */:
        case 0x1406 /* GL_FLOAT */:
          sizeBytes = 4;
          break;
        case 0x140A /* GL_DOUBLE */:
          sizeBytes = 8;
          break;
        default:
#if MAX_WEBGL_VERSION >= 2
          if ({{{ isCurrentContextWebGL2() }}}) {
            if (dataType == 0x8368 /* GL_UNSIGNED_INT_2_10_10_10_REV */ || dataType == 0x8D9F /* GL_INT_2_10_10_10_REV */) {
              sizeBytes = 4;
              break;
            } else if (dataType == 0x140B /* GL_HALF_FLOAT */) {
              sizeBytes = 2;
              break;
            } else {
              // else fall through
            }
          }
#endif
          err(`Invalid vertex attribute data type GLenum ${dataType} passed to GL function!`);
      }
      if (dimension == 0x80E1 /* GL_BGRA */) {
        err('WebGL does not support size=GL_BGRA in a call to glVertexAttribPointer! Please use size=4 and type=GL_UNSIGNED_BYTE instead');
      } else if (dimension < 1 || dimension > 4) {
        err(`Invalid dimension=${dimension} in call to glVertexAttribPointer, must be 1,2,3 or 4.`);
      }
      if (stride < 0 || stride > 255) {
        err(`Invalid stride=${stride} in call to glVertexAttribPointer. Note that maximum supported stride in WebGL is 255!`);
      }
      if (offset % sizeBytes != 0) {
        err(`GL spec section 6.4 error: vertex attribute data offset of ${offset} bytes should have been a multiple of the data type size that was used: GLenum ${dataType} has size of ${sizeBytes} bytes!`);
      }
      if (stride % sizeBytes != 0) {
        err(`GL spec section 6.4 error: vertex attribute data stride of ${stride} bytes should have been a multiple of the data type size that was used: GLenum ${dataType} has size of ${sizeBytes} bytes!`);
      }
    },
#endif

#if TRACE_WEBGL_CALLS
    hookWebGLFunction: (f, glCtx) => {
      var orig = glCtx[f];
      var contextHandle = glCtx.canvas.GLctxObject.handle;
      glCtx[f] = function(...args) {
        var ret = orig.apply(this, args);
        for (var i in args) {
          if (ArrayBuffer.isView(args[i])) {
            // Some GL functions take a view of the entire linear memory.  Replace
            // such arguments with the string 'HEAP' to avoid serializing all of
            // memory.
            if (args[i].byteLength === HEAPU8.byteLength) {
              args[i] = 'HEAP';
              continue;
            }
            // For large arrays just take the first N elements.
            const MAX_ARRAY_ELEMS = 30;
            if (args[i].length > MAX_ARRAY_ELEMS) {
              const notShown = args[i].length - MAX_ARRAY_ELEMS;
              args[i] = args[i].subarray(0, MAX_ARRAY_ELEMS);
              args[i] = `[${args[i]}, ... <${notShown} more elements not shown>]`;
            } else {
              args[i] = `[${args[i]}]`;
            }
          }
        }
#if PTHREADS
        err(`[Thread ${_pthread_self()}, GL ctx: ${contextHandle}]: ${f}(${args}) -> ${ret}`);
#else
        err(`[ctx: ${contextHandle}]: ${f}(${args}) -> ${ret}`);
#endif
        return ret;
      };
    },

    hookWebGL: function(glCtx) {
      glCtx ??= this.detectWebGLContext();
      if (!glCtx) return;
      if (!((globalThis.WebGLRenderingContext && glCtx instanceof WebGLRenderingContext)
            || (globalThis.WebGL2RenderingContext && glCtx instanceof WebGL2RenderingContext))) {
        return;
      }

      if (glCtx.webGlTracerAlreadyHooked) return;
      glCtx.webGlTracerAlreadyHooked = true;

      for (var f in glCtx) {
        if (typeof glCtx[f] === 'function') {
          this.hookWebGLFunction(f, glCtx);
        }
      }
    },
#endif
    // Returns the context handle to the new context.
    createContext: (/** @type {HTMLCanvasElement} */ canvas, webGLContextAttributes) => {
#if OFFSCREEN_FRAMEBUFFER
      // In proxied operation mode, rAF()/setTimeout() functions do not delimit
      // frame boundaries, so can't have WebGL implementation try to detect when
      // it's ok to discard contents of the rendered backbuffer.
      if (webGLContextAttributes.renderViaOffscreenBackBuffer) webGLContextAttributes['preserveDrawingBuffer'] = true;
#endif

#if GL_TESTING
      webGLContextAttributes['preserveDrawingBuffer'] = true;
#endif

#if GL_DEBUG
      var errorInfo = '?';
      function onContextCreationError(event) {
        errorInfo = event.statusMessage || errorInfo;
      }
      canvas.addEventListener('webglcontextcreationerror', onContextCreationError);
#endif

#if expectToReceiveOnModule('preinitializedWebGLContext')
      // If WebGL context has already been preinitialized for the page on the JS
      // side, reuse that context instead. This is useful for example when the
      // main page precompiles shaders for the application, in which case the
      // WebGL context is created already before any Emscripten compiled code
      // has been downloaded.
      if (Module['preinitializedWebGLContext']) {
        var ctx = Module['preinitializedWebGLContext'];
#if MAX_WEBGL_VERSION >= 2
        // The ctx object may not be of a known class (e.g. it may be a debug
        // wrapper), so we ask it for its version rather than use instanceof.
        webGLContextAttributes.majorVersion = Number(ctx.getParameter(ctx.VERSION).match(/^WebGL (\d+).\d+/)[1]);
#else
        webGLContextAttributes.majorVersion = 1;
#endif
      } else {
#endif

#if MIN_SAFARI_VERSION != TARGET_NOT_SUPPORTED && GL_WORKAROUND_SAFARI_GETCONTEXT_BUG
      // BUG: Workaround Safari WebGL issue: After successfully acquiring WebGL
      // context on a canvas, calling .getContext() will always return that
      // context independent of which 'webgl' or 'webgl2'
      // context version was passed. See:
      //   https://webkit.org/b/222758
      // and:
      //   https://github.com/emscripten-core/emscripten/issues/13295.
      // TODO: Once the bug is fixed and shipped in Safari, adjust the Safari
      // version field in above check.
      if (!canvas.getContextSafariWebGL2Fixed) {
        canvas.getContextSafariWebGL2Fixed = canvas.getContext;
        /** @type {function(this:HTMLCanvasElement, string, (Object|null)=): (RenderingContext|null)} */
        function fixedGetContext(ver, attrs) {
          var gl = canvas.getContextSafariWebGL2Fixed(ver, attrs);
          return ((ver == 'webgl') == (gl instanceof WebGLRenderingContext)) ? gl : null;
        }
        canvas.getContext = fixedGetContext;
      }
#endif

#if MIN_WEBGL_VERSION >= 2
      var ctx = canvas.getContext('webgl2', webGLContextAttributes);
#else
      var ctx =
#if MAX_WEBGL_VERSION >= 2
        (webGLContextAttributes.majorVersion > 1)
        ? canvas.getContext('webgl2', webGLContextAttributes) :
#endif
        canvas.getContext('webgl', webGLContextAttributes);
#endif // MIN_WEBGL_VERSION >= 2

#if expectToReceiveOnModule('preinitializedWebGLContext')
      }
#endif

#if GL_DEBUG
      canvas.removeEventListener('webglcontextcreationerror', onContextCreationError);
      if (!ctx) {
        dbg('Could not create canvas: ' + [errorInfo, JSON.stringify(webGLContextAttributes)]);
        return 0;
      }
#else
      if (!ctx) return 0;
#endif

      var handle = GL.registerContext(ctx, webGLContextAttributes);

#if TRACE_WEBGL_CALLS
      GL.hookWebGL(ctx);
#endif

#if GL_DISABLE_HALF_FLOAT_EXTENSION_IF_BROKEN
      const disableHalfFloatExtensionIfBroken = (ctx) => {
        var t = ctx.createTexture();
        ctx.bindTexture(0xDE1/*GL_TEXTURE_2D*/, t);
        for (var i = 0; i < 8 && ctx.getError(); ++i) /*no-op*/;
        var ext = ctx.getExtension('OES_texture_half_float');
        if (!ext) return; // no half-float extension - nothing needed to fix.
        // Bug on Safari on iOS and macOS: texImage2D() and texSubImage2D() do
        // not allow uploading pixel data to half float textures, rendering them
        // useless.
        // See https://webkit.org/b/183321, https://webkit.org/b/169999,
        // https://stackoverflow.com/questions/54248633/cannot-create-half-float-oes-texture-from-uint16array-on-ipad
        ctx.texImage2D(0xDE1/*GL_TEXTURE_2D*/, 0, 0x1908/*GL_RGBA*/, 1, 1, 0, 0x1908/*GL_RGBA*/, 0x8d61/*HALF_FLOAT_OES*/, new Uint16Array(4));
        var broken = ctx.getError();
        ctx.bindTexture(0xDE1/*GL_TEXTURE_2D*/, null);
        ctx.deleteTexture(t);
        if (broken) {
          ctx.realGetSupportedExtensions = ctx.getSupportedExtensions;
          ctx.getSupportedExtensions = function() {
#if GL_ASSERTIONS
            warnOnce('Removed broken support for half-float textures. See e.g. https://webkit.org/b/183321');
#endif
            // .getSupportedExtensions() can return null if context is lost, so
            // coerce to empty array.
            return (this.realGetSupportedExtensions() || []).filter((ext) => !ext.includes('texture_half_float'));
          }
        }
      }
      disableHalfFloatExtensionIfBroken(ctx);
#endif

      return handle;
    },

#if OFFSCREEN_FRAMEBUFFER
    enableOffscreenFramebufferAttributes: (webGLContextAttributes) => {
      webGLContextAttributes.renderViaOffscreenBackBuffer = true;
      webGLContextAttributes.preserveDrawingBuffer = true;
    },

    // If WebGL is being proxied from a pthread to the main thread, we can't
    // directly render to the WebGL default back buffer because of WebGL's
    // implicit swap behavior. Therefore in such modes, create an offscreen
    // render target surface to which rendering is performed to, and finally
    // flipped to the main screen.
    createOffscreenFramebuffer: (context) => {
      var gl = context.GLctx;

      // Create FBO
      var fbo = gl.createFramebuffer();
      gl.bindFramebuffer(0x8D40 /*GL_FRAMEBUFFER*/, fbo);
      context.defaultFbo = fbo;

#if MAX_WEBGL_VERSION >= 2
      context.defaultFboForbidBlitFramebuffer = false;
      if (gl.getContextAttributes().antialias) {
        context.defaultFboForbidBlitFramebuffer = true;
      }
#endif

      // Create render targets to the FBO
      context.defaultColorTarget = gl.createTexture();
      context.defaultDepthTarget = gl.createRenderbuffer();
      // Size them up correctly (use the same mechanism when resizing on demand)
      GL.resizeOffscreenFramebuffer(context);

      gl.bindTexture(0xDE1 /*GL_TEXTURE_2D*/, context.defaultColorTarget);
      gl.texParameteri(0xDE1 /*GL_TEXTURE_2D*/, 0x2801 /*GL_TEXTURE_MIN_FILTER*/, 0x2600 /*GL_NEAREST*/);
      gl.texParameteri(0xDE1 /*GL_TEXTURE_2D*/, 0x2800 /*GL_TEXTURE_MAG_FILTER*/, 0x2600 /*GL_NEAREST*/);
      gl.texParameteri(0xDE1 /*GL_TEXTURE_2D*/, 0x2802 /*GL_TEXTURE_WRAP_S*/, 0x812F /*GL_CLAMP_TO_EDGE*/);
      gl.texParameteri(0xDE1 /*GL_TEXTURE_2D*/, 0x2803 /*GL_TEXTURE_WRAP_T*/, 0x812F /*GL_CLAMP_TO_EDGE*/);
      gl.texImage2D(0xDE1 /*GL_TEXTURE_2D*/, 0, 0x1908 /*GL_RGBA*/, gl.canvas.width, gl.canvas.height, 0, 0x1908 /*GL_RGBA*/, 0x1401 /*GL_UNSIGNED_BYTE*/, null);
      gl.framebufferTexture2D(0x8D40 /*GL_FRAMEBUFFER*/, 0x8CE0 /*GL_COLOR_ATTACHMENT0*/, 0xDE1 /*GL_TEXTURE_2D*/, context.defaultColorTarget, 0);
      gl.bindTexture(0xDE1 /*GL_TEXTURE_2D*/, null);

      // Create depth render target to the FBO
      var depthTarget = gl.createRenderbuffer();
      gl.bindRenderbuffer(0x8D41 /*GL_RENDERBUFFER*/, context.defaultDepthTarget);
      gl.renderbufferStorage(0x8D41 /*GL_RENDERBUFFER*/, 0x81A5 /*GL_DEPTH_COMPONENT16*/, gl.canvas.width, gl.canvas.height);
      gl.framebufferRenderbuffer(0x8D40 /*GL_FRAMEBUFFER*/, 0x8D00 /*GL_DEPTH_ATTACHMENT*/, 0x8D41 /*GL_RENDERBUFFER*/, context.defaultDepthTarget);
      gl.bindRenderbuffer(0x8D41 /*GL_RENDERBUFFER*/, null);

      // Create blitter
      var vertices = [
        -1, -1,
        -1,  1,
         1, -1,
         1,  1
      ];
      var vb = gl.createBuffer();
      gl.bindBuffer(0x8892 /*GL_ARRAY_BUFFER*/, vb);
      gl.bufferData(0x8892 /*GL_ARRAY_BUFFER*/, new Float32Array(vertices), 0x88E4 /*GL_STATIC_DRAW*/);
      gl.bindBuffer(0x8892 /*GL_ARRAY_BUFFER*/, null);
      context.blitVB = vb;

      var vsCode =
        'attribute vec2 pos;' +
        'varying lowp vec2 tex;' +
        'void main() { tex = pos * 0.5 + vec2(0.5,0.5); gl_Position = vec4(pos, 0.0, 1.0); }';
      var vs = gl.createShader(0x8B31 /*GL_VERTEX_SHADER*/);
      gl.shaderSource(vs, vsCode);
      gl.compileShader(vs);

      var fsCode =
        'varying lowp vec2 tex;' +
        'uniform sampler2D sampler;' +
        'void main() { gl_FragColor = texture2D(sampler, tex); }';
      var fs = gl.createShader(0x8B30 /*GL_FRAGMENT_SHADER*/);
      gl.shaderSource(fs, fsCode);
      gl.compileShader(fs);

      var blitProgram = gl.createProgram();
      gl.attachShader(blitProgram, vs);
      gl.attachShader(blitProgram, fs);
      gl.linkProgram(blitProgram);
      context.blitProgram = blitProgram;
      context.blitPosLoc = gl.getAttribLocation(blitProgram, 'pos');
      gl.useProgram(blitProgram);
      gl.uniform1i(gl.getUniformLocation(blitProgram, 'sampler'), 0);
      gl.useProgram(null);

      if (gl.createVertexArray) {
        context.defaultVao = gl.createVertexArray();
        gl.bindVertexArray(context.defaultVao);
        gl.enableVertexAttribArray(context.blitPosLoc);
        gl.bindVertexArray(null);
      }
    },

    resizeOffscreenFramebuffer: (context) => {
      var gl = context.GLctx;

      // Resize color buffer
      if (context.defaultColorTarget) {
        var prevTextureBinding = gl.getParameter(0x8069 /*GL_TEXTURE_BINDING_2D*/);
        gl.bindTexture(0xDE1 /*GL_TEXTURE_2D*/, context.defaultColorTarget);
        gl.texImage2D(0xDE1 /*GL_TEXTURE_2D*/, 0, 0x1908 /*GL_RGBA*/, gl.drawingBufferWidth, gl.drawingBufferHeight, 0, 0x1908 /*GL_RGBA*/, 0x1401 /*GL_UNSIGNED_BYTE*/, null);
        gl.bindTexture(0xDE1 /*GL_TEXTURE_2D*/, prevTextureBinding);
      }

      // Resize depth buffer
      if (context.defaultDepthTarget) {
        var prevRenderBufferBinding = gl.getParameter(0x8CA7 /*GL_RENDERBUFFER_BINDING*/);
        gl.bindRenderbuffer(0x8D41 /*GL_RENDERBUFFER*/, context.defaultDepthTarget);
        gl.renderbufferStorage(0x8D41 /*GL_RENDERBUFFER*/, 0x81A5 /*GL_DEPTH_COMPONENT16*/, gl.drawingBufferWidth, gl.drawingBufferHeight); // TODO: Read context creation parameters for what type of depth and stencil to use
        gl.bindRenderbuffer(0x8D41 /*GL_RENDERBUFFER*/, prevRenderBufferBinding);
      }
    },

    // Renders the contents of the offscreen render target onto the visible screen.
    blitOffscreenFramebuffer: (context) => {
      var gl = context.GLctx;

      var prevScissorTest = gl.getParameter(0xC11 /*GL_SCISSOR_TEST*/);
      if (prevScissorTest) gl.disable(0xC11 /*GL_SCISSOR_TEST*/);

      var prevFbo = gl.getParameter(0x8CA6 /*GL_FRAMEBUFFER_BINDING*/);

#if MAX_WEBGL_VERSION >= 2
      if (gl.blitFramebuffer && !context.defaultFboForbidBlitFramebuffer) {
        gl.bindFramebuffer(0x8CA8 /*GL_READ_FRAMEBUFFER*/, context.defaultFbo);
        gl.bindFramebuffer(0x8CA9 /*GL_DRAW_FRAMEBUFFER*/, null);
        gl.blitFramebuffer(0, 0, gl.canvas.width, gl.canvas.height,
                           0, 0, gl.canvas.width, gl.canvas.height,
                           0x4000 /*GL_COLOR_BUFFER_BIT*/, 0x2600/*GL_NEAREST*/);
      }
      else
#endif
      {
        gl.bindFramebuffer(0x8D40 /*GL_FRAMEBUFFER*/, null);

        var prevProgram = gl.getParameter(0x8B8D /*GL_CURRENT_PROGRAM*/);
        gl.useProgram(context.blitProgram);
        // If prevProgram was already marked for deletion, then, since it was
        // still bound, it was not *actually* deleted. Binding a new program
        // just now, thus, deleted the old one. This makes it impossible to
        // restore. Hopefully the application didn't actually need it!
        if (!gl.isProgram(prevProgram)) prevProgram = null;

        var prevVB = gl.getParameter(0x8894 /*GL_ARRAY_BUFFER_BINDING*/);
        gl.bindBuffer(0x8892 /*GL_ARRAY_BUFFER*/, context.blitVB);

        var prevActiveTexture = gl.getParameter(0x84E0 /*GL_ACTIVE_TEXTURE*/);
        gl.activeTexture(0x84C0 /*GL_TEXTURE0*/);

        var prevTextureBinding = gl.getParameter(0x8069 /*GL_TEXTURE_BINDING_2D*/);
        gl.bindTexture(0xDE1 /*GL_TEXTURE_2D*/, context.defaultColorTarget);

        var prevBlend = gl.getParameter(0xBE2 /*GL_BLEND*/);
        if (prevBlend) gl.disable(0xBE2 /*GL_BLEND*/);

        var prevCullFace = gl.getParameter(0xB44 /*GL_CULL_FACE*/);
        if (prevCullFace) gl.disable(0xB44 /*GL_CULL_FACE*/);

        var prevDepthTest = gl.getParameter(0xB71 /*GL_DEPTH_TEST*/);
        if (prevDepthTest) gl.disable(0xB71 /*GL_DEPTH_TEST*/);

        var prevStencilTest = gl.getParameter(0xB90 /*GL_STENCIL_TEST*/);
        if (prevStencilTest) gl.disable(0xB90 /*GL_STENCIL_TEST*/);

        function draw() {
          gl.vertexAttribPointer(context.blitPosLoc, 2, 0x1406 /*GL_FLOAT*/, false, 0, 0);
          gl.drawArrays(5/*GL_TRIANGLE_STRIP*/, 0, 4);
        }

        if (context.defaultVao) {
          // WebGL 2 or OES_vertex_array_object
          var prevVAO = gl.getParameter(0x85B5 /*GL_VERTEX_ARRAY_BINDING*/);
          gl.bindVertexArray(context.defaultVao);
          draw();
          gl.bindVertexArray(prevVAO);
        } else {
          var prevVertexAttribPointer = {
            buffer: gl.getVertexAttrib(context.blitPosLoc, 0x889F /*GL_VERTEX_ATTRIB_ARRAY_BUFFER_BINDING*/),
            size: gl.getVertexAttrib(context.blitPosLoc, 0x8623 /*GL_VERTEX_ATTRIB_ARRAY_SIZE*/),
            stride: gl.getVertexAttrib(context.blitPosLoc, 0x8624 /*GL_VERTEX_ATTRIB_ARRAY_STRIDE*/),
            type: gl.getVertexAttrib(context.blitPosLoc, 0x8625 /*GL_VERTEX_ATTRIB_ARRAY_TYPE*/),
            normalized: gl.getVertexAttrib(context.blitPosLoc, 0x886A /*GL_VERTEX_ATTRIB_ARRAY_NORMALIZED*/),
            pointer: gl.getVertexAttribOffset(context.blitPosLoc, 0x8645 /*GL_VERTEX_ATTRIB_ARRAY_POINTER*/),
          };
          var maxVertexAttribs = gl.getParameter(0x8869 /*GL_MAX_VERTEX_ATTRIBS*/);
          var prevVertexAttribEnables = [];
          for (var i = 0; i < maxVertexAttribs; ++i) {
            var prevEnabled = gl.getVertexAttrib(i, 0x8622 /*GL_VERTEX_ATTRIB_ARRAY_ENABLED*/);
            var wantEnabled = i == context.blitPosLoc;
            if (prevEnabled && !wantEnabled) {
              gl.disableVertexAttribArray(i);
            }
            if (!prevEnabled && wantEnabled) {
              gl.enableVertexAttribArray(i);
            }
            prevVertexAttribEnables[i] = prevEnabled;
          }

          draw();

          for (var i = 0; i < maxVertexAttribs; ++i) {
            var prevEnabled = prevVertexAttribEnables[i];
            var nowEnabled = i == context.blitPosLoc;
            if (prevEnabled && !nowEnabled) {
              gl.enableVertexAttribArray(i);
            }
            if (!prevEnabled && nowEnabled) {
              gl.disableVertexAttribArray(i);
            }
          }
          gl.bindBuffer(0x8892 /*GL_ARRAY_BUFFER*/, prevVertexAttribPointer.buffer);
          gl.vertexAttribPointer(context.blitPosLoc,
                                 prevVertexAttribPointer.size,
                                 prevVertexAttribPointer.type,
                                 prevVertexAttribPointer.normalized,
                                 prevVertexAttribPointer.stride,
                                 prevVertexAttribPointer.offset);
        }

        if (prevStencilTest) gl.enable(0xB90 /*GL_STENCIL_TEST*/);
        if (prevDepthTest) gl.enable(0xB71 /*GL_DEPTH_TEST*/);
        if (prevCullFace) gl.enable(0xB44 /*GL_CULL_FACE*/);
        if (prevBlend) gl.enable(0xBE2 /*GL_BLEND*/);

        gl.bindTexture(0xDE1 /*GL_TEXTURE_2D*/, prevTextureBinding);
        gl.activeTexture(prevActiveTexture);
        gl.bindBuffer(0x8892 /*GL_ARRAY_BUFFER*/, prevVB);
        gl.useProgram(prevProgram);
      }
      gl.bindFramebuffer(0x8D40 /*GL_FRAMEBUFFER*/, prevFbo);
      if (prevScissorTest) gl.enable(0xC11 /*GL_SCISSOR_TEST*/);
    },
#endif

    registerContext: (ctx, webGLContextAttributes) => {
#if PTHREADS
      // with pthreads a context is a location in memory with some synchronized
      // data between threads
      var handle = _malloc({{{ 2 * POINTER_SIZE }}});
#if GL_ASSERTIONS
      assert(handle, 'malloc() failed in GL.registerContext');
#endif
#if GL_SUPPORT_EXPLICIT_SWAP_CONTROL
      {{{ makeSetValue('handle', 0, 'webGLContextAttributes.explicitSwapControl', 'i8')}}};
#endif
      {{{ makeSetValue('handle', POINTER_SIZE, '_pthread_self()', '*')}}}; // the thread pointer of the thread that owns the control of the context
#else // PTHREADS
      // without pthreads a context is just an integer ID
      var handle = GL.getNewId(GL.contexts);
#endif // PTHREADS

      var context = {
        handle,
        attributes: webGLContextAttributes,
        version: webGLContextAttributes.majorVersion,
        GLctx: ctx
      };

      // Store the created context object so that we can access the context
      // given a canvas without having to pass the parameters again.
      if (ctx.canvas) ctx.canvas.GLctxObject = context;
      GL.contexts[handle] = context;
#if GL_SUPPORT_AUTOMATIC_ENABLE_EXTENSIONS
      if (typeof webGLContextAttributes.enableExtensionsByDefault === 'undefined' || webGLContextAttributes.enableExtensionsByDefault) {
        GL.initExtensions(context);
      }
#endif

#if FULL_ES2
      context.maxVertexAttribs = context.GLctx.getParameter(0x8869 /*GL_MAX_VERTEX_ATTRIBS*/);
      context.clientBuffers = [];
      for (var i = 0; i < context.maxVertexAttribs; i++) {
        context.clientBuffers[i] = {
          enabled: false,
          clientside: false,
          size: 0,
          type: 0,
          normalized: 0,
          stride: 0,
          ptr: 0,
          vertexAttribPointerAdaptor: null,
        };
      }

      GL.generateTempBuffers(false, context);
#endif

#if OFFSCREEN_FRAMEBUFFER
      if (webGLContextAttributes.renderViaOffscreenBackBuffer) GL.createOffscreenFramebuffer(context);
#else

#if GL_DEBUG
      if (webGLContextAttributes.renderViaOffscreenBackBuffer) {
        dbg('renderViaOffscreenBackBuffer=true specified in WebGL context creation attributes, pass linker flag -sOFFSCREEN_FRAMEBUFFER to enable support');
      }
#endif

#endif
      return handle;
    },

    makeContextCurrent: (contextHandle) => {
#if GL_DEBUG
      if (contextHandle && !GL.contexts[contextHandle]) {
#if PTHREADS
        dbg(`GL.makeContextCurrent() failed! WebGL context ${contextHandle} does not exist, or was created on another thread!`);
#else
        dbg(`GL.makeContextCurrent() failed! WebGL context ${contextHandle} does not exist!`);
#endif
      }
#endif

      // Active Emscripten GL layer context object.
      GL.currentContext = GL.contexts[contextHandle];
      // Active WebGL context object.
      Module['ctx'] = GLctx = GL.currentContext?.GLctx;
      return !(contextHandle && !GLctx);
    },

    getContext: (contextHandle) => {
      return GL.contexts[contextHandle];
    },

    deleteContext: (contextHandle) => {
      if (GL.currentContext === GL.contexts[contextHandle]) {
        GL.currentContext = null;
      }
      if (typeof JSEvents === 'object') {
        // Release all JS event handlers on the DOM element that the GL context is
        // associated with since the context is now deleted.
        JSEvents.removeAllHandlersOnTarget(GL.contexts[contextHandle].GLctx.canvas);
      }
      // Make sure the canvas object no longer refers to the context object so
      // there are no GC surprises.
      if (GL.contexts[contextHandle]?.GLctx.canvas) {
        GL.contexts[contextHandle].GLctx.canvas.GLctxObject = undefined;
      }
#if PTHREADS
      _free(GL.contexts[contextHandle].handle);
#endif
      GL.contexts[contextHandle] = null;
    },

#if GL_SUPPORT_AUTOMATIC_ENABLE_EXTENSIONS
    // In WebGL, extensions must be explicitly enabled to be active, see
    // http://www.khronos.org/registry/webgl/specs/latest/1.0/#5.14.14
    // In GLES2, all extensions are enabled by default without additional
    // operations. Init all extensions we need to give to GLES2 user code here,
    // so that GLES2 code can operate without changing behavior.
    initExtensions: (context) => {
      // If this function is called without a specific context object, init the
      // extensions of the currently active context.
      context ||= GL.currentContext;

      if (context.initExtensionsDone) return;
      context.initExtensionsDone = true;

      var gl = context.GLctx;

      // Detect the presence of a few extensions manually, since the GL interop
      // layer itself will need to know if they exist.
#if LEGACY_GL_EMULATION
      context.compressionExt = gl.getExtension('WEBGL_compressed_texture_s3tc');
      context.anisotropicExt = gl.getExtension('EXT_texture_filter_anisotropic');
#endif

      // Extensions that are available in both WebGL 1 and WebGL 2
      webgl_enable_WEBGL_multi_draw(gl);
      webgl_enable_EXT_polygon_offset_clamp(gl);
      webgl_enable_EXT_clip_control(gl);
      webgl_enable_WEBGL_polygon_mode(gl);
#if MIN_WEBGL_VERSION == 1
      // Extensions that are only available in WebGL 1 (the calls will be no-ops
      // if called on a WebGL 2 context active)
      webgl_enable_ANGLE_instanced_arrays(gl);
      webgl_enable_OES_vertex_array_object(gl);
      webgl_enable_WEBGL_draw_buffers(gl);
#endif
#if MAX_WEBGL_VERSION >= 2
      // Extensions that are available from WebGL >= 2 (no-op if called on a WebGL 1 context active)
      webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance(gl);
      webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance(gl);

      // On WebGL 2, EXT_disjoint_timer_query is replaced with an alternative
      // that's based on core APIs, and exposes only the queryCounterEXT()
      // entrypoint.
      if (context.version >= 2) {
        gl.disjointTimerQueryExt = gl.getExtension('EXT_disjoint_timer_query_webgl2');
      }

      // However, Firefox exposes the WebGL 1 version on WebGL 2 as well and
      // thus we look for the WebGL 1 version again if the WebGL 2 version
      // isn't present. https://bugzil.la/1328882
      if (context.version < 2 || !gl.disjointTimerQueryExt)
#endif
      {
        gl.disjointTimerQueryExt = gl.getExtension('EXT_disjoint_timer_query');
      }

      for (var ext of getEmscriptenSupportedExtensions(gl)) {
        // WEBGL_lose_context, WEBGL_debug_renderer_info and WEBGL_debug_shaders
        // are not enabled by default.
        if (!ext.includes('lose_context') && !ext.includes('debug')) {
          // Call .getExtension() to enable that extension permanently.
          gl.getExtension(ext);
        }
      }
    },
#endif

  },

  // Wrapper around GLctx.bufferSubData that can hangle both WebGL1 (which
  // requires new subarray on each call) and WebGL2 (which does not).
  // Argument ordering is a little strange here, since we want a default
  // for `src` is has to come last.
  $webglBufferSubData__internal: true,
  $webglBufferSubData: (target, offset, size, data, src = HEAPU8) => {
#if WEBGL_USE_GARBAGE_FREE_APIS
    if ({{{ isCurrentContextWebGL2() }}}) {
      size && GLctx.bufferSubData(target, offset, src, data, size);
      return;
    }
#endif
#if INCLUDE_WEBGL1_FALLBACK
    GLctx.bufferSubData(target, offset, src.subarray(data, data + size));
#endif
  },

  $webglGetExtensions__internal: true,
  $webglGetExtensions: () => {
    var exts = getEmscriptenSupportedExtensions(GLctx);
#if GL_EXTENSIONS_IN_PREFIXED_FORMAT
    exts = exts.concat(exts.map((e) => 'GL_' + e));
#endif
    return exts;
  },

  glPixelStorei: (pname, param) => {
    if (pname == GL_UNPACK_ALIGNMENT) {
      GL.unpackAlignment = param;
    } else if (pname == GL_UNPACK_ROW_LENGTH) {
      GL.unpackRowLength = param;
    }
    GLctx.pixelStorei(pname, param);
  },

  // The allocated strings are cached and never freed.
  glGetString__noleakcheck: true,
  glGetString: (name_) => {
    var ret = GL.stringCache[name_];
    if (!ret) {
      switch (name_) {
        case 0x1F03 /* GL_EXTENSIONS */:
          ret = stringToNewUTF8(webglGetExtensions().join(' '));
          break;
        case 0x1F00 /* GL_VENDOR */:
        case 0x1F01 /* GL_RENDERER */:
        case 0x9245 /* UNMASKED_VENDOR_WEBGL */:
        case 0x9246 /* UNMASKED_RENDERER_WEBGL */:
#if !GL_EMULATE_GLES_VERSION_STRING_FORMAT
        case 0x1F02 /* GL_VERSION */:
        case 0x8B8C /* GL_SHADING_LANGUAGE_VERSION */:
#endif
          var s = GLctx.getParameter(name_);
#if GL_TRACK_ERRORS
          if (!s) {
            GL.recordError(0x500/*GL_INVALID_ENUM*/);
#if GL_ASSERTIONS
            // This occurs e.g. if one attempts GL_UNMASKED_VENDOR_WEBGL when it is not supported.
            err(`GL_INVALID_ENUM in glGetString: Received empty parameter for query name ${name_}!`);
#endif
          }
#endif
          ret = s ? stringToNewUTF8(s) : 0;
          break;

#if GL_EMULATE_GLES_VERSION_STRING_FORMAT
        case 0x1F02 /* GL_VERSION */:
          var webGLVersion = GLctx.getParameter(0x1F02 /*GL_VERSION*/);
          // return GLES version string corresponding to the version of the WebGL context
          var glVersion = `OpenGL ES 2.0 (${webGLVersion})`;
#if MAX_WEBGL_VERSION >= 2
          if ({{{ isCurrentContextWebGL2() }}}) glVersion = `OpenGL ES 3.0 (${webGLVersion})`;
#endif
          ret = stringToNewUTF8(glVersion);
          break;
        case 0x8B8C /* GL_SHADING_LANGUAGE_VERSION */:
          var glslVersion = GLctx.getParameter(0x8B8C /*GL_SHADING_LANGUAGE_VERSION*/);
          // extract the version number 'N.M' from the string 'WebGL GLSL ES N.M ...'
          var ver_re = /^WebGL GLSL ES ([0-9]\.[0-9][0-9]?)(?:$| .*)/;
          var ver_num = glslVersion.match(ver_re);
          if (ver_num !== null) {
            if (ver_num[1].length == 3) ver_num[1] = ver_num[1] + '0'; // ensure minor version has 2 digits
            glslVersion = `OpenGL ES GLSL ES ${ver_num[1]} (${glslVersion})`;
          }
          ret = stringToNewUTF8(glslVersion);
          break;
#endif
#if GL_TRACK_ERRORS
        default:
          GL.recordError(0x500/*GL_INVALID_ENUM*/);
#if GL_ASSERTIONS
          err(`GL_INVALID_ENUM in glGetString: Unknown parameter ${name_}!`);
#endif
#endif
          // fall through
      }
      GL.stringCache[name_] = ret;
    }
    return ret;
  },

  $emscriptenWebGLGet: (name_, p, type) => {
    // Guard against user passing a null pointer.
    // Note that GLES2 spec does not say anything about how passing a null
    // pointer should be treated.  Testing on desktop core GL 3, the application
    // crashes on glGetIntegerv to a null pointer, but better to report an error
    // instead of doing anything random.
    if (!p) {
#if GL_ASSERTIONS
      err(`GL_INVALID_VALUE in glGet${type}v(name=${name_}: Function called with null out pointer!`);
#endif
      GL.recordError(0x501 /* GL_INVALID_VALUE */);
      return;
    }
    var ret = undefined;
    switch (name_) { // Handle a few trivial GLES values
      case 0x8DFA: // GL_SHADER_COMPILER
        ret = 1;
        break;
      case 0x8DF8: // GL_SHADER_BINARY_FORMATS
#if GL_TRACK_ERRORS
        if (type != EM_FUNC_SIG_PARAM_I && type != EM_FUNC_SIG_PARAM_J) {
          GL.recordError(0x500); // GL_INVALID_ENUM
#if GL_ASSERTIONS
          err(`GL_INVALID_ENUM in glGet${type}v(GL_SHADER_BINARY_FORMATS): Invalid parameter type!`);
#endif
        }
#endif
        // Do not write anything to the out pointer, since no binary formats are
        // supported.
        return;
#if MAX_WEBGL_VERSION >= 2
      case 0x87FE: // GL_NUM_PROGRAM_BINARY_FORMATS
#endif
      case 0x8DF9: // GL_NUM_SHADER_BINARY_FORMATS
        ret = 0;
        break;
      case 0x86A2: // GL_NUM_COMPRESSED_TEXTURE_FORMATS
        // WebGL doesn't have GL_NUM_COMPRESSED_TEXTURE_FORMATS (it's obsolete
        // since GL_COMPRESSED_TEXTURE_FORMATS returns a JS array that can be
        // queried for length), so implement it ourselves to allow C++ GLES2
        // code to get the length.
        var formats = GLctx.getParameter(0x86A3 /*GL_COMPRESSED_TEXTURE_FORMATS*/);
        ret = formats ? formats.length : 0;
        break;
#if GL_EXPLICIT_UNIFORM_LOCATION
      case 0x826E: // GL_MAX_UNIFORM_LOCATIONS
        // This is an arbitrary limit, must be large enough to allow practical
        // use, but small enough to still keep a range for automatic uniform
        // locations, which get assigned numbers larger than this.
        ret = 1048576;
        break;
#endif

#if MAX_WEBGL_VERSION >= 2
      case 0x821D: // GL_NUM_EXTENSIONS
#if GL_TRACK_ERRORS
        if (GL.currentContext.version < 2) {
          // Calling GLES3/WebGL2 function with a GLES2/WebGL1 context
          GL.recordError(0x502 /* GL_INVALID_OPERATION */);
          return;
        }
#endif
        ret = webglGetExtensions().length;
        break;
      case 0x821B: // GL_MAJOR_VERSION
      case 0x821C: // GL_MINOR_VERSION
#if GL_TRACK_ERRORS
        if (GL.currentContext.version < 2) {
          GL.recordError(0x500); // GL_INVALID_ENUM
          return;
        }
#endif
        ret = name_ == 0x821B ? 3 : 0; // return version 3.0
        break;
#endif // ~MAX_WEBGL_VERSION >= 2
    }

    if (ret === undefined) {
      var result = GLctx.getParameter(name_);
      switch (typeof result) {
        case 'number':
          ret = result;
          break;
        case 'boolean':
          ret = result ? 1 : 0;
          break;
        case 'string':
          GL.recordError(0x500); // GL_INVALID_ENUM
#if GL_ASSERTIONS
          err(`GL_INVALID_ENUM in glGet${type}v(${name}) on a name which returns a string!`);
#endif
          return;
        case 'object':
          if (result === null) {
            // null is a valid result for some (e.g., which buffer is bound -
            // perhaps nothing is bound), but otherwise can mean an invalid
            // name_, which we need to report as an error
            switch (name_) {
              case 0x8894: // ARRAY_BUFFER_BINDING
              case 0x8B8D: // CURRENT_PROGRAM
              case 0x8895: // ELEMENT_ARRAY_BUFFER_BINDING
              case 0x8CA6: // FRAMEBUFFER_BINDING or DRAW_FRAMEBUFFER_BINDING
              case 0x8CA7: // RENDERBUFFER_BINDING
              case 0x8069: // TEXTURE_BINDING_2D
              case 0x85B5: // WebGL 2 GL_VERTEX_ARRAY_BINDING, or WebGL 1 extension OES_vertex_array_object GL_VERTEX_ARRAY_BINDING_OES
#if MAX_WEBGL_VERSION >= 2
              case 0x8F36: // COPY_READ_BUFFER_BINDING or COPY_READ_BUFFER
              case 0x8F37: // COPY_WRITE_BUFFER_BINDING or COPY_WRITE_BUFFER
              case 0x88ED: // PIXEL_PACK_BUFFER_BINDING
              case 0x88EF: // PIXEL_UNPACK_BUFFER_BINDING
              case 0x8CAA: // READ_FRAMEBUFFER_BINDING
              case 0x8919: // SAMPLER_BINDING
              case 0x8C1D: // TEXTURE_BINDING_2D_ARRAY
              case 0x806A: // TEXTURE_BINDING_3D
              case 0x8E25: // TRANSFORM_FEEDBACK_BINDING
              case 0x8C8F: // TRANSFORM_FEEDBACK_BUFFER_BINDING
              case 0x8A28: // UNIFORM_BUFFER_BINDING
#endif
              case 0x8514: { // TEXTURE_BINDING_CUBE_MAP
                ret = 0;
                break;
              }
              default: {
                GL.recordError(0x500); // GL_INVALID_ENUM
#if GL_ASSERTIONS
                err(`GL_INVALID_ENUM in glGet${type}v(${name}) and it returns null!`);
#endif
                return;
              }
            }
          } else if (result instanceof Float32Array ||
                     result instanceof Uint32Array ||
                     result instanceof Int32Array ||
                     result instanceof Array) {
            for (var i = 0; i < result.length; ++i) {
              switch (type) {
                case EM_FUNC_SIG_PARAM_I: {{{ makeSetValue('p', 'i*4', 'result[i]', 'i32') }}}; break;
                case EM_FUNC_SIG_PARAM_F: {{{ makeSetValue('p', 'i*4', 'result[i]', 'float') }}}; break;
                case EM_FUNC_SIG_PARAM_B: {{{ makeSetValue('p', 'i',   'result[i] ? 1 : 0', 'i8') }}}; break;
#if GL_ASSERTIONS
                default: abort(`internal glGet error, bad type: ${type}`);
#endif
              }
            }
            return;
          } else {
#if GL_TRACK_ERRORS
            try {
#endif
              ret = result.name | 0;
#if GL_TRACK_ERRORS
            } catch(e) {
              GL.recordError(0x500); // GL_INVALID_ENUM
              err(`GL_INVALID_ENUM in glGet${type}v: Unknown object returned from WebGL getParameter(${name_})! (error: ${e})`);
              return;
            }
#endif
          }
          break;
#if GL_TRACK_ERRORS
        default:
          GL.recordError(0x500); // GL_INVALID_ENUM
          err(`GL_INVALID_ENUM in glGet${type}v: Native code calling glGet${type}v(${name_}) and it returns ${result} of type ${typeof(result)}!`);
          return;
#endif
      }
    }

    switch (type) {
      case EM_FUNC_SIG_PARAM_J: writeI53ToI64(p, ret); break;
      case EM_FUNC_SIG_PARAM_I: {{{ makeSetValue('p', '0', 'ret', 'i32') }}}; break;
      case EM_FUNC_SIG_PARAM_F:   {{{ makeSetValue('p', '0', 'ret', 'float') }}}; break;
      case EM_FUNC_SIG_PARAM_B: {{{ makeSetValue('p', '0', 'ret ? 1 : 0', 'i8') }}}; break;
#if GL_ASSERTIONS
      default: abort(`internal glGet error, bad type: ${type}`);
#endif
    }
  },

  glGetIntegerv: (name_, p) => emscriptenWebGLGet(name_, p, EM_FUNC_SIG_PARAM_I),

  glGetFloatv: (name_, p) => emscriptenWebGLGet(name_, p, EM_FUNC_SIG_PARAM_F),

  glGetBooleanv: (name_, p) => emscriptenWebGLGet(name_, p, EM_FUNC_SIG_PARAM_B),

  glDeleteTextures: (n, textures) => {
    for (var i = 0; i < n; i++) {
      var id = {{{ makeGetValue('textures', 'i*4', 'i32') }}};
      var texture = GL.textures[id];
      // GL spec: "glDeleteTextures silently ignores 0s and names that do not
      // correspond to existing textures".
      if (!texture) continue;
      GLctx.deleteTexture(texture);
      texture.name = 0;
      GL.textures[id] = null;
    }
  },

  glCompressedTexImage2D: (target, level, internalFormat, width, height, border, imageSize, data) => {
    // `data` may be null here, which means "allocate uninitialized space but
    // don't upload" in GLES parlance, but `compressedTexImage2D` requires the
    // final data parameter, so we simply pass a heap view starting at zero
    // effectively uploading whatever happens to be near address zero.  See
    // https://github.com/emscripten-core/emscripten/issues/19300.
#if MAX_WEBGL_VERSION >= 2
    if ({{{ isCurrentContextWebGL2() }}}) {
      if (GLctx.currentPixelUnpackBufferBinding || !imageSize) {
        GLctx.compressedTexImage2D(target, level, internalFormat, width, height, border, imageSize, data);
        return;
      }
#if WEBGL_USE_GARBAGE_FREE_APIS
      GLctx.compressedTexImage2D(target, level, internalFormat, width, height, border, HEAPU8, data, imageSize);
      return;
#endif
    }
#endif
#if INCLUDE_WEBGL1_FALLBACK
    GLctx.compressedTexImage2D(target, level, internalFormat, width, height, border, HEAPU8.subarray(data, data + imageSize));
#endif
  },


  glCompressedTexSubImage2D: (target, level, xoffset, yoffset, width, height, format, imageSize, data) => {
#if MAX_WEBGL_VERSION >= 2
    if ({{{ isCurrentContextWebGL2() }}}) {
      if (GLctx.currentPixelUnpackBufferBinding || !imageSize) {
        GLctx.compressedTexSubImage2D(target, level, xoffset, yoffset, width, height, format, imageSize, data);
        return;
      }
#if WEBGL_USE_GARBAGE_FREE_APIS
      GLctx.compressedTexSubImage2D(target, level, xoffset, yoffset, width, height, format, HEAPU8, data, imageSize);
      return;
#endif
    }
#endif
#if INCLUDE_WEBGL1_FALLBACK
    GLctx.compressedTexSubImage2D(target, level, xoffset, yoffset, width, height, format, HEAPU8.subarray(data, data + imageSize));
#endif
  },

  $computeUnpackAlignedImageSize: (width, height, sizePerPixel) => {
    function roundedToNextMultipleOf(x, y) {
#if GL_ASSERTIONS
      assert((y & (y-1)) === 0, 'Unpack alignment must be a power of 2! (Allowed values per WebGL spec are 1, 2, 4 or 8)');
#endif
      return (x + y - 1) & -y;
    }
    var plainRowSize = (GL.unpackRowLength || width) * sizePerPixel;
    var alignedRowSize = roundedToNextMultipleOf(plainRowSize, GL.unpackAlignment);
    return height * alignedRowSize;
  },

  $colorChannelsInGlTextureFormat: (format) => {
    // Micro-optimizations for size: map format to size by subtracting smallest
    // enum value (0x1902) from all values first.  Also omit the most common
    // size value (1) from the list, which is assumed by formats not on the
    // list.
    var colorChannels = {
      // 0x1902 /* GL_DEPTH_COMPONENT */ - 0x1902: 1,
      // 0x1906 /* GL_ALPHA */ - 0x1902: 1,
      {{{ 0x1907 /* GL_RGB */ - 0x1902 }}}: 3,
      {{{ 0x1908 /* GL_RGBA */ - 0x1902 }}}: 4,
      // 0x1909 /* GL_LUMINANCE */ - 0x1902: 1,
      {{{ 0x190A /*GL_LUMINANCE_ALPHA*/ - 0x1902 }}}: 2,
      {{{ 0x8C40 /*(GL_SRGB_EXT)*/ - 0x1902 }}}: 3,
      {{{ 0x8C42 /*(GL_SRGB_ALPHA_EXT*/ - 0x1902 }}}: 4,
#if MAX_WEBGL_VERSION >= 2
      // 0x1903 /* GL_RED */ - 0x1902: 1,
      {{{ 0x8227 /*GL_RG*/ - 0x1902 }}}: 2,
      {{{ 0x8228 /*GL_RG_INTEGER*/ - 0x1902 }}}: 2,
      // 0x8D94 /* GL_RED_INTEGER */ - 0x1902: 1,
      {{{ 0x8D98 /*GL_RGB_INTEGER*/ - 0x1902 }}}: 3,
      {{{ 0x8D99 /*GL_RGBA_INTEGER*/ - 0x1902 }}}: 4
#endif
    };
#if GL_ASSERTIONS
    if (!colorChannels[format - 0x1902]
      && format != 0x1902 /* GL_DEPTH_COMPONENT */
      && format != 0x1906 /* GL_ALPHA */
      && format != 0x1909 /* GL_LUMINANCE */
      && format != 0x1903 /* GL_RED */
      && format != 0x8D94 /* GL_RED_INTEGER */) {
      err(`Invalid format=${ptrToString(format)} passed to function colorChannelsInGlTextureFormat()!`);
    }
#endif
    return colorChannels[format - 0x1902]||1;
  },

  $emscriptenWebGLGetTexPixelData: (type, format, width, height, pixels) => {
    var heap = heapObjectForWebGLType(type);
    var sizePerPixel = colorChannelsInGlTextureFormat(format) * heap.BYTES_PER_ELEMENT;
    var bytes = computeUnpackAlignedImageSize(width, height, sizePerPixel);
#if GL_ASSERTIONS
    assert(pixels % heap.BYTES_PER_ELEMENT == 0, 'Pointer to texture data passed to texture get function must be aligned to the byte size of the pixel type');
#endif
    return heap.subarray(toTypedArrayIndex(pixels, heap), toTypedArrayIndex(pixels + bytes, heap));
  },

  glTexImage2D: (target, level, internalFormat, width, height, border, format, type, pixels) => {
#if MAX_WEBGL_VERSION >= 2
#if WEBGL2_BACKWARDS_COMPATIBILITY_EMULATION
    if ({{{ isCurrentContextWebGL2() }}}) {
      // WebGL 1 unsized texture internalFormats are no longer supported in
      // WebGL 2, so patch those format enums to the ones that are present in
      // WebGL 2.
      if (format == 0x1902/*GL_DEPTH_COMPONENT*/ && internalFormat == 0x1902/*GL_DEPTH_COMPONENT*/ && type == 0x1405/*GL_UNSIGNED_INT*/) {
        internalFormat = 0x81A6 /*GL_DEPTH_COMPONENT24*/;
      }
      if (type == 0x8d61/*GL_HALF_FLOAT_OES*/) {
        type = 0x140B /*GL_HALF_FLOAT*/;
        if (format == 0x1908/*GL_RGBA*/ && internalFormat == 0x1908/*GL_RGBA*/) {
          internalFormat = 0x881A/*GL_RGBA16F*/;
        }
      }
      if (internalFormat == 0x84f9 /*GL_DEPTH_STENCIL*/) {
        internalFormat = 0x88F0 /*GL_DEPTH24_STENCIL8*/;
      }
      if (internalFormat == 0x1908 /*GL_RGBA*/ && type == 0x1406 /*GL_FLOAT*/) {
        internalFormat = 0x8814 /*GL_RGBA32F*/;
      }
    }
#endif
    if ({{{ isCurrentContextWebGL2() }}}) {
      if (GLctx.currentPixelUnpackBufferBinding) {
        GLctx.texImage2D(target, level, internalFormat, width, height, border, format, type, pixels);
        return;
      }
#if WEBGL_USE_GARBAGE_FREE_APIS
      if (pixels) {
        var heap = heapObjectForWebGLType(type);
        var index = toTypedArrayIndex(pixels, heap);
        GLctx.texImage2D(target, level, internalFormat, width, height, border, format, type, heap, index);
        return;
      }
#endif
    }
#endif
    var pixelData = pixels ? emscriptenWebGLGetTexPixelData(type, format, width, height, pixels) : null;
    GLctx.texImage2D(target, level, internalFormat, width, height, border, format, type, pixelData);
  },

  glTexSubImage2D: (target, level, xoffset, yoffset, width, height, format, type, pixels) => {
#if MAX_WEBGL_VERSION >= 2
#if WEBGL2_BACKWARDS_COMPATIBILITY_EMULATION
    if ({{{ isCurrentContextWebGL2() }}}) {
      // In WebGL 1 to do half float textures, one uses the type enum
      // GL_HALF_FLOAT_OES, but in WebGL 2 when half float textures were adopted
      // to the core spec, the enum changed value which breaks backwards
      // compatibility. Route old enum number to the new one.
      if (type == 0x8d61/*GL_HALF_FLOAT_OES*/) type = 0x140B /*GL_HALF_FLOAT*/;
    }
#endif
    if ({{{ isCurrentContextWebGL2() }}}) {
      if (GLctx.currentPixelUnpackBufferBinding) {
        GLctx.texSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels);
        return;
      }
#if WEBGL_USE_GARBAGE_FREE_APIS
      if (pixels) {
        var heap = heapObjectForWebGLType(type);
        GLctx.texSubImage2D(target, level, xoffset, yoffset, width, height, format, type, heap, toTypedArrayIndex(pixels, heap));
        return;
      }
#endif
    }
#endif
    var pixelData = pixels ? emscriptenWebGLGetTexPixelData(type, format, width, height, pixels) : null;
    GLctx.texSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixelData);
  },

  glReadPixels: (x, y, width, height, format, type, pixels) => {
#if MAX_WEBGL_VERSION >= 2
    if ({{{ isCurrentContextWebGL2() }}}) {
      if (GLctx.currentPixelPackBufferBinding) {
        GLctx.readPixels(x, y, width, height, format, type, pixels);
        return;
      }
#if WEBGL_USE_GARBAGE_FREE_APIS
      var heap = heapObjectForWebGLType(type);
      var target = toTypedArrayIndex(pixels, heap);
      GLctx.readPixels(x, y, width, height, format, type, heap, target);
      return;
#endif
    }
#endif
#if INCLUDE_WEBGL1_FALLBACK
    var pixelData = emscriptenWebGLGetTexPixelData(type, format, width, height, pixels);
    if (!pixelData) {
      GL.recordError(0x500/*GL_INVALID_ENUM*/);
#if GL_ASSERTIONS
      err(`GL_INVALID_ENUM in glReadPixels: Unrecognized combination of type=${type} and format=${format}!`);
#endif
      return;
    }
    GLctx.readPixels(x, y, width, height, format, type, pixelData);
#endif
  },

  glBindTexture: (target, texture) => {
#if GL_ASSERTIONS
    GL.validateGLObjectID(GL.textures, texture, 'glBindTexture', 'texture');
#endif
    GLctx.bindTexture(target, GL.textures[texture]);
  },

  glGetTexParameterfv: (target, pname, params) => {
    if (!params) {
      // GLES2 specification does not specify how to behave if params is a null
      // pointer. Since calling this function does not make sense if p == null,
      // issue a GL error to notify user about it.
#if GL_ASSERTIONS
      err(`GL_INVALID_VALUE in glGetTexParameterfv(target=${target}, pname=${pname}, params=0): Function called with null out pointer!`);
#endif
      GL.recordError(0x501 /* GL_INVALID_VALUE */);
      return;
    }
    {{{ makeSetValue('params', '0', 'GLctx.getTexParameter(target, pname)', 'float') }}};
  },

  glGetTexParameteriv: (target, pname, params) => {
    if (!params) {
      // GLES2 specification does not specify how to behave if params is a null
      // pointer. Since calling this function does not make sense if p == null,
      // issue a GL error to notify user about it.
#if GL_ASSERTIONS
      err(`GL_INVALID_VALUE in glGetTexParameteriv(target=${target}, pname=${pname}, params=0): Function called with null out pointer!`);
#endif
      GL.recordError(0x501 /* GL_INVALID_VALUE */);
      return;
    }
    {{{ makeSetValue('params', '0', 'GLctx.getTexParameter(target, pname)', 'i32') }}};
  },

  glTexParameterfv: (target, pname, params) => {
    var param = {{{ makeGetValue('params', '0', 'float') }}};
    GLctx.texParameterf(target, pname, param);
  },

  glTexParameteriv: (target, pname, params) => {
    var param = {{{ makeGetValue('params', '0', 'i32') }}};
    GLctx.texParameteri(target, pname, param);
  },

  glIsTexture: (id) => {
    var texture = GL.textures[id];
    if (!texture) return 0;
    return GLctx.isTexture(texture);
  },

  glGenBuffers: (n, buffers) => {
    GL.genObject(n, buffers, 'createBuffer', GL.buffers
#if GL_ASSERTIONS
    , 'glGenBuffers'
#endif
      );
  },

  glGenTextures: (n, textures) => {
    GL.genObject(n, textures, 'createTexture', GL.textures
#if GL_ASSERTIONS
    , 'glGenTextures'
#endif
      );
  },

  glDeleteBuffers: (n, buffers) => {
    for (var i = 0; i < n; i++) {
      var id = {{{ makeGetValue('buffers', 'i*4', 'i32') }}};
      var buffer = GL.buffers[id];

      // From spec: "glDeleteBuffers silently ignores 0's and names that do not
      // correspond to existing buffer objects."
      if (!buffer) continue;

      GLctx.deleteBuffer(buffer);
      buffer.name = 0;
      GL.buffers[id] = null;

#if FULL_ES2 || LEGACY_GL_EMULATION
      if (id == GLctx.currentArrayBufferBinding) GLctx.currentArrayBufferBinding = 0;
      if (id == GLctx.currentElementArrayBufferBinding) GLctx.currentElementArrayBufferBinding = 0;
#endif
#if MAX_WEBGL_VERSION >= 2
      if (id == GLctx.currentPixelPackBufferBinding) GLctx.currentPixelPackBufferBinding = 0;
      if (id == GLctx.currentPixelUnpackBufferBinding) GLctx.currentPixelUnpackBufferBinding = 0;
#endif
    }
  },

  glGetBufferParameteriv: (target, value, data) => {
    if (!data) {
      // GLES2 specification does not specify how to behave if data is a null
      // pointer. Since calling this function does not make sense if data ==
      // null, issue a GL error to notify user about it.
#if GL_ASSERTIONS
      err(`GL_INVALID_VALUE in glGetBufferParameteriv(target=${target}, value=${value}, data=0): Function called with null out pointer!`);
#endif
      GL.recordError(0x501 /* GL_INVALID_VALUE */);
      return;
    }
    {{{ makeSetValue('data', '0', 'GLctx.getBufferParameter(target, value)', 'i32') }}};
  },

  glBufferData: (target, size, data, usage) => {
#if LEGACY_GL_EMULATION
    switch (usage) { // fix usages, WebGL 1 only has *_DRAW
      case 0x88E1: // GL_STREAM_READ
      case 0x88E2: // GL_STREAM_COPY
        usage = 0x88E0; // GL_STREAM_DRAW
        break;
      case 0x88E5: // GL_STATIC_READ
      case 0x88E6: // GL_STATIC_COPY
        usage = 0x88E4; // GL_STATIC_DRAW
        break;
      case 0x88E9: // GL_DYNAMIC_READ
      case 0x88EA: // GL_DYNAMIC_COPY
        usage = 0x88E8; // GL_DYNAMIC_DRAW
        break;
    }
#endif

#if WEBGL_USE_GARBAGE_FREE_APIS
    if ({{{ isCurrentContextWebGL2() }}}) {
      // If size is zero, WebGL would interpret uploading the whole input
      // arraybuffer (starting from given offset), which would not make sense in
      // WebAssembly, so avoid uploading if size is zero. However we must still
      // call bufferData to establish a backing storage of zero bytes.
      if (data && size) {
        GLctx.bufferData(target, HEAPU8, usage, data, size);
      } else {
        GLctx.bufferData(target, size, usage);
      }
      return;
    }
#endif
#if INCLUDE_WEBGL1_FALLBACK
    // N.b. here first form specifies a heap subarray, second form an integer
    // size, so the ?: code here is polymorphic. It is advised to avoid
    // randomly mixing both uses in calling code, to avoid any potential JS
    // engine JIT issues.
    GLctx.bufferData(target, data ? HEAPU8.subarray(data, data+size) : size, usage);
#endif
  },

  // This cannot be simple alias because under wasm64 we need to be able modify
  // the function at compile time to provide automatically marshal of the pointer arguments.
  glBufferSubData: (target, offset, size, data) => webglBufferSubData(target, offset, size, data),

  // Queries EXT
  glGenQueriesEXT__sig: 'vip',
  glGenQueriesEXT: (n, ids) => {
    for (var i = 0; i < n; i++) {
      var query = GLctx.disjointTimerQueryExt['createQueryEXT']();
      if (!query) {
        GL.recordError(0x502 /* GL_INVALID_OPERATION */);
#if GL_ASSERTIONS
        err('GL_INVALID_OPERATION in glGenQueriesEXT: GLctx.disjointTimerQueryExt.createQueryEXT returned null - most likely GL context is lost');
#endif
        while (i < n) {{{ makeSetValue('ids', 'i++*4', 0, 'i32') }}};
        return;
      }
      var id = GL.getNewId(GL.queries);
      query.name = id;
      GL.queries[id] = query;
      {{{ makeSetValue('ids', 'i*4', 'id', 'i32') }}};
    }
  },

  glDeleteQueriesEXT__sig: 'vip',
  glDeleteQueriesEXT: (n, ids) => {
    for (var i = 0; i < n; i++) {
      var id = {{{ makeGetValue('ids', 'i*4', 'i32') }}};
      var query = GL.queries[id];
      if (!query) continue; // GL spec: "unused names in ids are ignored, as is the name zero."
      GLctx.disjointTimerQueryExt['deleteQueryEXT'](query);
      GL.queries[id] = null;
    }
  },

  glIsQueryEXT__sig: 'ii',
  glIsQueryEXT: (id) => {
    var query = GL.queries[id];
    if (!query) return 0;
    return GLctx.disjointTimerQueryExt['isQueryEXT'](query);
  },

  glBeginQueryEXT__sig: 'vii',
  glBeginQueryEXT: (target, id) => {
#if GL_ASSERTIONS
    GL.validateGLObjectID(GL.queries, id, 'glBeginQueryEXT', 'id');
#endif
    GLctx.disjointTimerQueryExt['beginQueryEXT'](target, GL.queries[id]);
  },

  glEndQueryEXT__sig: 'vi',
  glEndQueryEXT: (target) => {
    GLctx.disjointTimerQueryExt['endQueryEXT'](target);
  },

  // This one is either from EXT_disjoint_timer_query on WebGL 1 (taking a
  // WebGLTimerQueryEXT) or from EXT_disjoint_timer_query_webgl2 (taking a
  // WebGLQuery)
  glQueryCounterEXT__sig: 'vii',
  glQueryCounterEXT: (id, target) => {
#if GL_ASSERTIONS
    GL.validateGLObjectID(GL.queries, id, 'glQueryCounterEXT', 'id');
#endif
    GLctx.disjointTimerQueryExt['queryCounterEXT'](GL.queries[id], target);
  },

  glGetQueryivEXT__sig: 'viip',
  glGetQueryivEXT: (target, pname, params) => {
    if (!params) {
      // GLES2 specification does not specify how to behave if params is a null pointer. Since calling this function does not make sense
      // if p == null, issue a GL error to notify user about it.
#if GL_ASSERTIONS
      err(`GL_INVALID_VALUE in glGetQueryivEXT(target=${target}, pname=${pname}, params=0): Function called with null out pointer!`);
#endif
      GL.recordError(0x501 /* GL_INVALID_VALUE */);
      return;
    }
    {{{ makeSetValue('params', '0', 'GLctx.disjointTimerQueryExt[\'getQueryEXT\'](target, pname)', 'i32') }}};
  },

  glGetQueryObjectivEXT__sig: 'viip',
  glGetQueryObjectivEXT: (id, pname, params) => {
    if (!params) {
      // GLES2 specification does not specify how to behave if params is a null pointer. Since calling this function does not make sense
      // if p == null, issue a GL error to notify user about it.
#if GL_ASSERTIONS
      err(`GL_INVALID_VALUE in glGetQueryObject(u)ivEXT(id=${id}, pname=${pname}, params=0): Function called with null out pointer!`);
#endif
      GL.recordError(0x501 /* GL_INVALID_VALUE */);
      return;
    }
#if GL_ASSERTIONS
    GL.validateGLObjectID(GL.queries, id, 'glGetQueryObjectivEXT', 'id');
#endif
    var query = GL.queries[id];
    var param = GLctx.disjointTimerQueryExt['getQueryObjectEXT'](query, pname);
    var ret;
    if (typeof param === 'boolean') {
      ret = param ? 1 : 0;
    } else {
      ret = param;
    }
    {{{ makeSetValue('params', '0', 'ret', 'i32') }}};
  },
  glGetQueryObjectuivEXT: 'glGetQueryObjectivEXT',

  glGetQueryObjecti64vEXT__sig: 'viip',
  glGetQueryObjecti64vEXT: (id, pname, params) => {
    if (!params) {
      // GLES2 specification does not specify how to behave if params is a null pointer. Since calling this function does not make sense
      // if p == null, issue a GL error to notify user about it.
#if GL_ASSERTIONS
      err(`GL_INVALID_VALUE in glGetQueryObject(u)i64vEXT(id=${id}, pname=${pname}, params=0): Function called with null out pointer!`);
#endif
      GL.recordError(0x501 /* GL_INVALID_VALUE */);
      return;
    }
#if GL_ASSERTIONS
    GL.validateGLObjectID(GL.queries, id, 'glGetQueryObjecti64vEXT', 'id');
#endif
    var query = GL.queries[id];
    var param;
#if MAX_WEBGL_VERSION >= 2
    if (GL.currentContext.version < 2)
#endif
    {
      param = GLctx.disjointTimerQueryExt['getQueryObjectEXT'](query, pname);
    }
#if MAX_WEBGL_VERSION >= 2
    else {
      param = GLctx.getQueryParameter(query, pname);
    }
#endif
    var ret;
    if (typeof param === 'boolean') {
      ret = param ? 1 : 0;
    } else {
      ret = param;
    }
    writeI53ToI64(params, ret);
  },
  glGetQueryObjectui64vEXT: 'glGetQueryObjecti64vEXT',

  glIsBuffer: (buffer) => {
    var b = GL.buffers[buffer];
    if (!b) return 0;
    return GLctx.isBuffer(b);
  },

  glGenRenderbuffers: (n, renderbuffers) => {
    GL.genObject(n, renderbuffers, 'createRenderbuffer', GL.renderbuffers
#if GL_ASSERTIONS
    , 'glGenRenderbuffers'
#endif
      );
  },

  glDeleteRenderbuffers: (n, renderbuffers) => {
    for (var i = 0; i < n; i++) {
      var id = {{{ makeGetValue('renderbuffers', 'i*4', 'i32') }}};
      var renderbuffer = GL.renderbuffers[id];
      if (!renderbuffer) continue; // GL spec: "glDeleteRenderbuffers silently ignores 0s and names that do not correspond to existing renderbuffer objects".
      GLctx.deleteRenderbuffer(renderbuffer);
      renderbuffer.name = 0;
      GL.renderbuffers[id] = null;
    }
  },

  glBindRenderbuffer: (target, renderbuffer) => {
#if GL_ASSERTIONS
    GL.validateGLObjectID(GL.renderbuffers, renderbuffer, 'glBindRenderbuffer', 'renderbuffer');
#endif
    GLctx.bindRenderbuffer(target, GL.renderbuffers[renderbuffer]);
  },

  glGetRenderbufferParameteriv: (target, pname, params) => {
    if (!params) {
      // GLES2 specification does not specify how to behave if params is a null pointer. Since calling this function does not make sense
      // if params == null, issue a GL error to notify user about it.
#if GL_ASSERTIONS
      err(`GL_INVALID_VALUE in glGetRenderbufferParameteriv(target=${target}, pname=${pname}, params=0): Function called with null out pointer!`);
#endif
      GL.recordError(0x501 /* GL_INVALID_VALUE */);
      return;
    }
    {{{ makeSetValue('params', '0', 'GLctx.getRenderbufferParameter(target, pname)', 'i32') }}};
  },

  glIsRenderbuffer: (renderbuffer) => {
    var rb = GL.renderbuffers[renderbuffer];
    if (!rb) return 0;
    return GLctx.isRenderbuffer(rb);
  },

  // This function intentionally assigns `HEAP32[x] = someBoolean;` Don't let
  // Closure mind about that.
  $emscriptenWebGLGetUniform__docs: '/** @suppress{checkTypes} */',
  $emscriptenWebGLGetUniform: (program, location, params, type) => {
    if (!params) {
      // GLES2 specification does not specify how to behave if params is a null
      // pointer. Since calling this function does not make sense if params ==
      // null, issue a GL error to notify user about it.
#if GL_ASSERTIONS
      err(`GL_INVALID_VALUE in glGetUniform*v(program=${program}, location=${location}, params=0): Function called with null out pointer!`);
#endif
      GL.recordError(0x501 /* GL_INVALID_VALUE */);
      return;
    }
#if GL_ASSERTIONS
    GL.validateGLObjectID(GL.programs, program, 'glGetUniform*v', 'program');
    GL.validateGLObjectID(program.uniformLocsById, location, 'glGetUniform*v', 'location');
#endif
    program = GL.programs[program];
    webglPrepareUniformLocationsBeforeFirstUse(program);
    var data = GLctx.getUniform(program, webglGetProgramUniformLocation(program, location));
    if (typeof data === 'number' || typeof data === 'boolean') {
      switch (type) {
        case EM_FUNC_SIG_PARAM_I: {{{ makeSetValue('params', '0', 'data', 'i32') }}}; break;
        case EM_FUNC_SIG_PARAM_F: {{{ makeSetValue('params', '0', 'data', 'float') }}}; break;
#if GL_ASSERTIONS
        default: abort('internal emscriptenWebGLGetUniform() error, bad type: ' + type);
#endif
      }
    } else {
      for (var i = 0; i < data.length; i++) {
        switch (type) {
          case EM_FUNC_SIG_PARAM_I: {{{ makeSetValue('params', 'i*4', 'data[i]', 'i32') }}}; break;
          case EM_FUNC_SIG_PARAM_F: {{{ makeSetValue('params', 'i*4', 'data[i]', 'float') }}}; break;
#if GL_ASSERTIONS
          default: abort('internal emscriptenWebGLGetUniform() error, bad type: ' + type);
#endif
        }
      }
    }
  },

  glGetUniformfv: (program, location, params) => {
    emscriptenWebGLGetUniform(program, location, params, EM_FUNC_SIG_PARAM_F);
  },

  glGetUniformiv: (program, location, params) => {
    emscriptenWebGLGetUniform(program, location, params, EM_FUNC_SIG_PARAM_I);
  },

  // Returns the WebGLUniformLocation object corresponding to the location index
  // integer on the currently active shader in this GL context.
  $webglGetProgramUniformLocation: (program, location) => {
#if !GL_TRACK_ERRORS && ASSERTIONS
    // In -sGL_TRACK_ERRORS=0 build mode do not allow calling glUniform*()
    // without an active GL program.
    assert(program, 'When building with !GL_TRACK_ERRORS, program cannot be null, in a call to webglGetProgramUniformLocation()');
#endif

#if GL_TRACK_ERRORS
    if (program) {
#endif
#if GL_EXPLICIT_UNIFORM_LOCATION
      // Ensure `uniformLocsById`/`uniformArrayNamesById` are populated. Without
      // this, calling `glUniform*()` on a freshly linked program before any
      // `glGetUniformLocation()` silently no-ops: `glLinkProgram` resets
      // `uniformLocsById` to 0 and only `$webglPrepareUniformLocationsBeforeFirstUse`
      // refills it. The call below is idempotent (guards on `!uniformLocsById`).
      webglPrepareUniformLocationsBeforeFirstUse(program);
#endif
      var webglLoc = program.uniformLocsById[location];
      // program.uniformLocsById[location] stores either an integer, or a
      // WebGLUniformLocation.
      // If an integer, we have not yet bound the location, so do it now. The
      // integer value specifies the array index we should bind to.
      if (typeof webglLoc === 'number') {
        program.uniformLocsById[location] = webglLoc = GLctx.getUniformLocation(program, program.uniformArrayNamesById[location] + (webglLoc > 0 ? `[${webglLoc}]` : ''));
      }
      // Else an already cached WebGLUniformLocation, return it.
      return webglLoc;
#if GL_TRACK_ERRORS
    } else {
      GL.recordError(0x502/*GL_INVALID_OPERATION*/);
    }
#endif
  },

  $webglGetUniformLocation: (location) => {
#if !GL_TRACK_ERRORS && ASSERTIONS
    // In -sGL_TRACK_ERRORS=0 build mode do not allow calling glUniform*()
    // without an active GL program.
    assert(GLctx.currentProgram, 'Attempted to call glUniform*()/webglGetUniformLocation() without an active GL program set! (build with -sGL_TRACK_ERRORS for standards-conformant behavior)');
#endif

    return webglGetProgramUniformLocation(GLctx.currentProgram, location);
  },

  $webglPrepareUniformLocationsBeforeFirstUse: (program) => {
    var uniformLocsById = program.uniformLocsById, // Maps GLuint -> WebGLUniformLocation
      uniformSizeAndIdsByName = program.uniformSizeAndIdsByName, // Maps name -> [uniform array length, GLuint]
      i, j;

    // On the first time invocation of glGetUniformLocation on this shader program:
    // initialize cache data structures and discover which uniforms are arrays.
    if (!uniformLocsById) {
      // maps GLint integer locations to WebGLUniformLocations
      program.uniformLocsById = uniformLocsById = {};
      // maps integer locations back to uniform name strings, so that we can lazily fetch uniform array locations
      program.uniformArrayNamesById = {};

      var numActiveUniforms = GLctx.getProgramParameter(program, 0x8B86/*GL_ACTIVE_UNIFORMS*/);
      for (i = 0; i < numActiveUniforms; ++i) {
        var u = GLctx.getActiveUniform(program, i);
        var nm = u.name;
        var sz = u.size;
        var lb = webglGetLeftBracePos(nm);
        var arrayName = lb > 0 ? nm.slice(0, lb) : nm;

#if GL_EXPLICIT_UNIFORM_LOCATION
        // Acquire the preset location from the explicit uniform location if one was specified, or
        // programmatically assign a new one if not.
        var id = uniformSizeAndIdsByName[arrayName] ? uniformSizeAndIdsByName[arrayName][1] : program.uniformIdCounter;
        program.uniformIdCounter = Math.max(id + sz, program.uniformIdCounter);
#else
        // Assign a new location.
        var id = program.uniformIdCounter;
        program.uniformIdCounter += sz;
#endif
        // Eagerly get the location of the uniformArray[0] base element.
        // The remaining indices >0 will be left for lazy evaluation to
        // improve performance. Those may never be needed to fetch, if the
        // application fills arrays always in full starting from the first
        // element of the array.
        uniformSizeAndIdsByName[arrayName] = [sz, id];

        // Store placeholder integers in place that highlight that these
        // >0 index locations are array indices pending population.
        for (j = 0; j < sz; ++j) {
          uniformLocsById[id] = j;
          program.uniformArrayNamesById[id++] = arrayName;
        }
      }
    }
  },

  // Returns the index of '[' character in a uniform that represents an array
  // of uniforms (e.g. colors[10])
  // Closure does counterproductive inlining:
  // https://github.com/google/closure-compiler/issues/3203, so prevent inlining
  // manually.
  $webglGetLeftBracePos__docs: '/** @noinline */',
  $webglGetLeftBracePos: (name) => name.slice(-1) == ']' && name.lastIndexOf('['),

  glGetUniformLocation: (program, name) => {

#if GL_ASSERTIONS
    GL.validateGLObjectID(GL.programs, program, 'glGetUniformLocation', 'program');
#endif
    name = UTF8ToString(name);

#if GL_ASSERTIONS
    assert(!name.includes(' '), `Uniform names passed to glGetUniformLocation() should not contain spaces! (received "${name}")`);
#endif

    if (program = GL.programs[program]) {
      webglPrepareUniformLocationsBeforeFirstUse(program);
      var uniformLocsById = program.uniformLocsById; // Maps GLuint -> WebGLUniformLocation
      var arrayIndex = 0;
      var uniformBaseName = name;

      // Invariant: when populating integer IDs for uniform locations, we must
      // maintain the precondition that arrays reside in contiguous addresses,
      // i.e. for a 'vec4 colors[10];', colors[4] must be at location
      // colors[0]+4.  However, user might call glGetUniformLocation(program,
      // "colors") for an array, so we cannot discover based on the user input
      // arguments whether the uniform we are dealing with is an array. The only
      // way to discover which uniforms are arrays is to enumerate over all the
      // active uniforms in the program.
      var leftBrace = webglGetLeftBracePos(name);

      // If user passed an array accessor "[index]", parse the array index off the accessor.
      if (leftBrace > 0) {
  #if GL_ASSERTIONS
        assert(name.slice(leftBrace + 1).length == 1 || !isNaN(jstoi_q(name.slice(leftBrace + 1))), `Malformed input parameter name "${name}" passed to glGetUniformLocation!`);
  #endif
        arrayIndex = jstoi_q(name.slice(leftBrace + 1)) >>> 0; // "index]", coerce parseInt(']') with >>>0 to treat "foo[]" as "foo[0]" and foo[-1] as unsigned out-of-bounds.
        uniformBaseName = name.slice(0, leftBrace);
      }

      // Have we cached the location of this uniform before?
      // A pair [array length, GLint of the uniform location]
      var sizeAndId = program.uniformSizeAndIdsByName[uniformBaseName];

      // If a uniform with this name exists, and if its index is within the
      // array limits (if it's even an array), query the WebGLlocation, or
      // return an existing cached location.
      if (sizeAndId && arrayIndex < sizeAndId[0]) {
        arrayIndex += sizeAndId[1]; // Add the base location of the uniform to the array index offset.
        if ((uniformLocsById[arrayIndex] = uniformLocsById[arrayIndex] || GLctx.getUniformLocation(program, name))) {
          return arrayIndex;
        }
      }
    }
#if GL_TRACK_ERRORS
    else {
      // N.b. we are currently unable to distinguish between GL program IDs that
      // never existed vs GL program IDs that have been deleted, so report
      // GL_INVALID_VALUE in both cases.
      GL.recordError(0x501 /* GL_INVALID_VALUE */);
    }
#endif
    return -1;
  },

  // This function intentionally assigns `HEAP32[x] = someBoolean;` Don't let
  // Closure mind about that.
  $emscriptenWebGLGetVertexAttrib__docs: '/** @suppress{checkTypes} */',
  $emscriptenWebGLGetVertexAttrib: (index, pname, params, type) => {
    if (!params) {
      // GLES2 specification does not specify how to behave if params is a null
      // pointer. Since calling this function does not make sense if params ==
      // null, issue a GL error to notify user about it.
#if GL_ASSERTIONS
      err(`GL_INVALID_VALUE in glGetVertexAttrib*v(index=${index}, pname=${pname}, params=0): Function called with null out pointer!`);
#endif
      GL.recordError(0x501 /* GL_INVALID_VALUE */);
      return;
    }
#if FULL_ES2
    if (GL.currentContext.clientBuffers[index].enabled) {
      err('glGetVertexAttrib*v on client-side array: not supported, bad data returned');
    }
#endif
    var data = GLctx.getVertexAttrib(index, pname);
    if (pname == 0x889F/*VERTEX_ATTRIB_ARRAY_BUFFER_BINDING*/) {
      {{{ makeSetValue('params', '0', 'data && data["name"]', 'i32') }}};
    } else if (typeof data === 'number' || typeof data === 'boolean') {
      switch (type) {
        case EM_FUNC_SIG_PARAM_I: {{{ makeSetValue('params', '0', 'data', 'i32') }}}; break;
        case EM_FUNC_SIG_PARAM_F: {{{ makeSetValue('params', '0', 'data', 'float') }}}; break;
        case EM_FUNC_SIG_PARAM_F2I: {{{ makeSetValue('params', '0', 'Math.fround(data)', 'i32') }}}; break;
#if GL_ASSERTIONS
        default: abort('internal emscriptenWebGLGetVertexAttrib() error, bad type: ' + type);
#endif
      }
    } else {
      for (var i = 0; i < data.length; i++) {
        switch (type) {
          case EM_FUNC_SIG_PARAM_I: {{{ makeSetValue('params', 'i*4', 'data[i]', 'i32') }}}; break;
          case EM_FUNC_SIG_PARAM_F: {{{ makeSetValue('params', 'i*4', 'data[i]', 'float') }}}; break;
          case EM_FUNC_SIG_PARAM_F2I: {{{ makeSetValue('params', 'i*4', 'Math.fround(data[i])', 'i32') }}}; break;
#if GL_ASSERTIONS
          default: abort('internal emscriptenWebGLGetVertexAttrib() error, bad type: ' + type);
#endif
        }
      }
    }
  },

  glGetVertexAttribfv: (index, pname, params) => {
    // N.B. This function may only be called if the vertex attribute was
    // specified using the function glVertexAttrib*f(), otherwise the results
    // are undefined. (GLES3 spec 6.1.12)
    emscriptenWebGLGetVertexAttrib(index, pname, params, EM_FUNC_SIG_PARAM_F);
  },

  glGetVertexAttribiv: (index, pname, params) => {
    // N.B. This function may only be called if the vertex attribute was
    // specified using the function glVertexAttrib*f(), otherwise the results
    // are undefined. (GLES3 spec 6.1.12)
    emscriptenWebGLGetVertexAttrib(index, pname, params, EM_FUNC_SIG_PARAM_F2I);
  },

  glGetVertexAttribPointerv: (index, pname, pointer) => {
    if (!pointer) {
      // GLES2 specification does not specify how to behave if pointer is a null
      // pointer. Since calling this function does not make sense if pointer ==
      // null, issue a GL error to notify user about it.
#if GL_ASSERTIONS
      err(`GL_INVALID_VALUE in glGetVertexAttribPointerv(index=${index}, pname=${pname}, pointer=0): Function called with null out pointer!`);
#endif
      GL.recordError(0x501 /* GL_INVALID_VALUE */);
      return;
    }
#if FULL_ES2
    if (GL.currentContext.clientBuffers[index].enabled) {
      err('glGetVertexAttribPointer on client-side array: not supported, bad data returned');
    }
#endif
    {{{ makeSetValue('pointer', '0', 'GLctx.getVertexAttribOffset(index, pname)', 'i32') }}};
  },

  glUniform1f: (location, v0) => {
#if GL_ASSERTIONS
    GL.validateGLObjectID(GLctx.currentProgram.uniformLocsById, location, 'glUniform1f', 'location');
#endif
    GLctx.uniform1f(webglGetUniformLocation(location), v0);
  },

  glUniform2f: (location, v0, v1) => {
#if GL_ASSERTIONS
    GL.validateGLObjectID(GLctx.currentProgram.uniformLocsById, location, 'glUniform2f', 'location');
#endif
    GLctx.uniform2f(webglGetUniformLocation(location), v0, v1);
  },

  glUniform3f: (location, v0, v1, v2) => {
#if GL_ASSERTIONS
    GL.validateGLObjectID(GLctx.currentProgram.uniformLocsById, location, 'glUniform3f', 'location');
#endif
    GLctx.uniform3f(webglGetUniformLocation(location), v0, v1, v2);
  },

  glUniform4f: (location, v0, v1, v2, v3) => {
#if GL_ASSERTIONS
    GL.validateGLObjectID(GLctx.currentProgram.uniformLocsById, location, 'glUniform4f', 'location');
#endif
    GLctx.uniform4f(webglGetUniformLocation(location), v0, v1, v2, v3);
  },

  glUniform1i: (location, v0) => {
#if GL_ASSERTIONS
    GL.validateGLObjectID(GLctx.currentProgram.uniformLocsById, location, 'glUniform1i', 'location');
#endif
    GLctx.uniform1i(webglGetUniformLocation(location), v0);
  },

  glUniform2i: (location, v0, v1) => {
#if GL_ASSERTIONS
    GL.validateGLObjectID(GLctx.currentProgram.uniformLocsById, location, 'glUniform2i', 'location');
#endif
    GLctx.uniform2i(webglGetUniformLocation(location), v0, v1);
  },

  glUniform3i: (location, v0, v1, v2) => {
#if GL_ASSERTIONS
    GL.validateGLObjectID(GLctx.currentProgram.uniformLocsById, location, 'glUniform3i', 'location');
#endif
    GLctx.uniform3i(webglGetUniformLocation(location), v0, v1, v2);
  },

  glUniform4i: (location, v0, v1, v2, v3) => {
#if GL_ASSERTIONS
    GL.validateGLObjectID(GLctx.currentProgram.uniformLocsById, location, 'glUniform4i', 'location');
#endif
    GLctx.uniform4i(webglGetUniformLocation(location), v0, v1, v2, v3);
  },

  glUniform1iv: (location, count, value) => {
#if GL_ASSERTIONS
    GL.validateGLObjectID(GLctx.currentProgram.uniformLocsById, location, 'glUniform1iv', 'location');
    assert((value & 3) == 0, 'pointer passed to glUniform1iv must be 4-byte aligned');
#endif

#if MIN_WEBGL_VERSION >= 2 && WEBGL_USE_GARBAGE_FREE_APIS
#if GL_ASSERTIONS
    assert(GL.currentContext.version >= 2);
#endif
    count && GLctx.uniform1iv(webglGetUniformLocation(location), HEAP32, {{{ getHeapOffset('value', 'i32') }}}, count);
#else

#if WEBGL_USE_GARBAGE_FREE_APIS
    if ({{{ isCurrentContextWebGL2() }}}) {
      count && GLctx.uniform1iv(webglGetUniformLocation(location), HEAP32, {{{ getHeapOffset('value', 'i32') }}}, count);
      return;
    }
#endif

#if GL_POOL_TEMP_BUFFERS
    if (count <= GL_POOL_TEMP_BUFFERS_SIZE) {
      // avoid allocation when uploading few enough uniforms
      var view = miniTempWebGLIntBuffers[count];
      for (var i = 0; i < count; ++i) {
        view[i] = {{{ makeGetValue('value', '4*i', 'i32') }}};
      }
    } else
#endif
    {
      var view = {{{ makeHEAPView('32', 'value', 'value+count*4') }}};
    }
    GLctx.uniform1iv(webglGetUniformLocation(location), view);
#endif // MIN_WEBGL_VERSION >= 2 && WEBGL_USE_GARBAGE_FREE_APIS
  },

  glUniform2iv: (location, count, value) => {
#if GL_ASSERTIONS
    GL.validateGLObjectID(GLctx.currentProgram.uniformLocsById, location, 'glUniform2iv', 'location');
    assert((value & 3) == 0, 'pointer passed to glUniform2iv must be 4-byte aligned');
#endif

#if MIN_WEBGL_VERSION >= 2 && WEBGL_USE_GARBAGE_FREE_APIS
#if GL_ASSERTIONS
    assert(GL.currentContext.version >= 2);
#endif
    count && GLctx.uniform2iv(webglGetUniformLocation(location), HEAP32, {{{ getHeapOffset('value', 'i32') }}}, count*2);
#else

#if WEBGL_USE_GARBAGE_FREE_APIS
    if ({{{ isCurrentContextWebGL2() }}}) {
      count && GLctx.uniform2iv(webglGetUniformLocation(location), HEAP32, {{{ getHeapOffset('value', 'i32') }}}, count*2);
      return;
    }
#endif

#if GL_POOL_TEMP_BUFFERS
    if (count <= GL_POOL_TEMP_BUFFERS_SIZE / 2) {
      // avoid allocation when uploading few enough uniforms
      count *= 2;
      var view = miniTempWebGLIntBuffers[count];
      for (var i = 0; i < count; i += 2) {
        view[i] = {{{ makeGetValue('value', '4*i', 'i32') }}};
        view[i+1] = {{{ makeGetValue('value', '4*i+4', 'i32') }}};
      }
    } else
#endif
    {
      var view = {{{ makeHEAPView('32', 'value', 'value+count*8') }}};
    }
    GLctx.uniform2iv(webglGetUniformLocation(location), view);
#endif // MIN_WEBGL_VERSION >= 2 && WEBGL_USE_GARBAGE_FREE_APIS
  },

  glUniform3iv: (location, count, value) => {
#if GL_ASSERTIONS
    GL.validateGLObjectID(GLctx.currentProgram.uniformLocsById, location, 'glUniform3iv', 'location');
    assert((value & 3) == 0, 'pointer passed to glUniform3iv must be 4-byte aligned');
#endif

#if MIN_WEBGL_VERSION >= 2 && WEBGL_USE_GARBAGE_FREE_APIS
#if GL_ASSERTIONS
    assert(GL.currentContext.version >= 2);
#endif
    count && GLctx.uniform3iv(webglGetUniformLocation(location), HEAP32, {{{ getHeapOffset('value', 'i32') }}}, count*3);
#else

#if WEBGL_USE_GARBAGE_FREE_APIS
    if ({{{ isCurrentContextWebGL2() }}}) {
      count && GLctx.uniform3iv(webglGetUniformLocation(location), HEAP32, {{{ getHeapOffset('value', 'i32') }}}, count*3);
      return;
    }
#endif

#if GL_POOL_TEMP_BUFFERS
    if (count <= GL_POOL_TEMP_BUFFERS_SIZE / 3) {
      // avoid allocation when uploading few enough uniforms
      count *= 3;
      var view = miniTempWebGLIntBuffers[count];
      for (var i = 0; i < count; i += 3) {
        view[i] = {{{ makeGetValue('value', '4*i', 'i32') }}};
        view[i+1] = {{{ makeGetValue('value', '4*i+4', 'i32') }}};
        view[i+2] = {{{ makeGetValue('value', '4*i+8', 'i32') }}};
      }
    } else
#endif
    {
      var view = {{{ makeHEAPView('32', 'value', 'value+count*12') }}};
    }
    GLctx.uniform3iv(webglGetUniformLocation(location), view);
#endif // MIN_WEBGL_VERSION >= 2 && WEBGL_USE_GARBAGE_FREE_APIS
  },

  glUniform4iv: (location, count, value) => {
#if GL_ASSERTIONS
    GL.validateGLObjectID(GLctx.currentProgram.uniformLocsById, location, 'glUniform4iv', 'location');
    assert((value & 3) == 0, 'pointer passed to glUniform4iv must be 4-byte aligned');
#endif

#if MIN_WEBGL_VERSION >= 2 && WEBGL_USE_GARBAGE_FREE_APIS
#if GL_ASSERTIONS
    assert(GL.currentContext.version >= 2);
#endif
    count && GLctx.uniform4iv(webglGetUniformLocation(location), HEAP32, {{{ getHeapOffset('value', 'i32') }}}, count*4);
#else

#if WEBGL_USE_GARBAGE_FREE_APIS
    if ({{{ isCurrentContextWebGL2() }}}) {
      count && GLctx.uniform4iv(webglGetUniformLocation(location), HEAP32, {{{ getHeapOffset('value', 'i32') }}}, count*4);
      return;
    }
#endif

#if GL_POOL_TEMP_BUFFERS
    if (count <= GL_POOL_TEMP_BUFFERS_SIZE / 4) {
      // avoid allocation when uploading few enough uniforms
      count *= 4;
      var view = miniTempWebGLIntBuffers[count];
      for (var i = 0; i < count; i += 4) {
        view[i] = {{{ makeGetValue('value', '4*i', 'i32') }}};
        view[i+1] = {{{ makeGetValue('value', '4*i+4', 'i32') }}};
        view[i+2] = {{{ makeGetValue('value', '4*i+8', 'i32') }}};
        view[i+3] = {{{ makeGetValue('value', '4*i+12', 'i32') }}};
      }
    } else
#endif
    {
      var view = {{{ makeHEAPView('32', 'value', 'value+count*16') }}};
    }
    GLctx.uniform4iv(webglGetUniformLocation(location), view);
#endif // MIN_WEBGL_VERSION >= 2 && WEBGL_USE_GARBAGE_FREE_APIS
  },

  glUniform1fv: (location, count, value) => {
#if GL_ASSERTIONS
    GL.validateGLObjectID(GLctx.currentProgram.uniformLocsById, location, 'glUniform1fv', 'location');
    assert((value & 3) == 0, 'pointer passed to glUniform1fv must be 4-byte aligned');
#endif

#if MIN_WEBGL_VERSION >= 2 && WEBGL_USE_GARBAGE_FREE_APIS
#if GL_ASSERTIONS
    assert(GL.currentContext.version >= 2);
#endif
    count && GLctx.uniform1fv(webglGetUniformLocation(location), HEAPF32, {{{ getHeapOffset('value', 'float') }}}, count);
#else

#if WEBGL_USE_GARBAGE_FREE_APIS
    if ({{{ isCurrentContextWebGL2() }}}) {
      count && GLctx.uniform1fv(webglGetUniformLocation(location), HEAPF32, {{{ getHeapOffset('value', 'float') }}}, count);
      return;
    }
#endif

#if GL_POOL_TEMP_BUFFERS
    if (count <= GL_POOL_TEMP_BUFFERS_SIZE) {
      // avoid allocation when uploading few enough uniforms
      var view = miniTempWebGLFloatBuffers[count];
      for (var i = 0; i < count; ++i) {
        view[i] = {{{ makeGetValue('value', '4*i', 'float') }}};
      }
    } else
#endif
    {
      var view = {{{ makeHEAPView('F32', 'value', 'value+count*4') }}};
    }
    GLctx.uniform1fv(webglGetUniformLocation(location), view);
#endif // MIN_WEBGL_VERSION >= 2 && WEBGL_USE_GARBAGE_FREE_APIS
  },

  glUniform2fv: (location, count, value) => {
#if GL_ASSERTIONS
    GL.validateGLObjectID(GLctx.currentProgram.uniformLocsById, location, 'glUniform2fv', 'location');
    assert((value & 3) == 0, 'pointer passed to glUniform2fv must be 4-byte aligned');
#endif

#if MIN_WEBGL_VERSION >= 2 && WEBGL_USE_GARBAGE_FREE_APIS
#if GL_ASSERTIONS
    assert(GL.currentContext.version >= 2);
#endif
    count && GLctx.uniform2fv(webglGetUniformLocation(location), HEAPF32, {{{ getHeapOffset('value', 'float') }}}, count*2);
#else

#if WEBGL_USE_GARBAGE_FREE_APIS
    if ({{{ isCurrentContextWebGL2() }}}) {
      count && GLctx.uniform2fv(webglGetUniformLocation(location), HEAPF32, {{{ getHeapOffset('value', 'float') }}}, count*2);
      return;
    }
#endif

#if GL_POOL_TEMP_BUFFERS
    if (count <= GL_POOL_TEMP_BUFFERS_SIZE / 2) {
      // avoid allocation when uploading few enough uniforms
      count *= 2;
      var view = miniTempWebGLFloatBuffers[count];
      for (var i = 0; i < count; i += 2) {
        view[i] = {{{ makeGetValue('value', '4*i', 'float') }}};
        view[i+1] = {{{ makeGetValue('value', '4*i+4', 'float') }}};
      }
    } else
#endif
    {
      var view = {{{ makeHEAPView('F32', 'value', 'value+count*8') }}};
    }
    GLctx.uniform2fv(webglGetUniformLocation(location), view);
#endif // MIN_WEBGL_VERSION >= 2 && WEBGL_USE_GARBAGE_FREE_APIS
  },

  glUniform3fv: (location, count, value) => {
#if GL_ASSERTIONS
    GL.validateGLObjectID(GLctx.currentProgram.uniformLocsById, location, 'glUniform3fv', 'location');
    assert((value % 4) == 0, 'pointer passed to glUniform3fv must be 4-byte aligned');
#endif

#if MIN_WEBGL_VERSION >= 2 && WEBGL_USE_GARBAGE_FREE_APIS
#if GL_ASSERTIONS
    assert(GL.currentContext.version >= 2);
#endif
    count && GLctx.uniform3fv(webglGetUniformLocation(location), HEAPF32, {{{ getHeapOffset('value', 'float') }}}, count*3);
#else

#if WEBGL_USE_GARBAGE_FREE_APIS
    if ({{{ isCurrentContextWebGL2() }}}) {
      count && GLctx.uniform3fv(webglGetUniformLocation(location), HEAPF32, {{{ getHeapOffset('value', 'float') }}}, count*3);
      return;
    }
#endif

#if GL_POOL_TEMP_BUFFERS
    if (count <= GL_POOL_TEMP_BUFFERS_SIZE / 3) {
      // avoid allocation when uploading few enough uniforms
      count *= 3;
      var view = miniTempWebGLFloatBuffers[count];
      for (var i = 0; i < count; i += 3) {
        view[i] = {{{ makeGetValue('value', '4*i', 'float') }}};
        view[i+1] = {{{ makeGetValue('value', '4*i+4', 'float') }}};
        view[i+2] = {{{ makeGetValue('value', '4*i+8', 'float') }}};
      }
    } else
#endif
    {
      var view = {{{ makeHEAPView('F32', 'value', 'value+count*12') }}};
    }
    GLctx.uniform3fv(webglGetUniformLocation(location), view);
#endif // MIN_WEBGL_VERSION >= 2 && WEBGL_USE_GARBAGE_FREE_APIS
  },

  glUniform4fv: (location, count, value) => {
#if GL_ASSERTIONS
    GL.validateGLObjectID(GLctx.currentProgram.uniformLocsById, location, 'glUniform4fv', 'location');
    assert((value & 3) == 0, 'pointer passed to glUniform4fv must be 4-byte aligned');
#endif

#if MIN_WEBGL_VERSION >= 2 && WEBGL_USE_GARBAGE_FREE_APIS
#if GL_ASSERTIONS
    assert(GL.currentContext.version >= 2);
#endif
    count && GLctx.uniform4fv(webglGetUniformLocation(location), HEAPF32, {{{ getHeapOffset('value', 'float') }}}, count*4);
#else

#if WEBGL_USE_GARBAGE_FREE_APIS
    if ({{{ isCurrentContextWebGL2() }}}) {
      count && GLctx.uniform4fv(webglGetUniformLocation(location), HEAPF32, {{{ getHeapOffset('value', 'float') }}}, count*4);
      return;
    }
#endif

#if GL_POOL_TEMP_BUFFERS
    if (count <= GL_POOL_TEMP_BUFFERS_SIZE / 4) {
      // avoid allocation when uploading few enough uniforms
      var view = miniTempWebGLFloatBuffers[4*count];
      // hoist the heap out of the loop for size and for pthreads+growth.
      var heap = HEAPF32;
      value = {{{ getHeapOffset('value', 'float') }}};
      count *= 4;
      for (var i = 0; i < count; i += 4) {
        var dst = value + i;
        view[i] = heap[dst];
        view[i + 1] = heap[dst + 1];
        view[i + 2] = heap[dst + 2];
        view[i + 3] = heap[dst + 3];
      }
    } else
#endif
    {
      var view = {{{ makeHEAPView('F32', 'value', 'value+count*16') }}};
    }
    GLctx.uniform4fv(webglGetUniformLocation(location), view);
#endif // MIN_WEBGL_VERSION >= 2 && WEBGL_USE_GARBAGE_FREE_APIS
  },

  glUniformMatrix2fv: (location, count, transpose, value) => {
#if GL_ASSERTIONS
    GL.validateGLObjectID(GLctx.currentProgram.uniformLocsById, location, 'glUniformMatrix2fv', 'location');
    assert((value & 3) == 0, 'pointer passed to glUniformMatrix2fv must be 4-byte aligned');
#endif

#if MIN_WEBGL_VERSION >= 2 && WEBGL_USE_GARBAGE_FREE_APIS
#if GL_ASSERTIONS
    assert(GL.currentContext.version >= 2);
#endif
    count && GLctx.uniformMatrix2fv(webglGetUniformLocation(location), !!transpose, HEAPF32, {{{ getHeapOffset('value', 'float') }}}, count*4);
#else

#if WEBGL_USE_GARBAGE_FREE_APIS
    if ({{{ isCurrentContextWebGL2() }}}) {
      count && GLctx.uniformMatrix2fv(webglGetUniformLocation(location), !!transpose, HEAPF32, {{{ getHeapOffset('value', 'float') }}}, count*4);
      return;
    }
#endif

#if GL_POOL_TEMP_BUFFERS
    if (count <= GL_POOL_TEMP_BUFFERS_SIZE / 4) {
      // avoid allocation when uploading few enough uniforms
      count *= 4;
      var view = miniTempWebGLFloatBuffers[count];
      for (var i = 0; i < count; i += 4) {
        view[i] = {{{ makeGetValue('value', '4*i', 'float') }}};
        view[i+1] = {{{ makeGetValue('value', '4*i+4', 'float') }}};
        view[i+2] = {{{ makeGetValue('value', '4*i+8', 'float') }}};
        view[i+3] = {{{ makeGetValue('value', '4*i+12', 'float') }}};
      }
    } else
#endif
    {
      var view = {{{ makeHEAPView('F32', 'value', 'value+count*16') }}};
    }
    GLctx.uniformMatrix2fv(webglGetUniformLocation(location), !!transpose, view);
#endif // MIN_WEBGL_VERSION >= 2 && WEBGL_USE_GARBAGE_FREE_APIS
  },

  glUniformMatrix3fv: (location, count, transpose, value) => {
#if GL_ASSERTIONS
    GL.validateGLObjectID(GLctx.currentProgram.uniformLocsById, location, 'glUniformMatrix3fv', 'location');
    assert((value & 3) == 0, 'pointer passed to glUniformMatrix3fv must be 4-byte aligned');
#endif

#if MIN_WEBGL_VERSION >= 2 && WEBGL_USE_GARBAGE_FREE_APIS
#if GL_ASSERTIONS
    assert(GL.currentContext.version >= 2);
#endif
    count && GLctx.uniformMatrix3fv(webglGetUniformLocation(location), !!transpose, HEAPF32, {{{ getHeapOffset('value', 'float') }}}, count*9);
#else

#if WEBGL_USE_GARBAGE_FREE_APIS
    if ({{{ isCurrentContextWebGL2() }}}) {
      count && GLctx.uniformMatrix3fv(webglGetUniformLocation(location), !!transpose, HEAPF32, {{{ getHeapOffset('value', 'float') }}}, count*9);
      return;
    }
#endif

#if GL_POOL_TEMP_BUFFERS
    if (count <= GL_POOL_TEMP_BUFFERS_SIZE / 9) {
      // avoid allocation when uploading few enough uniforms
      count *= 9;
      var view = miniTempWebGLFloatBuffers[count];
      for (var i = 0; i < count; i += 9) {
        view[i] = {{{ makeGetValue('value', '4*i', 'float') }}};
        view[i+1] = {{{ makeGetValue('value', '4*i+4', 'float') }}};
        view[i+2] = {{{ makeGetValue('value', '4*i+8', 'float') }}};
        view[i+3] = {{{ makeGetValue('value', '4*i+12', 'float') }}};
        view[i+4] = {{{ makeGetValue('value', '4*i+16', 'float') }}};
        view[i+5] = {{{ makeGetValue('value', '4*i+20', 'float') }}};
        view[i+6] = {{{ makeGetValue('value', '4*i+24', 'float') }}};
        view[i+7] = {{{ makeGetValue('value', '4*i+28', 'float') }}};
        view[i+8] = {{{ makeGetValue('value', '4*i+32', 'float') }}};
      }
    } else
#endif
    {
      var view = {{{ makeHEAPView('F32', 'value', 'value+count*36') }}};
    }
    GLctx.uniformMatrix3fv(webglGetUniformLocation(location), !!transpose, view);
#endif // MIN_WEBGL_VERSION >= 2 && WEBGL_USE_GARBAGE_FREE_APIS
  },

  glUniformMatrix4fv: (location, count, transpose, value) => {
#if GL_ASSERTIONS
    GL.validateGLObjectID(GLctx.currentProgram.uniformLocsById, location, 'glUniformMatrix4fv', 'location');
    assert((value & 3) == 0, 'pointer passed to glUniformMatrix4fv must be 4-byte aligned');
#endif

#if MIN_WEBGL_VERSION >= 2 && WEBGL_USE_GARBAGE_FREE_APIS
#if GL_ASSERTIONS
    assert(GL.currentContext.version >= 2);
#endif
    count && GLctx.uniformMatrix4fv(webglGetUniformLocation(location), !!transpose, HEAPF32, {{{ getHeapOffset('value', 'float') }}}, count*16);
#else

#if WEBGL_USE_GARBAGE_FREE_APIS
    if ({{{ isCurrentContextWebGL2() }}}) {
      count && GLctx.uniformMatrix4fv(webglGetUniformLocation(location), !!transpose, HEAPF32, {{{ getHeapOffset('value', 'float') }}}, count*16);
      return;
    }
#endif

#if GL_POOL_TEMP_BUFFERS
    if (count <= GL_POOL_TEMP_BUFFERS_SIZE / 16) {
      // avoid allocation when uploading few enough uniforms
      var view = miniTempWebGLFloatBuffers[16*count];
      // hoist the heap out of the loop for size and for pthreads+growth.
      var heap = HEAPF32;
      value = {{{ getHeapOffset('value', 'float') }}};
      count *= 16;
      for (var i = 0; i < count; i += 16) {
        var dst = value + i;
        view[i] = heap[dst];
        view[i + 1] = heap[dst + 1];
        view[i + 2] = heap[dst + 2];
        view[i + 3] = heap[dst + 3];
        view[i + 4] = heap[dst + 4];
        view[i + 5] = heap[dst + 5];
        view[i + 6] = heap[dst + 6];
        view[i + 7] = heap[dst + 7];
        view[i + 8] = heap[dst + 8];
        view[i + 9] = heap[dst + 9];
        view[i + 10] = heap[dst + 10];
        view[i + 11] = heap[dst + 11];
        view[i + 12] = heap[dst + 12];
        view[i + 13] = heap[dst + 13];
        view[i + 14] = heap[dst + 14];
        view[i + 15] = heap[dst + 15];
      }
    } else
#endif
    {
      var view = {{{ makeHEAPView('F32', 'value', 'value+count*64') }}};
    }
    GLctx.uniformMatrix4fv(webglGetUniformLocation(location), !!transpose, view);
#endif // MIN_WEBGL_VERSION >= 2 && WEBGL_USE_GARBAGE_FREE_APIS
  },

  glBindBuffer: (target, buffer) => {
#if FULL_ES2
    // Calling glBindBuffer with an unknown buffer will implicitly create a
    // new one.  Here we bypass `GL.counter` and directly using the ID passed
    // in.
    if (buffer && !GL.buffers[buffer]) {
      var b = GLctx.createBuffer();
      b.name = buffer;
      GL.buffers[buffer] = b;
    }
#endif
#if GL_ASSERTIONS
    GL.validateGLObjectID(GL.buffers, buffer, 'glBindBuffer', 'buffer');
#endif
#if FULL_ES2 || LEGACY_GL_EMULATION
    if (target == 0x8892 /*GL_ARRAY_BUFFER*/) {
      GLctx.currentArrayBufferBinding = buffer;
#if LEGACY_GL_EMULATION
      GLImmediate.lastArrayBuffer = buffer;
#endif
    } else if (target == 0x8893 /*GL_ELEMENT_ARRAY_BUFFER*/) {
      GLctx.currentElementArrayBufferBinding = buffer;
    }
#endif

#if MAX_WEBGL_VERSION >= 2
    if (target == 0x88EB /*GL_PIXEL_PACK_BUFFER*/) {
      // In WebGL 2 glReadPixels entry point, we need to use a different WebGL 2
      // API function call when a buffer is bound to
      // GL_PIXEL_PACK_BUFFER_BINDING point, so must keep track whether that
      // binding point is non-null to know what is the proper API function to
      // call.
      GLctx.currentPixelPackBufferBinding = buffer;
    } else if (target == 0x88EC /*GL_PIXEL_UNPACK_BUFFER*/) {
      // In WebGL 2 gl(Compressed)Tex(Sub)Image[23]D entry points, we need to
      // use a different WebGL 2 API function call when a buffer is bound to
      // GL_PIXEL_UNPACK_BUFFER_BINDING point, so must keep track whether that
      // binding point is non-null to know what is the proper API function to
      // call.
      GLctx.currentPixelUnpackBufferBinding = buffer;
    }
#endif
    GLctx.bindBuffer(target, GL.buffers[buffer]);
  },

  glVertexAttrib1fv: (index, v) => {
#if GL_ASSERTIONS
    assert((v & 3) == 0, 'pointer passed to glVertexAttrib1fv must be 4-byte aligned');
    assert(v != 0, 'null pointer passed to glVertexAttrib1fv');
#endif

    GLctx.vertexAttrib1f(index, HEAPF32[v>>2]);
  },

  glVertexAttrib2fv: (index, v) => {
#if GL_ASSERTIONS
    assert((v & 3) == 0, 'pointer passed to glVertexAttrib2fv must be 4-byte aligned');
    assert(v != 0, 'null pointer passed to glVertexAttrib2fv');
#endif

    GLctx.vertexAttrib2f(index, HEAPF32[v>>2], HEAPF32[v+4>>2]);
  },

  glVertexAttrib3fv: (index, v) => {
#if GL_ASSERTIONS
    assert((v & 3) == 0, 'pointer passed to glVertexAttrib3fv must be 4-byte aligned');
    assert(v != 0, 'null pointer passed to glVertexAttrib3fv');
#endif

    GLctx.vertexAttrib3f(index, HEAPF32[v>>2], HEAPF32[v+4>>2], HEAPF32[v+8>>2]);
  },

  glVertexAttrib4fv: (index, v) => {
#if GL_ASSERTIONS
    assert((v & 3) == 0, 'pointer passed to glVertexAttrib4fv must be 4-byte aligned');
    assert(v != 0, 'null pointer passed to glVertexAttrib4fv');
#endif

    GLctx.vertexAttrib4f(index, HEAPF32[v>>2], HEAPF32[v+4>>2], HEAPF32[v+8>>2], HEAPF32[v+12>>2]);
  },

  glGetAttribLocation: (program, name) =>
    GLctx.getAttribLocation(GL.programs[program], UTF8ToString(name)),

  $__glGetActiveAttribOrUniform: (funcName, program, index, bufSize, length, size, type, name) => {
#if GL_ASSERTIONS
    GL.validateGLObjectID(GL.programs, program, funcName, 'program');
#endif
    program = GL.programs[program];
    var info = GLctx[funcName](program, index);
    if (info) {
      // If an error occurs, nothing will be written to length, size and type and name.
      var numBytesWrittenExclNull = name && stringToUTF8(info.name, name, bufSize);
      if (length) {{{ makeSetValue('length', '0', 'numBytesWrittenExclNull', 'i32') }}};
      if (size) {{{ makeSetValue('size', '0', 'info.size', 'i32') }}};
      if (type) {{{ makeSetValue('type', '0', 'info.type', 'i32') }}};
    }
  },

  glGetActiveAttrib: (program, index, bufSize, length, size, type, name) =>
    __glGetActiveAttribOrUniform('getActiveAttrib', program, index, bufSize, length, size, type, name),

  glGetActiveUniform: (program, index, bufSize, length, size, type, name) =>
    __glGetActiveAttribOrUniform('getActiveUniform', program, index, bufSize, length, size, type, name),

  glCreateShader: (shaderType) => {
    var id = GL.getNewId(GL.shaders);
    GL.shaders[id] = GLctx.createShader(shaderType);

#if GL_EXPLICIT_UNIFORM_LOCATION || GL_EXPLICIT_UNIFORM_BINDING
    // GL_VERTEX_SHADER = 0x8B31, GL_FRAGMENT_SHADER = 0x8B30
    GL.shaders[id].shaderType = shaderType&1?'vs':'fs';
#endif

    return id;
  },

  glDeleteShader: (id) => {
    if (!id) return;
    var shader = GL.shaders[id];
    if (!shader) {
      // glDeleteShader actually signals an error when deleting a nonexisting
      // object, unlike some other GL delete functions.
      GL.recordError(0x501 /* GL_INVALID_VALUE */);
      return;
    }
    GLctx.deleteShader(shader);
    GL.shaders[id] = null;
  },

  glGetAttachedShaders: (program, maxCount, count, shaders) => {
#if GL_ASSERTIONS
    GL.validateGLObjectID(GL.programs, program, 'glGetAttachedShaders', 'program');
#endif
    var result = GLctx.getAttachedShaders(GL.programs[program]);
    var len = result.length;
    if (len > maxCount) {
      len = maxCount;
    }
    {{{ makeSetValue('count', '0', 'len', 'i32') }}};
    for (var i = 0; i < len; ++i) {
      var id = GL.shaders.indexOf(result[i]);
#if GL_ASSERTIONS
      assert(id !== -1, 'shader not bound to local id');
#endif
      {{{ makeSetValue('shaders', 'i*4', 'id', 'i32') }}};
    }
  },

  glShaderSource: (shader, count, string, length) => {
#if GL_ASSERTIONS
    GL.validateGLObjectID(GL.shaders, shader, 'glShaderSource', 'shader');
#endif
    var source = GL.getSource(shader, count, string, length);

#if WEBGL2_BACKWARDS_COMPATIBILITY_EMULATION
    if ({{{ isCurrentContextWebGL2() }}}) {
      // If a WebGL 1 shader happens to use GL_EXT_shader_texture_lod extension,
      // it will not compile on WebGL 2, because WebGL 2 no longer supports that
      // extension for WebGL 1 shaders. Therefore upgrade shaders to WebGL 2
      // by doing a bunch of dirty hacks. Not guaranteed to work on all shaders.
      // One might consider doing this for only the shaders that actually use
      // the GL_EXT_shader_texture_lod extension, but the problem is that
      // vertex and fragment shader versions need to match, and when compiling
      // the corresponding vertex shader, we would not know if that needed to
      // be compiled with or without the patch, so we must patch all shaders.
      if (source.includes('#version 100')) {
        source = source.replace(/#extension GL_OES_standard_derivatives : enable/g, '');
        source = source.replace(/#extension GL_EXT_shader_texture_lod : enable/g, '');
        var prelude = '';
        if (source.includes('gl_FragColor')) {
          prelude += 'out mediump vec4 GL_FragColor;\n';
          source = source.replace(/gl_FragColor/g, 'GL_FragColor');
        }
        if (source.includes('attribute')) {
          source = source.replace(/attribute/g, 'in');
          source = source.replace(/varying/g, 'out');
        } else {
          source = source.replace(/varying/g, 'in');
        }

        source = source.replace(/textureCubeLodEXT/g, 'textureCubeLod');
        source = source.replace(/texture2DLodEXT/g, 'texture2DLod');
        source = source.replace(/texture2DProjLodEXT/g, 'texture2DProjLod');
        source = source.replace(/texture2DGradEXT/g, 'texture2DGrad');
        source = source.replace(/texture2DProjGradEXT/g, 'texture2DProjGrad');
        source = source.replace(/textureCubeGradEXT/g, 'textureCubeGrad');

        source = source.replace(/textureCube/g, 'texture');
        source = source.replace(/texture1D/g, 'texture');
        source = source.replace(/texture2D/g, 'texture');
        source = source.replace(/texture3D/g, 'texture');
        source = source.replace(/#version 100/g, '#version 300 es\n' + prelude);
      }
    }
#endif

#if GL_EXPLICIT_UNIFORM_LOCATION || GL_EXPLICIT_UNIFORM_BINDING
#if GL_DEBUG
    dbg(`Input shader source: ${source}`);
#endif

#if ASSERTIONS
    // These are not expected to be meaningful in WebGL, but issue a warning if
    // they are present, to give some diagnostics about if they are present.
    if (source.includes('__FILE__')) warnOnce(`When compiling shader: ${source}: Preprocessor variable __FILE__ is not handled by -sGL_EXPLICIT_UNIFORM_LOCATION/-sGL_EXPLICIT_UNIFORM_BINDING options!`);
    if (source.includes('__LINE__')) warnOnce(`When compiling shader: ${source}: Preprocessor variable __LINE__ is not handled by -sGL_EXPLICIT_UNIFORM_LOCATION/-sGL_EXPLICIT_UNIFORM_BINDING options!`);
#endif
    // Remove comments and C-preprocess the input shader first, so that we can
    // appropriately parse the layout location directives.
    source = preprocess_c_code(remove_cpp_comments_in_shaders(source), {
      'GL_FRAGMENT_PRECISION_HIGH': () => 1,
      'GL_ES': () => 1,
      '__VERSION__': () => source.includes('#version 300') ? 300 : 100
    });
#if GL_DEBUG
    dbg(`Shader source after preprocessing: ${source}`);
#endif
#endif // ~GL_EXPLICIT_UNIFORM_LOCATION || GL_EXPLICIT_UNIFORM_BINDING

#if GL_EXPLICIT_UNIFORM_LOCATION
    // Extract the layout(location = x) directives.
    var regex = /layout\s*\(\s*location\s*=\s*(-?\d+)\s*\)\s*(uniform\s+((lowp|mediump|highp)\s+)?\w+\s+(\w+))/g, explicitUniformLocations = {}, match;
    while (match = regex.exec(source)) {
#if GL_DEBUG
      console.dir(match);
#endif
      explicitUniformLocations[match[5]] = Number(match[1]);
#if GL_TRACK_ERRORS
      if (!(explicitUniformLocations[match[5]] >= 0 && explicitUniformLocations[match[5]] < 1048576)) {
        err(`Specified an out of range layout(location=x) directive "${explicitUniformLocations[match[5]]}"! (${match[0]})`);
        GL.recordError(0x501 /* GL_INVALID_VALUE */);
        return;
      }
#endif
    }

    // Remove all the layout(location = x) directives so that they do not make
    // their way to the actual WebGL shader compiler.
    source = source.replace(regex, '$2');

    // Remember all the directives to be handled after glLinkProgram is called.
    GL.shaders[shader].explicitUniformLocations = explicitUniformLocations;

#if GL_DEBUG
    dbg(`Shader source after removing layout location directives: ${source}`);
    dbg('Explicit uniform locations recorded in the shader:');
    console.dir(explicitUniformLocations);
#endif

#endif // ~GL_EXPLICIT_UNIFORM_LOCATION

#if GL_EXPLICIT_UNIFORM_BINDING
    // Extract the layout(binding = x) directives. Four types we need to handle:
    // layout(binding = 3) uniform sampler2D mainTexture;
    // layout(binding = 1, std140) uniform MainBlock { ... };
    // layout(std140, binding = 1) uniform MainBlock { ... };
    // layout(binding = 1) uniform MainBlock { ... };
    var bindingRegex = /layout\s*\(.*?binding\s*=\s*(-?\d+).*?\)\s*uniform\s+(\w+)\s+(\w+)?/g, samplerBindings = {}, uniformBindings = {}, bindingMatch;
    while (bindingMatch = bindingRegex.exec(source)) {
      // We have a layout(binding=x) enabled uniform. Parse the array length of
      // that uniform, if it is an array, i.e. a
      //    layout(binding = 3) uniform sampler2D mainTexture[arrayLength];
      // or
      //    layout(binding = 1, std140) uniform MainBlock { ... } name[arrayLength];
      var arrayLength = 1;
      for (var i = bindingMatch.index; i < source.length && source[i] != ';'; ++i) {
        if (source[i] == '[') {
          arrayLength = jstoi_q(source.slice(i+1));
          break;
        }
        if (source[i] == '{') i = find_closing_parens_index(source, i, '{', '}') - 1;
      }
#if GL_DEBUG
      console.dir(bindingMatch);
#endif
      var binding = jstoi_q(bindingMatch[1]);
#if GL_TRACK_ERRORS
      var bindingsType = 0x8872/*GL_MAX_TEXTURE_IMAGE_UNITS*/;
#endif
      if (bindingMatch[3] && bindingMatch[2].includes('sampler')) {
        samplerBindings[bindingMatch[3]] = [binding, arrayLength];
      } else {
#if GL_TRACK_ERRORS
        bindingsType = 0x8A2E/*GL_MAX_COMBINED_UNIFORM_BLOCKS*/;
#endif
        uniformBindings[bindingMatch[2]] = [binding, arrayLength];
      }
#if GL_TRACK_ERRORS
      var numBindingPoints = GLctx.getParameter(bindingsType);
      if (!(binding >= 0 && binding + arrayLength <= numBindingPoints)) {
        err(`Specified an out of range layout(binding=x) directive "${binding}"! (${bindingMatch[0]}). Valid range is [0, ${numBindingPoints}-1]`);
        GL.recordError(0x501 /* GL_INVALID_VALUE */);
        return;
      }
#endif
    }

    // Remove all the layout(binding = x) directives so that they do not make
    // their way to the actual WebGL shader compiler. These regexes get quite
    // hairy, check against https://regex101.com/ when working on these.
    source = source.replace(/layout\s*\(\s*binding\s*=\s*([-\d]+)\s*\)/g, ''); // "layout(binding = 3)" -> ""
    source = source.replace(/(layout\s*\((.*?)),\s*binding\s*=\s*([-\d]+)\)/g, '$1)'); // "layout(std140, binding = 1)" -> "layout(std140)"
    source = source.replace(/layout\s*\(\s*binding\s*=\s*([-\d]+)\s*,\s*(.*?)\)/g, 'layout($2)'); // "layout(binding = 1, std140)" -> "layout(std140)"

#if GL_DEBUG
    dbg(`Shader source after removing layout binding directives: ${source}`);
    dbg('Sampler binding locations recorded in the shader:');
    console.dir(samplerBindings);
    dbg('Uniform binding locations recorded in the shader:');
    console.dir(uniformBindings);
#endif

    // Remember all the directives to be handled after glLinkProgram is called.
    GL.shaders[shader].explicitSamplerBindings = samplerBindings;
    GL.shaders[shader].explicitUniformBindings = uniformBindings;

#endif // ~GL_EXPLICIT_UNIFORM_BINDING

    GLctx.shaderSource(GL.shaders[shader], source);
  },

  glGetShaderSource: (shader, bufSize, length, source) => {
#if GL_ASSERTIONS
    GL.validateGLObjectID(GL.shaders, shader, 'glGetShaderSource', 'shader');
#endif
    var result = GLctx.getShaderSource(GL.shaders[shader]);
    if (!result) return; // If an error occurs, nothing will be written to length or source.
    var numBytesWrittenExclNull = (bufSize > 0 && source) ? stringToUTF8(result, source, bufSize) : 0;
    if (length) {{{ makeSetValue('length', '0', 'numBytesWrittenExclNull', 'i32') }}};
  },

  glCompileShader: (shader) => {
#if GL_ASSERTIONS
    GL.validateGLObjectID(GL.shaders, shader, 'glCompileShader', 'shader');
#endif
    GLctx.compileShader(GL.shaders[shader]);
#if GL_DEBUG
    var log = GLctx.getShaderInfoLog(GL.shaders[shader])?.trim();
    if (log) dbg(`glCompileShader: ${log}`);
#endif
  },

  glGetShaderInfoLog: (shader, maxLength, length, infoLog) => {
#if GL_ASSERTIONS
    GL.validateGLObjectID(GL.shaders, shader, 'glGetShaderInfoLog', 'shader');
#endif
    var log = GLctx.getShaderInfoLog(GL.shaders[shader]);
#if GL_ASSERTIONS || GL_TRACK_ERRORS
    if (log === null) log = '(unknown error)';
#endif
    var numBytesWrittenExclNull = (maxLength > 0 && infoLog) ? stringToUTF8(log, infoLog, maxLength) : 0;
    if (length) {{{ makeSetValue('length', '0', 'numBytesWrittenExclNull', 'i32') }}};
  },

  glGetShaderiv: (shader, pname, p) => {
    if (!p) {
      // GLES2 specification does not specify how to behave if p is a null
      // pointer. Since calling this function does not make sense if p == null,
      // issue a GL error to notify user about it.
#if GL_ASSERTIONS
      err(`GL_INVALID_VALUE in glGetShaderiv(shader=${shader}, pname=${pname}, p=0): Function called with null out pointer!`);
#endif
      GL.recordError(0x501 /* GL_INVALID_VALUE */);
      return;
    }
#if GL_ASSERTIONS
    GL.validateGLObjectID(GL.shaders, shader, 'glGetShaderiv', 'shader');
#endif
    if (pname == 0x8B84) { // GL_INFO_LOG_LENGTH
      var log = GLctx.getShaderInfoLog(GL.shaders[shader]);
#if GL_ASSERTIONS || GL_TRACK_ERRORS
      if (log === null) log = '(unknown error)';
#endif
      // The GLES2 specification says that if the shader has an empty info log,
      // a value of 0 is returned. Otherwise the log has a null char appended.
      // (An empty string is falsey, so we can just check that instead of
      // looking at log.length.)
      var logLength = log ? log.length + 1 : 0;
      {{{ makeSetValue('p', '0', 'logLength', 'i32') }}};
    } else if (pname == 0x8B88) { // GL_SHADER_SOURCE_LENGTH
      var source = GLctx.getShaderSource(GL.shaders[shader]);
      // source may be a null, or the empty string, both of which are falsey
      // values that we report a 0 length for.
      var sourceLength = source ? source.length + 1 : 0;
      {{{ makeSetValue('p', '0', 'sourceLength', 'i32') }}};
    } else {
      {{{ makeSetValue('p', '0', 'GLctx.getShaderParameter(GL.shaders[shader], pname)', 'i32') }}};
    }
  },

  glGetProgramiv : (program, pname, p) => {
    if (!p) {
      // GLES2 specification does not specify how to behave if p is a null
      // pointer. Since calling this function does not make sense if p == null,
      // issue a GL error to notify user about it.
#if GL_ASSERTIONS
      err(`GL_INVALID_VALUE in glGetProgramiv(program=${program}, pname=${pname}, p=0): Function called with null out pointer!`);
#endif
      GL.recordError(0x501 /* GL_INVALID_VALUE */);
      return;
    }
#if GL_ASSERTIONS
    GL.validateGLObjectID(GL.programs, program, 'glGetProgramiv', 'program');
#endif

    if (program >= GL.counter) {
#if GL_ASSERTIONS
      err(`GL_INVALID_VALUE in glGetProgramiv(program=${program}, pname=${pname}, p=${ptrToString(p)}): The specified program object name was not generated by GL!`);
#endif
      GL.recordError(0x501 /* GL_INVALID_VALUE */);
      return;
    }

    program = GL.programs[program];

    if (pname == 0x8B84) { // GL_INFO_LOG_LENGTH
      var log = GLctx.getProgramInfoLog(program);
#if GL_ASSERTIONS || GL_TRACK_ERRORS
      if (log === null) log = '(unknown error)';
#endif
      {{{ makeSetValue('p', '0', 'log.length + 1', 'i32') }}};
    } else if (pname == 0x8B87 /* GL_ACTIVE_UNIFORM_MAX_LENGTH */) {
      if (!program.maxUniformLength) {
        var numActiveUniforms = GLctx.getProgramParameter(program, 0x8B86/*GL_ACTIVE_UNIFORMS*/);
        for (var i = 0; i < numActiveUniforms; ++i) {
          program.maxUniformLength = Math.max(program.maxUniformLength, GLctx.getActiveUniform(program, i).name.length+1);
        }
      }
      {{{ makeSetValue('p', '0', 'program.maxUniformLength', 'i32') }}};
    } else if (pname == 0x8B8A /* GL_ACTIVE_ATTRIBUTE_MAX_LENGTH */) {
      if (!program.maxAttributeLength) {
        var numActiveAttributes = GLctx.getProgramParameter(program, 0x8B89/*GL_ACTIVE_ATTRIBUTES*/);
        for (var i = 0; i < numActiveAttributes; ++i) {
          program.maxAttributeLength = Math.max(program.maxAttributeLength, GLctx.getActiveAttrib(program, i).name.length+1);
        }
      }
      {{{ makeSetValue('p', '0', 'program.maxAttributeLength', 'i32') }}};
    } else if (pname == 0x8A35 /* GL_ACTIVE_UNIFORM_BLOCK_MAX_NAME_LENGTH */) {
      if (!program.maxUniformBlockNameLength) {
        var numActiveUniformBlocks = GLctx.getProgramParameter(program, 0x8A36/*GL_ACTIVE_UNIFORM_BLOCKS*/);
        for (var i = 0; i < numActiveUniformBlocks; ++i) {
          program.maxUniformBlockNameLength = Math.max(program.maxUniformBlockNameLength, GLctx.getActiveUniformBlockName(program, i).length+1);
        }
      }
      {{{ makeSetValue('p', '0', 'program.maxUniformBlockNameLength', 'i32') }}};
    } else {
      {{{ makeSetValue('p', '0', 'GLctx.getProgramParameter(program, pname)', 'i32') }}};
    }
  },

  glIsShader: (shader) => {
    var s = GL.shaders[shader];
    if (!s) return 0;
    return GLctx.isShader(s);
  },

  glCreateProgram: () => {
    var id = GL.getNewId(GL.programs);
    var program = GLctx.createProgram();
    // Store additional information needed for each shader program:
    program.name = id;
    // Lazy cache results of
    // glGetProgramiv(GL_ACTIVE_UNIFORM_MAX_LENGTH/GL_ACTIVE_ATTRIBUTE_MAX_LENGTH/GL_ACTIVE_UNIFORM_BLOCK_MAX_NAME_LENGTH)
    program.maxUniformLength = program.maxAttributeLength = program.maxUniformBlockNameLength = 0;
    program.uniformIdCounter = 1;
    GL.programs[id] = program;
    return id;
  },

  glDeleteProgram: (id) => {
    if (!id) return;
    var program = GL.programs[id];
    if (!program) {
      // glDeleteProgram actually signals an error when deleting a nonexisting
      // object, unlike some other GL delete functions.
      GL.recordError(0x501 /* GL_INVALID_VALUE */);
      return;
    }
    GLctx.deleteProgram(program);
    program.name = 0;
    GL.programs[id] = null;
  },

  glAttachShader: (program, shader) => {
#if GL_ASSERTIONS
    GL.validateGLObjectID(GL.programs, program, 'glAttachShader', 'program');
    GL.validateGLObjectID(GL.shaders, shader, 'glAttachShader', 'shader');
#endif
#if GL_EXPLICIT_UNIFORM_LOCATION || GL_EXPLICIT_UNIFORM_BINDING
    program = GL.programs[program];
    shader = GL.shaders[shader];
    program[shader.shaderType] = shader;
    GLctx.attachShader(program, shader);
#else
    GLctx.attachShader(GL.programs[program], GL.shaders[shader]);
#endif
  },

  glDetachShader: (program, shader) => {
#if GL_ASSERTIONS
    GL.validateGLObjectID(GL.programs, program, 'glDetachShader', 'program');
    GL.validateGLObjectID(GL.shaders, shader, 'glDetachShader', 'shader');
#endif
    GLctx.detachShader(GL.programs[program], GL.shaders[shader]);
  },

  glGetShaderPrecisionFormat: (shaderType, precisionType, range, precision) => {
    var result = GLctx.getShaderPrecisionFormat(shaderType, precisionType);
    {{{ makeSetValue('range', '0', 'result.rangeMin', 'i32') }}};
    {{{ makeSetValue('range', '4', 'result.rangeMax', 'i32') }}};
    {{{ makeSetValue('precision', '0', 'result.precision', 'i32') }}};
  },

  glLinkProgram: (program) => {
#if GL_ASSERTIONS
    GL.validateGLObjectID(GL.programs, program, 'glLinkProgram', 'program');
#endif
    program = GL.programs[program];
    GLctx.linkProgram(program);
#if GL_DEBUG
    var log = GLctx.getProgramInfoLog(program)?.trim();
    if (log) dbg(`glLinkProgram: ${log}`);
    if (program.uniformLocsById) dbg(`glLinkProgram invalidated ${Object.keys(program.uniformLocsById).length} uniform location mappings`);
#endif
    // Invalidate earlier computed uniform->ID mappings, those have now become stale
    program.uniformLocsById = 0; // Mark as null-like so that glGetUniformLocation() knows to populate this again.
    program.uniformSizeAndIdsByName = {};

#if GL_EXPLICIT_UNIFORM_LOCATION
    // Collect explicit uniform locations from the vertex and fragment shaders.
    for (var s of [program['vs'], program['fs']]) {
      for (var [shaderLocation, loc] of Object.entries(s.explicitUniformLocations)) {
        // Record each explicit uniform location temporarily as a non-array uniform
        // with size=1. This is not true, but on the first glGetUniformLocation() call
        // the array sizes will get populated to correct sizes.
        program.uniformSizeAndIdsByName[shaderLocation] = [1, loc];
#if GL_DEBUG
        dbg(`Marking uniform ${loc} to location ${shaderLocation}`);
#endif

        // Make sure we will never automatically assign locations within the range
        // used for explicit layout(location=x) variables.
        program.uniformIdCounter = Math.max(program.uniformIdCounter, loc + 1);
      }
    }
#endif

#if GL_EXPLICIT_UNIFORM_BINDING
    function copyKeys(dst, src) {
      for (var key of Object.keys(src)) { dst[key] = src[key] };
    }
    // Collect sampler and ubo binding locations from the vertex and fragment shaders.
    program.explicitUniformBindings = {};
    program.explicitSamplerBindings = {};
    for (var s of [program['vs'], program['fs']]) {
      copyKeys(program.explicitUniformBindings, s.explicitUniformBindings);
      copyKeys(program.explicitSamplerBindings, s.explicitSamplerBindings);
    }
    // Record that we need to apply these explicit bindings when glUseProgram() is
    // first called on this program.
    program.explicitProgramBindingsApplied = 0;
#endif
  },

  glGetProgramInfoLog: (program, maxLength, length, infoLog) => {
#if GL_ASSERTIONS
    GL.validateGLObjectID(GL.programs, program, 'glGetProgramInfoLog', 'program');
#endif
    var log = GLctx.getProgramInfoLog(GL.programs[program]);
#if GL_ASSERTIONS || GL_TRACK_ERRORS
    if (log === null) log = '(unknown error)';
#endif
    var numBytesWrittenExclNull = (maxLength > 0 && infoLog) ? stringToUTF8(log, infoLog, maxLength) : 0;
    if (length) {{{ makeSetValue('length', '0', 'numBytesWrittenExclNull', 'i32') }}};
  },

#if GL_EXPLICIT_UNIFORM_BINDING
  // Applies the explicit sampler and ubo binding locations to the current program. This is done
  // lazily the first time we glUseProgram() so that parallel shader compilation is not disturbed.
  $webglApplyExplicitProgramBindings: () => {
    var p = GLctx.currentProgram;
    if (!p.explicitProgramBindingsApplied) {
#if MAX_WEBGL_VERSION >= 2
#if MIN_WEBGL_VERSION < 2
      if (GL.currentContext.version >= 2) {
#endif
        for (var [ubo, bindings] of Object.entries(p.explicitUniformBindings)) {
          for (var i = 0; i < bindings[1]; ++i) {
            var blockIndex = GLctx.getUniformBlockIndex(p, ubo + (bindings[1] > 1 ? `[${i}]` : ''));
#if GL_DEBUG
            dbg('Applying initial UBO binding point ' + (bindings[0]+i) + ' for UBO "' + (ubo + (bindings[1] > 1 ? '[' + i + ']' : '')) + '" at block index ' + blockIndex + ' ' + (bindings[1] > 1 ? ' (array index='+i+')' : ''));
#endif
            GLctx.uniformBlockBinding(p, blockIndex, bindings[0]+i);
          }
        }
#if MIN_WEBGL_VERSION < 2
      }
#endif
#endif
      for (var [sampler, bindings] of Object.entries(p.explicitSamplerBindings)) {
        for (var i = 0; i < bindings[1]; ++i) {
#if GL_DEBUG
          dbg('Applying initial sampler binding point ' + (bindings[0]+i) + ' for sampler "' + sampler + (i > 0 ? '['+i+']' : '') +  '"');
#endif
          GLctx.uniform1i(GLctx.getUniformLocation(p, sampler + (i ? `[${i}]` : '')), bindings[0]+i);
        }
      }
      p.explicitProgramBindingsApplied = 1;
    }
  },
#endif

  glUseProgram: (program) => {
#if GL_ASSERTIONS
    GL.validateGLObjectID(GL.programs, program, 'glUseProgram', 'program');
#endif
    program = GL.programs[program];
    GLctx.useProgram(program);
    // Record the currently active program so that we can access the uniform
    // mapping table of that program.
#if GL_EXPLICIT_UNIFORM_BINDING
    if ((GLctx.currentProgram = program)) {
      webglApplyExplicitProgramBindings();
    }
#else
    GLctx.currentProgram = program;
#endif
  },

  glValidateProgram: (program) => {
#if GL_ASSERTIONS
    GL.validateGLObjectID(GL.programs, program, 'glValidateProgram', 'program');
#endif
    GLctx.validateProgram(GL.programs[program]);
  },

  glIsProgram: (program) => {
    program = GL.programs[program];
    if (!program) return 0;
    return GLctx.isProgram(program);
  },

  glBindAttribLocation: (program, index, name) => {
#if GL_ASSERTIONS
    GL.validateGLObjectID(GL.programs, program, 'glBindAttribLocation', 'program');
#endif
    GLctx.bindAttribLocation(GL.programs[program], index, UTF8ToString(name));
  },

  glBindFramebuffer: (target, framebuffer) => {
#if GL_ASSERTIONS
    GL.validateGLObjectID(GL.framebuffers, framebuffer, 'glBindFramebuffer', 'framebuffer');
#endif

#if OFFSCREEN_FRAMEBUFFER
    // defaultFbo may not be present if 'renderViaOffscreenBackBuffer' was not enabled during context creation time,
    // i.e. setting -sOFFSCREEN_FRAMEBUFFER at compilation time does not yet mandate that offscreen back buffer
    // is being used, but that is ultimately decided at context creation time.
    GLctx.bindFramebuffer(target, framebuffer ? GL.framebuffers[framebuffer] : GL.currentContext.defaultFbo);
#else
    GLctx.bindFramebuffer(target, GL.framebuffers[framebuffer]);
#endif

  },

  glGenFramebuffers: (n, ids) => {
    GL.genObject(n, ids, 'createFramebuffer', GL.framebuffers
#if GL_ASSERTIONS
    , 'glGenFramebuffers'
#endif
      );
  },

  glDeleteFramebuffers: (n, framebuffers) => {
    for (var i = 0; i < n; ++i) {
      var id = {{{ makeGetValue('framebuffers', 'i*4', 'i32') }}};
      var framebuffer = GL.framebuffers[id];
      if (!framebuffer) continue; // GL spec: "glDeleteFramebuffers silently ignores 0s and names that do not correspond to existing framebuffer objects".
      GLctx.deleteFramebuffer(framebuffer);
      framebuffer.name = 0;
      GL.framebuffers[id] = null;
    }
  },

  glFramebufferRenderbuffer: (target, attachment, renderbuffertarget, renderbuffer) => {
#if GL_ASSERTIONS
    GL.validateGLObjectID(GL.renderbuffers, renderbuffer, 'glFramebufferRenderbuffer', 'renderbuffer');
#endif
    GLctx.framebufferRenderbuffer(target, attachment, renderbuffertarget,
                                       GL.renderbuffers[renderbuffer]);
  },

  glFramebufferTexture2D: (target, attachment, textarget, texture, level) => {
#if GL_ASSERTIONS
    GL.validateGLObjectID(GL.textures, texture, 'glFramebufferTexture2D', 'texture');
#endif
    GLctx.framebufferTexture2D(target, attachment, textarget,
                                    GL.textures[texture], level);
  },

  glGetFramebufferAttachmentParameteriv: (target, attachment, pname, params) => {
    var result = GLctx.getFramebufferAttachmentParameter(target, attachment, pname);
    if (result instanceof WebGLRenderbuffer ||
        result instanceof WebGLTexture) {
      result = result.name | 0;
    }
    {{{ makeSetValue('params', '0', 'result', 'i32') }}};
  },

  glIsFramebuffer: (framebuffer) => {
    var fb = GL.framebuffers[framebuffer];
    if (!fb) return 0;
    return GLctx.isFramebuffer(fb);
  },

  glGenVertexArrays: (n, arrays) => {
#if LEGACY_GL_EMULATION
    emulGlGenVertexArrays(n, arrays);
#else
#if GL_ASSERTIONS
    assert(GLctx.createVertexArray, 'Must have WebGL2 or OES_vertex_array_object to use vao');
#endif
    GL.genObject(n, arrays, 'createVertexArray', GL.vaos
#if GL_ASSERTIONS
    , 'glGenVertexArrays'
#endif
      );
#endif
  },

  glDeleteVertexArrays: (n, vaos) => {
#if LEGACY_GL_EMULATION
    emulGlDeleteVertexArrays(n, vaos);
#else
#if GL_ASSERTIONS
    assert(GLctx.deleteVertexArray, 'Must have WebGL2 or OES_vertex_array_object to use vao');
#endif
    for (var i = 0; i < n; i++) {
      var id = {{{ makeGetValue('vaos', 'i*4', 'i32') }}};
      GLctx.deleteVertexArray(GL.vaos[id]);
      GL.vaos[id] = null;
    }
#endif
  },

  glBindVertexArray: (vao) => {
#if LEGACY_GL_EMULATION
    emulGlBindVertexArray(vao);
#else
#if GL_ASSERTIONS
    assert(GLctx.bindVertexArray, 'Must have WebGL2 or OES_vertex_array_object to use vao');
#endif
    GLctx.bindVertexArray(GL.vaos[vao]);
#endif
#if FULL_ES2 || LEGACY_GL_EMULATION
    var ibo = GLctx.getParameter(0x8895 /*ELEMENT_ARRAY_BUFFER_BINDING*/);
    GLctx.currentElementArrayBufferBinding = ibo ? (ibo.name | 0) : 0;
#endif
  },

  glIsVertexArray: (array) => {
#if LEGACY_GL_EMULATION
    return emulGlIsVertexArray(array);
#else
#if GL_ASSERTIONS
    assert(GLctx.isVertexArray, 'Must have WebGL2 or OES_vertex_array_object to use vao');
#endif

    var vao = GL.vaos[array];
    if (!vao) return 0;
    return GLctx.isVertexArray(vao);
#endif
  },

#if !LEGACY_GL_EMULATION

  glVertexPointer: (size, type, stride, ptr) =>
    abort('Legacy GL function (glVertexPointer) called. If you want legacy GL emulation, you need to compile with -sLEGACY_GL_EMULATION to enable legacy GL emulation.'),
  glMatrixMode: () =>
    abort('Legacy GL function (glMatrixMode) called. If you want legacy GL emulation, you need to compile with -sLEGACY_GL_EMULATION to enable legacy GL emulation.'),
  glBegin: () =>
    abort('Legacy GL function (glBegin) called. If you want legacy GL emulation, you need to compile with -sLEGACY_GL_EMULATION to enable legacy GL emulation.'),
  glLoadIdentity: () =>
    abort('Legacy GL function (glLoadIdentity) called. If you want legacy GL emulation, you need to compile with -sLEGACY_GL_EMULATION to enable legacy GL emulation.'),

#endif // LEGACY_GL_EMULATION

  // Open GLES1.1 vao compatibility (Could work w/o -sLEGACY_GL_EMULATION)

  glGenVertexArraysOES: 'glGenVertexArrays',
  glDeleteVertexArraysOES: 'glDeleteVertexArrays',
  glBindVertexArrayOES: 'glBindVertexArray',
  glIsVertexArrayOES: 'glIsVertexArray',

  // GLES2 emulation

  glVertexAttribPointer: (index, size, type, normalized, stride, ptr) => {
#if FULL_ES2
    var cb = GL.currentContext.clientBuffers[index];
#if GL_ASSERTIONS
    assert(cb, index);
#endif
    if (!GLctx.currentArrayBufferBinding) {
      cb.size = size;
      cb.type = type;
      cb.normalized = normalized;
      cb.stride = stride;
      cb.ptr = ptr;
      cb.clientside = true;
      cb.vertexAttribPointerAdaptor = /** @this {WebGLRenderingContext} */ function(index, size, type, normalized, stride, ptr) {
        this.vertexAttribPointer(index, size, type, normalized, stride, ptr);
      };
      return;
    }
    cb.clientside = false;
#endif
#if GL_ASSERTIONS
    GL.validateVertexAttribPointer(size, type, stride, ptr);
#endif
    GLctx.vertexAttribPointer(index, size, type, !!normalized, stride, ptr);
  },

  glEnableVertexAttribArray: (index) => {
#if FULL_ES2
    var cb = GL.currentContext.clientBuffers[index];
#if GL_ASSERTIONS
    assert(cb, index);
#endif
    cb.enabled = true;
#endif
    GLctx.enableVertexAttribArray(index);
  },

  glDisableVertexAttribArray: (index) => {
#if FULL_ES2
    var cb = GL.currentContext.clientBuffers[index];
#if GL_ASSERTIONS
    assert(cb, index);
#endif
    cb.enabled = false;
#endif
    GLctx.disableVertexAttribArray(index);
  },

#if !LEGACY_GL_EMULATION
  glDrawArrays: (mode, first, count) => {
#if FULL_ES2
    // bind any client-side buffers
    GL.preDrawHandleClientVertexAttribBindings(first + count);
#endif

    GLctx.drawArrays(mode, first, count);

#if FULL_ES2
    GL.postDrawHandleClientVertexAttribBindings();
#endif
  },

  glDrawElements: (mode, count, type, indices) => {
#if FULL_ES2
    var buf;
    var vertexes = 0;
    if (!GLctx.currentElementArrayBufferBinding) {
      var size = GL.calcBufLength(1, type, 0, count);
      buf = GL.getTempIndexBuffer(size);
      GLctx.bindBuffer(0x8893 /*GL_ELEMENT_ARRAY_BUFFER*/, buf);
      webglBufferSubData(0x8893 /*GL_ELEMENT_ARRAY_BUFFER*/, 0, size, indices);

      // Calculating vertex count if shader's attribute data is on client side
      if (count > 0) {
        for (var i = 0; i < GL.currentContext.maxVertexAttribs; ++i) {
          var cb = GL.currentContext.clientBuffers[i];
          if (cb.clientside && cb.enabled) {
            let arrayClass;
            switch(type) {
              case 0x1401 /* GL_UNSIGNED_BYTE */: arrayClass = Uint8Array; break;
              case 0x1403 /* GL_UNSIGNED_SHORT */: arrayClass = Uint16Array; break;
#if FULL_ES3
              case 0x1405 /* GL_UNSIGNED_INT */: arrayClass = Uint32Array; break;
#endif
              default:
                GL.recordError(0x502 /* GL_INVALID_OPERATION */);
#if GL_ASSERTIONS
                err('type is not supported in glDrawElements');
#endif
                return;
            }

            vertexes = new arrayClass(HEAPU8.buffer, indices, count).reduce((max, current) => Math.max(max, current)) + 1;
            break;
          }
        }
      }

      // the index is now 0
      indices = 0;
    }

    // bind any client-side buffers
    GL.preDrawHandleClientVertexAttribBindings(vertexes);
#endif

    GLctx.drawElements(mode, count, type, indices);

#if FULL_ES2
    GL.postDrawHandleClientVertexAttribBindings();

    if (!GLctx.currentElementArrayBufferBinding) {
      GLctx.bindBuffer(0x8893 /*GL_ELEMENT_ARRAY_BUFFER*/, null);
    }
#endif
  },
#endif // ~#if !LEGACY_GL_EMULATION

  glShaderBinary: (count, shaders, binaryformat, binary, length) => {
    GL.recordError(0x500/*GL_INVALID_ENUM*/);
#if GL_ASSERTIONS
    err('GL_INVALID_ENUM in glShaderBinary: WebGL does not support binary shader formats! Calls to glShaderBinary always fail.');
#endif
  },

  glReleaseShaderCompiler: () => {
    // NOP (as allowed by GLES 2.0 spec)
  },

  glGetError: () => {
#if GL_TRACK_ERRORS
    var error = GLctx.getError() || GL.lastError;
    GL.lastError = 0/*GL_NO_ERROR*/;
    return error;
#else
    return GLctx.getError();
#endif
  },

  // ANGLE_instanced_arrays WebGL extension related functions (in core in WebGL 2)

  glVertexAttribDivisor: (index, divisor) => {
#if GL_ASSERTIONS
    assert(GLctx.vertexAttribDivisor, 'Must have ANGLE_instanced_arrays extension or WebGL 2 to use WebGL instancing');
#endif
    GLctx.vertexAttribDivisor(index, divisor);
  },

  glDrawArraysInstanced: (mode, first, count, primcount) => {
#if GL_ASSERTIONS
    assert(GLctx.drawArraysInstanced, 'Must have ANGLE_instanced_arrays extension or WebGL 2 to use WebGL instancing');
#endif
    GLctx.drawArraysInstanced(mode, first, count, primcount);
  },

  glDrawElementsInstanced: (mode, count, type, indices, primcount) => {
#if GL_ASSERTIONS
    assert(GLctx.drawElementsInstanced, 'Must have ANGLE_instanced_arrays extension or WebGL 2 to use WebGL instancing');
#endif
    GLctx.drawElementsInstanced(mode, count, type, indices, primcount);
  },

  // OpenGL Desktop/ES 2.0 instancing extensions compatibility

  glVertexAttribDivisorNV: 'glVertexAttribDivisor',
  glDrawArraysInstancedNV: 'glDrawArraysInstanced',
  glDrawElementsInstancedNV: 'glDrawElementsInstanced',
  glVertexAttribDivisorEXT: 'glVertexAttribDivisor',
  glDrawArraysInstancedEXT: 'glDrawArraysInstanced',
  glDrawElementsInstancedEXT: 'glDrawElementsInstanced',
  glVertexAttribDivisorARB: 'glVertexAttribDivisor',
  glDrawArraysInstancedARB: 'glDrawArraysInstanced',
  glDrawElementsInstancedARB: 'glDrawElementsInstanced',
  glVertexAttribDivisorANGLE: 'glVertexAttribDivisor',
  glDrawArraysInstancedANGLE: 'glDrawArraysInstanced',
  glDrawElementsInstancedANGLE: 'glDrawElementsInstanced',


  glDrawBuffers: (n, bufs) => {
#if GL_ASSERTIONS
    assert(GLctx.drawBuffers, 'Must have WebGL2 or WEBGL_draw_buffers extension to use drawBuffers');
#endif
#if GL_ASSERTIONS
    assert(n < tempFixedLengthArray.length, `Invalid count of numBuffers=${n} passed to glDrawBuffers (that many draw buffer points do not exist in GL)`);
#endif

    var bufArray = tempFixedLengthArray[n];
    for (var i = 0; i < n; i++) {
      bufArray[i] = {{{ makeGetValue('bufs', 'i*4', 'i32') }}};
    }

    GLctx.drawBuffers(bufArray);
  },

  // OpenGL ES 2.0 draw buffer extensions compatibility

  glDrawBuffersEXT: 'glDrawBuffers',
  glDrawBuffersWEBGL: 'glDrawBuffers',

  // passthrough functions with GLboolean parameters

  glColorMask: (red, green, blue, alpha) => {
    GLctx.colorMask(!!red, !!green, !!blue, !!alpha);
  },

  glDepthMask: (flag) => {
    GLctx.depthMask(!!flag);
  },

  glSampleCoverage: (value, invert) => {
    GLctx.sampleCoverage(value, !!invert);
  },

  glMultiDrawArraysWEBGL__sig: 'vippi',
  glMultiDrawArrays: 'glMultiDrawArraysWEBGL',
  glMultiDrawArraysANGLE: 'glMultiDrawArraysWEBGL',
  glMultiDrawArraysWEBGL: (mode, firsts, counts, drawcount) => {
    GLctx.multiDrawWebgl['multiDrawArraysWEBGL'](
      mode,
      HEAP32,
      {{{ getHeapOffset('firsts', 'i32') }}},
      HEAP32,
      {{{ getHeapOffset('counts', 'i32') }}},
      drawcount);
  },

  glMultiDrawArraysInstancedWEBGL__sig: 'vipppi',
  glMultiDrawArraysInstancedANGLE: 'glMultiDrawArraysInstancedWEBGL',
  glMultiDrawArraysInstancedWEBGL: (mode, firsts, counts, instanceCounts, drawcount) => {
    GLctx.multiDrawWebgl['multiDrawArraysInstancedWEBGL'](
      mode,
      HEAP32,
      {{{ getHeapOffset('firsts', 'i32') }}},
      HEAP32,
      {{{ getHeapOffset('counts', 'i32') }}},
      HEAP32,
      {{{ getHeapOffset('instanceCounts', 'i32') }}},
      drawcount);
  },

#if MEMORY64
  // Convert an array of i64 offsets to an array of i32 offsets returning a
  // pointer to the new (stack allocated) array.
  $convertOffsets__internal: true,
  $convertOffsets: (offsets, count) => {
    var offsets32 = stackAlloc(count * 4);
    var i64ptr = {{{ getHeapOffset('offsets', 'i64') }}};
    var i32ptr = {{{ getHeapOffset('offsets32', 'i32') }}};
    for (var i = 0; i < count; i++, i32ptr++, i64ptr++) {
      var i64val = HEAPU64[i64ptr];
#if ASSERTIONS
      assert(i64val >= 0 && i64val <= 0xffffffff);
#endif
      HEAPU32[i32ptr] = Number(i64val);
    }
    return offsets32;
  },
#endif

  glMultiDrawElementsWEBGL__sig: 'vipipi',
  glMultiDrawElements: 'glMultiDrawElementsWEBGL',
  glMultiDrawElementsANGLE: 'glMultiDrawElementsWEBGL',
  glMultiDrawElementsWEBGL: (mode, counts, type, offsets, drawcount) => {
#if MEMORY64
    var stack = stackSave();
    offsets = convertOffsets(offsets, drawcount);
#endif
    GLctx.multiDrawWebgl['multiDrawElementsWEBGL'](
      mode,
      HEAP32,
      {{{ getHeapOffset('counts', 'i32') }}},
      type,
      HEAP32,
      {{{ getHeapOffset('offsets', 'i32') }}},
      drawcount);
#if MEMORY64
    stackRestore(stack);
#endif
  },

  glMultiDrawElementsInstancedWEBGL__sig: 'vipippi',
  glMultiDrawElementsInstancedANGLE: 'glMultiDrawElementsInstancedWEBGL',
  glMultiDrawElementsInstancedWEBGL: (mode, counts, type, offsets, instanceCounts, drawcount) => {
#if MEMORY64
    var stack = stackSave();
    offsets = convertOffsets(offsets, drawcount);
#endif
    GLctx.multiDrawWebgl['multiDrawElementsInstancedWEBGL'](
      mode,
      HEAP32,
      {{{ getHeapOffset('counts', 'i32') }}},
      type,
      HEAP32,
      {{{ getHeapOffset('offsets', 'i32') }}},
      HEAP32,
      {{{ getHeapOffset('instanceCounts', 'i32') }}},
      drawcount);
#if MEMORY64
    stackRestore(stack);
#endif
  },

  // As a small peculiarity, we currently allow building with -sFULL_ES3 to emulate client side arrays,
  // but without targeting WebGL 2, so this FULL_ES3 block is in library_webgl.js instead of library_webgl2.js
#if FULL_ES3
  $emscriptenWebGLGetBufferBinding: (target) => {
    switch (target) {
      case 0x8892 /*GL_ARRAY_BUFFER*/: target = 0x8894 /*GL_ARRAY_BUFFER_BINDING*/; break;
      case 0x8893 /*GL_ELEMENT_ARRAY_BUFFER*/: target = 0x8895 /*GL_ELEMENT_ARRAY_BUFFER_BINDING*/; break;
      case 0x88EB /*GL_PIXEL_PACK_BUFFER*/: target = 0x88ED /*GL_PIXEL_PACK_BUFFER_BINDING*/; break;
      case 0x88EC /*GL_PIXEL_UNPACK_BUFFER*/: target = 0x88EF /*GL_PIXEL_UNPACK_BUFFER_BINDING*/; break;
      case 0x8C8E /*GL_TRANSFORM_FEEDBACK_BUFFER*/: target = 0x8C8F /*GL_TRANSFORM_FEEDBACK_BUFFER_BINDING*/; break;
      case 0x8F36 /*GL_COPY_READ_BUFFER*/: target = 0x8F36 /*GL_COPY_READ_BUFFER_BINDING*/; break;
      case 0x8F37 /*GL_COPY_WRITE_BUFFER*/: target = 0x8F37 /*GL_COPY_WRITE_BUFFER_BINDING*/; break;
      case 0x8A11 /*GL_UNIFORM_BUFFER*/: target = 0x8A28 /*GL_UNIFORM_BUFFER_BINDING*/; break;
      // In default case, fall through and assume passed one of the _BINDING enums directly.
    }
    var buffer = GLctx.getParameter(target);
    if (buffer) return buffer.name|0;
    else return 0;
  },

  $emscriptenWebGLValidateMapBufferTarget: (target) => {
    switch (target) {
      case 0x8892: // GL_ARRAY_BUFFER
      case 0x8893: // GL_ELEMENT_ARRAY_BUFFER
      case 0x8F36: // GL_COPY_READ_BUFFER
      case 0x8F37: // GL_COPY_WRITE_BUFFER
      case 0x88EB: // GL_PIXEL_PACK_BUFFER
      case 0x88EC: // GL_PIXEL_UNPACK_BUFFER
      case 0x8C2A: // GL_TEXTURE_BUFFER
      case 0x8C8E: // GL_TRANSFORM_FEEDBACK_BUFFER
      case 0x8A11: // GL_UNIFORM_BUFFER
        return true;
      default:
        return false;
    }
  },

  glMapBufferRange__deps: ['malloc'],
  glMapBufferRange: (target, offset, length, access) => {
    if ((access & (0x1/*GL_MAP_READ_BIT*/ | 0x20/*GL_MAP_UNSYNCHRONIZED_BIT*/)) != 0) {
      err('glMapBufferRange access does not support MAP_READ or MAP_UNSYNCHRONIZED');
      return 0;
    }

    if ((access & 0x2/*GL_MAP_WRITE_BIT*/) == 0) {
      err('glMapBufferRange access must include MAP_WRITE');
      return 0;
    }

    if ((access & (0x4/*GL_MAP_INVALIDATE_BUFFER_BIT*/ | 0x8/*GL_MAP_INVALIDATE_RANGE_BIT*/)) == 0) {
      err('glMapBufferRange access must include INVALIDATE_BUFFER or INVALIDATE_RANGE');
      return 0;
    }

    if (!emscriptenWebGLValidateMapBufferTarget(target)) {
      GL.recordError(0x500/*GL_INVALID_ENUM*/);
      err('GL_INVALID_ENUM in glMapBufferRange');
      return 0;
    }

    var mem = _malloc(length), binding = emscriptenWebGLGetBufferBinding(target);
    if (!mem) return 0;

    binding = GL.mappedBuffers[binding] ??= {};
    binding.offset = offset;
    binding.length = length;
    binding.mem = mem;
    binding.access = access;
    return mem;
  },

  glGetBufferPointerv: (target, pname, params) => {
    if (pname == 0x88BD/*GL_BUFFER_MAP_POINTER*/) {
      var ptr = 0;
      var mappedBuffer = GL.mappedBuffers[emscriptenWebGLGetBufferBinding(target)];
      if (mappedBuffer) {
        ptr = mappedBuffer.mem;
      }
      {{{ makeSetValue('params', '0', 'ptr', 'i32') }}};
    } else {
      GL.recordError(0x500/*GL_INVALID_ENUM*/);
      err('GL_INVALID_ENUM in glGetBufferPointerv');
    }
  },

  glFlushMappedBufferRange: (target, offset, length) => {
    if (!emscriptenWebGLValidateMapBufferTarget(target)) {
      GL.recordError(0x500/*GL_INVALID_ENUM*/);
      err('GL_INVALID_ENUM in glFlushMappedBufferRange');
      return;
    }

    var mapping = GL.mappedBuffers[emscriptenWebGLGetBufferBinding(target)];
    if (!mapping) {
      GL.recordError(0x502 /* GL_INVALID_OPERATION */);
      err('buffer was never mapped in glFlushMappedBufferRange');
      return;
    }

    if (!(mapping.access & 0x10)) {
      GL.recordError(0x502 /* GL_INVALID_OPERATION */);
      err('buffer was not mapped with GL_MAP_FLUSH_EXPLICIT_BIT in glFlushMappedBufferRange');
      return;
    }
    if (offset < 0 || length < 0 || offset + length > mapping.length) {
      GL.recordError(0x501 /* GL_INVALID_VALUE */);
      err('invalid range in glFlushMappedBufferRange');
      return;
    }

    webglBufferSubData(target, mapping.offset, length, mapping.mem + offset);
  },

  glUnmapBuffer__deps: ['free'],
  glUnmapBuffer: (target) => {
    if (!emscriptenWebGLValidateMapBufferTarget(target)) {
      GL.recordError(0x500/*GL_INVALID_ENUM*/);
      err('GL_INVALID_ENUM in glUnmapBuffer');
      return 0;
    }

    var buffer = emscriptenWebGLGetBufferBinding(target);
    var mapping = GL.mappedBuffers[buffer];
    if (!mapping || !mapping.mem) {
      GL.recordError(0x502 /* GL_INVALID_OPERATION */);
      err('buffer was never mapped in glUnmapBuffer');
      return 0;
    }

    if (!(mapping.access & 0x10)) { /* GL_MAP_FLUSH_EXPLICIT_BIT */
      webglBufferSubData(target, mapping.offset, mapping.length, mapping.mem);
    }

    _free(mapping.mem);
    mapping.mem = 0;
    return 1;
  },
#endif

  glPolygonOffsetClampEXT__sig: 'vfff',
  glPolygonOffsetClampEXT: (factor, units, clamp) => {
#if GL_ASSERTIONS
    assert(GLctx.extPolygonOffsetClamp, 'EXT_polygon_offset_clamp not supported, or not enabled. Before calling glPolygonOffsetClampEXT(), call emscripten_webgl_enable_EXT_polygon_offset_clamp() to enable this extension, and verify that it returns true to indicate support. (alternatively, build with -sGL_SUPPORT_AUTOMATIC_ENABLE_EXTENSIONS=1 to enable all GL extensions by default)');
#endif
    GLctx.extPolygonOffsetClamp['polygonOffsetClampEXT'](factor, units, clamp);
  },

  glClipControlEXT__sig: 'vii',
  glClipControlEXT: (origin, depth) => {
#if GL_ASSERTIONS
    assert(GLctx.extClipControl, 'EXT_clip_control not supported, or not enabled. Before calling glClipControlEXT(), call emscripten_webgl_enable_EXT_clip_control() to enable this extension, and verify that it returns true to indicate support. (alternatively, build with -sGL_SUPPORT_AUTOMATIC_ENABLE_EXTENSIONS=1 to enable all GL extensions by default)');
#endif
    GLctx.extClipControl['clipControlEXT'](origin, depth);
  },

  glPolygonModeWEBGL__sig: 'vii',
  glPolygonModeWEBGL: (face, mode) => {
#if GL_ASSERTIONS
    assert(GLctx.webglPolygonMode, 'WEBGL_polygon_mode not supported, or not enabled. Before calling glPolygonModeWEBGL(), call emscripten_webgl_enable_WEBGL_polygon_mode() to enable this extension, and verify that it returns true to indicate support. (alternatively, build with -sGL_SUPPORT_AUTOMATIC_ENABLE_EXTENSIONS=1 to enable all GL extensions by default)');
#endif
    GLctx.webglPolygonMode['polygonModeWEBGL'](face, mode);
  },
};

#if !GL_ENABLE_GET_PROC_ADDRESS
[
  'emscripten_webgl1_get_proc_address',
  'emscripten_webgl2_get_proc_address',
  'emscripten_webgl_get_proc_address',
  'SDL_GL_GetProcAddress',
  'eglGetProcAddress',
  'glfwGetProcAddress'
].forEach((name) => {
  LibraryGL[name] = (name) => { abort(); return 0; };
  // Due to the two pass nature of compiling .js files,
  // in INCLUDE_FULL_LIBRARY mode, we must include the above
  // stub functions, but not their __deps message handlers.
#if !INCLUDE_FULL_LIBRARY
  LibraryGL[name + '__deps'] = [() => {
    error(`linker: Undefined symbol: ${name}(). Please pass -sGL_ENABLE_GET_PROC_ADDRESS at link time to link in ${name}().`);
  }];
#endif
});
#endif

// Simple pass-through functions.
// - Starred ones have return values.
// - [X] ones have X in the C name but not in the JS name
var glPassthroughFuncs = [
  [0, 'finish flush'],
  [1, 'clearDepth clearDepth[f] depthFunc enable disable frontFace cullFace clear lineWidth clearStencil stencilMask checkFramebufferStatus* generateMipmap activeTexture blendEquation isEnabled*'],
  [2, 'blendFunc blendEquationSeparate depthRange depthRange[f] stencilMaskSeparate hint polygonOffset vertexAttrib1f'],
  [3, 'texParameteri texParameterf vertexAttrib2f stencilFunc stencilOp'],
  [4, 'viewport clearColor scissor vertexAttrib3f renderbufferStorage blendFuncSeparate blendColor stencilFuncSeparate stencilOpSeparate'],
  [5, 'vertexAttrib4f'],
  [8, 'copyTexImage2D copyTexSubImage2D'],
];

function createGLPassthroughFunctions(lib, funcs) {
  for (const [num, names] of funcs) {
    const args = range(num).map((i) => 'x' + i ).join(', ');
    const stub = `(${args}) => GLctx.NAME(${args})`;
    const sigEnd = range(num).map(() => 'i').join('');
    for (var name of names.split(' ')) {
      let sig;
      if (name.endsWith('*')) {
        name = name.slice(0, -1);
        sig = 'i' + sigEnd;
      } else {
        sig = 'v' + sigEnd;
      }
      let cName = name;
      if (name.includes('[')) {
        cName = name.replace('[', '').replace(']', '');
        name = cName.slice(0, -1);
      }
      cName = 'gl' + cName[0].toUpperCase() + cName.slice(1);
      assert(!(cName in lib), 'Cannot reimplement the existing function ' + cName);
      lib[cName] = eval(stub.replace('NAME', name));
      assert(lib[cName + '__sig'] || LibraryManager.library[cName + '__sig'], 'missing sig for ' + cName);
    }
  }
}

createGLPassthroughFunctions(LibraryGL, glPassthroughFuncs);

function renameSymbol(lib, oldName, newName) {
  lib[newName] = lib[oldName];
  delete lib[oldName];
  for (const suffix of decoratorSuffixes) {
    const oldDecorator = oldName + suffix;
    if (Object.hasOwn(lib, oldDecorator)) {
      const newDecorator = newName + suffix;
      lib[newDecorator] = lib[oldDecorator];
      delete lib[oldDecorator];
    }
  }
}

function recordGLProcAddressGet(lib) {
  // GL proc address retrieval - allow access through glX and emscripten_glX, to
  // allow name collisions with user-implemented things having the same name
  // (see gl.c)
  //
  // We do this by renaming `glX` symbols to `emscripten_glX` and then setting
  // `glX` as an alias of `emscripten_glX`.  The reason for this renaming is to
  // ensure that `emscripten_glX` is always available, even in cases where native
  // code defines `glX`.
  const glSyms = [];
  for (const sym of Object.keys(lib)) {
    if (sym.startsWith('gl') && !isDecorator(sym)) {
      const newSym = 'emscripten_' + sym;
      renameSymbol(lib, sym, newSym);
      lib[sym] = newSym;
      var sig = LibraryManager.library[sym + '__sig'];
      if (sig) {
        lib[newSym + '__sig'] = sig;
      }
    }
  }
  // Also update any alias targets to point to the emscripten_-prefixed
  // versions. e.g. both the name of the symbol *and* its target need updating
  // in cases such as `glMultiDrawElements: 'glMultiDrawElementsWEBGL'`.
  // Cross-library targets (such as glGenFramebuffers in LibraryGLEmulation)
  // are looked up in LibraryManager.library as well.
  for (const [sym, target] of Object.entries(lib)) {
    if (sym.startsWith('emscripten_gl') && typeof target === 'string' &&
        (Object.hasOwn(lib, 'emscripten_' + target) ||
         Object.hasOwn(LibraryManager.library, 'emscripten_' + target))) {
      lib[sym] = 'emscripten_' + target;
    }
  }
}

recordGLProcAddressGet(LibraryGL);

// Final merge
addToLibrary(LibraryGL);