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

assert(LEGACY_GL_EMULATION, 'libglemu.js should only be included with LEGACY_GL_EMULATION set')
assert(!FULL_ES2, 'cannot emulate both ES2 and legacy GL');
assert(!FULL_ES3, 'cannot emulate both ES3 and legacy GL');

{{{
  const copySigs = (func) => {
    if (!MAIN_MODULE) return '';
    return ` _${func}.sig = _emscripten_${func}.sig = orig_${func}.sig;`;
  };
  const fromPtr = (arg) => {
    if (CAN_ADDRESS_2GB) {
      return `${arg} >>>= 0`;
    } else if (MEMORY64) {
      return `${arg} = Number(${arg})`;
    }
    return '';
  };
}}}

var LibraryGLEmulation = {
  // GL emulation: provides misc. functionality not present in OpenGL ES 2.0 or WebGL
  $GLEmulation__deps: ['$GLImmediateSetup', 'glEnable', 'glDisable',
    'glIsEnabled', 'glGetBooleanv', 'glGetIntegerv', 'glGetString',
    'glCreateShader', 'glShaderSource', 'glCompileShader', 'glAttachShader',
    'glDetachShader', 'glUseProgram', 'glDeleteProgram', 'glBindAttribLocation',
    'glLinkProgram', 'glBindBuffer', 'glGetFloatv', 'glHint',
    'glEnableVertexAttribArray', 'glDisableVertexAttribArray',
    'glVertexAttribPointer', 'glActiveTexture', '$stringToNewUTF8',
    '$ptrToString', '$getEmscriptenSupportedExtensions',
  ],
  $GLEmulation__postset: `
    // Forward declare GL functions that are overridden by GLEmulation.
    /**@suppress {duplicate, undefinedVars}*/var _emscripten_glDrawArrays;
    /**@suppress {duplicate, undefinedVars}*/var _emscripten_glDrawElements;
    /**@suppress {duplicate, undefinedVars}*/var _emscripten_glActiveTexture;
    /**@suppress {duplicate, undefinedVars}*/var _emscripten_glEnable;
    /**@suppress {duplicate, undefinedVars}*/var _emscripten_glDisable;
    /**@suppress {duplicate, undefinedVars}*/var _emscripten_glTexEnvf;
    /**@suppress {duplicate, undefinedVars}*/var _emscripten_glTexEnvi;
    /**@suppress {duplicate, undefinedVars}*/var _emscripten_glTexEnvfv;
    /**@suppress {duplicate, undefinedVars}*/var _emscripten_glGetIntegerv;
    /**@suppress {duplicate, undefinedVars}*/var _emscripten_glIsEnabled;
    /**@suppress {duplicate, undefinedVars}*/var _emscripten_glGetBooleanv;
    /**@suppress {duplicate, undefinedVars}*/var _emscripten_glGetString;
    /**@suppress {duplicate, undefinedVars}*/var _emscripten_glCreateShader;
    /**@suppress {duplicate, undefinedVars}*/var _emscripten_glShaderSource;
    /**@suppress {duplicate, undefinedVars}*/var _emscripten_glCompileShader;
    /**@suppress {duplicate, undefinedVars}*/var _emscripten_glAttachShader;
    /**@suppress {duplicate, undefinedVars}*/var _emscripten_glDetachShader;
    /**@suppress {duplicate, undefinedVars}*/var _emscripten_glUseProgram;
    /**@suppress {duplicate, undefinedVars}*/var _emscripten_glDeleteProgram;
    /**@suppress {duplicate, undefinedVars}*/var _emscripten_glBindAttribLocation;
    /**@suppress {duplicate, undefinedVars}*/var _emscripten_glLinkProgram;
    /**@suppress {duplicate, undefinedVars}*/var _emscripten_glBindBuffer;
    /**@suppress {duplicate, undefinedVars}*/var _emscripten_glGetFloatv;
    /**@suppress {duplicate, undefinedVars}*/var _emscripten_glHint;
    /**@suppress {duplicate, undefinedVars}*/var _emscripten_glEnableVertexAttribArray;
    /**@suppress {duplicate, undefinedVars}*/var _emscripten_glDisableVertexAttribArray;
    /**@suppress {duplicate, undefinedVars}*/var _emscripten_glVertexAttribPointer;
    /**@suppress {duplicate, undefinedVars}*/var _glTexEnvf;
    /**@suppress {duplicate, undefinedVars}*/var _glTexEnvi;
    /**@suppress {duplicate, undefinedVars}*/var _glTexEnvfv;
    /**@suppress {duplicate, undefinedVars}*/var _glGetTexEnviv;
    /**@suppress {duplicate, undefinedVars}*/var _glGetTexEnvfv;
    GLEmulation.init();`,
  $GLEmulation: {
    // Fog support. Partial, we assume shaders are used that implement fog. We just pass them uniforms
    fogStart: 0,
    fogEnd: 1,
    fogDensity: 1.0,
    fogColor: null,
    fogMode: 0x800, // GL_EXP
    fogEnabled: false,

    // GL_CLIP_PLANE support
    MAX_CLIP_PLANES: 6,
    clipPlaneEnabled: [false, false, false, false, false, false],
    clipPlaneEquation: [],

    // GL_LIGHTING support
    lightingEnabled: false,

    lightModelAmbient: null,
    lightModelLocalViewer: false,
    lightModelTwoSide: false,

    materialAmbient: null,
    materialDiffuse: null,
    materialSpecular: null,
    materialShininess: null,
    materialEmission: null,

    MAX_LIGHTS: 8,
    lightEnabled: [false, false, false, false, false, false, false, false],
    lightAmbient: [],
    lightDiffuse: [],
    lightSpecular: [],
    lightPosition: [],
    // TODO attenuation modes of lights

    // GL_ALPHA_TEST support
    alphaTestEnabled: false,
    alphaTestFunc: 0x207, // GL_ALWAYS
    alphaTestRef: 0.0,

    // GL_POINTS support.
    pointSize: 1.0,

    // VAO support
    vaos: [],
    currentVao: null,
    enabledVertexAttribArrays: {}, // helps with vao cleanups

    hasRunInit: false,

    // Find a token in a shader source string
    findToken(source, token) {
      function isIdentChar(ch) {
        if (ch >= 48 && ch <= 57) // 0-9
          return true;
        if (ch >= 65 && ch <= 90) // A-Z
          return true;
        if (ch >= 97 && ch <= 122) // a-z
          return true;
        return false;
      }
      var i = -1;
      do {
        i = source.indexOf(token, i + 1);
        if (i < 0) {
          break;
        }
        if (i > 0 && isIdentChar(source[i - 1])) {
          continue;
        }
        i += token.length;
        if (i < source.length - 1 && isIdentChar(source[i + 1])) {
          continue;
        }
        return true;
      } while (true);
      return false;
    },

    init() {
      // Do not activate immediate/emulation code (e.g. replace glDrawElements)
      // when in FULL_ES2 mode.  We do not need full emulation, we instead
      // emulate client-side arrays etc. in FULL_ES2 code in a straightforward
      // manner, and avoid not having a bound buffer be ambiguous between es2
      // emulation code and legacy gl emulation code.
#if FULL_ES2
      return;
#endif

      if (GLEmulation.hasRunInit) {
        return;
      }
      GLEmulation.hasRunInit = true;

      GLEmulation.fogColor = new Float32Array(4);

      for (var clipPlaneId = 0; clipPlaneId < GLEmulation.MAX_CLIP_PLANES; clipPlaneId++) {
        GLEmulation.clipPlaneEquation[clipPlaneId] = new Float32Array(4);
      }

      // set defaults for GL_LIGHTING
      GLEmulation.lightModelAmbient = new Float32Array([0.2, 0.2, 0.2, 1.0]);
      GLEmulation.materialAmbient = new Float32Array([0.2, 0.2, 0.2, 1.0]);
      GLEmulation.materialDiffuse = new Float32Array([0.8, 0.8, 0.8, 1.0]);
      GLEmulation.materialSpecular = new Float32Array([0.0, 0.0, 0.0, 1.0]);
      GLEmulation.materialShininess = new Float32Array([0.0]);
      GLEmulation.materialEmission = new Float32Array([0.0, 0.0, 0.0, 1.0]);

      for (var lightId = 0; lightId < GLEmulation.MAX_LIGHTS; lightId++) {
        GLEmulation.lightAmbient[lightId] = new Float32Array([0.0, 0.0, 0.0, 1.0]);
        GLEmulation.lightDiffuse[lightId] = lightId ? new Float32Array([0.0, 0.0, 0.0, 1.0]) : new Float32Array([1.0, 1.0, 1.0, 1.0]);
        GLEmulation.lightSpecular[lightId] = lightId ? new Float32Array([0.0, 0.0, 0.0, 1.0]) : new Float32Array([1.0, 1.0, 1.0, 1.0]);
        GLEmulation.lightPosition[lightId] = new Float32Array([0.0, 0.0, 1.0, 0.0]);
      }


      // Add some emulation workarounds
      err('WARNING: using emscripten GL emulation. This is a collection of limited workarounds, do not expect it to work.');
#if GL_UNSAFE_OPTS == 1
      err('WARNING: using emscripten GL emulation unsafe opts. If weirdness happens, try -sGL_UNSAFE_OPTS=0');
#endif

      // XXX some of the capabilities we don't support may lead to incorrect rendering, if we do not emulate them in shaders
      var validCapabilities = {
        0xB44: 1, // GL_CULL_FACE
        0xBE2: 1, // GL_BLEND
        0xBD0: 1, // GL_DITHER,
        0xB90: 1, // GL_STENCIL_TEST
        0xB71: 1, // GL_DEPTH_TEST
        0xC11: 1, // GL_SCISSOR_TEST
        0x8037: 1, // GL_POLYGON_OFFSET_FILL
        0x809E: 1, // GL_SAMPLE_ALPHA_TO_COVERAGE
        0x80A0: 1  // GL_SAMPLE_COVERAGE
      };

      var orig_glEnable = _glEnable;
      _glEnable = _emscripten_glEnable = (cap) => {
        // Clean up the renderer on any change to the rendering state. The optimization of
        // skipping renderer setup is aimed at the case of multiple glDraw* right after each other
        GLImmediate.lastRenderer?.cleanup();
        if (cap == 0xB60 /* GL_FOG */) {
          if (GLEmulation.fogEnabled != true) {
            GLImmediate.currentRenderer = null; // Fog parameter is part of the FFP shader state, we must re-lookup the renderer to use.
            GLEmulation.fogEnabled = true;
          }
          return;
        } else if ((cap >= 0x3000) && (cap < 0x3006)  /* GL_CLIP_PLANE0 to GL_CLIP_PLANE5 */) {
          var clipPlaneId = cap - 0x3000;
          if (GLEmulation.clipPlaneEnabled[clipPlaneId] != true) {
            GLImmediate.currentRenderer = null; // clip plane parameter is part of the FFP shader state, we must re-lookup the renderer to use.
            GLEmulation.clipPlaneEnabled[clipPlaneId] = true;
          }
          return;
        } else if ((cap >= 0x4000) && (cap < 0x4008)  /* GL_LIGHT0 to GL_LIGHT7 */) {
          var lightId = cap - 0x4000;
          if (GLEmulation.lightEnabled[lightId] != true) {
            GLImmediate.currentRenderer = null; // light parameter is part of the FFP shader state, we must re-lookup the renderer to use.
            GLEmulation.lightEnabled[lightId] = true;
          }
          return;
        } else if (cap == 0xB50 /* GL_LIGHTING */) {
          if (GLEmulation.lightingEnabled != true) {
            GLImmediate.currentRenderer = null; // light parameter is part of the FFP shader state, we must re-lookup the renderer to use.
            GLEmulation.lightingEnabled = true;
          }
          return;
        } else if (cap == 0xBC0 /* GL_ALPHA_TEST */) {
          if (GLEmulation.alphaTestEnabled != true) {
            GLImmediate.currentRenderer = null; // alpha testing is part of the FFP shader state, we must re-lookup the renderer to use.
            GLEmulation.alphaTestEnabled = true;
          }
          return;
        } else if (cap == 0xDE1 /* GL_TEXTURE_2D */) {
          // XXX not according to spec, and not in desktop GL, but works in some GLES1.x apparently, so support
          // it by forwarding to glEnableClientState
          /* Actually, let's not, for now. (This sounds exceedingly broken)
           * This is in gl_ps_workaround2.c.
          _glEnableClientState(cap);
          */
          return;
        } else if (!(cap in validCapabilities)) {
          return;
        }
        orig_glEnable(cap);
      };
      {{{ copySigs('glEnable') }}}

      var orig_glDisable = _glDisable;
      _glDisable = _emscripten_glDisable = (cap) => {
        GLImmediate.lastRenderer?.cleanup();
        if (cap == 0xB60 /* GL_FOG */) {
          if (GLEmulation.fogEnabled != false) {
            GLImmediate.currentRenderer = null; // Fog parameter is part of the FFP shader state, we must re-lookup the renderer to use.
            GLEmulation.fogEnabled = false;
          }
          return;
        } else if ((cap >= 0x3000) && (cap < 0x3006)  /* GL_CLIP_PLANE0 to GL_CLIP_PLANE5 */) {
          var clipPlaneId = cap - 0x3000;
          if (GLEmulation.clipPlaneEnabled[clipPlaneId] != false) {
            GLImmediate.currentRenderer = null; // clip plane parameter is part of the FFP shader state, we must re-lookup the renderer to use.
            GLEmulation.clipPlaneEnabled[clipPlaneId] = false;
          }
          return;
        } else if ((cap >= 0x4000) && (cap < 0x4008)  /* GL_LIGHT0 to GL_LIGHT7 */) {
          var lightId = cap - 0x4000;
          if (GLEmulation.lightEnabled[lightId] != false) {
            GLImmediate.currentRenderer = null; // light parameter is part of the FFP shader state, we must re-lookup the renderer to use.
            GLEmulation.lightEnabled[lightId] = false;
          }
          return;
        } else if (cap == 0xB50 /* GL_LIGHTING */) {
          if (GLEmulation.lightingEnabled != false) {
            GLImmediate.currentRenderer = null; // light parameter is part of the FFP shader state, we must re-lookup the renderer to use.
            GLEmulation.lightingEnabled = false;
          }
          return;
        } else if (cap == 0xBC0 /* GL_ALPHA_TEST */) {
          if (GLEmulation.alphaTestEnabled != false) {
            GLImmediate.currentRenderer = null; // alpha testing is part of the FFP shader state, we must re-lookup the renderer to use.
            GLEmulation.alphaTestEnabled = false;
          }
          return;
        } else if (cap == 0xDE1 /* GL_TEXTURE_2D */) {
          // XXX not according to spec, and not in desktop GL, but works in some GLES1.x apparently, so support
          // it by forwarding to glDisableClientState
          /* Actually, let's not, for now. (This sounds exceedingly broken)
           * This is in gl_ps_workaround2.c.
          _glDisableClientState(cap);
          */
          return;
        } else if (!(cap in validCapabilities)) {
          return;
        }
        orig_glDisable(cap);
      };
      {{{ copySigs('glDisable') }}}

      var orig_glIsEnabled = _glIsEnabled;
      _glIsEnabled = _emscripten_glIsEnabled = (cap) => {
        if (cap == 0xB60 /* GL_FOG */) {
          return GLEmulation.fogEnabled ? 1 : 0;
        } else if ((cap >= 0x3000) && (cap < 0x3006)  /* GL_CLIP_PLANE0 to GL_CLIP_PLANE5 */) {
          var clipPlaneId = cap - 0x3000;
          return GLEmulation.clipPlaneEnabled[clipPlaneId] ? 1 : 0;
        } else if ((cap >= 0x4000) && (cap < 0x4008)  /* GL_LIGHT0 to GL_LIGHT7 */) {
          var lightId = cap - 0x4000;
          return GLEmulation.lightEnabled[lightId] ? 1 : 0;
        } else if (cap == 0xB50 /* GL_LIGHTING */) {
          return GLEmulation.lightingEnabled ? 1 : 0;
        } else if (cap == 0xBC0 /* GL_ALPHA_TEST */) {
          return GLEmulation.alphaTestEnabled ? 1 : 0;
        } else if (!(cap in validCapabilities)) {
          return 0;
        }
        return GLctx.isEnabled(cap);
      };
      {{{ copySigs('glIsEnabled') }}}

      var orig_glGetBooleanv = _glGetBooleanv;
      _glGetBooleanv = _emscripten_glGetBooleanv = (pname, p) => {
        var attrib = GLEmulation.getAttributeFromCapability(pname);
        if (attrib !== null) {
          {{{ fromPtr('p') }}}
          var result = GLImmediate.enabledClientAttributes[attrib];
          {{{ makeSetValue('p', '0', 'result === true ? 1 : 0', 'i8') }}};
          return;
        }
        orig_glGetBooleanv(pname, p);
      };
      {{{ copySigs('glGetBooleanv') }}}

      var orig_glGetIntegerv = _glGetIntegerv;
      _glGetIntegerv = _emscripten_glGetIntegerv = (pname, params) => {
        {{{ fromPtr('params') }}}
        switch (pname) {
          case 0x84E2: pname = GLctx.MAX_TEXTURE_IMAGE_UNITS /* fake it */; break; // GL_MAX_TEXTURE_UNITS
          case 0x8B4A: { // GL_MAX_VERTEX_UNIFORM_COMPONENTS_ARB
            var result = GLctx.getParameter(GLctx.MAX_VERTEX_UNIFORM_VECTORS);
            {{{ makeSetValue('params', '0', 'result*4', 'i32') }}}; // GLES gives num of 4-element vectors, GL wants individual components, so multiply
            return;
          }
          case 0x8B49: { // GL_MAX_FRAGMENT_UNIFORM_COMPONENTS_ARB
            var result = GLctx.getParameter(GLctx.MAX_FRAGMENT_UNIFORM_VECTORS);
            {{{ makeSetValue('params', '0', 'result*4', 'i32') }}}; // GLES gives num of 4-element vectors, GL wants individual components, so multiply
            return;
          }
          case 0x8B4B: { // GL_MAX_VARYING_FLOATS_ARB
            var result = GLctx.getParameter(GLctx.MAX_VARYING_VECTORS);
            {{{ makeSetValue('params', '0', 'result*4', 'i32') }}}; // GLES gives num of 4-element vectors, GL wants individual components, so multiply
            return;
          }
          case 0x8871: pname = GLctx.MAX_COMBINED_TEXTURE_IMAGE_UNITS /* close enough */; break; // GL_MAX_TEXTURE_COORDS
          case 0x807A: { // GL_VERTEX_ARRAY_SIZE
            var attribute = GLImmediate.clientAttributes[GLImmediate.VERTEX];
            {{{ makeSetValue('params', '0', 'attribute ? attribute.size : 0', 'i32') }}};
            return;
          }
          case 0x807B: { // GL_VERTEX_ARRAY_TYPE
            var attribute = GLImmediate.clientAttributes[GLImmediate.VERTEX];
            {{{ makeSetValue('params', '0', 'attribute ? attribute.type : 0', 'i32') }}};
            return;
          }
          case 0x807C: { // GL_VERTEX_ARRAY_STRIDE
            var attribute = GLImmediate.clientAttributes[GLImmediate.VERTEX];
            {{{ makeSetValue('params', '0', 'attribute ? attribute.stride : 0', 'i32') }}};
            return;
          }
          case 0x8081: { // GL_COLOR_ARRAY_SIZE
            var attribute = GLImmediate.clientAttributes[GLImmediate.COLOR];
            {{{ makeSetValue('params', '0', 'attribute ? attribute.size : 0', 'i32') }}};
            return;
          }
          case 0x8082: { // GL_COLOR_ARRAY_TYPE
            var attribute = GLImmediate.clientAttributes[GLImmediate.COLOR];
            {{{ makeSetValue('params', '0', 'attribute ? attribute.type : 0', 'i32') }}};
            return;
          }
          case 0x8083: { // GL_COLOR_ARRAY_STRIDE
            var attribute = GLImmediate.clientAttributes[GLImmediate.COLOR];
            {{{ makeSetValue('params', '0', 'attribute ? attribute.stride : 0', 'i32') }}};
            return;
          }
          case 0x8088: { // GL_TEXTURE_COORD_ARRAY_SIZE
            var attribute = GLImmediate.clientAttributes[GLImmediate.TEXTURE0 + GLImmediate.clientActiveTexture];
            {{{ makeSetValue('params', '0', 'attribute ? attribute.size : 0', 'i32') }}};
            return;
          }
          case 0x8089: { // GL_TEXTURE_COORD_ARRAY_TYPE
            var attribute = GLImmediate.clientAttributes[GLImmediate.TEXTURE0 + GLImmediate.clientActiveTexture];
            {{{ makeSetValue('params', '0', 'attribute ? attribute.type : 0', 'i32') }}};
            return;
          }
          case 0x808A: { // GL_TEXTURE_COORD_ARRAY_STRIDE
            var attribute = GLImmediate.clientAttributes[GLImmediate.TEXTURE0 + GLImmediate.clientActiveTexture];
            {{{ makeSetValue('params', '0', 'attribute ? attribute.stride : 0', 'i32') }}};
            return;
          }
          case 0x0D32: { // GL_MAX_CLIP_PLANES
            {{{ makeSetValue('params', '0', 'GLEmulation.MAX_CLIP_PLANES', 'i32') }}}; // all implementations need to support at least 6
            return;
          }
          case 0x0BA0: { // GL_MATRIX_MODE
            {{{ makeSetValue('params', '0', 'GLImmediate.currentMatrix + 0x1700', 'i32') }}};
            return;
          }
          case 0x0BC1: { // GL_ALPHA_TEST_FUNC
            {{{ makeSetValue('params', '0', 'GLEmulation.alphaTestFunc', 'i32') }}};
            return;
          }
        }
        orig_glGetIntegerv(pname, params);
      };
      {{{ copySigs('glGetIntegerv') }}}

      var orig_glGetString = _glGetString;
      _glGetString = _emscripten_glGetString = (name_) => {
        if (GL.stringCache[name_]) return GL.stringCache[name_];
        switch (name_) {
          case 0x1F03 /* GL_EXTENSIONS */: // Add various extensions that we can support
            var ret = stringToNewUTF8(getEmscriptenSupportedExtensions(GLctx).join(' ') +
                   ' GL_EXT_texture_env_combine GL_ARB_texture_env_crossbar GL_ATI_texture_env_combine3 GL_NV_texture_env_combine4 GL_EXT_texture_env_dot3 GL_ARB_multitexture GL_ARB_vertex_buffer_object GL_EXT_framebuffer_object GL_ARB_vertex_program GL_ARB_fragment_program GL_ARB_shading_language_100 GL_ARB_shader_objects GL_ARB_vertex_shader GL_ARB_fragment_shader GL_ARB_texture_cube_map GL_EXT_draw_range_elements' +
                   (GL.currentContext.compressionExt ? ' GL_ARB_texture_compression GL_EXT_texture_compression_s3tc' : '') +
                   (GL.currentContext.anisotropicExt ? ' GL_EXT_texture_filter_anisotropic' : '')
            );
            return GL.stringCache[name_] = {{{ to64('ret') }}};
        }
        return orig_glGetString(name_);
      };
      {{{ copySigs('glGetString') }}}

      // Do some automatic rewriting to work around GLSL differences. Note that this must be done in
      // tandem with the rest of the program, by itself it cannot suffice.
      // Note that we need to remember shader types for this rewriting, saving sources makes it easier to debug.
      GL.shaderInfos = {};
#if GL_DEBUG
      GL.shaderSources = {};
      GL.shaderOriginalSources = {};
#endif
      var orig_glCreateShader = _glCreateShader;
      _glCreateShader = _emscripten_glCreateShader = (shaderType) => {
        var id = orig_glCreateShader(shaderType);
        GL.shaderInfos[id] = {
          type: shaderType,
          ftransform: false
        };
        return id;
      };
      {{{ copySigs('glCreateShader') }}}

      function ensurePrecision(source) {
        if (!/precision +(low|medium|high)p +float *;/.test(source)) {
          source = '#ifdef GL_FRAGMENT_PRECISION_HIGH\nprecision highp float;\n#else\nprecision mediump float;\n#endif\n' + source;
        }
        return source;
      }

      var orig_glShaderSource = _glShaderSource;
      _glShaderSource = _emscripten_glShaderSource = (shader, count, string, length) => {
        {{{ fromPtr('string') }}}
        {{{ fromPtr('length') }}}
        var source = GL.getSource(shader, count, string, length);
#if GL_DEBUG
        dbg("glShaderSource: Input: \n" + source);
        GL.shaderOriginalSources[shader] = source;
#endif
        // XXX We add attributes and uniforms to shaders. The program can ask for the # of them, and see the
        // ones we generated, potentially confusing it? Perhaps we should hide them.
        if (GL.shaderInfos[shader].type == GLctx.VERTEX_SHADER) {
          // Replace ftransform() with explicit project/modelview transforms, and add position and matrix info.
          var has_pm = source.search(/u_projection/) >= 0;
          var has_mm = source.search(/u_modelView/) >= 0;
          var has_pv = source.search(/a_position/) >= 0;
          var need_pm = 0, need_mm = 0, need_pv = 0;
          var old = source;
          source = source.replace(/ftransform\(\)/g, '(u_projection * u_modelView * a_position)');
          if (old != source) need_pm = need_mm = need_pv = 1;
          old = source;
          source = source.replace(/gl_ProjectionMatrix/g, 'u_projection');
          if (old != source) need_pm = 1;
          old = source;
          source = source.replace(/gl_ModelViewMatrixTranspose\[2\]/g, 'vec4(u_modelView[0][2], u_modelView[1][2], u_modelView[2][2], u_modelView[3][2])'); // XXX extremely inefficient
          if (old != source) need_mm = 1;
          old = source;
          source = source.replace(/gl_ModelViewMatrix/g, 'u_modelView');
          if (old != source) need_mm = 1;
          old = source;
          source = source.replace(/gl_Vertex/g, 'a_position');
          if (old != source) need_pv = 1;
          old = source;
          source = source.replace(/gl_ModelViewProjectionMatrix/g, '(u_projection * u_modelView)');
          if (old != source) need_pm = need_mm = 1;
          if (need_pv && !has_pv) source = 'attribute vec4 a_position; \n' + source;
          if (need_mm && !has_mm) source = 'uniform mat4 u_modelView; \n' + source;
          if (need_pm && !has_pm) source = 'uniform mat4 u_projection; \n' + source;
          GL.shaderInfos[shader].ftransform = need_pm || need_mm || need_pv; // we will need to provide the fixed function stuff as attributes and uniforms
          for (var i = 0; i < GLImmediate.MAX_TEXTURES; i++) {
            // XXX To handle both regular texture mapping and cube mapping, we use vec4 for tex coordinates.
            old = source;
            var need_vtc = source.search(`v_texCoord${i}`) == -1;
            source = source.replace(new RegExp(`gl_TexCoord\\[${i}\\]`, 'g'), `v_texCoord${i}`)
                           .replace(new RegExp(`gl_MultiTexCoord${i}`, 'g'), `a_texCoord${i}`);
            if (source != old) {
              source = `attribute vec4 a_texCoord${i}; \n${source}`;
              if (need_vtc) {
                source = `varying vec4 v_texCoord${i};   \n${source}`;
              }
            }

            old = source;
            source = source.replace(new RegExp(`gl_TextureMatrix\\[${i}\\]`, 'g'), `u_textureMatrix${i}`);
            if (source != old) {
              source = `uniform mat4 u_textureMatrix${i}; \n${source}`;
            }
          }
          if (source.includes('gl_FrontColor')) {
            source = 'varying vec4 v_color; \n' +
                     source.replace(/gl_FrontColor/g, 'v_color');
          }
          if (source.includes('gl_Color')) {
            source = 'attribute vec4 a_color; \n' +
                     source.replace(/gl_Color/g, 'a_color');
          }
          if (source.includes('gl_Normal')) {
            source = 'attribute vec3 a_normal; \n' +
                     source.replace(/gl_Normal/g, 'a_normal');
          }
          // fog
          if (source.includes('gl_FogFragCoord')) {
            source = 'varying float v_fogFragCoord;   \n' +
                     source.replace(/gl_FogFragCoord/g, 'v_fogFragCoord');
          }
        } else { // Fragment shader
          for (i = 0; i < GLImmediate.MAX_TEXTURES; i++) {
            old = source;
            source = source.replace(new RegExp(`gl_TexCoord\\[${i}\\]`, 'g'), `v_texCoord${i}`);
            if (source != old) {
              source = 'varying vec4 v_texCoord' + i + ';   \n' + source;
            }
          }
          if (source.includes('gl_Color')) {
            source = 'varying vec4 v_color; \n' + source.replace(/gl_Color/g, 'v_color');
          }
          if (source.includes('gl_Fog.color')) {
            source = 'uniform vec4 u_fogColor;   \n' +
                     source.replace(/gl_Fog.color/g, 'u_fogColor');
          }
          if (source.includes('gl_Fog.end')) {
            source = 'uniform float u_fogEnd;   \n' +
                     source.replace(/gl_Fog.end/g, 'u_fogEnd');
          }
          if (source.includes('gl_Fog.scale')) {
            source = 'uniform float u_fogScale;   \n' +
                     source.replace(/gl_Fog.scale/g, 'u_fogScale');
          }
          if (source.includes('gl_Fog.density')) {
            source = 'uniform float u_fogDensity;   \n' +
                     source.replace(/gl_Fog.density/g, 'u_fogDensity');
          }
          if (source.includes('gl_FogFragCoord')) {
            source = 'varying float v_fogFragCoord;   \n' +
                     source.replace(/gl_FogFragCoord/g, 'v_fogFragCoord');
          }
          source = ensurePrecision(source);
        }
#if GL_DEBUG
        GL.shaderSources[shader] = source;
        dbg("glShaderSource: Output: \n" + source);
#endif
        GLctx.shaderSource(GL.shaders[shader], source);
      };
      {{{ copySigs('glShaderSource') }}}

      var orig_glCompileShader = _glCompileShader;
      _glCompileShader = _emscripten_glCompileShader = (shader) => {
        GLctx.compileShader(GL.shaders[shader]);
#if GL_DEBUG
        if (!GLctx.getShaderParameter(GL.shaders[shader], GLctx.COMPILE_STATUS)) {
          dbg(`Failed to compile shader: ${GLctx.getShaderInfoLog(GL.shaders[shader])}`);
          dbg(`Info: ${JSON.stringify(GL.shaderInfos[shader])}`);
          dbg(`Original source: ${GL.shaderOriginalSources[shader]}`);
          dbg(`Source: ${GL.shaderSources[shader]}`);
          abort('Shader compilation halt');
        }
#endif
      };
      {{{ copySigs('glCompileShader') }}}

      GL.programShaders = {};
      var orig_glAttachShader = _glAttachShader;
      _glAttachShader = _emscripten_glAttachShader = (program, shader) => {
        GL.programShaders[program] ||= [];
        GL.programShaders[program].push(shader);
        orig_glAttachShader(program, shader);
      };
      {{{ copySigs('glAttachShader') }}}

      var orig_glDetachShader = _glDetachShader;
      _glDetachShader = _emscripten_glDetachShader = (program, shader) => {
        var programShader = GL.programShaders[program];
        if (!programShader) {
          err(`WARNING: _glDetachShader received invalid program: ${program}`);
          return;
        }
        var index = programShader.indexOf(shader);
        programShader.splice(index, 1);
        orig_glDetachShader(program, shader);
      };
      {{{ copySigs('glDetachShader') }}}

      var orig_glUseProgram = _glUseProgram;
      _glUseProgram = _emscripten_glUseProgram = (program) => {
#if GL_DEBUG
        if (GL.debug) {
          dbg('[using program with shaders]');
          if (program) {
            for (var shader of GL.programShaders[program]) {
              dbg(`  shader ${shader}, original source: ${GL.shaderOriginalSources[shader]}`);
              dbg(`         Source: ${GL.shaderSources[shader]}`);
            }
          }
        }
#endif
        if (GL.currProgram != program) {
          GLImmediate.currentRenderer = null; // This changes the FFP emulation shader program, need to recompute that.
          GL.currProgram = program;
          GLImmediate.fixedFunctionProgram = 0;
          orig_glUseProgram(program);
        }
      }
      {{{ copySigs('glUseProgram') }}}

      var orig_glDeleteProgram = _glDeleteProgram;
      _glDeleteProgram = _emscripten_glDeleteProgram = (program) => {
        orig_glDeleteProgram(program);
        if (program == GL.currProgram) {
          GLImmediate.currentRenderer = null; // This changes the FFP emulation shader program, need to recompute that.
          GL.currProgram = 0;
        }
      };
      {{{ copySigs('glDeleteProgram') }}}

      // If attribute 0 was not bound, bind it to 0 for WebGL performance reasons. Track if 0 is free for that.
      var zeroUsedPrograms = {};
      var orig_glBindAttribLocation = _glBindAttribLocation;
      _glBindAttribLocation = _emscripten_glBindAttribLocation = (program, index, name) => {
        if (index == 0) zeroUsedPrograms[program] = true;
        orig_glBindAttribLocation(program, index, name);
      };
      {{{ copySigs('glBindAttribLocation') }}}

      var orig_glLinkProgram = _glLinkProgram;
      _glLinkProgram = _emscripten_glLinkProgram = (program) => {
        if (!(program in zeroUsedPrograms)) {
          GLctx.bindAttribLocation(GL.programs[program], 0, 'a_position');
        }
        orig_glLinkProgram(program);
      };
      {{{ copySigs('glLinkProgram') }}}

      var orig_glBindBuffer = _glBindBuffer;
      _glBindBuffer = _emscripten_glBindBuffer = (target, buffer) => {
        orig_glBindBuffer(target, buffer);
        if (target == GLctx.ARRAY_BUFFER) {
          if (GLEmulation.currentVao) {
#if ASSERTIONS
            assert(GLEmulation.currentVao.arrayBuffer == buffer || GLEmulation.currentVao.arrayBuffer == 0 || buffer == 0, 'TODO: support for multiple array buffers in vao');
#endif
            GLEmulation.currentVao.arrayBuffer = buffer;
          }
        } else if (target == GLctx.ELEMENT_ARRAY_BUFFER) {
          if (GLEmulation.currentVao) GLEmulation.currentVao.elementArrayBuffer = buffer;
        }
      };
      {{{ copySigs('glBindBuffer') }}}

      var orig_glGetFloatv = _glGetFloatv;
      _glGetFloatv = _emscripten_glGetFloatv = (pname, params) => {
        {{{ fromPtr('params') }}}
        if (pname == 0xBA6) { // GL_MODELVIEW_MATRIX
          HEAPF32.set(GLImmediate.matrix[0/*m*/], {{{ getHeapOffset('params', 'float') }}});
        } else if (pname == 0xBA7) { // GL_PROJECTION_MATRIX
          HEAPF32.set(GLImmediate.matrix[1/*p*/], {{{ getHeapOffset('params', 'float') }}});
        } else if (pname == 0xBA8) { // GL_TEXTURE_MATRIX
          HEAPF32.set(GLImmediate.matrix[2/*t*/ + GLImmediate.clientActiveTexture], {{{ getHeapOffset('params', 'float') }}});
        } else if (pname == 0xB66) { // GL_FOG_COLOR
          HEAPF32.set(GLEmulation.fogColor, {{{ getHeapOffset('params', 'float') }}});
        } else if (pname == 0xB63) { // GL_FOG_START
          {{{ makeSetValue('params', '0', 'GLEmulation.fogStart', 'float') }}};
        } else if (pname == 0xB64) { // GL_FOG_END
          {{{ makeSetValue('params', '0', 'GLEmulation.fogEnd', 'float') }}};
        } else if (pname == 0xB62) { // GL_FOG_DENSITY
          {{{ makeSetValue('params', '0', 'GLEmulation.fogDensity', 'float') }}};
        } else if (pname == 0xB65) { // GL_FOG_MODE
          {{{ makeSetValue('params', '0', 'GLEmulation.fogMode', 'float') }}};
        } else if (pname == 0xB53) { // GL_LIGHT_MODEL_AMBIENT
          {{{ makeSetValue('params', '0', 'GLEmulation.lightModelAmbient[0]', 'float') }}};
          {{{ makeSetValue('params', '4', 'GLEmulation.lightModelAmbient[1]', 'float') }}};
          {{{ makeSetValue('params', '8', 'GLEmulation.lightModelAmbient[2]', 'float') }}};
          {{{ makeSetValue('params', '12', 'GLEmulation.lightModelAmbient[3]', 'float') }}};
        } else if (pname == 0xBC2) { // GL_ALPHA_TEST_REF
          {{{ makeSetValue('params', '0', 'GLEmulation.alphaTestRef', 'float') }}};
        } else {
          orig_glGetFloatv(pname, params);
        }
      };
      {{{ copySigs('glGetFloatv') }}}

      var orig_glHint = _glHint;
      _glHint = _emscripten_glHint = (target, mode) => {
        if (target == 0x84EF) { // GL_TEXTURE_COMPRESSION_HINT
          return;
        }
        orig_glHint(target, mode);
      };
      {{{ copySigs('glHint') }}}

      var orig_glEnableVertexAttribArray = _glEnableVertexAttribArray;
      _glEnableVertexAttribArray = _emscripten_glEnableVertexAttribArray = (index) => {
        orig_glEnableVertexAttribArray(index);
        GLEmulation.enabledVertexAttribArrays[index] = 1;
        if (GLEmulation.currentVao) GLEmulation.currentVao.enabledVertexAttribArrays[index] = 1;
      };
      {{{ copySigs('glEnableVertexAttribArray') }}}

      var orig_glDisableVertexAttribArray = _glDisableVertexAttribArray;
      _glDisableVertexAttribArray = _emscripten_glDisableVertexAttribArray = (index) => {
        orig_glDisableVertexAttribArray(index);
        delete GLEmulation.enabledVertexAttribArrays[index];
        if (GLEmulation.currentVao) delete GLEmulation.currentVao.enabledVertexAttribArrays[index];
      };
      {{{ copySigs('glDisableVertexAttribArray') }}}

      var orig_glVertexAttribPointer = _glVertexAttribPointer;
      _glVertexAttribPointer = _emscripten_glVertexAttribPointer = (index, size, type, normalized, stride, pointer) => {
        orig_glVertexAttribPointer(index, size, type, normalized, stride, pointer);
        if (GLEmulation.currentVao) { // TODO: avoid object creation here? likely not hot though
          GLEmulation.currentVao.vertexAttribPointers[index] = [index, size, type, normalized, stride, pointer];
        }
      };
      {{{ copySigs('glVertexAttribPointer') }}}
    },

    getAttributeFromCapability(cap) {
      var attrib = null;
      switch (cap) {
        case 0xDE1: // GL_TEXTURE_2D - XXX not according to spec, and not in desktop GL, but works in some GLES1.x apparently, so support it
#if ASSERTIONS
          abort("GL_TEXTURE_2D is not a spec-defined capability for gl{Enable,Disable}ClientState.");
#endif
          // Fall through:
        case 0x8078: // GL_TEXTURE_COORD_ARRAY
          attrib = GLImmediate.TEXTURE0 + GLImmediate.clientActiveTexture; break;
        case 0x8074: // GL_VERTEX_ARRAY
          attrib = GLImmediate.VERTEX; break;
        case 0x8075: // GL_NORMAL_ARRAY
          attrib = GLImmediate.NORMAL; break;
        case 0x8076: // GL_COLOR_ARRAY
          attrib = GLImmediate.COLOR; break;
      }
      return attrib;
    },
  },

  glDeleteObject__deps: ['glDeleteProgram', 'glDeleteShader'],
  glDeleteObject: (id) => {
    if (GL.programs[id]) {
      _glDeleteProgram(id);
    } else if (GL.shaders[id]) {
      _glDeleteShader(id);
    } else {
      err(`WARNING: deleteObject received invalid id: ${id}`);
    }
  },
  glDeleteObjectARB: 'glDeleteObject',

  glGetObjectParameteriv__deps: ['glGetProgramiv', 'glGetShaderiv'],
  glGetObjectParameteriv: (id, type, result) => {
    if (GL.programs[id]) {
      if (type == 0x8B84) { // GL_OBJECT_INFO_LOG_LENGTH_ARB
        var log = GLctx.getProgramInfoLog(GL.programs[id]);
        if (log === null) log = '(unknown error)';
        {{{ makeSetValue('result', '0', 'log.length', 'i32') }}};
        return;
      }
      _glGetProgramiv(id, type, result);
    } else if (GL.shaders[id]) {
      if (type == 0x8B84) { // GL_OBJECT_INFO_LOG_LENGTH_ARB
        var log = GLctx.getShaderInfoLog(GL.shaders[id]);
        if (log === null) log = '(unknown error)';
        {{{ makeSetValue('result', '0', 'log.length', 'i32') }}};
        return;
      } else if (type == 0x8B88) { // GL_OBJECT_SHADER_SOURCE_LENGTH_ARB
        var source = GLctx.getShaderSource(GL.shaders[id]);
        if (source === null) return; // If an error occurs, nothing will be written to result
        {{{ makeSetValue('result', '0', 'source.length', 'i32') }}};
        return;
      }
      _glGetShaderiv(id, type, result);
    } else {
      err(`WARNING: getObjectParameteriv received invalid id: ${id}`);
    }
  },
  glGetObjectParameterivARB: 'glGetObjectParameteriv',

  glGetInfoLog__deps: ['glGetProgramInfoLog', 'glGetShaderInfoLog'],
  glGetInfoLog: (id, maxLength, length, infoLog) => {
    if (GL.programs[id]) {
      _glGetProgramInfoLog(id, maxLength, length, infoLog);
    } else if (GL.shaders[id]) {
      _glGetShaderInfoLog(id, maxLength, length, infoLog);
    } else {
      err(`WARNING: glGetInfoLog received invalid id: ${id}`);
    }
  },
  glGetInfoLogARB: 'glGetInfoLog',

  glBindProgram: (type, id) => {
#if ASSERTIONS
    assert(id == 0);
#endif
  },
  glBindProgramARB: 'glBindProgram',

  glGetPointerv: (name, p) => {
    var attribute;
    switch (name) {
      case 0x808E: // GL_VERTEX_ARRAY_POINTER
        attribute = GLImmediate.clientAttributes[GLImmediate.VERTEX]; break;
      case 0x8090: // GL_COLOR_ARRAY_POINTER
        attribute = GLImmediate.clientAttributes[GLImmediate.COLOR]; break;
      case 0x8092: // GL_TEXTURE_COORD_ARRAY_POINTER
        attribute = GLImmediate.clientAttributes[GLImmediate.TEXTURE0 + GLImmediate.clientActiveTexture]; break;
      default:
        GL.recordError(0x500/*GL_INVALID_ENUM*/);
#if GL_ASSERTIONS
        err(`GL_INVALID_ENUM in glGetPointerv: Unsupported name ${name}!`);
#endif
        return;
    }
    {{{ makeSetValue('p', '0', 'attribute ? attribute.pointer : 0', 'i32') }}};
  },

  // GL Immediate mode

  // See comment in GLEmulation.init()
#if !FULL_ES2
  $GLImmediate__postset: 'GLImmediate.setupFuncs(); Browser.moduleContextCreatedCallbacks.push(() => GLImmediate.init());',
#endif
  $GLImmediate__deps: ['$Browser', '$GL', '$GLEmulation', '$webglBufferSubData'],
  $GLImmediate: {
    MapTreeLib: null,
    spawnMapTreeLib: () => {
      /**
       * A naive implementation of a map backed by an array, and accessed by
       * naive iteration along the array. (hashmap with only one bucket)
       * @constructor
       */
      function CNaiveListMap() {
        var list = [];

        this.insert = function CNaiveListMap_insert(key, val) {
          if (this.contains(key|0)) return false;
          list.push([key, val]);
          return true;
        };

        var __contains_i;
        this.contains = function CNaiveListMap_contains(key) {
          for (__contains_i = 0; __contains_i < list.length; ++__contains_i) {
            if (list[__contains_i][0] === key) return true;
          }
          return false;
        };

        var __get_i;
        this.get = function CNaiveListMap_get(key) {
          for (__get_i = 0; __get_i < list.length; ++__get_i) {
            if (list[__get_i][0] === key) return list[__get_i][1];
          }
          return undefined;
        };
      };

      /**
       * A tree of map nodes.
       * Uses `KeyView`s to allow descending the tree without garbage.
       * Example: {
       *   // Create our map object.
       *   var map = new ObjTreeMap();
       *
       *   // Grab the static keyView for the map.
       *   var keyView = map.GetStaticKeyView();
       *
       *   // Let's make a map for:
       *   // root: <undefined>
       *   //   1: <undefined>
       *   //     2: <undefined>
       *   //       5: "Three, sir!"
       *   //       3: "Three!"
       *
       *   // Note how we can chain together `Reset` and `Next` to
       *   // easily descend based on multiple key fragments.
       *   keyView.Reset().Next(1).Next(2).Next(5).Set("Three, sir!");
       *   keyView.Reset().Next(1).Next(2).Next(3).Set("Three!");
       * }
       * @constructor
       */
      function CMapTree() {
        /** @constructor */
        function CNLNode() {
          var map = new CNaiveListMap();

          this.child = function CNLNode_child(keyFrag) {
            if (!map.contains(keyFrag|0)) {
              map.insert(keyFrag|0, new CNLNode());
            }
            return map.get(keyFrag|0);
          };

          this.value = undefined;
          this.get = function CNLNode_get() {
            return this.value;
          };

          this.set = function CNLNode_set(val) {
            this.value = val;
          };
        }

        /** @constructor */
        function CKeyView(root) {
          var cur;

          this.reset = function CKeyView_reset() {
            cur = root;
            return this;
          };
          this.reset();

          this.next = function CKeyView_next(keyFrag) {
            cur = cur.child(keyFrag);
            return this;
          };

          this.get = function CKeyView_get() {
            return cur.get();
          };

          this.set = function CKeyView_set(val) {
            cur.set(val);
          };
        };

        var root;
        var staticKeyView;

        this.createKeyView = function CNLNode_createKeyView() {
          return new CKeyView(root);
        }

        this.clear = function CNLNode_clear() {
          root = new CNLNode();
          staticKeyView = this.createKeyView();
        };
        this.clear();

        this.getStaticKeyView = function CNLNode_getStaticKeyView() {
          staticKeyView.reset();
          return staticKeyView;
        };
      };

      // Exports:
      return {
        create: () => new CMapTree(),
      };
    },

    TexEnvJIT: null,
    spawnTexEnvJIT: () => {
      // GL defs:
      var GL_TEXTURE0 = 0x84C0;
      var GL_TEXTURE_1D = 0xDE0;
      var GL_TEXTURE_2D = 0xDE1;
      var GL_TEXTURE_3D = 0x806f;
      var GL_TEXTURE_CUBE_MAP = 0x8513;
      var GL_TEXTURE_ENV = 0x2300;
      var GL_TEXTURE_ENV_MODE = 0x2200;
      var GL_TEXTURE_ENV_COLOR = 0x2201;
      var GL_TEXTURE_CUBE_MAP_POSITIVE_X = 0x8515;
      var GL_TEXTURE_CUBE_MAP_NEGATIVE_X = 0x8516;
      var GL_TEXTURE_CUBE_MAP_POSITIVE_Y = 0x8517;
      var GL_TEXTURE_CUBE_MAP_NEGATIVE_Y = 0x8518;
      var GL_TEXTURE_CUBE_MAP_POSITIVE_Z = 0x8519;
      var GL_TEXTURE_CUBE_MAP_NEGATIVE_Z = 0x851A;

      var GL_SRC0_RGB = 0x8580;
      var GL_SRC1_RGB = 0x8581;
      var GL_SRC2_RGB = 0x8582;

      var GL_SRC0_ALPHA = 0x8588;
      var GL_SRC1_ALPHA = 0x8589;
      var GL_SRC2_ALPHA = 0x858A;

      var GL_OPERAND0_RGB = 0x8590;
      var GL_OPERAND1_RGB = 0x8591;
      var GL_OPERAND2_RGB = 0x8592;

      var GL_OPERAND0_ALPHA = 0x8598;
      var GL_OPERAND1_ALPHA = 0x8599;
      var GL_OPERAND2_ALPHA = 0x859A;

      var GL_COMBINE_RGB = 0x8571;
      var GL_COMBINE_ALPHA = 0x8572;

      var GL_RGB_SCALE = 0x8573;
      var GL_ALPHA_SCALE = 0xD1C;

      // env.mode
      var GL_ADD      = 0x104;
      var GL_BLEND    = 0xBE2;
      var GL_REPLACE  = 0x1E01;
      var GL_MODULATE = 0x2100;
      var GL_DECAL    = 0x2101;
      var GL_COMBINE  = 0x8570;

      // env.color/alphaCombiner
      //var GL_ADD         = 0x104;
      //var GL_REPLACE     = 0x1E01;
      //var GL_MODULATE    = 0x2100;
      var GL_SUBTRACT    = 0x84E7;
      var GL_INTERPOLATE = 0x8575;

      // env.color/alphaSrc
      var GL_TEXTURE       = 0x1702;
      var GL_CONSTANT      = 0x8576;
      var GL_PRIMARY_COLOR = 0x8577;
      var GL_PREVIOUS      = 0x8578;

      // env.color/alphaOp
      var GL_SRC_COLOR           = 0x300;
      var GL_ONE_MINUS_SRC_COLOR = 0x301;
      var GL_SRC_ALPHA           = 0x302;
      var GL_ONE_MINUS_SRC_ALPHA = 0x303;

      var GL_RGB  = 0x1907;
      var GL_RGBA = 0x1908;

      // Our defs:
      var TEXENVJIT_NAMESPACE_PREFIX = "tej_";
      // Not actually constant, as they can be changed between JIT passes:
      var TEX_UNIT_UNIFORM_PREFIX = "uTexUnit";
      var TEX_COORD_VARYING_PREFIX = "vTexCoord";
      var PRIM_COLOR_VARYING = "vPrimColor";
      var TEX_MATRIX_UNIFORM_PREFIX = "uTexMatrix";

      // Static vars:
      var s_texUnits = null; //[];
      var s_activeTexture = 0;

      var s_requiredTexUnitsForPass = [];

      // Static funcs:
      function abort_noSupport(info) {
        abort("[TexEnvJIT] ABORT: No support: " + info);
      }

      function abort_sanity(info) {
        abort("[TexEnvJIT] ABORT: Sanity failure: " + info);
      }

      function genTexUnitSampleExpr(texUnitID) {
        var texUnit = s_texUnits[texUnitID];
        var texType = texUnit.getTexType();

        var func = null;
        switch (texType) {
          case GL_TEXTURE_1D:
            func = "texture2D";
            break;
          case GL_TEXTURE_2D:
            func = "texture2D";
            break;
          case GL_TEXTURE_3D:
            return abort_noSupport("No support for 3D textures.");
          case GL_TEXTURE_CUBE_MAP:
            func = "textureCube";
            break;
          default:
            return abort_sanity(`Unknown texType: ${ptrToString(texType)}`);
        }

        var texCoordExpr = TEX_COORD_VARYING_PREFIX + texUnitID;
        if (TEX_MATRIX_UNIFORM_PREFIX != null) {
          texCoordExpr = `(${TEX_MATRIX_UNIFORM_PREFIX}${texUnitID} * ${texCoordExpr})`;
        }
        return `${func}(${TEX_UNIT_UNIFORM_PREFIX}${texUnitID}, ${texCoordExpr}.xy)`;
      }

      function getTypeFromCombineOp(op) {
        switch (op) {
          case GL_SRC_COLOR:
          case GL_ONE_MINUS_SRC_COLOR:
            return "vec3";
          case GL_SRC_ALPHA:
          case GL_ONE_MINUS_SRC_ALPHA:
            return "float";
        }

        return abort_noSupport("Unsupported combiner op: " + ptrToString(op));
      }

      function getCurTexUnit() {
        return s_texUnits[s_activeTexture];
      }

      function genCombinerSourceExpr(texUnitID, constantExpr, previousVar,
                                     src, op)
      {
        var srcExpr = null;
        switch (src) {
          case GL_TEXTURE:
            srcExpr = genTexUnitSampleExpr(texUnitID);
            break;
          case GL_CONSTANT:
            srcExpr = constantExpr;
            break;
          case GL_PRIMARY_COLOR:
            srcExpr = PRIM_COLOR_VARYING;
            break;
          case GL_PREVIOUS:
            srcExpr = previousVar;
            break;
          default:
              return abort_noSupport("Unsupported combiner src: " + ptrToString(src));
        }

        var expr = null;
        switch (op) {
          case GL_SRC_COLOR:
            expr = srcExpr + ".rgb";
            break;
          case GL_ONE_MINUS_SRC_COLOR:
            expr = "(vec3(1.0) - " + srcExpr + ".rgb)";
            break;
          case GL_SRC_ALPHA:
            expr = srcExpr + ".a";
            break;
          case GL_ONE_MINUS_SRC_ALPHA:
            expr = "(1.0 - " + srcExpr + ".a)";
            break;
          default:
            return abort_noSupport("Unsupported combiner op: " + ptrToString(op));
        }

        return expr;
      }

      function valToFloatLiteral(val) {
        if (val == Math.round(val)) return val + '.0';
        return val;
      }


      // Classes:
      /** @constructor */
      function CTexEnv() {
        this.mode = GL_MODULATE;
        this.colorCombiner = GL_MODULATE;
        this.alphaCombiner = GL_MODULATE;
        this.colorScale = 1;
        this.alphaScale = 1;
        this.envColor = [0, 0, 0, 0];

        this.colorSrc = [
          GL_TEXTURE,
          GL_PREVIOUS,
          GL_CONSTANT
        ];
        this.alphaSrc = [
          GL_TEXTURE,
          GL_PREVIOUS,
          GL_CONSTANT
        ];
        this.colorOp = [
          GL_SRC_COLOR,
          GL_SRC_COLOR,
          GL_SRC_ALPHA
        ];
        this.alphaOp = [
          GL_SRC_ALPHA,
          GL_SRC_ALPHA,
          GL_SRC_ALPHA
        ];

        // Map GLenums to small values to efficiently pack the enums to bits for tighter access.
        this.traverseKey = {
          // mode
          0x1E01 /* GL_REPLACE */: 0,
          0x2100 /* GL_MODULATE */: 1,
          0x104 /* GL_ADD */: 2,
          0xBE2 /* GL_BLEND */: 3,
          0x2101 /* GL_DECAL */: 4,
          0x8570 /* GL_COMBINE */: 5,

          // additional color and alpha combiners
          0x84E7 /* GL_SUBTRACT */: 3,
          0x8575 /* GL_INTERPOLATE */: 4,

          // color and alpha src
          0x1702 /* GL_TEXTURE */: 0,
          0x8576 /* GL_CONSTANT */: 1,
          0x8577 /* GL_PRIMARY_COLOR */: 2,
          0x8578 /* GL_PREVIOUS */: 3,

          // color and alpha op
          0x300 /* GL_SRC_COLOR */: 0,
          0x301 /* GL_ONE_MINUS_SRC_COLOR */: 1,
          0x302 /* GL_SRC_ALPHA */: 2,
          0x303 /* GL_ONE_MINUS_SRC_ALPHA */: 3
        };

        // The tuple (key0,key1,key2) uniquely identifies the state of the variables in CTexEnv.
        // -1 on key0 denotes 'the whole cached key is dirty'
        this.key0 = -1;
        this.key1 = 0;
        this.key2 = 0;

        this.computeKey0 = function() {
          var k = this.traverseKey;
          var key = k[this.mode] * 1638400; // 6 distinct values.
          key += k[this.colorCombiner] * 327680; // 5 distinct values.
          key += k[this.alphaCombiner] * 65536; // 5 distinct values.
          // The above three fields have 6*5*5=150 distinct values -> 8 bits.
          key += (this.colorScale-1) * 16384; // 10 bits used.
          key += (this.alphaScale-1) * 4096; // 12 bits used.
          key += k[this.colorSrc[0]] * 1024; // 14
          key += k[this.colorSrc[1]] * 256; // 16
          key += k[this.colorSrc[2]] * 64; // 18
          key += k[this.alphaSrc[0]] * 16; // 20
          key += k[this.alphaSrc[1]] * 4; // 22
          key += k[this.alphaSrc[2]]; // 24 bits used total.
          return key;
        }
        this.computeKey1 = function() {
          var k = this.traverseKey;
          var key = k[this.colorOp[0]] * 4096;
          key += k[this.colorOp[1]] * 1024;
          key += k[this.colorOp[2]] * 256;
          key += k[this.alphaOp[0]] * 16;
          key += k[this.alphaOp[1]] * 4;
          key += k[this.alphaOp[2]];
          return key;
        }
        // TODO: remove this. The color should not be part of the key!
        this.computeKey2 = function() {
          return this.envColor[0] * 16777216 + this.envColor[1] * 65536 + this.envColor[2] * 256 + 1 + this.envColor[3];
        }
        this.recomputeKey = function() {
          this.key0 = this.computeKey0();
          this.key1 = this.computeKey1();
          this.key2 = this.computeKey2();
        }
        this.invalidateKey = function() {
          this.key0 = -1; // The key of this texture unit must be recomputed when rendering the next time.
          GLImmediate.currentRenderer = null; // The currently used renderer must be re-evaluated at next render.
        }
      }

      /** @constructor */
      function CTexUnit() {
        this.env = new CTexEnv();
        this.enabled_tex1D   = false;
        this.enabled_tex2D   = false;
        this.enabled_tex3D   = false;
        this.enabled_texCube = false;
        this.texTypesEnabled = 0; // A bitfield combination of the four flags above, used for fast access to operations.

        this.traverseState = function CTexUnit_traverseState(keyView) {
          if (this.texTypesEnabled) {
            if (this.env.key0 == -1) {
              this.env.recomputeKey();
            }
            keyView.next(this.texTypesEnabled | (this.env.key0 << 4));
            keyView.next(this.env.key1);
            keyView.next(this.env.key2);
          } else {
            // For correctness, must traverse a zero value, theoretically a subsequent integer key could collide with this value otherwise.
            keyView.next(0);
          }
        };
      };

      // Class impls:
      CTexUnit.prototype.enabled = function CTexUnit_enabled() {
        return this.texTypesEnabled;
      }

      CTexUnit.prototype.genPassLines = function CTexUnit_genPassLines(passOutputVar, passInputVar, texUnitID) {
        if (!this.enabled()) {
          return ["vec4 " + passOutputVar + " = " + passInputVar + ";"];
        }
        var lines = this.env.genPassLines(passOutputVar, passInputVar, texUnitID).join('\n');

        var texLoadLines = '';
        var texLoadRegex = /(texture.*?\(.*?\))/g;
        var loadCounter = 0;
        var load;

        // As an optimization, merge duplicate identical texture loads to one var.
        while (load = texLoadRegex.exec(lines)) {
          var texLoadExpr = load[1];
          var secondOccurrence = lines.slice(load.index+1).indexOf(texLoadExpr);
          if (secondOccurrence != -1) { // And also has a second occurrence of same load expression..
            // Create new var to store the common load.
            var prefix = TEXENVJIT_NAMESPACE_PREFIX + 'env' + texUnitID + "_";
            var texLoadVar = prefix + 'texload' + loadCounter++;
            var texLoadLine = 'vec4 ' + texLoadVar + ' = ' + texLoadExpr + ';\n';
            texLoadLines += texLoadLine + '\n'; // Store the generated texture load statements in a temp string to not confuse regex search in progress.
            lines = lines.split(texLoadExpr).join(texLoadVar);
            // Reset regex search, since we modified the string.
            texLoadRegex = /(texture.*\(.*\))/g;
          }
        }
        return [texLoadLines + lines];
      }

      CTexUnit.prototype.getTexType = function CTexUnit_getTexType() {
        if (this.enabled_texCube) {
          return GL_TEXTURE_CUBE_MAP;
        } else if (this.enabled_tex3D) {
          return GL_TEXTURE_3D;
        } else if (this.enabled_tex2D) {
          return GL_TEXTURE_2D;
        } else if (this.enabled_tex1D) {
          return GL_TEXTURE_1D;
        }
        return 0;
      }

      CTexEnv.prototype.genPassLines = function CTexEnv_genPassLines(passOutputVar, passInputVar, texUnitID) {
        switch (this.mode) {
          case GL_REPLACE: {
            /* RGB:
             * Cv = Cs
             * Av = Ap // Note how this is different, and that we'll
             *            need to track the bound texture internalFormat
             *            to get this right.
             *
             * RGBA:
             * Cv = Cs
             * Av = As
             */
            return [
              "vec4 " + passOutputVar + " = " + genTexUnitSampleExpr(texUnitID) + ";",
            ];
          }
          case GL_ADD: {
            /* RGBA:
             * Cv = Cp + Cs
             * Av = ApAs
             */
            var prefix = TEXENVJIT_NAMESPACE_PREFIX + 'env' + texUnitID + "_";
            var texVar = prefix + "tex";
            var colorVar = prefix + "color";
            var alphaVar = prefix + "alpha";

            return [
              "vec4 " + texVar + " = " + genTexUnitSampleExpr(texUnitID) + ";",
              "vec3 " + colorVar + " = " + passInputVar + ".rgb + " + texVar + ".rgb;",
              "float " + alphaVar + " = " + passInputVar + ".a * " + texVar + ".a;",
              "vec4 " + passOutputVar + " = vec4(" + colorVar + ", " + alphaVar + ");",
            ];
          }
          case GL_MODULATE: {
            /* RGBA:
             * Cv = CpCs
             * Av = ApAs
             */
            var line = [
              "vec4 " + passOutputVar,
              " = ",
                passInputVar,
                " * ",
                genTexUnitSampleExpr(texUnitID),
              ";",
            ];
            return [line.join("")];
          }
          case GL_DECAL: {
            /* RGBA:
             * Cv = Cp(1 - As) + CsAs
             * Av = Ap
             */
            var prefix = TEXENVJIT_NAMESPACE_PREFIX + 'env' + texUnitID + "_";
            var texVar = prefix + "tex";
            var colorVar = prefix + "color";
            var alphaVar = prefix + "alpha";

            return [
              "vec4 " + texVar + " = " + genTexUnitSampleExpr(texUnitID) + ";",
              [
                "vec3 " + colorVar + " = ",
                  passInputVar + ".rgb * (1.0 - " + texVar + ".a)",
                    " + ",
                  texVar + ".rgb * " + texVar + ".a",
                ";"
              ].join(""),
              "float " + alphaVar + " = " + passInputVar + ".a;",
              "vec4 " + passOutputVar + " = vec4(" + colorVar + ", " + alphaVar + ");",
            ];
          }
          case GL_BLEND: {
            /* RGBA:
             * Cv = Cp(1 - Cs) + CcCs
             * Av = As
             */
            var prefix = TEXENVJIT_NAMESPACE_PREFIX + 'env' + texUnitID + "_";
            var texVar = prefix + "tex";
            var colorVar = prefix + "color";
            var alphaVar = prefix + "alpha";

            return [
              "vec4 " + texVar + " = " + genTexUnitSampleExpr(texUnitID) + ";",
              [
                "vec3 " + colorVar + " = ",
                  passInputVar + ".rgb * (1.0 - " + texVar + ".rgb)",
                    " + ",
                  PRIM_COLOR_VARYING + ".rgb * " + texVar + ".rgb",
                ";"
              ].join(""),
              "float " + alphaVar + " = " + texVar + ".a;",
              "vec4 " + passOutputVar + " = vec4(" + colorVar + ", " + alphaVar + ");",
            ];
          }
          case GL_COMBINE: {
            var prefix = TEXENVJIT_NAMESPACE_PREFIX + 'env' + texUnitID + "_";
            var colorVar = prefix + "color";
            var alphaVar = prefix + "alpha";
            var colorLines = this.genCombinerLines(true, colorVar,
                                                   passInputVar, texUnitID,
                                                   this.colorCombiner, this.colorSrc, this.colorOp);
            var alphaLines = this.genCombinerLines(false, alphaVar,
                                                   passInputVar, texUnitID,
                                                   this.alphaCombiner, this.alphaSrc, this.alphaOp);

            // Generate scale, but avoid generating an identity op that multiplies by one.
            var scaledColor = (this.colorScale == 1) ? colorVar : (colorVar + " * " + valToFloatLiteral(this.colorScale));
            var scaledAlpha = (this.alphaScale == 1) ? alphaVar : (alphaVar + " * " + valToFloatLiteral(this.alphaScale));

            var line = [
              "vec4 " + passOutputVar,
              " = ",
                "vec4(",
                    scaledColor,
                    ", ",
                    scaledAlpha,
                ")",
              ";",
            ].join("");
            return [].concat(colorLines, alphaLines, [line]);
          }
        }

        return abort_noSupport("Unsupported TexEnv mode: " + ptrToString(this.mode));
      }

      CTexEnv.prototype.genCombinerLines = function CTexEnv_getCombinerLines(isColor, outputVar,
                                                                             passInputVar, texUnitID,
                                                                             combiner, srcArr, opArr)
      {
        var argsNeeded = null;
        switch (combiner) {
          case GL_REPLACE:
            argsNeeded = 1;
            break;

          case GL_MODULATE:
          case GL_ADD:
          case GL_SUBTRACT:
            argsNeeded = 2;
            break;

          case GL_INTERPOLATE:
            argsNeeded = 3;
            break;

          default:
            return abort_noSupport("Unsupported combiner: " + ptrToString(combiner));
        }

        var constantExpr = [
          "vec4(",
            valToFloatLiteral(this.envColor[0]),
            ", ",
            valToFloatLiteral(this.envColor[1]),
            ", ",
            valToFloatLiteral(this.envColor[2]),
            ", ",
            valToFloatLiteral(this.envColor[3]),
          ")",
        ].join("");
        var src0Expr = (argsNeeded >= 1) ? genCombinerSourceExpr(texUnitID, constantExpr, passInputVar, srcArr[0], opArr[0])
                                         : null;
        var src1Expr = (argsNeeded >= 2) ? genCombinerSourceExpr(texUnitID, constantExpr, passInputVar, srcArr[1], opArr[1])
                                         : null;
        var src2Expr = (argsNeeded >= 3) ? genCombinerSourceExpr(texUnitID, constantExpr, passInputVar, srcArr[2], opArr[2])
                                         : null;

        var outputType = isColor ? "vec3" : "float";
        var lines = null;
        switch (combiner) {
          case GL_REPLACE: {
            lines = [`${outputType} ${outputVar} = ${src0Expr};`]
            break;
          }
          case GL_MODULATE: {
            lines = [`${outputType} ${outputVar} = ${src0Expr} * ${src1Expr};`];
            break;
          }
          case GL_ADD: {
            lines = [`${outputType} ${outputVar} = ${src0Expr} + ${src1Expr};`]
            break;
          }
          case GL_SUBTRACT: {
            lines = [`${outputType} ${outputVar} = ${src0Expr} - ${src1Expr};`]
            break;
          }
          case GL_INTERPOLATE: {
            var prefix = `${TEXENVJIT_NAMESPACE_PREFIX}env${texUnitID}_`;
            var arg2Var = `${prefix}colorSrc2`;
            var arg2Type = getTypeFromCombineOp(this.colorOp[2]);

            lines = [
              `${arg2Type} ${arg2Var} = ${src2Expr};`,
              `${outputType} ${outputVar} = ${src0Expr} * ${arg2Var} + ${src1Expr} * (1.0 - ${arg2Var});`,
            ];
            break;
          }

          default:
            return abort_sanity("Unmatched TexEnv.colorCombiner?");
        }

        return lines;
      }

      return {
        // Exports:
        init: (gl, specifiedMaxTextureImageUnits) => {
          var maxTexUnits = 0;
          if (specifiedMaxTextureImageUnits) {
            maxTexUnits = specifiedMaxTextureImageUnits;
          } else if (gl) {
            maxTexUnits = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS);
          }
#if ASSERTIONS
          assert(maxTexUnits > 0);
#endif
          s_texUnits = [];
          for (var i = 0; i < maxTexUnits; i++) {
            s_texUnits.push(new CTexUnit());
          }
        },

        setGLSLVars: (uTexUnitPrefix, vTexCoordPrefix, vPrimColor, uTexMatrixPrefix) => {
          TEX_UNIT_UNIFORM_PREFIX   = uTexUnitPrefix;
          TEX_COORD_VARYING_PREFIX  = vTexCoordPrefix;
          PRIM_COLOR_VARYING        = vPrimColor;
          TEX_MATRIX_UNIFORM_PREFIX = uTexMatrixPrefix;
        },

        genAllPassLines: (resultDest, indentSize = 0) => {
          s_requiredTexUnitsForPass.length = 0; // Clear the list.
          var lines = [];
          var lastPassVar = PRIM_COLOR_VARYING;
          for (var i = 0; i < s_texUnits.length; i++) {
            if (!s_texUnits[i].enabled()) continue;

            s_requiredTexUnitsForPass.push(i);

            var prefix = TEXENVJIT_NAMESPACE_PREFIX + 'env' + i + "_";
            var passOutputVar = prefix + "result";

            var newLines = s_texUnits[i].genPassLines(passOutputVar, lastPassVar, i);
            lines = lines.concat(newLines, [""]);

            lastPassVar = passOutputVar;
          }
          lines.push(resultDest + " = " + lastPassVar + ";");

          var indent = "";
          for (var i = 0; i < indentSize; i++) indent += " ";

          var output = indent + lines.join("\n" + indent);

          return output;
        },

        getUsedTexUnitList: () => s_requiredTexUnitsForPass,

        getActiveTexture: () => s_activeTexture,

        traverseState: (keyView) => {
          for (var texUnit of s_texUnits) {
            texUnit.traverseState(keyView);
          }
        },

        getTexUnitType: (texUnitID) => {
#if ASSERTIONS
          assert(texUnitID >= 0 &&
                 texUnitID < s_texUnits.length);
#endif
          return s_texUnits[texUnitID].getTexType();
        },

        // Hooks:
        hook_activeTexture: (texture) => {
          s_activeTexture = texture - GL_TEXTURE0;
          // Check if the current matrix mode is GL_TEXTURE.
          if (GLImmediate.currentMatrix >= 2) {
            // Switch to the corresponding texture matrix stack.
            GLImmediate.currentMatrix = 2 + s_activeTexture;
          }
        },

        hook_enable: (cap) => {
          var cur = getCurTexUnit();
          switch (cap) {
            case GL_TEXTURE_1D:
              if (!cur.enabled_tex1D) {
                GLImmediate.currentRenderer = null; // Renderer state changed, and must be recreated or looked up again.
                cur.enabled_tex1D = true;
                cur.texTypesEnabled |= 1;
              }
              break;
            case GL_TEXTURE_2D:
              if (!cur.enabled_tex2D) {
                GLImmediate.currentRenderer = null;
                cur.enabled_tex2D = true;
                cur.texTypesEnabled |= 2;
              }
              break;
            case GL_TEXTURE_3D:
              if (!cur.enabled_tex3D) {
                GLImmediate.currentRenderer = null;
                cur.enabled_tex3D = true;
                cur.texTypesEnabled |= 4;
              }
              break;
            case GL_TEXTURE_CUBE_MAP:
              if (!cur.enabled_texCube) {
                GLImmediate.currentRenderer = null;
                cur.enabled_texCube = true;
                cur.texTypesEnabled |= 8;
              }
              break;
          }
        },

        hook_disable: (cap) => {
          var cur = getCurTexUnit();
          switch (cap) {
            case GL_TEXTURE_1D:
              if (cur.enabled_tex1D) {
                GLImmediate.currentRenderer = null; // Renderer state changed, and must be recreated or looked up again.
                cur.enabled_tex1D = false;
                cur.texTypesEnabled &= ~1;
              }
              break;
            case GL_TEXTURE_2D:
              if (cur.enabled_tex2D) {
                GLImmediate.currentRenderer = null;
                cur.enabled_tex2D = false;
                cur.texTypesEnabled &= ~2;
              }
              break;
            case GL_TEXTURE_3D:
              if (cur.enabled_tex3D) {
                GLImmediate.currentRenderer = null;
                cur.enabled_tex3D = false;
                cur.texTypesEnabled &= ~4;
              }
              break;
            case GL_TEXTURE_CUBE_MAP:
              if (cur.enabled_texCube) {
                GLImmediate.currentRenderer = null;
                cur.enabled_texCube = false;
                cur.texTypesEnabled &= ~8;
              }
              break;
          }
        },

        hook_texEnvf(target, pname, param) {
          if (target != GL_TEXTURE_ENV)
            return;

          var env = getCurTexUnit().env;
          switch (pname) {
            case GL_RGB_SCALE:
              if (env.colorScale != param) {
                env.invalidateKey(); // We changed FFP emulation renderer state.
                env.colorScale = param;
              }
              break;
            case GL_ALPHA_SCALE:
              if (env.alphaScale != param) {
                env.invalidateKey();
                env.alphaScale = param;
              }
              break;

            default:
              err('WARNING: Unhandled `pname` in call to `glTexEnvf`.');
          }
        },

        hook_texEnvi(target, pname, param) {
          if (target != GL_TEXTURE_ENV)
            return;

          var env = getCurTexUnit().env;
          switch (pname) {
            case GL_TEXTURE_ENV_MODE:
              if (env.mode != param) {
                env.invalidateKey(); // We changed FFP emulation renderer state.
                env.mode = param;
              }
              break;

            case GL_COMBINE_RGB:
              if (env.colorCombiner != param) {
                env.invalidateKey();
                env.colorCombiner = param;
              }
              break;
            case GL_COMBINE_ALPHA:
              if (env.alphaCombiner != param) {
                env.invalidateKey();
                env.alphaCombiner = param;
              }
              break;

            case GL_SRC0_RGB:
              if (env.colorSrc[0] != param) {
                env.invalidateKey();
                env.colorSrc[0] = param;
              }
              break;
            case GL_SRC1_RGB:
              if (env.colorSrc[1] != param) {
                env.invalidateKey();
                env.colorSrc[1] = param;
              }
              break;
            case GL_SRC2_RGB:
              if (env.colorSrc[2] != param) {
                env.invalidateKey();
                env.colorSrc[2] = param;
              }
              break;

            case GL_SRC0_ALPHA:
              if (env.alphaSrc[0] != param) {
                env.invalidateKey();
                env.alphaSrc[0] = param;
              }
              break;
            case GL_SRC1_ALPHA:
              if (env.alphaSrc[1] != param) {
                env.invalidateKey();
                env.alphaSrc[1] = param;
              }
              break;
            case GL_SRC2_ALPHA:
              if (env.alphaSrc[2] != param) {
                env.invalidateKey();
                env.alphaSrc[2] = param;
              }
              break;

            case GL_OPERAND0_RGB:
              if (env.colorOp[0] != param) {
                env.invalidateKey();
                env.colorOp[0] = param;
              }
              break;
            case GL_OPERAND1_RGB:
              if (env.colorOp[1] != param) {
                env.invalidateKey();
                env.colorOp[1] = param;
              }
              break;
            case GL_OPERAND2_RGB:
              if (env.colorOp[2] != param) {
                env.invalidateKey();
                env.colorOp[2] = param;
              }
              break;

            case GL_OPERAND0_ALPHA:
              if (env.alphaOp[0] != param) {
                env.invalidateKey();
                env.alphaOp[0] = param;
              }
              break;
            case GL_OPERAND1_ALPHA:
              if (env.alphaOp[1] != param) {
                env.invalidateKey();
                env.alphaOp[1] = param;
              }
              break;
            case GL_OPERAND2_ALPHA:
              if (env.alphaOp[2] != param) {
                env.invalidateKey();
                env.alphaOp[2] = param;
              }
              break;

            case GL_RGB_SCALE:
              if (env.colorScale != param) {
                env.invalidateKey();
                env.colorScale = param;
              }
              break;
            case GL_ALPHA_SCALE:
              if (env.alphaScale != param) {
                env.invalidateKey();
                env.alphaScale = param;
              }
              break;

            default:
              err('WARNING: Unhandled `pname` in call to `glTexEnvi`.');
          }
        },

        hook_texEnvfv(target, pname, params) {
          if (target != GL_TEXTURE_ENV) return;

          var env = getCurTexUnit().env;
          switch (pname) {
            case GL_TEXTURE_ENV_COLOR: {
              for (var i = 0; i < 4; i++) {
                var param = {{{ makeGetValue('params', 'i*4', 'float') }}};
                if (env.envColor[i] != param) {
                  env.invalidateKey(); // We changed FFP emulation renderer state.
                  env.envColor[i] = param;
                }
              }
              break
            }
            default:
              err('WARNING: Unhandled `pname` in call to `glTexEnvfv`.');
          }
        },

        hook_getTexEnviv(target, pname, param) {
          if (target != GL_TEXTURE_ENV)
            return;

          var env = getCurTexUnit().env;
          switch (pname) {
            case GL_TEXTURE_ENV_MODE:
              {{{ makeSetValue('param', '0', 'env.mode', 'i32') }}};
              return;

            case GL_TEXTURE_ENV_COLOR:
              {{{ makeSetValue('param', '0', 'Math.max(Math.min(env.envColor[0]*255, 255, -255))', 'i32') }}};
              {{{ makeSetValue('param', '1', 'Math.max(Math.min(env.envColor[1]*255, 255, -255))', 'i32') }}};
              {{{ makeSetValue('param', '2', 'Math.max(Math.min(env.envColor[2]*255, 255, -255))', 'i32') }}};
              {{{ makeSetValue('param', '3', 'Math.max(Math.min(env.envColor[3]*255, 255, -255))', 'i32') }}};
              return;

            case GL_COMBINE_RGB:
              {{{ makeSetValue('param', '0', 'env.colorCombiner', 'i32') }}};
              return;

            case GL_COMBINE_ALPHA:
              {{{ makeSetValue('param', '0', 'env.alphaCombiner', 'i32') }}};
              return;

            case GL_SRC0_RGB:
              {{{ makeSetValue('param', '0', 'env.colorSrc[0]', 'i32') }}};
              return;

            case GL_SRC1_RGB:
              {{{ makeSetValue('param', '0', 'env.colorSrc[1]', 'i32') }}};
              return;

            case GL_SRC2_RGB:
              {{{ makeSetValue('param', '0', 'env.colorSrc[2]', 'i32') }}};
              return;

            case GL_SRC0_ALPHA:
              {{{ makeSetValue('param', '0', 'env.alphaSrc[0]', 'i32') }}};
              return;

            case GL_SRC1_ALPHA:
              {{{ makeSetValue('param', '0', 'env.alphaSrc[1]', 'i32') }}};
              return;

            case GL_SRC2_ALPHA:
              {{{ makeSetValue('param', '0', 'env.alphaSrc[2]', 'i32') }}};
              return;

            case GL_OPERAND0_RGB:
              {{{ makeSetValue('param', '0', 'env.colorOp[0]', 'i32') }}};
              return;

            case GL_OPERAND1_RGB:
              {{{ makeSetValue('param', '0', 'env.colorOp[1]', 'i32') }}};
              return;

            case GL_OPERAND2_RGB:
              {{{ makeSetValue('param', '0', 'env.colorOp[2]', 'i32') }}};
              return;

            case GL_OPERAND0_ALPHA:
              {{{ makeSetValue('param', '0', 'env.alphaOp[0]', 'i32') }}};
              return;

            case GL_OPERAND1_ALPHA:
              {{{ makeSetValue('param', '0', 'env.alphaOp[1]', 'i32') }}};
              return;

            case GL_OPERAND2_ALPHA:
              {{{ makeSetValue('param', '0', 'env.alphaOp[2]', 'i32') }}};
              return;

            case GL_RGB_SCALE:
              {{{ makeSetValue('param', '0', 'env.colorScale', 'i32') }}};
              return;

            case GL_ALPHA_SCALE:
              {{{ makeSetValue('param', '0', 'env.alphaScale', 'i32') }}};
              return;

            default:
              err('WARNING: Unhandled `pname` in call to `glGetTexEnvi`.');
          }
        },

        hook_getTexEnvfv: (target, pname, param) => {
          if (target != GL_TEXTURE_ENV)
            return;

          var env = getCurTexUnit().env;
          switch (pname) {
            case GL_TEXTURE_ENV_COLOR:
              {{{ makeSetValue('param', '0', 'env.envColor[0]', 'float') }}};
              {{{ makeSetValue('param', '4', 'env.envColor[1]', 'float') }}};
              {{{ makeSetValue('param', '8', 'env.envColor[2]', 'float') }}};
              {{{ makeSetValue('param', '12', 'env.envColor[3]', 'float') }}};
              return;
          }
        }
      };
    },

    // Vertex and index data
    vertexData: null, // current vertex data. either tempData (glBegin etc.) or a view into the heap (gl*Pointer). Default view is F32
    vertexDataU8: null, // U8 view
    tempData: null,
    indexData: null,
    vertexCounter: 0,
    mode: -1,

    rendererCache: null,
    rendererComponents: [], // small cache for calls inside glBegin/end. counts how many times the element was seen
    rendererComponentPointer: 0, // next place to start a glBegin/end component
    lastRenderer: null, // used to avoid cleaning up and re-preparing the same renderer
    lastArrayBuffer: null, // used in conjunction with lastRenderer
    lastProgram: null, // ""
    lastStride: -1, // ""

    // The following data structures are used for OpenGL Immediate Mode matrix routines.
    matrix: [],
    matrixStack: [],
    currentMatrix: 0, // 0: modelview, 1: projection, 2+i, texture matrix i.
    tempMatrix: null,
    matricesModified: false,
    useTextureMatrix: false,

    // Clientside attributes
    VERTEX: 0,
    NORMAL: 1,
    COLOR: 2,
    TEXTURE0: 3,
    NUM_ATTRIBUTES: -1, // Initialized in GL emulation init().
    MAX_TEXTURES: -1,   // Initialized in GL emulation init().

    totalEnabledClientAttributes: 0,
    enabledClientAttributes: [0, 0],
    clientAttributes: [], // raw data, including possible unneeded ones
    liveClientAttributes: [], // the ones actually alive in the current computation, sorted
    currentRenderer: null, // Caches the currently active FFP emulation renderer, so that it does not have to be re-looked up unless relevant state changes.
    modifiedClientAttributes: false,
    clientActiveTexture: 0,
    clientColor: null,
    usedTexUnitList: [],
    fixedFunctionProgram: null,

    setClientAttribute(name, size, type, stride, pointer) {
      var attrib = GLImmediate.clientAttributes[name];
      if (!attrib) {
        for (var i = 0; i <= name; i++) { // keep flat
          GLImmediate.clientAttributes[i] ||= {
            name,
            size,
            type,
            stride,
            pointer,
            offset: 0
          };
        }
      } else {
        attrib.name = name;
        attrib.size = size;
        attrib.type = type;
        attrib.stride = stride;
        attrib.pointer = pointer;
        attrib.offset = 0;
      }
      GLImmediate.modifiedClientAttributes = true;
    },

    // Renderers
    addRendererComponent(name, size, type) {
      if (!GLImmediate.rendererComponents[name]) {
        GLImmediate.rendererComponents[name] = 1;
#if ASSERTIONS
        if (GLImmediate.enabledClientAttributes[name]) {
          warnOnce("Warning: glTexCoord used after EnableClientState for TEXTURE_COORD_ARRAY for TEXTURE0. Disabling TEXTURE_COORD_ARRAY...");
        }
#endif
        GLImmediate.enabledClientAttributes[name] = true;
        GLImmediate.setClientAttribute(name, size, type, 0, GLImmediate.rendererComponentPointer);
        GLImmediate.rendererComponentPointer += size * GL.byteSizeByType[type - GL.byteSizeByTypeRoot];
#if GL_FFP_ONLY
        // We can enable the correct attribute stream index immediately here, since the same attribute in each shader
        // will be bound to this same index.
        GL.enableVertexAttribArray(name);
#endif
      } else {
        GLImmediate.rendererComponents[name]++;
      }
    },

    disableBeginEndClientAttributes() {
      for (var i = 0; i < GLImmediate.NUM_ATTRIBUTES; i++) {
        if (GLImmediate.rendererComponents[i]) GLImmediate.enabledClientAttributes[i] = false;
      }
    },

    getRenderer() {
      // If no FFP state has changed that would have forced to re-evaluate which FFP emulation shader to use,
      // we have the currently used renderer in cache, and can immediately return that.
      if (GLImmediate.currentRenderer) {
        return GLImmediate.currentRenderer;
      }
      // return a renderer object given the liveClientAttributes
      // we maintain a cache of renderers, optimized to not generate garbage
      var attributes = GLImmediate.liveClientAttributes;
      var cacheMap = GLImmediate.rendererCache;
      var keyView = cacheMap.getStaticKeyView().reset();

      // By attrib state:
      var enabledAttributesKey = 0;
      for (var attr of attributes) {
        enabledAttributesKey |= 1 << attr.name;
      }

      // To prevent using more than 31 bits add another level to the maptree
      // and reset the enabledAttributesKey for the next glemulation state bits
      keyView.next(enabledAttributesKey);
      enabledAttributesKey = 0;

      // By fog state:
      var fogParam = 0;
      if (GLEmulation.fogEnabled) {
        switch (GLEmulation.fogMode) {
          case 0x801: // GL_EXP2
            fogParam = 1;
            break;
          case 0x2601: // GL_LINEAR
            fogParam = 2;
            break;
          default: // default to GL_EXP
            fogParam = 3;
            break;
        }
      }
      enabledAttributesKey = (enabledAttributesKey << 2) | fogParam;

      // By clip plane mode
      for (var clipPlaneId = 0; clipPlaneId < GLEmulation.MAX_CLIP_PLANES; clipPlaneId++) {
        enabledAttributesKey = (enabledAttributesKey << 1) | GLEmulation.clipPlaneEnabled[clipPlaneId];
      }

      // By lighting mode and enabled lights
      enabledAttributesKey = (enabledAttributesKey << 1) | GLEmulation.lightingEnabled;
      for (var lightId = 0; lightId < GLEmulation.MAX_LIGHTS; lightId++) {
        enabledAttributesKey = (enabledAttributesKey << 1) | (GLEmulation.lightingEnabled ? GLEmulation.lightEnabled[lightId] : 0);
      }

      // By alpha testing mode
      enabledAttributesKey = (enabledAttributesKey << 3) | (GLEmulation.alphaTestEnabled ? (GLEmulation.alphaTestFunc - 0x200) : 0x7);

      // By drawing mode:
      enabledAttributesKey = (enabledAttributesKey << 1) | (GLImmediate.mode == GLctx.POINTS ? 1 : 0);

      keyView.next(enabledAttributesKey);

#if !GL_FFP_ONLY
      // By cur program:
      keyView.next(GL.currProgram);
      if (!GL.currProgram) {
#endif
        GLImmediate.TexEnvJIT.traverseState(keyView);
#if !GL_FFP_ONLY
      }
#endif

      // If we don't already have it, create it.
      var renderer = keyView.get();
      if (!renderer) {
#if GL_DEBUG
        dbg(`generating renderer for ${JSON.stringify(attributes)}`);
#endif
        renderer = GLImmediate.createRenderer();
        GLImmediate.currentRenderer = renderer;
        keyView.set(renderer);
        return renderer;
      }
      GLImmediate.currentRenderer = renderer; // Cache the currently used renderer, so later lookups without state changes can get this fast.
      return renderer;
    },

    createRenderer(renderer) {
      var useCurrProgram = !!GL.currProgram;
      var hasTextures = false;
      for (var i = 0; i < GLImmediate.MAX_TEXTURES; i++) {
        var texAttribName = GLImmediate.TEXTURE0 + i;
        if (!GLImmediate.enabledClientAttributes[texAttribName])
          continue;

#if ASSERTIONS
        if (!useCurrProgram) {
          if (GLImmediate.TexEnvJIT.getTexUnitType(i) == 0) {
             warnOnce("GL_TEXTURE" + i + " coords are supplied, but that texture unit is disabled in the fixed-function pipeline.");
          }
        }
#endif

        hasTextures = true;
      }

      /** @constructor */
      function Renderer() {
        this.init = function() {
          // For fixed-function shader generation.
          var uTexUnitPrefix = 'u_texUnit';
          var aTexCoordPrefix = 'a_texCoord';
          var vTexCoordPrefix = 'v_texCoord';
          var vPrimColor = 'v_color';
          var uTexMatrixPrefix = GLImmediate.useTextureMatrix ? 'u_textureMatrix' : null;

          if (useCurrProgram) {
            if (GL.shaderInfos[GL.programShaders[GL.currProgram][0]].type == GLctx.VERTEX_SHADER) {
              this.vertexShader = GL.shaders[GL.programShaders[GL.currProgram][0]];
              this.fragmentShader = GL.shaders[GL.programShaders[GL.currProgram][1]];
            } else {
              this.vertexShader = GL.shaders[GL.programShaders[GL.currProgram][1]];
              this.fragmentShader = GL.shaders[GL.programShaders[GL.currProgram][0]];
            }
            this.program = GL.programs[GL.currProgram];
            this.usedTexUnitList = [];
          } else {
            // IMPORTANT NOTE: If you parameterize the shader source based on any runtime values
            // in order to create the least expensive shader possible based on the features being
            // used, you should also update the code in the beginning of getRenderer to make sure
            // that you cache the renderer based on the said parameters.
            if (GLEmulation.fogEnabled) {
              switch (GLEmulation.fogMode) {
                case 0x801: // GL_EXP2
                  // fog = exp(-(gl_Fog.density * gl_FogFragCoord)^2)
                  var fogFormula = '  float fog = exp(-u_fogDensity * u_fogDensity * ecDistance * ecDistance); \n';
                  break;
                case 0x2601: // GL_LINEAR
                  // fog = (gl_Fog.end - gl_FogFragCoord) * gl_fog.scale
                  var fogFormula = '  float fog = (u_fogEnd - ecDistance) * u_fogScale; \n';
                  break;
                default: // default to GL_EXP
                  // fog = exp(-gl_Fog.density * gl_FogFragCoord)
                  var fogFormula = '  float fog = exp(-u_fogDensity * ecDistance); \n';
                  break;
              }
            }

            GLImmediate.TexEnvJIT.setGLSLVars(uTexUnitPrefix, vTexCoordPrefix, vPrimColor, uTexMatrixPrefix);
            var fsTexEnvPass = GLImmediate.TexEnvJIT.genAllPassLines('gl_FragColor', 2);

            var texUnitAttribList = '';
            var texUnitVaryingList = '';
            var texUnitUniformList = '';
            var vsTexCoordInits = '';
            this.usedTexUnitList = GLImmediate.TexEnvJIT.getUsedTexUnitList();
            for (var texUnit of this.usedTexUnitList) {
              texUnitAttribList += 'attribute vec4 ' + aTexCoordPrefix + texUnit + ';\n';
              texUnitVaryingList += 'varying vec4 ' + vTexCoordPrefix + texUnit + ';\n';
              texUnitUniformList += 'uniform sampler2D ' + uTexUnitPrefix + texUnit + ';\n';
              vsTexCoordInits += '  ' + vTexCoordPrefix + texUnit + ' = ' + aTexCoordPrefix + texUnit + ';\n';

              if (GLImmediate.useTextureMatrix) {
                texUnitUniformList += 'uniform mat4 ' + uTexMatrixPrefix + texUnit + ';\n';
              }
            }

            var vsFogVaryingInit = null;
            if (GLEmulation.fogEnabled) {
              vsFogVaryingInit = '  v_fogFragCoord = abs(ecPosition.z);\n';
            }

            var vsPointSizeDefs = null;
            var vsPointSizeInit = null;
            if (GLImmediate.mode == GLctx.POINTS) {
              vsPointSizeDefs = 'uniform float u_pointSize;\n';
              vsPointSizeInit = '  gl_PointSize = u_pointSize;\n';
            }

            var vsClipPlaneDefs = '';
            var vsClipPlaneInit = '';
            var fsClipPlaneDefs = '';
            var fsClipPlanePass = '';
            for (var clipPlaneId = 0; clipPlaneId < GLEmulation.MAX_CLIP_PLANES; clipPlaneId++) {
              if (GLEmulation.clipPlaneEnabled[clipPlaneId]) {
                vsClipPlaneDefs += 'uniform vec4 u_clipPlaneEquation' + clipPlaneId + ';';
                vsClipPlaneDefs += 'varying float v_clipDistance' + clipPlaneId + ';';
                vsClipPlaneInit += '  v_clipDistance' + clipPlaneId + ' = dot(ecPosition, u_clipPlaneEquation' + clipPlaneId + ');';
                fsClipPlaneDefs += 'varying float v_clipDistance' + clipPlaneId + ';';
                fsClipPlanePass += '  if (v_clipDistance' + clipPlaneId + ' < 0.0) discard;';
              }
            }

            var vsLightingDefs = '';
            var vsLightingPass = '';
            if (GLEmulation.lightingEnabled) {
              vsLightingDefs += 'attribute vec3 a_normal;';
              vsLightingDefs += 'uniform mat3 u_normalMatrix;';
              vsLightingDefs += 'uniform vec4 u_lightModelAmbient;';
              vsLightingDefs += 'uniform vec4 u_materialAmbient;';
              vsLightingDefs += 'uniform vec4 u_materialDiffuse;';
              vsLightingDefs += 'uniform vec4 u_materialSpecular;';
              vsLightingDefs += 'uniform float u_materialShininess;';
              vsLightingDefs += 'uniform vec4 u_materialEmission;';

              vsLightingPass += '  vec3 ecNormal = normalize(u_normalMatrix * a_normal);';
              vsLightingPass += '  v_color.w = u_materialDiffuse.w;';
              vsLightingPass += '  v_color.xyz = u_materialEmission.xyz;';
              vsLightingPass += '  v_color.xyz += u_lightModelAmbient.xyz * u_materialAmbient.xyz;';

              for (var lightId = 0; lightId < GLEmulation.MAX_LIGHTS; lightId++) {
                if (GLEmulation.lightEnabled[lightId]) {
                  vsLightingDefs += 'uniform vec4 u_lightAmbient' + lightId + ';';
                  vsLightingDefs += 'uniform vec4 u_lightDiffuse' + lightId + ';';
                  vsLightingDefs += 'uniform vec4 u_lightSpecular' + lightId + ';';
                  vsLightingDefs += 'uniform vec4 u_lightPosition' + lightId + ';';

                  vsLightingPass += '  {';
                  vsLightingPass += '    vec3 lightDirection = normalize(u_lightPosition' + lightId + ').xyz;';
                  vsLightingPass += '    vec3 halfVector = normalize(lightDirection + vec3(0,0,1));';
                  vsLightingPass += '    vec3 ambient = u_lightAmbient' + lightId + '.xyz * u_materialAmbient.xyz;';
                  vsLightingPass += '    float diffuseI = max(dot(ecNormal, lightDirection), 0.0);';
                  vsLightingPass += '    float specularI = max(dot(ecNormal, halfVector), 0.0);';
                  vsLightingPass += '    vec3 diffuse = diffuseI * u_lightDiffuse' + lightId + '.xyz * u_materialDiffuse.xyz;';
                  vsLightingPass += '    specularI = (diffuseI > 0.0 && specularI > 0.0) ? exp(u_materialShininess * log(specularI)) : 0.0;';
                  vsLightingPass += '    vec3 specular = specularI * u_lightSpecular' + lightId + '.xyz * u_materialSpecular.xyz;';
                  vsLightingPass += '    v_color.xyz += ambient + diffuse + specular;';
                  vsLightingPass += '  }';
                }
              }
              vsLightingPass += '  v_color = clamp(v_color, 0.0, 1.0);';
            }

            var vsSource = [
              'attribute vec4 a_position;',
              'attribute vec4 a_color;',
              'varying vec4 v_color;',
              texUnitAttribList,
              texUnitVaryingList,
              (GLEmulation.fogEnabled ? 'varying float v_fogFragCoord;' : null),
              'uniform mat4 u_modelView;',
              'uniform mat4 u_projection;',
              vsPointSizeDefs,
              vsClipPlaneDefs,
              vsLightingDefs,
              'void main()',
              '{',
              '  vec4 ecPosition = u_modelView * a_position;', // eye-coordinate position
              '  gl_Position = u_projection * ecPosition;',
              '  v_color = a_color;',
              vsTexCoordInits,
              vsFogVaryingInit,
              vsPointSizeInit,
              vsClipPlaneInit,
              vsLightingPass,
              '}',
              ''
            ].join('\n').replace(/\n\n+/g, '\n');

            this.vertexShader = GLctx.createShader(GLctx.VERTEX_SHADER);
            GLctx.shaderSource(this.vertexShader, vsSource);
            GLctx.compileShader(this.vertexShader);

            var fogHeaderIfNeeded = null;
            if (GLEmulation.fogEnabled) {
              fogHeaderIfNeeded = [
                '',
                'varying float v_fogFragCoord; ',
                'uniform vec4 u_fogColor;      ',
                'uniform float u_fogEnd;       ',
                'uniform float u_fogScale;     ',
                'uniform float u_fogDensity;   ',
                'float ffog(in float ecDistance) { ',
                fogFormula,
                '  fog = clamp(fog, 0.0, 1.0); ',
                '  return fog;                 ',
                '}',
                '',
              ].join("\n");
            }

            var fogPass = null;
            if (GLEmulation.fogEnabled) {
              fogPass = 'gl_FragColor = vec4(mix(u_fogColor.rgb, gl_FragColor.rgb, ffog(v_fogFragCoord)), gl_FragColor.a);\n';
            }

            var fsAlphaTestDefs = '';
            var fsAlphaTestPass = '';
            if (GLEmulation.alphaTestEnabled) {
              fsAlphaTestDefs = 'uniform float u_alphaTestRef;';
              switch (GLEmulation.alphaTestFunc) {
                case 0x200: // GL_NEVER
                  fsAlphaTestPass = 'discard;';
                  break;
                case 0x201: // GL_LESS
                  fsAlphaTestPass = 'if (!(gl_FragColor.a < u_alphaTestRef)) { discard; }';
                  break;
                case 0x202: // GL_EQUAL
                  fsAlphaTestPass = 'if (!(gl_FragColor.a == u_alphaTestRef)) { discard; }';
                  break;
                case 0x203: // GL_LEQUAL
                  fsAlphaTestPass = 'if (!(gl_FragColor.a <= u_alphaTestRef)) { discard; }';
                  break;
                case 0x204: // GL_GREATER
                  fsAlphaTestPass = 'if (!(gl_FragColor.a > u_alphaTestRef)) { discard; }';
                  break;
                case 0x205: // GL_NOTEQUAL
                  fsAlphaTestPass = 'if (!(gl_FragColor.a != u_alphaTestRef)) { discard; }';
                  break;
                case 0x206: // GL_GEQUAL
                  fsAlphaTestPass = 'if (!(gl_FragColor.a >= u_alphaTestRef)) { discard; }';
                  break;
                case 0x207: // GL_ALWAYS
                  fsAlphaTestPass = '';
                  break;
              }
            }

            var fsSource = [
              'precision mediump float;',
              texUnitVaryingList,
              texUnitUniformList,
              'varying vec4 v_color;',
              fogHeaderIfNeeded,
              fsClipPlaneDefs,
              fsAlphaTestDefs,
              'void main()',
              '{',
              fsClipPlanePass,
              fsTexEnvPass,
              fogPass,
              fsAlphaTestPass,
              '}',
              ''
            ].join("\n").replace(/\n\n+/g, '\n');

            this.fragmentShader = GLctx.createShader(GLctx.FRAGMENT_SHADER);
            GLctx.shaderSource(this.fragmentShader, fsSource);
            GLctx.compileShader(this.fragmentShader);

            this.program = GLctx.createProgram();
            GLctx.attachShader(this.program, this.vertexShader);
            GLctx.attachShader(this.program, this.fragmentShader);

            // As optimization, bind all attributes to prespecified locations, so that the FFP emulation
            // code can submit attributes to any generated FFP shader without having to examine each shader in turn.
            // These prespecified locations are only assumed if GL_FFP_ONLY is specified, since user could also create their
            // own shaders that didn't have attributes in the same locations.
            GLctx.bindAttribLocation(this.program, GLImmediate.VERTEX, 'a_position');
            GLctx.bindAttribLocation(this.program, GLImmediate.COLOR, 'a_color');
            GLctx.bindAttribLocation(this.program, GLImmediate.NORMAL, 'a_normal');
            var maxVertexAttribs = GLctx.getParameter(GLctx.MAX_VERTEX_ATTRIBS);
            for (var i = 0; i < GLImmediate.MAX_TEXTURES && GLImmediate.TEXTURE0 + i < maxVertexAttribs; i++) {
              GLctx.bindAttribLocation(this.program, GLImmediate.TEXTURE0 + i, 'a_texCoord'+i);
              GLctx.bindAttribLocation(this.program, GLImmediate.TEXTURE0 + i, aTexCoordPrefix+i);
            }
            GLctx.linkProgram(this.program);
          }

          // Stores an array that remembers which matrix uniforms are up-to-date in this FFP renderer, so they don't need to be resubmitted
          // each time we render with this program.
          this.textureMatrixVersion = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ];

          this.positionLocation = GLctx.getAttribLocation(this.program, 'a_position');

          this.texCoordLocations = [];

          for (var i = 0; i < GLImmediate.MAX_TEXTURES; i++) {
            if (!GLImmediate.enabledClientAttributes[GLImmediate.TEXTURE0 + i]) {
              this.texCoordLocations[i] = -1;
              continue;
            }

            if (useCurrProgram) {
              this.texCoordLocations[i] = GLctx.getAttribLocation(this.program, `a_texCoord${i}`);
            } else {
              this.texCoordLocations[i] = GLctx.getAttribLocation(this.program, aTexCoordPrefix + i);
            }
          }
          this.colorLocation = GLctx.getAttribLocation(this.program, 'a_color');
          if (!useCurrProgram) {
            // Temporarily switch to the program so we can set our sampler uniforms early.
            var prevBoundProg = GLctx.getParameter(GLctx.CURRENT_PROGRAM);
            GLctx.useProgram(this.program);
            {
              for (var i = 0; i < this.usedTexUnitList.length; i++) {
                var texUnitID = this.usedTexUnitList[i];
                var texSamplerLoc = GLctx.getUniformLocation(this.program, uTexUnitPrefix + texUnitID);
                GLctx.uniform1i(texSamplerLoc, texUnitID);
              }
            }
            // The default color attribute value is not the same as the default for all other attribute streams (0,0,0,1) but (1,1,1,1),
            // so explicitly set it right at start.
            GLctx.vertexAttrib4fv(this.colorLocation, [1,1,1,1]);
            GLctx.useProgram(prevBoundProg);
          }

          this.textureMatrixLocations = [];
          for (var i = 0; i < GLImmediate.MAX_TEXTURES; i++) {
            this.textureMatrixLocations[i] = GLctx.getUniformLocation(this.program, `u_textureMatrix${i}`);
          }
          this.normalLocation = GLctx.getAttribLocation(this.program, 'a_normal');

          this.modelViewLocation = GLctx.getUniformLocation(this.program, 'u_modelView');
          this.projectionLocation = GLctx.getUniformLocation(this.program, 'u_projection');
          this.normalMatrixLocation = GLctx.getUniformLocation(this.program, 'u_normalMatrix');

          this.hasTextures = hasTextures;
          this.hasNormal = GLImmediate.enabledClientAttributes[GLImmediate.NORMAL] &&
                           GLImmediate.clientAttributes[GLImmediate.NORMAL].size > 0 &&
                           this.normalLocation >= 0;
          this.hasColor = (this.colorLocation === 0) || this.colorLocation > 0;

          this.floatType = GLctx.FLOAT; // minor optimization

          this.fogColorLocation = GLctx.getUniformLocation(this.program, 'u_fogColor');
          this.fogEndLocation = GLctx.getUniformLocation(this.program, 'u_fogEnd');
          this.fogScaleLocation = GLctx.getUniformLocation(this.program, 'u_fogScale');
          this.fogDensityLocation = GLctx.getUniformLocation(this.program, 'u_fogDensity');
          this.hasFog = !!(this.fogColorLocation || this.fogEndLocation ||
                           this.fogScaleLocation || this.fogDensityLocation);

          this.pointSizeLocation = GLctx.getUniformLocation(this.program, 'u_pointSize');

          this.hasClipPlane = false;
          this.clipPlaneEquationLocation = [];
          for (var clipPlaneId = 0; clipPlaneId < GLEmulation.MAX_CLIP_PLANES; clipPlaneId++) {
            this.clipPlaneEquationLocation[clipPlaneId] = GLctx.getUniformLocation(this.program, `u_clipPlaneEquation${clipPlaneId}`);
            this.hasClipPlane = (this.hasClipPlane || this.clipPlaneEquationLocation[clipPlaneId]);
          }

          this.hasLighting = GLEmulation.lightingEnabled;
          this.lightModelAmbientLocation = GLctx.getUniformLocation(this.program, 'u_lightModelAmbient');
          this.materialAmbientLocation = GLctx.getUniformLocation(this.program, 'u_materialAmbient');
          this.materialDiffuseLocation = GLctx.getUniformLocation(this.program, 'u_materialDiffuse');
          this.materialSpecularLocation = GLctx.getUniformLocation(this.program, 'u_materialSpecular');
          this.materialShininessLocation = GLctx.getUniformLocation(this.program, 'u_materialShininess');
          this.materialEmissionLocation = GLctx.getUniformLocation(this.program, 'u_materialEmission');
          this.lightAmbientLocation = []
          this.lightDiffuseLocation = []
          this.lightSpecularLocation = []
          this.lightPositionLocation = []
          for (var lightId = 0; lightId < GLEmulation.MAX_LIGHTS; lightId++) {
            this.lightAmbientLocation[lightId] = GLctx.getUniformLocation(this.program, `u_lightAmbient${lightId}`);
            this.lightDiffuseLocation[lightId] = GLctx.getUniformLocation(this.program, `u_lightDiffuse${lightId}`);
            this.lightSpecularLocation[lightId] = GLctx.getUniformLocation(this.program, `u_lightSpecular${lightId}`);
            this.lightPositionLocation[lightId] = GLctx.getUniformLocation(this.program, `u_lightPosition${lightId}`);
          }

          this.hasAlphaTest = GLEmulation.alphaTestEnabled;
          this.alphaTestRefLocation = GLctx.getUniformLocation(this.program, 'u_alphaTestRef');

        };

        this.prepare = function() {
          // Calculate the array buffer
          var arrayBuffer;
          if (!GLctx.currentArrayBufferBinding) {
            var start = GLImmediate.firstVertex*GLImmediate.stride;
            var end = GLImmediate.lastVertex*GLImmediate.stride;
#if ASSERTIONS
            assert(end <= GL.MAX_TEMP_BUFFER_SIZE, 'too much vertex data');
#endif
            arrayBuffer = GL.getTempVertexBuffer(end);
            // TODO: consider using the last buffer we bound, if it was larger. downside is larger buffer, but we might avoid rebinding and preparing
          } else {
            arrayBuffer = GLctx.currentArrayBufferBinding;
          }

#if GL_UNSAFE_OPTS
          // If the array buffer is unchanged and the renderer as well, then we can avoid all the work here
          // XXX We use some heuristics here, and this may not work in all cases. Try disabling GL_UNSAFE_OPTS if you
          // have odd glitches
          var lastRenderer = GLImmediate.lastRenderer;
          var canSkip = this == lastRenderer &&
                        arrayBuffer == GLImmediate.lastArrayBuffer &&
                        (GL.currProgram || this.program) == GLImmediate.lastProgram &&
                        GLImmediate.stride == GLImmediate.lastStride &&
                        !GLImmediate.matricesModified;
          if (!canSkip && lastRenderer) lastRenderer.cleanup();
#endif
          if (!GLctx.currentArrayBufferBinding) {
            // Bind the array buffer and upload data after cleaning up the previous renderer

            if (arrayBuffer != GLImmediate.lastArrayBuffer) {
              GLctx.bindBuffer(GLctx.ARRAY_BUFFER, arrayBuffer);
              GLImmediate.lastArrayBuffer = arrayBuffer;
            }

            webglBufferSubData(GLctx.ARRAY_BUFFER, start, (end - start) >> 2, start >> 2, GLImmediate.vertexData);
          }
#if GL_UNSAFE_OPTS
          if (canSkip) return;
          GLImmediate.lastRenderer = this;
          GLImmediate.lastProgram = GL.currProgram || this.program;
          GLImmediate.lastStride = GLImmediate.stride;
          GLImmediate.matricesModified = false;
#endif

          if (!GL.currProgram) {
            if (GLImmediate.fixedFunctionProgram != this.program) {
              GLctx.useProgram(this.program);
              GLImmediate.fixedFunctionProgram = this.program;
            }
          }

          if (this.modelViewLocation && this.modelViewMatrixVersion != GLImmediate.matrixVersion[0/*m*/]) {
            this.modelViewMatrixVersion = GLImmediate.matrixVersion[0/*m*/];
            GLctx.uniformMatrix4fv(this.modelViewLocation, false, GLImmediate.matrix[0/*m*/]);

            // set normal matrix to the upper 3x3 of the inverse transposed current modelview matrix
            if (GLEmulation.lightEnabled) {
              var tmpMVinv = GLImmediate.matrixLib.mat4.create(GLImmediate.matrix[0]);
              GLImmediate.matrixLib.mat4.inverse(tmpMVinv);
              GLImmediate.matrixLib.mat4.transpose(tmpMVinv);
              GLctx.uniformMatrix3fv(this.normalMatrixLocation, false, GLImmediate.matrixLib.mat4.toMat3(tmpMVinv));
            }
          }
          if (this.projectionLocation && this.projectionMatrixVersion != GLImmediate.matrixVersion[1/*p*/]) {
            this.projectionMatrixVersion = GLImmediate.matrixVersion[1/*p*/];
            GLctx.uniformMatrix4fv(this.projectionLocation, false, GLImmediate.matrix[1/*p*/]);
          }

          var clientAttributes = GLImmediate.clientAttributes;
          var posAttr = clientAttributes[GLImmediate.VERTEX];

#if GL_ASSERTIONS
          GL.validateVertexAttribPointer(posAttr.size, posAttr.type, GLImmediate.stride, clientAttributes[GLImmediate.VERTEX].offset);
#endif

#if GL_FFP_ONLY
          if (!GLctx.currentArrayBufferBinding) {
            GLctx.vertexAttribPointer(GLImmediate.VERTEX, posAttr.size, posAttr.type, false, GLImmediate.stride, posAttr.offset);
            if (this.hasNormal) {
              var normalAttr = clientAttributes[GLImmediate.NORMAL];
              GLctx.vertexAttribPointer(GLImmediate.NORMAL, normalAttr.size, normalAttr.type, true, GLImmediate.stride, normalAttr.offset);
            }
          }
#else
          GLctx.vertexAttribPointer(this.positionLocation, posAttr.size, posAttr.type, false, GLImmediate.stride, posAttr.offset);
          GLctx.enableVertexAttribArray(this.positionLocation);
          if (this.hasNormal) {
            var normalAttr = clientAttributes[GLImmediate.NORMAL];
#if GL_ASSERTIONS
            GL.validateVertexAttribPointer(normalAttr.size, normalAttr.type, GLImmediate.stride, normalAttr.offset);
#endif
            GLctx.vertexAttribPointer(this.normalLocation, normalAttr.size, normalAttr.type, true, GLImmediate.stride, normalAttr.offset);
            GLctx.enableVertexAttribArray(this.normalLocation);
          }
#endif
          if (this.hasTextures) {
            for (var i = 0; i < GLImmediate.MAX_TEXTURES; i++) {
#if GL_FFP_ONLY
              if (!GLctx.currentArrayBufferBinding) {
                var attribLoc = GLImmediate.TEXTURE0+i;
                var texAttr = clientAttributes[attribLoc];
                if (texAttr.size) {
                  GLctx.vertexAttribPointer(attribLoc, texAttr.size, texAttr.type, false, GLImmediate.stride, texAttr.offset);
                } else {
                  // These two might be dangerous, but let's try them.
                  GLctx.vertexAttrib4f(attribLoc, 0, 0, 0, 1);
                }
              }
#else
              var attribLoc = this.texCoordLocations[i];
              if (attribLoc === undefined || attribLoc < 0) continue;
              var texAttr = clientAttributes[GLImmediate.TEXTURE0+i];

              if (texAttr.size) {
#if GL_ASSERTIONS
                GL.validateVertexAttribPointer(texAttr.size, texAttr.type, GLImmediate.stride, texAttr.offset);
#endif
                GLctx.vertexAttribPointer(attribLoc, texAttr.size, texAttr.type, false, GLImmediate.stride, texAttr.offset);
                GLctx.enableVertexAttribArray(attribLoc);
              } else {
                // These two might be dangerous, but let's try them.
                GLctx.vertexAttrib4f(attribLoc, 0, 0, 0, 1);
                GLctx.disableVertexAttribArray(attribLoc);
              }
#endif
              var t = 2/*t*/+i;
              if (this.textureMatrixLocations[i] && this.textureMatrixVersion[t] != GLImmediate.matrixVersion[t]) { // XXX might we need this even without the condition we are currently in?
                this.textureMatrixVersion[t] = GLImmediate.matrixVersion[t];
                GLctx.uniformMatrix4fv(this.textureMatrixLocations[i], false, GLImmediate.matrix[t]);
              }
            }
          }
          if (GLImmediate.enabledClientAttributes[GLImmediate.COLOR]) {
            var colorAttr = clientAttributes[GLImmediate.COLOR];
#if GL_ASSERTIONS
            GL.validateVertexAttribPointer(colorAttr.size, colorAttr.type, GLImmediate.stride, colorAttr.offset);
#endif
#if GL_FFP_ONLY
            if (!GLctx.currentArrayBufferBinding) {
              GLctx.vertexAttribPointer(GLImmediate.COLOR, colorAttr.size, colorAttr.type, true, GLImmediate.stride, colorAttr.offset);
            }
#else
            GLctx.vertexAttribPointer(this.colorLocation, colorAttr.size, colorAttr.type, true, GLImmediate.stride, colorAttr.offset);
            GLctx.enableVertexAttribArray(this.colorLocation);
#endif
          }
#if !GL_FFP_ONLY
          else if (this.hasColor) {
            GLctx.disableVertexAttribArray(this.colorLocation);
            GLctx.vertexAttrib4fv(this.colorLocation, GLImmediate.clientColor);
          }
#endif
          if (this.hasFog) {
            if (this.fogColorLocation) GLctx.uniform4fv(this.fogColorLocation, GLEmulation.fogColor);
            if (this.fogEndLocation) GLctx.uniform1f(this.fogEndLocation, GLEmulation.fogEnd);
            if (this.fogScaleLocation) GLctx.uniform1f(this.fogScaleLocation, 1/(GLEmulation.fogEnd - GLEmulation.fogStart));
            if (this.fogDensityLocation) GLctx.uniform1f(this.fogDensityLocation, GLEmulation.fogDensity);
          }

          if (this.hasClipPlane) {
            for (var clipPlaneId = 0; clipPlaneId < GLEmulation.MAX_CLIP_PLANES; clipPlaneId++) {
              if (this.clipPlaneEquationLocation[clipPlaneId]) GLctx.uniform4fv(this.clipPlaneEquationLocation[clipPlaneId], GLEmulation.clipPlaneEquation[clipPlaneId]);
            }
          }

          if (this.hasLighting) {
            if (this.lightModelAmbientLocation) GLctx.uniform4fv(this.lightModelAmbientLocation, GLEmulation.lightModelAmbient);
            if (this.materialAmbientLocation) GLctx.uniform4fv(this.materialAmbientLocation, GLEmulation.materialAmbient);
            if (this.materialDiffuseLocation) GLctx.uniform4fv(this.materialDiffuseLocation, GLEmulation.materialDiffuse);
            if (this.materialSpecularLocation) GLctx.uniform4fv(this.materialSpecularLocation, GLEmulation.materialSpecular);
            if (this.materialShininessLocation) GLctx.uniform1f(this.materialShininessLocation, GLEmulation.materialShininess[0]);
            if (this.materialEmissionLocation) GLctx.uniform4fv(this.materialEmissionLocation, GLEmulation.materialEmission);
            for (var lightId = 0; lightId < GLEmulation.MAX_LIGHTS; lightId++) {
              if (this.lightAmbientLocation[lightId]) GLctx.uniform4fv(this.lightAmbientLocation[lightId], GLEmulation.lightAmbient[lightId]);
              if (this.lightDiffuseLocation[lightId]) GLctx.uniform4fv(this.lightDiffuseLocation[lightId], GLEmulation.lightDiffuse[lightId]);
              if (this.lightSpecularLocation[lightId]) GLctx.uniform4fv(this.lightSpecularLocation[lightId], GLEmulation.lightSpecular[lightId]);
              if (this.lightPositionLocation[lightId]) GLctx.uniform4fv(this.lightPositionLocation[lightId], GLEmulation.lightPosition[lightId]);
            }
          }

          if (this.hasAlphaTest) {
            if (this.alphaTestRefLocation) GLctx.uniform1f(this.alphaTestRefLocation, GLEmulation.alphaTestRef);
          }

          if (GLImmediate.mode == GLctx.POINTS) {
            if (this.pointSizeLocation) {
              GLctx.uniform1f(this.pointSizeLocation, GLEmulation.pointSize);
            }
          }
        };

        this.cleanup = function() {
#if !GL_FFP_ONLY
          GLctx.disableVertexAttribArray(this.positionLocation);
          if (this.hasTextures) {
            for (var i = 0; i < GLImmediate.MAX_TEXTURES; i++) {
              if (GLImmediate.enabledClientAttributes[GLImmediate.TEXTURE0+i] && this.texCoordLocations[i] >= 0) {
                GLctx.disableVertexAttribArray(this.texCoordLocations[i]);
              }
            }
          }
          if (this.hasColor) {
            GLctx.disableVertexAttribArray(this.colorLocation);
          }
          if (this.hasNormal) {
            GLctx.disableVertexAttribArray(this.normalLocation);
          }
          if (!GL.currProgram) {
            GLctx.useProgram(null);
            GLImmediate.fixedFunctionProgram = 0;
          }
          if (!GLctx.currentArrayBufferBinding) {
            GLctx.bindBuffer(GLctx.ARRAY_BUFFER, null);
            GLImmediate.lastArrayBuffer = null;
          }

#if GL_UNSAFE_OPTS
          GLImmediate.lastRenderer = null;
          GLImmediate.lastProgram = null;
#endif
          GLImmediate.matricesModified = true;
#endif
        }

        this.init();
      }
      return new Renderer();
    },

    setupFuncs() {
      // TexEnv stuff needs to be prepared early, so do it here.
      // init() is too late for -O2, since it freezes the GL functions
      // by that point.
      GLImmediate.MapTreeLib = GLImmediate.spawnMapTreeLib();
      GLImmediate.spawnMapTreeLib = null;

      GLImmediate.TexEnvJIT = GLImmediate.spawnTexEnvJIT();
      GLImmediate.spawnTexEnvJIT = null;

      GLImmediate.setupHooks();
    },

    setupHooks() {
      if (!GLEmulation.hasRunInit) {
        GLEmulation.init();
      }

      var glActiveTexture = _glActiveTexture;
      _glActiveTexture = _emscripten_glActiveTexture = (texture) => {
        GLImmediate.TexEnvJIT.hook_activeTexture(texture);
        glActiveTexture(texture);
      };

      var glEnable = _glEnable;
      _glEnable = _emscripten_glEnable = (cap) => {
        GLImmediate.TexEnvJIT.hook_enable(cap);
        glEnable(cap);
      };

      var glDisable = _glDisable;
      _glDisable = _emscripten_glDisable = (cap) => {
        GLImmediate.TexEnvJIT.hook_disable(cap);
        glDisable(cap);
      };

      var glTexEnvf = (typeof _glTexEnvf != 'undefined') ? _glTexEnvf : () => {};
      /** @suppress {checkTypes} */
      _glTexEnvf = _emscripten_glTexEnvf = (target, pname, param) => {
        GLImmediate.TexEnvJIT.hook_texEnvf(target, pname, param);
        // Don't call old func, since we are the implementor.
        //glTexEnvf(target, pname, param);
      };

      var glTexEnvi = (typeof _glTexEnvi != 'undefined') ? _glTexEnvi : () => {};
      /** @suppress {checkTypes} */
      _glTexEnvi = _emscripten_glTexEnvi = (target, pname, param) => {
        {{{ fromPtr('param') }}}
        GLImmediate.TexEnvJIT.hook_texEnvi(target, pname, param);
        // Don't call old func, since we are the implementor.
        //glTexEnvi(target, pname, param);
      };

      var glTexEnvfv = (typeof _glTexEnvfv != 'undefined') ? _glTexEnvfv : () => {};
      /** @suppress {checkTypes} */
      _glTexEnvfv = _emscripten_glTexEnvfv = (target, pname, param) => {
        {{{ fromPtr('param') }}}
        GLImmediate.TexEnvJIT.hook_texEnvfv(target, pname, param);
        // Don't call old func, since we are the implementor.
        //glTexEnvfv(target, pname, param);
      };

      _glGetTexEnviv = (target, pname, param) => {
        {{{ fromPtr('param') }}}
        GLImmediate.TexEnvJIT.hook_getTexEnviv(target, pname, param);
      };

      _glGetTexEnvfv = (target, pname, param) => {
        {{{ fromPtr('param') }}}
        GLImmediate.TexEnvJIT.hook_getTexEnvfv(target, pname, param);
      };

      var glGetIntegerv = _glGetIntegerv;
      _glGetIntegerv = _emscripten_glGetIntegerv = (pname, params) => {
        switch (pname) {
          case 0x8B8D: { // GL_CURRENT_PROGRAM
            // Just query directly so we're working with WebGL objects.
            var cur = GLctx.getParameter(GLctx.CURRENT_PROGRAM);
            if (cur == GLImmediate.fixedFunctionProgram) {
              // Pretend we're not using a program.
              {{{ makeSetValue('params', '0', '0', 'i32') }}};
              return;
            }
            break;
          }
        }
        glGetIntegerv(pname, params);
      };
    },

    // Main functions
    initted: false,
    init() {
      err('WARNING: using emscripten GL immediate mode emulation. This is very limited in what it supports');
      GLImmediate.initted = true;

      if (!Browser.useWebGL) return; // a 2D canvas may be currently used TODO: make sure we are actually called in that case

      // User can override the maximum number of texture units that we emulate. Using fewer texture units increases runtime performance
      // slightly, so it is advantageous to choose as small value as needed.
      // Limit to a maximum of 28 to not overflow the state bits used for renderer caching (31 bits = 3 attributes + 28 texture units).
      GLImmediate.MAX_TEXTURES = Math.min(Module['GL_MAX_TEXTURE_IMAGE_UNITS'] || GLctx.getParameter(GLctx.MAX_TEXTURE_IMAGE_UNITS), 28);

      GLImmediate.TexEnvJIT.init(GLctx, GLImmediate.MAX_TEXTURES);

      GLImmediate.NUM_ATTRIBUTES = 3 /*pos+normal+color attributes*/ + GLImmediate.MAX_TEXTURES;
      GLImmediate.clientAttributes = [];
      GLEmulation.enabledClientAttribIndices = [];
      for (var i = 0; i < GLImmediate.NUM_ATTRIBUTES; i++) {
        GLImmediate.clientAttributes.push({});
        GLEmulation.enabledClientAttribIndices.push(false);
      }

      // Initialize matrix library
      // When user sets a matrix, increment a 'version number' on the new data, and when rendering, submit
      // the matrices to the shader program only if they have an old version of the data.
      GLImmediate.matrix = [];
      GLImmediate.matrixStack = [];
      GLImmediate.matrixVersion = [];
      for (var i = 0; i < 2 + GLImmediate.MAX_TEXTURES; i++) { // Modelview, Projection, plus one matrix for each texture coordinate.
        GLImmediate.matrixStack.push([]);
        GLImmediate.matrixVersion.push(0);
        GLImmediate.matrix.push(GLImmediate.matrixLib.mat4.create());
        GLImmediate.matrixLib.mat4.identity(GLImmediate.matrix[i]);
      }

      // Renderer cache
      GLImmediate.rendererCache = GLImmediate.MapTreeLib.create();

      // Buffers for data
      GLImmediate.tempData = new Float32Array(GL.MAX_TEMP_BUFFER_SIZE >> 2);
      GLImmediate.indexData = new Uint16Array(GL.MAX_TEMP_BUFFER_SIZE >> 1);

      GLImmediate.vertexDataU8 = new Uint8Array(GLImmediate.tempData.buffer);

      GL.generateTempBuffers(true, GL.currentContext);

      GLImmediate.clientColor = new Float32Array([1, 1, 1, 1]);
    },

    // Prepares and analyzes client attributes.
    // Modifies liveClientAttributes, stride, vertexPointer, vertexCounter
    //   count: number of elements we will draw
    //   beginEnd: whether we are drawing the results of a begin/end block
    prepareClientAttributes(count, beginEnd) {
      // If no client attributes were modified since we were last called, do
      // nothing. Note that this does not work for glBegin/End, where we
      // generate renderer components dynamically and then disable them
      // ourselves, but it does help with glDrawElements/Arrays.
      if (!GLImmediate.modifiedClientAttributes) {
#if GL_ASSERTIONS
        if ((GLImmediate.stride & 3) != 0) {
          warnOnce(`Warning: Rendering from client side vertex arrays where stride (${GLImmediate.stride}) is not a multiple of four! This is not currently supported!`);
        }
#endif
        GLImmediate.vertexCounter = (GLImmediate.stride * count) / 4; // XXX assuming float
        return;
      }
      GLImmediate.modifiedClientAttributes = false;

      // The role of prepareClientAttributes is to examine the set of
      // client-side vertex attribute buffers that user code has submitted, and
      // to prepare them to be uploaded to a VBO in GPU memory (since WebGL does
      // not support client-side rendering, i.e. rendering from vertex data in
      // CPU memory). User can submit vertex data generally in three different
      // configurations:
      // 1. Fully planar: all attributes are in their own separate
      //                  tightly-packed arrays in CPU memory.
      // 2. Fully interleaved: all attributes share a single array where data is
      //                       interleaved something like (pos,uv,normal),
      //                       (pos,uv,normal), ...
      // 3. Complex hybrid: Multiple separate arrays that either are sparsely
      //                    strided, and/or partially interleaves vertex
      //                    attributes.

      // For simplicity, we support the case (2) as the fast case. For (1) and
      // (3), we do a memory copy of the vertex data here to prepare a
      // relayouted buffer that is of the structure in case (2). The reason
      // for this is that it allows the emulation code to get away with using
      // just one VBO buffer for rendering, and not have to maintain multiple
      // ones. Therefore cases (1) and (3) will be very slow, and case (2) is
      // fast.

      // Detect which case we are in by using a quick heuristic by examining the
      // strides of the buffers. If all the buffers have identical stride, we
      // assume we have case (2), otherwise we have something more complex.
      var clientStartPointer = {{{ POINTER_MAX }}};
      var bytes = 0; // Total number of bytes taken up by a single vertex.
      var minStride = {{{ POINTER_MAX }}};
      var maxStride = 0;
      var attributes = GLImmediate.liveClientAttributes;
      attributes.length = 0;
      for (var i = 0; i < 3+GLImmediate.MAX_TEXTURES; i++) {
        if (GLImmediate.enabledClientAttributes[i]) {
          var attr = GLImmediate.clientAttributes[i];
          attributes.push(attr);
          clientStartPointer = Math.min(clientStartPointer, attr.pointer);
          attr.sizeBytes = attr.size * GL.byteSizeByType[attr.type - GL.byteSizeByTypeRoot];
          bytes += attr.sizeBytes;
          minStride = Math.min(minStride, attr.stride);
          maxStride = Math.max(maxStride, attr.stride);
        }
      }

      if ((minStride != maxStride || maxStride < bytes) && !beginEnd) {
        // We are in cases (1) or (3): slow path, shuffle the data around into a
        // single interleaved vertex buffer.
        // The immediate-mode glBegin()/glEnd() vertex submission gets
        // automatically generated in appropriate layout, so never need to come
        // down this path if that was used.
#if GL_ASSERTIONS
        warnOnce('Rendering from planar client-side vertex arrays. This is a very slow emulation path! Use interleaved vertex arrays for best performance.');
#endif
        GLImmediate.restrideBuffer ||= _malloc(GL.MAX_TEMP_BUFFER_SIZE);
        var start = GLImmediate.restrideBuffer;
        bytes = 0;
        // calculate restrided offsets and total size
        for (var attr of attributes) {
          var size = attr.sizeBytes;
          if (size % 4 != 0) size += 4 - (size % 4); // align everything
          attr.offset = bytes;
          bytes += size;
        }
        // copy out the data (we need to know the stride for that, and define attr.pointer)
        for (var attr of attributes) {
          var srcStride = Math.max(attr.sizeBytes, attr.stride);
          if ((srcStride & 3) == 0 && (attr.sizeBytes & 3) == 0) {
            for (var j = 0; j < count; j++) {
              for (var k = 0; k < attr.sizeBytes; k+=4) { // copy in chunks of 4 bytes, our alignment makes this possible
                var val = {{{ makeGetValue('attr.pointer', 'j*srcStride + k', 'i32') }}};
                {{{ makeSetValue('start + attr.offset', 'bytes*j + k', 'val', 'i32') }}};
              }
            }
          } else {
            for (var j = 0; j < count; j++) {
              for (var k = 0; k < attr.sizeBytes; k++) { // source data was not aligned to multiples of 4, must copy byte by byte.
                HEAP8[start + attr.offset + bytes*j + k] = HEAP8[attr.pointer + j*srcStride + k];
              }
            }
          }
          attr.pointer = start + attr.offset;
        }
        GLImmediate.stride = bytes;
        GLImmediate.vertexPointer = start;
      } else {
        // case (2): fast path, all data is interleaved to a single vertex array so we can get away with a single VBO upload.
        if (GLctx.currentArrayBufferBinding) {
          GLImmediate.vertexPointer = 0;
        } else {
          GLImmediate.vertexPointer = clientStartPointer;
        }
        for (var attr of attributes) {
          attr.offset = attr.pointer - GLImmediate.vertexPointer; // Compute what will be the offset of this attribute in the VBO after we upload.
        }
        GLImmediate.stride = Math.max(maxStride, bytes);
      }
      if (!beginEnd) {
#if GL_ASSERTIONS
        if ((GLImmediate.stride & 3) != 0) {
          warnOnce(`Warning: Rendering from client side vertex arrays where stride (${GLImmediate.stride}) is not a multiple of four! This is not currently supported!`);
        }
#endif
        GLImmediate.vertexCounter = (GLImmediate.stride * count) / 4; // XXX assuming float
      }
    },

    flush(numProvidedIndexes, startIndex = 0, ptr = 0) {
#if ASSERTIONS
      assert(numProvidedIndexes >= 0 || !numProvidedIndexes);
#endif
      var renderer = GLImmediate.getRenderer();

      // Generate index data in a format suitable for GLES 2.0/WebGL
      var numVertices = 4 * GLImmediate.vertexCounter / GLImmediate.stride;
      if (!numVertices) return;
#if ASSERTIONS
      assert(numVertices % 1 == 0, "`numVertices` must be an integer.");
#endif
      var emulatedElementArrayBuffer = false;
      var numIndexes = 0;
      if (numProvidedIndexes) {
        numIndexes = numProvidedIndexes;
        if (!GLctx.currentArrayBufferBinding && GLImmediate.firstVertex > GLImmediate.lastVertex) {
          // Figure out the first and last vertex from the index data
#if ASSERTIONS
          // If we are going to upload array buffer data, we need to find which range to
          // upload based on the indices. If they are in a buffer on the GPU, that is very
          // inconvenient! So if you do not have an array buffer, you should also not have
          // an element array buffer. But best is to use both buffers!
          assert(!GLctx.currentElementArrayBufferBinding, 'must use array buffers when using element buffer');
#endif
          for (var i = 0; i < numProvidedIndexes; i++) {
            var currIndex = {{{ makeGetValue('ptr', 'i*2', 'u16') }}};
            GLImmediate.firstVertex = Math.min(GLImmediate.firstVertex, currIndex);
            GLImmediate.lastVertex = Math.max(GLImmediate.lastVertex, currIndex+1);
          }
        }
        if (!GLctx.currentElementArrayBufferBinding) {
          // If no element array buffer is bound, then indices is a literal pointer to clientside data
          var byteSize = numProvidedIndexes << 1;
#if ASSERTIONS
          assert(byteSize <= GL.MAX_TEMP_BUFFER_SIZE, 'too many immediate mode indexes (a)');
#endif
          var indexBuffer = GL.getTempIndexBuffer(byteSize);
          GLctx.bindBuffer(GLctx.ELEMENT_ARRAY_BUFFER, indexBuffer);
          webglBufferSubData(GLctx.ELEMENT_ARRAY_BUFFER, 0, byteSize, ptr);
          ptr = 0;
          emulatedElementArrayBuffer = true;
        }
      } else if (GLImmediate.mode > 6) { // above GL_TRIANGLE_FAN are the non-GL ES modes
        if (GLImmediate.mode != 7) abort('unsupported immediate mode ' + GLImmediate.mode); // GL_QUADS
        // GLImmediate.firstVertex is the first vertex we want. Quad indexes are
        // in the pattern 0 1 2, 0 2 3, 4 5 6, 4 6 7, so we need to look at
        // index firstVertex * 1.5 to see it.  Then since indexes are 2 bytes
        // each, that means 3
#if ASSERTIONS
        assert(GLImmediate.firstVertex % 4 == 0);
#endif
        ptr = GLImmediate.firstVertex * 3;
        var numQuads = numVertices / 4;
        numIndexes = numQuads * 6; // 0 1 2, 0 2 3 pattern
#if ASSERTIONS
        assert(ptr + (numIndexes << 1) <= GL.MAX_TEMP_BUFFER_SIZE, 'too many immediate mode indexes (b)');
#endif
        GLctx.bindBuffer(GLctx.ELEMENT_ARRAY_BUFFER, GL.currentContext.tempQuadIndexBuffer);
        emulatedElementArrayBuffer = true;
        GLImmediate.mode = GLctx.TRIANGLES;
      }

      renderer.prepare();

      if (numIndexes) {
        GLctx.drawElements(GLImmediate.mode, numIndexes, GLctx.UNSIGNED_SHORT, ptr);
      } else {
        GLctx.drawArrays(GLImmediate.mode, startIndex, numVertices);
      }

      if (emulatedElementArrayBuffer) {
        GLctx.bindBuffer(GLctx.ELEMENT_ARRAY_BUFFER, GL.buffers[GLctx.currentElementArrayBufferBinding] || null);
      }

#if !GL_UNSAFE_OPTS
#if !GL_FFP_ONLY
      renderer.cleanup();
#endif
#endif
    }
  },

  $GLImmediateSetup__deps: ['$GLImmediate', () => 'GLImmediate.matrixLib = ' + read('gl-matrix.js') + ';\n'],
  $GLImmediateSetup: {},

  glBegin__deps: ['$GLImmediateSetup'],
  glBegin: (mode) => {
    // Push the old state:
    GLImmediate.enabledClientAttributes_preBegin = GLImmediate.enabledClientAttributes;
    GLImmediate.enabledClientAttributes = [];

    GLImmediate.clientAttributes_preBegin = GLImmediate.clientAttributes;
    GLImmediate.clientAttributes = []
    for (var i = 0; i < GLImmediate.clientAttributes_preBegin.length; i++) {
      GLImmediate.clientAttributes.push({});
    }

    GLImmediate.mode = mode;
    GLImmediate.vertexCounter = 0;
    var components = GLImmediate.rendererComponents = [];
    for (var i = 0; i < GLImmediate.NUM_ATTRIBUTES; i++) {
      components[i] = 0;
    }
    GLImmediate.rendererComponentPointer = 0;
    GLImmediate.vertexData = GLImmediate.tempData;
  },

  glEnd: () => {
    GLImmediate.prepareClientAttributes(GLImmediate.rendererComponents[GLImmediate.VERTEX], true);
    GLImmediate.firstVertex = 0;
    GLImmediate.lastVertex = GLImmediate.vertexCounter / (GLImmediate.stride >> 2);
    GLImmediate.flush();
    GLImmediate.disableBeginEndClientAttributes();
    GLImmediate.mode = -1;

    // Pop the old state:
    GLImmediate.enabledClientAttributes = GLImmediate.enabledClientAttributes_preBegin;
    GLImmediate.clientAttributes = GLImmediate.clientAttributes_preBegin;
    GLImmediate.currentRenderer = null; // The set of active client attributes changed, we must re-lookup the renderer to use.
    GLImmediate.modifiedClientAttributes = true;
  },

  glVertex2f: (x, y) => {
#if ASSERTIONS
    assert(GLImmediate.mode >= 0); // must be in begin/end
#endif
    GLImmediate.vertexData[GLImmediate.vertexCounter++] = x;
    GLImmediate.vertexData[GLImmediate.vertexCounter++] = y;
    GLImmediate.vertexData[GLImmediate.vertexCounter++] = 0;
    GLImmediate.vertexData[GLImmediate.vertexCounter++] = 1;
#if ASSERTIONS
    assert(GLImmediate.vertexCounter << 2 < GL.MAX_TEMP_BUFFER_SIZE);
#endif
    GLImmediate.addRendererComponent(GLImmediate.VERTEX, 4, GLctx.FLOAT);
  },

  glVertex3f: (x, y, z) => {
#if ASSERTIONS
    assert(GLImmediate.mode >= 0); // must be in begin/end
#endif
    GLImmediate.vertexData[GLImmediate.vertexCounter++] = x;
    GLImmediate.vertexData[GLImmediate.vertexCounter++] = y;
    GLImmediate.vertexData[GLImmediate.vertexCounter++] = z;
    GLImmediate.vertexData[GLImmediate.vertexCounter++] = 1;
#if ASSERTIONS
    assert(GLImmediate.vertexCounter << 2 < GL.MAX_TEMP_BUFFER_SIZE);
#endif
    GLImmediate.addRendererComponent(GLImmediate.VERTEX, 4, GLctx.FLOAT);
  },

  glVertex4f: (x, y, z, w) => {
#if ASSERTIONS
    assert(GLImmediate.mode >= 0); // must be in begin/end
#endif
    GLImmediate.vertexData[GLImmediate.vertexCounter++] = x;
    GLImmediate.vertexData[GLImmediate.vertexCounter++] = y;
    GLImmediate.vertexData[GLImmediate.vertexCounter++] = z;
    GLImmediate.vertexData[GLImmediate.vertexCounter++] = w;
#if ASSERTIONS
    assert(GLImmediate.vertexCounter << 2 < GL.MAX_TEMP_BUFFER_SIZE);
#endif
    GLImmediate.addRendererComponent(GLImmediate.VERTEX, 4, GLctx.FLOAT);
  },

  glVertex2fv__deps: ['glVertex2f'],
  glVertex2fv: (p) => _glVertex2f({{{ makeGetValue('p', '0', 'float') }}},
                                  {{{ makeGetValue('p', '4', 'float') }}}),

  glVertex3fv__deps: ['glVertex3f'],
  glVertex3fv: (p) => _glVertex3f({{{ makeGetValue('p', '0', 'float') }}},
                                  {{{ makeGetValue('p', '4', 'float') }}},
                                  {{{ makeGetValue('p', '8', 'float') }}}),

  glVertex4fv__deps: ['glVertex4f'],
  glVertex4fv: (p) => _glVertex4f({{{ makeGetValue('p', '0', 'float') }}},
                                  {{{ makeGetValue('p', '4', 'float') }}},
                                  {{{ makeGetValue('p', '8', 'float') }}},
                                  {{{ makeGetValue('p', '12', 'float') }}}),

  glVertex2i: 'glVertex2f',

  glVertex3i: 'glVertex3f',

  glVertex4i: 'glVertex4f',

  glTexCoord2i: (u, v) => {
#if ASSERTIONS
    assert(GLImmediate.mode >= 0); // must be in begin/end
#endif
    GLImmediate.vertexData[GLImmediate.vertexCounter++] = u;
    GLImmediate.vertexData[GLImmediate.vertexCounter++] = v;
    GLImmediate.addRendererComponent(GLImmediate.TEXTURE0, 2, GLctx.FLOAT);
  },
  glTexCoord2f: 'glTexCoord2i',

  glTexCoord2fv__deps: ['glTexCoord2i'],
  glTexCoord2fv: (v) =>
    _glTexCoord2i({{{ makeGetValue('v', '0', 'float') }}}, {{{ makeGetValue('v', '4', 'float') }}}),

  glTexCoord4f: () => { abort('glTexCoord4f: TODO') },

  glColor4f: (r, g, b, a) => {
    r = Math.max(Math.min(r, 1), 0);
    g = Math.max(Math.min(g, 1), 0);
    b = Math.max(Math.min(b, 1), 0);
    a = Math.max(Math.min(a, 1), 0);

    // TODO: make ub the default, not f, save a few mathops
    if (GLImmediate.mode >= 0) {
      var start = GLImmediate.vertexCounter << 2;
      GLImmediate.vertexDataU8[start + 0] = r * 255;
      GLImmediate.vertexDataU8[start + 1] = g * 255;
      GLImmediate.vertexDataU8[start + 2] = b * 255;
      GLImmediate.vertexDataU8[start + 3] = a * 255;
      GLImmediate.vertexCounter++;
      GLImmediate.addRendererComponent(GLImmediate.COLOR, 4, GLctx.UNSIGNED_BYTE);
    } else {
      GLImmediate.clientColor[0] = r;
      GLImmediate.clientColor[1] = g;
      GLImmediate.clientColor[2] = b;
      GLImmediate.clientColor[3] = a;
#if GL_FFP_ONLY
      GLctx.vertexAttrib4fv(GLImmediate.COLOR, GLImmediate.clientColor);
#endif
    }
  },

  glColor4d: 'glColor4f',

  glColor4ub__deps: ['glColor4f'],
  glColor4ub: (r, g, b, a) => _glColor4f((r&255)/255, (g&255)/255, (b&255)/255, (a&255)/255),

  glColor4us__deps: ['glColor4f'],
  glColor4us: (r, g, b, a) => _glColor4f((r&65535)/65535, (g&65535)/65535, (b&65535)/65535, (a&65535)/65535),

  glColor4ui__deps: ['glColor4f'],
  glColor4ui: (r, g, b, a) => _glColor4f((r>>>0)/4294967295, (g>>>0)/4294967295, (b>>>0)/4294967295, (a>>>0)/4294967295),

  glColor3f__deps: ['glColor4f'],
  glColor3f: (r, g, b) => _glColor4f(r, g, b, 1),

  glColor3d: 'glColor3f',

  glColor3ub__deps: ['glColor4ub'],
  glColor3ub: (r, g, b) => _glColor4ub(r, g, b, 255),

  glColor3us__deps: ['glColor4us'],
  glColor3us: (r, g, b) => _glColor4us(r, g, b, 65535),

  glColor3ui__deps: ['glColor4ui'],
  glColor3ui: (r, g, b) => _glColor4ui(r, g, b, 4294967295),

  glColor3ubv__deps: ['glColor3ub'],
  glColor3ubv: (p) => _glColor3ub({{{ makeGetValue('p', '0', 'i8') }}},
                                  {{{ makeGetValue('p', '1', 'i8') }}},
                                  {{{ makeGetValue('p', '2', 'i8') }}}),

  glColor3usv__deps: ['glColor3us'],
  glColor3usv: (p) => _glColor3us({{{ makeGetValue('p', '0', 'i16') }}},
                                  {{{ makeGetValue('p', '2', 'i16') }}},
                                  {{{ makeGetValue('p', '4', 'i16') }}}),

  glColor3uiv__deps: ['glColor3ui'],
  glColor3uiv: (p) => _glColor3ui({{{ makeGetValue('p', '0', 'i32') }}},
                                  {{{ makeGetValue('p', '4', 'i32') }}},
                                  {{{ makeGetValue('p', '8', 'i32') }}}),

  glColor3fv__deps: ['glColor3f'],
  glColor3fv: (p) => _glColor3f({{{ makeGetValue('p', '0', 'float') }}},
                                {{{ makeGetValue('p', '4', 'float') }}},
                                {{{ makeGetValue('p', '8', 'float') }}}),

  glColor4fv__deps: ['glColor4f'],
  glColor4fv: (p) => _glColor4f({{{ makeGetValue('p', '0', 'float') }}},
                                {{{ makeGetValue('p', '4', 'float') }}},
                                {{{ makeGetValue('p', '8', 'float') }}},
                                {{{ makeGetValue('p', '12', 'float') }}}),

  glColor4ubv__deps: ['glColor4ub'],
  glColor4ubv: (p) => _glColor4ub({{{ makeGetValue('p', '0', 'i8') }}},
                                  {{{ makeGetValue('p', '1', 'i8') }}},
                                  {{{ makeGetValue('p', '2', 'i8') }}},
                                  {{{ makeGetValue('p', '3', 'i8') }}}),

  glFogf: (pname, param) => { // partial support, TODO
    switch (pname) {
      case 0xB63: // GL_FOG_START
        GLEmulation.fogStart = param; break;
      case 0xB64: // GL_FOG_END
        GLEmulation.fogEnd = param; break;
      case 0xB62: // GL_FOG_DENSITY
        GLEmulation.fogDensity = param; break;
      case 0xB65: // GL_FOG_MODE
        switch (param) {
          case 0x801: // GL_EXP2
          case 0x2601: // GL_LINEAR
            if (GLEmulation.fogMode != param) {
              GLImmediate.currentRenderer = null; // Fog mode is part of the FFP shader state, we must re-lookup the renderer to use.
              GLEmulation.fogMode = param;
            }
            break;
          default: // default to GL_EXP
            if (GLEmulation.fogMode != 0x800 /* GL_EXP */) {
              GLImmediate.currentRenderer = null; // Fog mode is part of the FFP shader state, we must re-lookup the renderer to use.
              GLEmulation.fogMode = 0x800 /* GL_EXP */;
            }
            break;
        }
        break;
    }
  },
  glFogi__deps: ['glFogf'],
  glFogi: (pname, param) => {
    return _glFogf(pname, param);
  },
  glFogfv__deps: ['glFogf'],
  glFogfv: (pname, param) => { // partial support, TODO
    switch (pname) {
      case 0xB66: // GL_FOG_COLOR
        GLEmulation.fogColor[0] = {{{ makeGetValue('param', '0', 'float') }}};
        GLEmulation.fogColor[1] = {{{ makeGetValue('param', '4', 'float') }}};
        GLEmulation.fogColor[2] = {{{ makeGetValue('param', '8', 'float') }}};
        GLEmulation.fogColor[3] = {{{ makeGetValue('param', '12', 'float') }}};
        break;
      case 0xB63: // GL_FOG_START
      case 0xB64: // GL_FOG_END
        _glFogf(pname, {{{ makeGetValue('param', '0', 'float') }}}); break;
    }
  },
  glFogiv__deps: ['glFogf'],
  glFogiv: (pname, param) => {
    switch (pname) {
      case 0xB66: // GL_FOG_COLOR
        GLEmulation.fogColor[0] = ({{{ makeGetValue('param', '0', 'i32') }}}/2147483647)/2.0+0.5;
        GLEmulation.fogColor[1] = ({{{ makeGetValue('param', '4', 'i32') }}}/2147483647)/2.0+0.5;
        GLEmulation.fogColor[2] = ({{{ makeGetValue('param', '8', 'i32') }}}/2147483647)/2.0+0.5;
        GLEmulation.fogColor[3] = ({{{ makeGetValue('param', '12', 'i32') }}}/2147483647)/2.0+0.5;
        break;
      default:
        _glFogf(pname, {{{ makeGetValue('param', '0', 'i32') }}}); break;
    }
  },
  glFogx: 'glFogi',
  glFogxv: 'glFogiv',

  glPointSize: (size) => {
    GLEmulation.pointSize = size;
  },

  glPolygonMode: () => {}, // TODO

  glAlphaFunc: (func, ref) => {
    switch(func) {
      case 0x200: // GL_NEVER
      case 0x201: // GL_LESS
      case 0x202: // GL_EQUAL
      case 0x203: // GL_LEQUAL
      case 0x204: // GL_GREATER
      case 0x205: // GL_NOTEQUAL
      case 0x206: // GL_GEQUAL
      case 0x207: // GL_ALWAYS
        GLEmulation.alphaTestRef = ref;
        if (GLEmulation.alphaTestFunc != func) {
          GLEmulation.alphaTestFunc = func;
          GLImmediate.currentRenderer = null; // alpha test mode is part of the FFP shader state, we must re-lookup the renderer to use.
        }
        break;
      default: // invalid value provided
#if GL_ASSERTIONS
        err(`glAlphaFunc: Invalid alpha comparison function ${ptrToString(func)}!`);
#endif
        break;
    }
  },

  glNormal3f: (x, y, z) => {
#if ASSERTIONS
    assert(GLImmediate.mode >= 0); // must be in begin/end
#endif
    GLImmediate.vertexData[GLImmediate.vertexCounter++] = x;
    GLImmediate.vertexData[GLImmediate.vertexCounter++] = y;
    GLImmediate.vertexData[GLImmediate.vertexCounter++] = z;
#if ASSERTIONS
    assert(GLImmediate.vertexCounter << 2 < GL.MAX_TEMP_BUFFER_SIZE);
#endif
    GLImmediate.addRendererComponent(GLImmediate.NORMAL, 3, GLctx.FLOAT);
  },

  glNormal3fv__deps: ['glNormal3f'],
  glNormal3fv: (p) => {
    _glNormal3f({{{ makeGetValue('p', '0', 'float') }}}, {{{ makeGetValue('p', '4', 'float') }}}, {{{ makeGetValue('p', '8', 'float') }}});
  },


  // Additional non-GLES rendering calls

  glDrawRangeElements__deps: ['glDrawElements'],
  glDrawRangeElements: (mode, start, end, count, type, indices) => {
    _glDrawElements(mode, count, type, indices, start, end);
  },

  // ClientState/gl*Pointer

  glEnableClientState: (cap) => {
    var attrib = GLEmulation.getAttributeFromCapability(cap);
    if (attrib === null) {
#if ASSERTIONS
      err(`WARNING: unhandled clientstate: ${cap}`);
#endif
      return;
    }
    if (!GLImmediate.enabledClientAttributes[attrib]) {
      GLImmediate.enabledClientAttributes[attrib] = true;
      GLImmediate.totalEnabledClientAttributes++;
      GLImmediate.currentRenderer = null; // Will need to change current renderer, since the set of active vertex pointers changed.
#if GL_FFP_ONLY
      // In GL_FFP_ONLY mode, attributes are bound to the same index in each FFP emulation shader, so we can immediately apply the change here.
      GL.enableVertexAttribArray(attrib);
#endif
      if (GLEmulation.currentVao) GLEmulation.currentVao.enabledClientStates[cap] = 1;
      GLImmediate.modifiedClientAttributes = true;
    }
  },
  glDisableClientState: (cap) => {
    var attrib = GLEmulation.getAttributeFromCapability(cap);
    if (attrib === null) {
#if ASSERTIONS
      err(`WARNING: unhandled clientstate: ${cap}`);
#endif
      return;
    }
    if (GLImmediate.enabledClientAttributes[attrib]) {
      GLImmediate.enabledClientAttributes[attrib] = false;
      GLImmediate.totalEnabledClientAttributes--;
      GLImmediate.currentRenderer = null; // Will need to change current renderer, since the set of active vertex pointers changed.
#if GL_FFP_ONLY
      // In GL_FFP_ONLY mode, attributes are bound to the same index in each FFP emulation shader, so we can immediately apply the change here.
      GL.disableVertexAttribArray(attrib);
#endif
      if (GLEmulation.currentVao) delete GLEmulation.currentVao.enabledClientStates[cap];
      GLImmediate.modifiedClientAttributes = true;
    }
  },

  glVertexPointer: (size, type, stride, pointer) => {
    GLImmediate.setClientAttribute(GLImmediate.VERTEX, size, type, stride, pointer);
#if GL_FFP_ONLY
    if (GLctx.currentArrayBufferBinding) {
      GLctx.vertexAttribPointer(GLImmediate.VERTEX, size, type, false, stride, pointer);
    }
#endif
  },
  glTexCoordPointer: (size, type, stride, pointer) => {
    GLImmediate.setClientAttribute(GLImmediate.TEXTURE0 + GLImmediate.clientActiveTexture, size, type, stride, pointer);
#if GL_FFP_ONLY
    if (GLctx.currentArrayBufferBinding) {
      var loc = GLImmediate.TEXTURE0 + GLImmediate.clientActiveTexture;
      GLctx.vertexAttribPointer(loc, size, type, false, stride, pointer);
    }
#endif
  },
  glNormalPointer: (type, stride, pointer) => {
    GLImmediate.setClientAttribute(GLImmediate.NORMAL, 3, type, stride, pointer);
#if GL_FFP_ONLY
    if (GLctx.currentArrayBufferBinding) {
      GLctx.vertexAttribPointer(GLImmediate.NORMAL, 3, type, true, stride, pointer);
    }
#endif
  },
  glColorPointer: (size, type, stride, pointer) => {
    GLImmediate.setClientAttribute(GLImmediate.COLOR, size, type, stride, pointer);
#if GL_FFP_ONLY
    if (GLctx.currentArrayBufferBinding) {
      GLctx.vertexAttribPointer(GLImmediate.COLOR, size, type, true, stride, pointer);
    }
#endif
  },

  glClientActiveTexture: (texture) => {
    GLImmediate.clientActiveTexture = texture - 0x84C0; // GL_TEXTURE0
  },

  // Replace some functions with immediate-mode aware versions. If there are no
  // client attributes enabled, and we use webgl-friendly modes (no GL_QUADS),
  // then no need for emulation
  glDrawArrays: (mode, first, count) => {
    if (GLImmediate.totalEnabledClientAttributes == 0 && mode <= 6) {
      GLctx.drawArrays(mode, first, count);
      return;
    }
    GLImmediate.prepareClientAttributes(count, false);
    GLImmediate.mode = mode;
    if (!GLctx.currentArrayBufferBinding) {
      GLImmediate.vertexData = {{{ makeHEAPView('F32', 'GLImmediate.vertexPointer', 'GLImmediate.vertexPointer + (first+count)*GLImmediate.stride') }}}; // XXX assuming float
      GLImmediate.firstVertex = first;
      GLImmediate.lastVertex = first + count;
    }
    GLImmediate.flush(null, first);
    GLImmediate.mode = -1;
  },

  // start, end are given if we come from glDrawRangeElements
  glDrawElements: (mode, count, type, indices, start, end) => {
    if (GLImmediate.totalEnabledClientAttributes == 0 && mode <= 6 && GLctx.currentElementArrayBufferBinding) {
      GLctx.drawElements(mode, count, type, indices);
      return;
    }
#if ASSERTIONS
    if (!GLctx.currentElementArrayBufferBinding) {
      assert(type == GLctx.UNSIGNED_SHORT); // We can only emulate buffers of this kind, for now
    }
    warnOnce("DrawElements doesn't actually prepareClientAttributes properly.");
#endif
    GLImmediate.prepareClientAttributes(count, false);
    GLImmediate.mode = mode;
    if (!GLctx.currentArrayBufferBinding) {
      GLImmediate.firstVertex = end ? start : HEAP8.length; // if we don't know the start, set an invalid value and we will calculate it later from the indices
      GLImmediate.lastVertex = end ? end + 1 : 0;
      start = GLImmediate.vertexPointer;
      // TODO(sbc): Combine these two subarray calls back into a single one if
      // we ever fix https://github.com/emscripten-core/emscripten/issues/21250.
      if (end) {
        end = GLImmediate.vertexPointer + (end +1 ) * GLImmediate.stride;
        GLImmediate.vertexData = HEAPF32.subarray({{{ getHeapOffset('start', 'float') }}}, {{{ getHeapOffset('end', 'float') }}});
      } else {
        GLImmediate.vertexData = HEAPF32.subarray({{{ getHeapOffset('start', 'float') }}});
      }
    }
    GLImmediate.flush(count, 0, indices);
    GLImmediate.mode = -1;
  },

  // Vertex array object (VAO) support. TODO: when the WebGL extension is
  // popular, use that and remove this code and GL.vaos
  $emulGlGenVertexArrays__deps: ['$GLEmulation'],
  $emulGlGenVertexArrays: (n, vaos) => {
    for (var i = 0; i < n; i++) {
      var id = GL.getNewId(GLEmulation.vaos);
      GLEmulation.vaos[id] = {
        id,
        arrayBuffer: 0,
        elementArrayBuffer: 0,
        enabledVertexAttribArrays: {},
        vertexAttribPointers: {},
        enabledClientStates: {},
      };
      {{{ makeSetValue('vaos', 'i*4', 'id', 'i32') }}};
    }
  },
  $emulGlDeleteVertexArrays: (n, vaos) => {
    for (var i = 0; i < n; i++) {
      var id = {{{ makeGetValue('vaos', 'i*4', 'i32') }}};
      GLEmulation.vaos[id] = null;
      if (GLEmulation.currentVao?.id == id) GLEmulation.currentVao = null;
    }
  },
  $emulGlIsVertexArray: (array) => {
    var vao = GLEmulation.vaos[array];
    if (!vao) return 0;
    return 1;
  },
  $emulGlBindVertexArray__deps: ['glBindBuffer', 'glEnableVertexAttribArray', 'glVertexAttribPointer', 'glEnableClientState'],
  $emulGlBindVertexArray: (vao) => {
    // undo vao-related things, wipe the slate clean, both for vao of 0 or an actual vao
    GLEmulation.currentVao = null; // make sure the commands we run here are not recorded
    GLImmediate.lastRenderer?.cleanup();
    _glBindBuffer(GLctx.ARRAY_BUFFER, 0); // XXX if one was there before we were bound?
    _glBindBuffer(GLctx.ELEMENT_ARRAY_BUFFER, 0);
    for (var vaa in GLEmulation.enabledVertexAttribArrays) {
      GLctx.disableVertexAttribArray(vaa);
    }
    GLEmulation.enabledVertexAttribArrays = {};
    GLImmediate.enabledClientAttributes = [0, 0];
    GLImmediate.totalEnabledClientAttributes = 0;
    GLImmediate.modifiedClientAttributes = true;
    if (vao) {
      // replay vao
      var info = GLEmulation.vaos[vao];
      _glBindBuffer(GLctx.ARRAY_BUFFER, info.arrayBuffer); // XXX overwrite current binding?
      _glBindBuffer(GLctx.ELEMENT_ARRAY_BUFFER, info.elementArrayBuffer);
      for (var vaa in info.enabledVertexAttribArrays) {
        _glEnableVertexAttribArray(vaa);
      }
      for (var vaa in info.vertexAttribPointers) {
        _glVertexAttribPointer(...info.vertexAttribPointers[vaa]);
      }
      for (var attrib in info.enabledClientStates) {
        _glEnableClientState(attrib|0);
      }
      GLEmulation.currentVao = info; // set currentVao last, so the commands we ran here were not recorded
    }
  },

  // OpenGL Immediate Mode matrix routines.
  // Note that in the future we might make these available only in certain modes.
  glMatrixMode__deps: ['$GL', '$GLImmediateSetup'],
  glMatrixMode: (mode) => {
    if (mode == 0x1700 /* GL_MODELVIEW */) {
      GLImmediate.currentMatrix = 0/*m*/;
    } else if (mode == 0x1701 /* GL_PROJECTION */) {
      GLImmediate.currentMatrix = 1/*p*/;
    } else if (mode == 0x1702) { // GL_TEXTURE
      GLImmediate.useTextureMatrix = true;
      GLImmediate.currentMatrix = 2/*t*/ + GLImmediate.TexEnvJIT.getActiveTexture();
    } else {
      throw `Wrong mode ${mode} passed to glMatrixMode`;
    }
  },

  glPushMatrix: () => {
    GLImmediate.matricesModified = true;
    GLImmediate.matrixVersion[GLImmediate.currentMatrix] = (GLImmediate.matrixVersion[GLImmediate.currentMatrix] + 1)|0;
    GLImmediate.matrixStack[GLImmediate.currentMatrix].push(
        Array.prototype.slice.call(GLImmediate.matrix[GLImmediate.currentMatrix]));
  },

  glPopMatrix: () => {
    if (GLImmediate.matrixStack[GLImmediate.currentMatrix].length == 0) {
      GL.recordError(0x504/*GL_STACK_UNDERFLOW*/);
      return;
    }
    GLImmediate.matricesModified = true;
    GLImmediate.matrixVersion[GLImmediate.currentMatrix] = (GLImmediate.matrixVersion[GLImmediate.currentMatrix] + 1)|0;
    GLImmediate.matrix[GLImmediate.currentMatrix] = GLImmediate.matrixStack[GLImmediate.currentMatrix].pop();
  },

  glLoadIdentity__deps: ['$GL', '$GLImmediateSetup'],
  glLoadIdentity: () => {
    GLImmediate.matricesModified = true;
    GLImmediate.matrixVersion[GLImmediate.currentMatrix] = (GLImmediate.matrixVersion[GLImmediate.currentMatrix] + 1)|0;
    GLImmediate.matrixLib.mat4.identity(GLImmediate.matrix[GLImmediate.currentMatrix]);
  },

  glLoadMatrixd: (matrix) => {
    GLImmediate.matricesModified = true;
    GLImmediate.matrixVersion[GLImmediate.currentMatrix] = (GLImmediate.matrixVersion[GLImmediate.currentMatrix] + 1)|0;
    GLImmediate.matrixLib.mat4.set({{{ makeHEAPView('F64', 'matrix', 'matrix+' + (16*8)) }}}, GLImmediate.matrix[GLImmediate.currentMatrix]);
  },

  glLoadMatrixf: (matrix) => {
#if GL_DEBUG
    if (GL.debug) dbg('glLoadMatrixf receiving: ' + Array.prototype.slice.call(HEAPF32.subarray(matrix >> 2, (matrix >> 2) + 16)));
#endif
    GLImmediate.matricesModified = true;
    GLImmediate.matrixVersion[GLImmediate.currentMatrix] = (GLImmediate.matrixVersion[GLImmediate.currentMatrix] + 1)|0;
    GLImmediate.matrixLib.mat4.set({{{ makeHEAPView('F32', 'matrix', 'matrix+' + (16*4)) }}}, GLImmediate.matrix[GLImmediate.currentMatrix]);
  },

  glLoadTransposeMatrixd: (matrix) => {
    GLImmediate.matricesModified = true;
    GLImmediate.matrixVersion[GLImmediate.currentMatrix] = (GLImmediate.matrixVersion[GLImmediate.currentMatrix] + 1)|0;
    GLImmediate.matrixLib.mat4.set({{{ makeHEAPView('F64', 'matrix', 'matrix+' + (16*8)) }}}, GLImmediate.matrix[GLImmediate.currentMatrix]);
    GLImmediate.matrixLib.mat4.transpose(GLImmediate.matrix[GLImmediate.currentMatrix]);
  },

  glLoadTransposeMatrixf: (matrix) => {
    GLImmediate.matricesModified = true;
    GLImmediate.matrixVersion[GLImmediate.currentMatrix] = (GLImmediate.matrixVersion[GLImmediate.currentMatrix] + 1)|0;
    GLImmediate.matrixLib.mat4.set({{{ makeHEAPView('F32', 'matrix', 'matrix+' + (16*4)) }}}, GLImmediate.matrix[GLImmediate.currentMatrix]);
    GLImmediate.matrixLib.mat4.transpose(GLImmediate.matrix[GLImmediate.currentMatrix]);
  },

  glMultMatrixd: (matrix) => {
    GLImmediate.matricesModified = true;
    GLImmediate.matrixVersion[GLImmediate.currentMatrix] = (GLImmediate.matrixVersion[GLImmediate.currentMatrix] + 1)|0;
    GLImmediate.matrixLib.mat4.multiply(GLImmediate.matrix[GLImmediate.currentMatrix],
        {{{ makeHEAPView('F64', 'matrix', 'matrix+' + (16*8)) }}});
  },

  glMultMatrixf: (matrix) => {
    GLImmediate.matricesModified = true;
    GLImmediate.matrixVersion[GLImmediate.currentMatrix] = (GLImmediate.matrixVersion[GLImmediate.currentMatrix] + 1)|0;
    GLImmediate.matrixLib.mat4.multiply(GLImmediate.matrix[GLImmediate.currentMatrix],
        {{{ makeHEAPView('F32', 'matrix', 'matrix+' + (16*4)) }}});
  },

  glMultTransposeMatrixd: (matrix) => {
    GLImmediate.matricesModified = true;
    GLImmediate.matrixVersion[GLImmediate.currentMatrix] = (GLImmediate.matrixVersion[GLImmediate.currentMatrix] + 1)|0;
    var colMajor = GLImmediate.matrixLib.mat4.create();
    GLImmediate.matrixLib.mat4.set({{{ makeHEAPView('F64', 'matrix', 'matrix+' + (16*8)) }}}, colMajor);
    GLImmediate.matrixLib.mat4.transpose(colMajor);
    GLImmediate.matrixLib.mat4.multiply(GLImmediate.matrix[GLImmediate.currentMatrix], colMajor);
  },

  glMultTransposeMatrixf: (matrix) => {
    GLImmediate.matricesModified = true;
    GLImmediate.matrixVersion[GLImmediate.currentMatrix] = (GLImmediate.matrixVersion[GLImmediate.currentMatrix] + 1)|0;
    var colMajor = GLImmediate.matrixLib.mat4.create();
    GLImmediate.matrixLib.mat4.set({{{ makeHEAPView('F32', 'matrix', 'matrix+' + (16*4)) }}}, colMajor);
    GLImmediate.matrixLib.mat4.transpose(colMajor);
    GLImmediate.matrixLib.mat4.multiply(GLImmediate.matrix[GLImmediate.currentMatrix], colMajor);
  },

  glFrustum: (left, right, bottom, top_, nearVal, farVal) => {
    GLImmediate.matricesModified = true;
    GLImmediate.matrixVersion[GLImmediate.currentMatrix] = (GLImmediate.matrixVersion[GLImmediate.currentMatrix] + 1)|0;
    GLImmediate.matrixLib.mat4.multiply(GLImmediate.matrix[GLImmediate.currentMatrix],
        GLImmediate.matrixLib.mat4.frustum(left, right, bottom, top_, nearVal, farVal));
  },
  glFrustumf: 'glFrustum',

  glOrtho: (left, right, bottom, top_, nearVal, farVal) => {
    GLImmediate.matricesModified = true;
    GLImmediate.matrixVersion[GLImmediate.currentMatrix] = (GLImmediate.matrixVersion[GLImmediate.currentMatrix] + 1)|0;
    GLImmediate.matrixLib.mat4.multiply(GLImmediate.matrix[GLImmediate.currentMatrix],
        GLImmediate.matrixLib.mat4.ortho(left, right, bottom, top_, nearVal, farVal));
  },
  glOrthof: 'glOrtho',

  glScaled: (x, y, z) => {
    GLImmediate.matricesModified = true;
    GLImmediate.matrixVersion[GLImmediate.currentMatrix] = (GLImmediate.matrixVersion[GLImmediate.currentMatrix] + 1)|0;
    GLImmediate.matrixLib.mat4.scale(GLImmediate.matrix[GLImmediate.currentMatrix], [x, y, z]);
  },
  glScalef: 'glScaled',

  glTranslated: (x, y, z) => {
    GLImmediate.matricesModified = true;
    GLImmediate.matrixVersion[GLImmediate.currentMatrix] = (GLImmediate.matrixVersion[GLImmediate.currentMatrix] + 1)|0;
    GLImmediate.matrixLib.mat4.translate(GLImmediate.matrix[GLImmediate.currentMatrix], [x, y, z]);
  },
  glTranslatef: 'glTranslated',

  glRotated: (angle, x, y, z) => {
    GLImmediate.matricesModified = true;
    GLImmediate.matrixVersion[GLImmediate.currentMatrix] = (GLImmediate.matrixVersion[GLImmediate.currentMatrix] + 1)|0;
    GLImmediate.matrixLib.mat4.rotate(GLImmediate.matrix[GLImmediate.currentMatrix], angle*Math.PI/180, [x, y, z]);
  },
  glRotatef: 'glRotated',

  glDrawBuffer: () => { abort('glDrawBuffer: TODO') },
#if MAX_WEBGL_VERSION < 2
  glReadBuffer: () => { abort('glReadBuffer: TODO') },
#endif

  glClipPlane: (pname, param) => {
    if ((pname >= 0x3000) && (pname < 0x3006)  /* GL_CLIP_PLANE0 to GL_CLIP_PLANE5 */) {
      var clipPlaneId = pname - 0x3000;

      GLEmulation.clipPlaneEquation[clipPlaneId][0] = {{{ makeGetValue('param', '0', 'double') }}};
      GLEmulation.clipPlaneEquation[clipPlaneId][1] = {{{ makeGetValue('param', '8', 'double') }}};
      GLEmulation.clipPlaneEquation[clipPlaneId][2] = {{{ makeGetValue('param', '16', 'double') }}};
      GLEmulation.clipPlaneEquation[clipPlaneId][3] = {{{ makeGetValue('param', '24', 'double') }}};

      // apply inverse transposed current modelview matrix when setting clip plane
      var tmpMV = GLImmediate.matrixLib.mat4.create(GLImmediate.matrix[0]);
      GLImmediate.matrixLib.mat4.inverse(tmpMV);
      GLImmediate.matrixLib.mat4.transpose(tmpMV);
      GLImmediate.matrixLib.mat4.multiplyVec4(tmpMV, GLEmulation.clipPlaneEquation[clipPlaneId]);
    }
  },

  glLightfv: (light, pname, param) => {
    if ((light >= 0x4000) && (light < 0x4008)  /* GL_LIGHT0 to GL_LIGHT7 */) {
      var lightId = light - 0x4000;

      if (pname == 0x1200) { // GL_AMBIENT
        GLEmulation.lightAmbient[lightId][0] = {{{ makeGetValue('param', '0', 'float') }}};
        GLEmulation.lightAmbient[lightId][1] = {{{ makeGetValue('param', '4', 'float') }}};
        GLEmulation.lightAmbient[lightId][2] = {{{ makeGetValue('param', '8', 'float') }}};
        GLEmulation.lightAmbient[lightId][3] = {{{ makeGetValue('param', '12', 'float') }}};
      } else if (pname == 0x1201) { // GL_DIFFUSE
        GLEmulation.lightDiffuse[lightId][0] = {{{ makeGetValue('param', '0', 'float') }}};
        GLEmulation.lightDiffuse[lightId][1] = {{{ makeGetValue('param', '4', 'float') }}};
        GLEmulation.lightDiffuse[lightId][2] = {{{ makeGetValue('param', '8', 'float') }}};
        GLEmulation.lightDiffuse[lightId][3] = {{{ makeGetValue('param', '12', 'float') }}};
      } else if (pname == 0x1202) { // GL_SPECULAR
        GLEmulation.lightSpecular[lightId][0] = {{{ makeGetValue('param', '0', 'float') }}};
        GLEmulation.lightSpecular[lightId][1] = {{{ makeGetValue('param', '4', 'float') }}};
        GLEmulation.lightSpecular[lightId][2] = {{{ makeGetValue('param', '8', 'float') }}};
        GLEmulation.lightSpecular[lightId][3] = {{{ makeGetValue('param', '12', 'float') }}};
      } else if (pname == 0x1203) { // GL_POSITION
        GLEmulation.lightPosition[lightId][0] = {{{ makeGetValue('param', '0', 'float') }}};
        GLEmulation.lightPosition[lightId][1] = {{{ makeGetValue('param', '4', 'float') }}};
        GLEmulation.lightPosition[lightId][2] = {{{ makeGetValue('param', '8', 'float') }}};
        GLEmulation.lightPosition[lightId][3] = {{{ makeGetValue('param', '12', 'float') }}};

        // multiply position with current modelviewmatrix
        GLImmediate.matrixLib.mat4.multiplyVec4(GLImmediate.matrix[0], GLEmulation.lightPosition[lightId]);
      } else {
        abort('glLightfv: TODO: ' + pname);
      }
    }
  },

  glLightModelf: (pname, param) => {
    if (pname == 0x0B52) { // GL_LIGHT_MODEL_TWO_SIDE
      GLEmulation.lightModelTwoSide = (param != 0) ? true : false;
    } else {
      abort('glLightModelf: TODO: ' + pname);
    }
  },

  glLightModelfv: (pname, param) => { // TODO: GL_LIGHT_MODEL_LOCAL_VIEWER
    if (pname == 0x0B53) { // GL_LIGHT_MODEL_AMBIENT
      GLEmulation.lightModelAmbient[0] = {{{ makeGetValue('param', '0', 'float') }}};
      GLEmulation.lightModelAmbient[1] = {{{ makeGetValue('param', '4', 'float') }}};
      GLEmulation.lightModelAmbient[2] = {{{ makeGetValue('param', '8', 'float') }}};
      GLEmulation.lightModelAmbient[3] = {{{ makeGetValue('param', '12', 'float') }}};
    } else {
      abort('glLightModelfv: TODO: ' + pname);
    }
  },

  glMaterialfv: (face, pname, param) => {
    if ((face != 0x0404) && (face != 0x0408)) { abort('glMaterialfv: TODO' + face); } // only GL_FRONT and GL_FRONT_AND_BACK supported

    if (pname == 0x1200) { // GL_AMBIENT
      GLEmulation.materialAmbient[0] = {{{ makeGetValue('param', '0', 'float') }}};
      GLEmulation.materialAmbient[1] = {{{ makeGetValue('param', '4', 'float') }}};
      GLEmulation.materialAmbient[2] = {{{ makeGetValue('param', '8', 'float') }}};
      GLEmulation.materialAmbient[3] = {{{ makeGetValue('param', '12', 'float') }}};
    } else if (pname == 0x1201) { // GL_DIFFUSE
      GLEmulation.materialDiffuse[0] = {{{ makeGetValue('param', '0', 'float') }}};
      GLEmulation.materialDiffuse[1] = {{{ makeGetValue('param', '4', 'float') }}};
      GLEmulation.materialDiffuse[2] = {{{ makeGetValue('param', '8', 'float') }}};
      GLEmulation.materialDiffuse[3] = {{{ makeGetValue('param', '12', 'float') }}};
    } else if (pname == 0x1202) { // GL_SPECULAR
      GLEmulation.materialSpecular[0] = {{{ makeGetValue('param', '0', 'float') }}};
      GLEmulation.materialSpecular[1] = {{{ makeGetValue('param', '4', 'float') }}};
      GLEmulation.materialSpecular[2] = {{{ makeGetValue('param', '8', 'float') }}};
      GLEmulation.materialSpecular[3] = {{{ makeGetValue('param', '12', 'float') }}};
    } else if (pname == 0x1601) { // GL_SHININESS
      GLEmulation.materialShininess[0] = {{{ makeGetValue('param', '0', 'float') }}};
    } else {
      abort('glMaterialfv: TODO: ' + pname);
    }
  },

  glTexGeni: (coord, pname, param) => abort('glTexGeni: TODO'),
  glTexGenfv: (coord, pname, param) => abort('glTexGenfv: TODO'),
  glTexEnvi: (target, pname, params) => warnOnce('glTexEnvi: TODO'),
  glTexEnvf: (target, pname, params) => warnOnce('glTexEnvf: TODO'),
  glTexEnvfv: (target, pname, params) => warnOnce('glTexEnvfv: TODO'),

  glGetTexEnviv: (target, pname, param) => abort('GL emulation not initialized!'),
  glGetTexEnvfv: (target, pname, param) => abort('GL emulation not initialized!'),

  glTexImage1D: (target, level, internalformat, width, border, format, type, data) => abort('glTexImage1D: TODO'),
  glTexCoord3f: (target, level, internalformat, width, border, format, type, data) => abort('glTexCoord3f: TODO'),
  glGetTexLevelParameteriv: (target, level, pname, params) => abort('glGetTexLevelParameteriv: TODO'),

  glShadeModel: () => warnOnce('TODO: glShadeModel'),

  // Open GLES1.1 compatibility

  glGenFramebuffersOES: 'glGenFramebuffers',
  glGenRenderbuffersOES: 'glGenRenderbuffers',
  glBindFramebufferOES: 'glBindFramebuffer',
  glBindRenderbufferOES: 'glBindRenderbuffer',
  glGetRenderbufferParameterivOES: 'glGetRenderbufferParameteriv',
  glFramebufferRenderbufferOES: 'glFramebufferRenderbuffer',
  glRenderbufferStorageOES: 'glRenderbufferStorage',
  glCheckFramebufferStatusOES: 'glCheckFramebufferStatus',
  glDeleteFramebuffersOES: 'glDeleteFramebuffers',
  glDeleteRenderbuffersOES: 'glDeleteRenderbuffers',
  glFramebufferTexture2DOES: 'glFramebufferTexture2D',

  // GLU

  gluPerspective: (fov, aspect, near, far) => {
    GLImmediate.matricesModified = true;
    GLImmediate.matrixVersion[GLImmediate.currentMatrix] = (GLImmediate.matrixVersion[GLImmediate.currentMatrix] + 1)|0;
    GLImmediate.matrix[GLImmediate.currentMatrix] =
      GLImmediate.matrixLib.mat4.perspective(fov, aspect, near, far,
                                               GLImmediate.matrix[GLImmediate.currentMatrix]);
  },

  gluLookAt: (ex, ey, ez, cx, cy, cz, ux, uy, uz) => {
    GLImmediate.matricesModified = true;
    GLImmediate.matrixVersion[GLImmediate.currentMatrix] = (GLImmediate.matrixVersion[GLImmediate.currentMatrix] + 1)|0;
    GLImmediate.matrixLib.mat4.lookAt(GLImmediate.matrix[GLImmediate.currentMatrix], [ex, ey, ez],
        [cx, cy, cz], [ux, uy, uz]);
  },

  gluProject: (objX, objY, objZ, model, proj, view, winX, winY, winZ) => {
    // The algorithm for this function comes from Mesa

    var inVec = new Float32Array(4);
    var outVec = new Float32Array(4);
    GLImmediate.matrixLib.mat4.multiplyVec4({{{ makeHEAPView('F64', 'model', 'model+' + (16*8)) }}},
        [objX, objY, objZ, 1.0], outVec);
    GLImmediate.matrixLib.mat4.multiplyVec4({{{ makeHEAPView('F64', 'proj', 'proj+' + (16*8)) }}},
        outVec, inVec);
    if (inVec[3] == 0.0) {
      return 0 /* GL_FALSE */;
    }
    inVec[0] /= inVec[3];
    inVec[1] /= inVec[3];
    inVec[2] /= inVec[3];
    // Map x, y and z to range 0-1 */
    inVec[0] = inVec[0] * 0.5 + 0.5;
    inVec[1] = inVec[1] * 0.5 + 0.5;
    inVec[2] = inVec[2] * 0.5 + 0.5;
    // Map x, y to viewport
    inVec[0] = inVec[0] * {{{ makeGetValue('view', 2*4, 'i32') }}} + {{{ makeGetValue('view', 0*4, 'i32') }}};
    inVec[1] = inVec[1] * {{{ makeGetValue('view', 3*4, 'i32') }}} + {{{ makeGetValue('view', 1*4, 'i32') }}};

    {{{ makeSetValue('winX', '0', 'inVec[0]', 'double') }}};
    {{{ makeSetValue('winY', '0', 'inVec[1]', 'double') }}};
    {{{ makeSetValue('winZ', '0', 'inVec[2]', 'double') }}};

    return 1 /* GL_TRUE */;
  },

  gluUnProject: (winX, winY, winZ, model, proj, view, objX, objY, objZ) => {
    var result = GLImmediate.matrixLib.vec3.unproject([winX, winY, winZ],
        {{{ makeHEAPView('F64', 'model', 'model+' + (16*8)) }}},
        {{{ makeHEAPView('F64', 'proj', 'proj+' + (16*8)) }}},
        {{{ makeHEAPView('32', 'view', 'view+' + (4*4)) }}});

    if (result === null) {
      return 0 /* GL_FALSE */;
    }

    {{{ makeSetValue('objX', '0', 'result[0]', 'double') }}};
    {{{ makeSetValue('objY', '0', 'result[1]', 'double') }}};
    {{{ makeSetValue('objZ', '0', 'result[2]', 'double') }}};

    return 1 /* GL_TRUE */;
  },

  gluOrtho2D__deps: ['glOrtho'],
  gluOrtho2D: (left, right, bottom, top) => _glOrtho(left, right, bottom, top, -1, 1),
};

extraLibraryFuncs.push('$GLEmulation');

recordGLProcAddressGet(LibraryGLEmulation);

addToLibrary(LibraryGLEmulation);
