/**
 * PANDA 3D SOFTWARE
 * Copyright (c) Carnegie Mellon University.  All rights reserved.
 *
 * All use of this software is subject to the terms of the revised BSD
 * license.  You should have received a copy of this license along
 * with this source code in a file named "LICENSE."
 *
 * @file glGraphicsStateGuardian_src.cxx
 * @author drose
 * @date 1999-02-02
 * @author fperazzi, PandaSE
 * @date 2010-05-05
 *   get_supports_cg_profile)
 */

#include "config_putil.h"
#include "displayRegion.h"
#include "renderBuffer.h"
#include "geom.h"
#include "geomVertexData.h"
#include "geomTriangles.h"
#include "geomTristrips.h"
#include "geomTrifans.h"
#include "geomLines.h"
#include "geomLinestrips.h"
#include "geomPoints.h"
#include "geomVertexReader.h"
#include "graphicsWindow.h"
#include "lens.h"
#include "perspectiveLens.h"
#include "directionalLight.h"
#include "pointLight.h"
#include "spotlight.h"
#include "planeNode.h"
#include "fog.h"
#include "clockObject.h"
#include "string_utils.h"
#include "nodePath.h"
#include "dcast.h"
#include "pvector.h"
#include "vector_string.h"
#include "string_utils.h"
#include "pnmImage.h"
#include "config_gobj.h"
#include "lightMutexHolder.h"
#include "indirectLess.h"
#include "pStatTimer.h"
#include "load_prc_file.h"
#include "bamCache.h"
#include "bamCacheRecord.h"
#include "alphaTestAttrib.h"
#include "clipPlaneAttrib.h"
#include "cullFaceAttrib.h"
#include "depthBiasAttrib.h"
#include "depthOffsetAttrib.h"
#include "depthWriteAttrib.h"
#include "fogAttrib.h"
#include "lightAttrib.h"
#include "logicOpAttrib.h"
#include "materialAttrib.h"
#include "rescaleNormalAttrib.h"
#include "scissorAttrib.h"
#include "shadeModelAttrib.h"
#include "stencilAttrib.h"
#include "graphicsEngine.h"
#include "shaderGenerator.h"
#include "samplerState.h"
#include "displayInformation.h"

#if defined(HAVE_CG) && !defined(OPENGLES)
#include <Cg/cgGL.h>
#endif

#include <algorithm>

using std::dec;
using std::endl;
using std::hex;
using std::max;
using std::min;
using std::string;

TypeHandle CLP(GraphicsStateGuardian)::_type_handle;

PStatCollector CLP(GraphicsStateGuardian)::_load_display_list_pcollector("Draw:Transfer data:Display lists");
PStatCollector CLP(GraphicsStateGuardian)::_primitive_batches_display_list_pcollector("Primitive batches:Display lists");
PStatCollector CLP(GraphicsStateGuardian)::_vertices_display_list_pcollector("Vertices:Display lists");
PStatCollector CLP(GraphicsStateGuardian)::_vertices_immediate_pcollector("Vertices:Immediate mode");
PStatCollector CLP(GraphicsStateGuardian)::_memory_barrier_pcollector("Draw:Memory barriers");
PStatCollector CLP(GraphicsStateGuardian)::_vertex_array_update_pcollector("Draw:Update arrays");
PStatCollector CLP(GraphicsStateGuardian)::_texture_update_pcollector("Draw:Update texture");
PStatCollector CLP(GraphicsStateGuardian)::_fbo_bind_pcollector("Draw:Bind FBO");
PStatCollector CLP(GraphicsStateGuardian)::_check_error_pcollector("Draw:Check errors");
PStatCollector CLP(GraphicsStateGuardian)::_check_residency_pcollector("*:PStats:Check residency");
PStatCollector CLP(GraphicsStateGuardian)::_wait_fence_pcollector("Wait:Fence");
PStatCollector CLP(GraphicsStateGuardian)::_copy_texture_finish_pcollector("Draw:Copy texture:Finish");

#if defined(HAVE_CG) && !defined(OPENGLES)
AtomicAdjust::Integer CLP(GraphicsStateGuardian)::_num_gsgs_with_cg_contexts = 0;
small_vector<CGcontext> CLP(GraphicsStateGuardian)::_destroyed_cg_contexts;
#endif

// The following noop functions are assigned to the corresponding glext
// function pointers in the class, in case the functions are not defined by
// the GL, just so it will always be safe to call the extension functions.

static void APIENTRY
null_glPointParameterfv(GLenum, const GLfloat *) {
}

#ifdef OPENGLES_1
// OpenGL ES 1 doesn't support this, period.  Might as well macro it.
#define _glDrawRangeElements(mode, start, end, count, type, indices) \
  glDrawElements(mode, count, type, indices)

#else
static void APIENTRY
null_glDrawRangeElements(GLenum mode, GLuint start, GLuint end,
                         GLsizei count, GLenum type, const GLvoid *indices) {
  // If we don't support glDrawRangeElements(), just use the original
  // glDrawElements() instead.
  glDrawElements(mode, count, type, indices);
}
#endif

#if defined(OPENGLES) && !defined(OPENGLES_1)
static void APIENTRY
null_glVertexAttrib4dv(GLuint index, const GLdouble *v) {
  GLfloat vf[4] = {(GLfloat)v[0], (GLfloat)v[1], (GLfloat)v[2], (GLfloat)v[3]};
  glVertexAttrib4fv(index, vf);
}
#endif

static void APIENTRY
null_glActiveTexture(GLenum gl_texture_stage) {
  // If we don't support multitexture, we'd better not try to request a
  // texture beyond the first texture stage.
  nassertv(gl_texture_stage == GL_TEXTURE0);
}

#ifdef OPENGLES_2
#define _glBlendEquation glBlendEquation
#define _glBlendEquationSeparate glBlendEquationSeparate
#define _glBlendFuncSeparate glBlendFuncSeparate
#define _glBlendColor glBlendColor
#else
static void APIENTRY
null_glBlendEquation(GLenum) {
}

static void APIENTRY
null_glBlendFuncSeparate(GLenum src, GLenum dest, GLenum, GLenum) {
  glBlendFunc(src, dest);
}

static void APIENTRY
null_glBlendColor(GLclampf, GLclampf, GLclampf, GLclampf) {
}
#endif

#ifndef OPENGLES_1
static void APIENTRY
null_glPolygonOffsetClamp(GLfloat factor, GLfloat units, GLfloat clamp) {
  glPolygonOffset(factor, units);
}
#endif

static void APIENTRY
null_glMemoryBarrier(GLbitfield barriers) {
}

#ifndef OPENGLES_1
// We have a default shader that will be applied when there isn't any shader
// applied (e.g.  if it failed to compile).  We need this because OpenGL ES
// 2.x and OpenGL 3.2+ core don't have a fixed-function pipeline.  This
// default shader just applies a single texture, which is good enough for
// drawing GUIs and such.
static const string default_vshader =
#ifndef OPENGLES
#ifdef __APPLE__ // Apple's GL 3.2 contexts require at least GLSL 1.50.
  "#version 150\n"
#else
  "#version 130\n"
#endif
  "in vec4 p3d_Vertex;\n"
  "in vec4 p3d_Color;\n"
  "in vec4 p3d_MultiTexCoord0;\n"
  "out vec3 texcoord;\n"
  "out vec4 color;\n"
#else
  "#version 100\n"
  "precision mediump float;\n"
  "attribute vec4 p3d_Vertex;\n"
  "attribute vec4 p3d_Color;\n"
  "attribute vec4 p3d_MultiTexCoord0;\n"
  "varying vec3 texcoord;\n"
  "varying lowp vec4 color;\n"
#endif
  "uniform mat4 p3d_ModelViewProjectionMatrix;\n"
  "uniform mat4 p3d_TextureMatrix;\n"
  "uniform vec4 p3d_ColorScale;\n"
  "void main(void) {\n"
  "  gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex;\n"
  "  texcoord = (p3d_TextureMatrix * p3d_MultiTexCoord0).xyw;\n"
  "  color = p3d_Color * p3d_ColorScale;\n"
  "}\n";

#ifndef OPENGLES
// This version of the shader is used if vertices-float64 is enabled.
static const string default_vshader_fp64 =
#ifdef __APPLE__
  "#version 150\n"
#else
  "#version 130\n"
#endif
  "#extension GL_ARB_vertex_attrib_64bit : require\n"
  "#extension GL_ARB_gpu_shader_fp64 : require\n"
  "in dvec3 p3d_Vertex;\n"
  "in vec4 p3d_Color;\n"
  "in dvec4 p3d_MultiTexCoord0;\n"
  "out vec3 texcoord;\n"
  "out vec4 color;\n"
  "uniform mat4 p3d_ModelViewMatrix;\n"
  "uniform mat4 p3d_ProjectionMatrix;\n"
  "uniform mat4 p3d_TextureMatrix;\n"
  "uniform vec4 p3d_ColorScale;\n"
  "void main(void) {\n" // Apply proj & modelview in two steps, more precise
  "  gl_Position = vec4(dmat4(p3d_ProjectionMatrix) * (dmat4(p3d_ModelViewMatrix) * dvec4(p3d_Vertex, 1)));\n"
  "  texcoord = (p3d_TextureMatrix * vec4(p3d_MultiTexCoord0)).xyw;\n"
  "  color = p3d_Color * p3d_ColorScale;\n"
  "}\n";

// Same as above, but for OpenGL 4.1.
static const string default_vshader_fp64_gl41 =
  "#version 410\n"
  "in dvec3 p3d_Vertex;\n"
  "in vec4 p3d_Color;\n"
  "in dvec4 p3d_MultiTexCoord0;\n"
  "out vec3 texcoord;\n"
  "out vec4 color;\n"
  "uniform mat4 p3d_ModelViewMatrix;\n"
  "uniform mat4 p3d_ProjectionMatrix;\n"
  "uniform mat4 p3d_TextureMatrix;\n"
  "uniform vec4 p3d_ColorScale;\n"
  "void main(void) {\n" // Apply proj & modelview in two steps, more precise
  "  gl_Position = vec4(dmat4(p3d_ProjectionMatrix) * (dmat4(p3d_ModelViewMatrix) * dvec4(p3d_Vertex, 1)));\n"
  "  texcoord = (p3d_TextureMatrix * vec4(p3d_MultiTexCoord0)).xyw;\n"
  "  color = p3d_Color * p3d_ColorScale;\n"
  "}\n";
#endif

static const string default_fshader =
#ifndef OPENGLES
#ifdef __APPLE__  // Apple's GL 3.2 contexts require at least GLSL 1.50.
  "#version 150\n"
#else
  "#version 130\n"
#endif
  "in vec3 texcoord;\n"
  "in vec4 color;\n"
  "out vec4 p3d_FragColor;\n"
  "uniform sampler2D p3d_Texture0;\n"
  "uniform vec4 p3d_TexAlphaOnly;\n"
#else
  "#version 100\n"
  "precision mediump float;\n"
  "varying vec3 texcoord;\n"
  "varying lowp vec4 color;\n"
  "uniform lowp sampler2D p3d_Texture0;\n"
  "uniform lowp vec4 p3d_TexAlphaOnly;\n"
#endif
  "void main(void) {\n"
#ifndef OPENGLES
  "  p3d_FragColor = textureProj(p3d_Texture0, texcoord);\n"
  "  p3d_FragColor += p3d_TexAlphaOnly;\n" // Hack for text rendering
  "  p3d_FragColor *= color;\n"
#else
  "  gl_FragColor = texture2DProj(p3d_Texture0, texcoord);\n"
  "  gl_FragColor += p3d_TexAlphaOnly;\n" // Hack for text rendering
  "  gl_FragColor *= color;\n"
#endif
  "}\n";
#endif


/**
 * Recopies the given array of pixels, converting from BGR to RGB arrangement.
 */
static void
uchar_bgr_to_rgb(unsigned char *dest, const unsigned char *source,
                 int num_pixels) {
  for (int i = 0; i < num_pixels; i++) {
    dest[0] = source[2];
    dest[1] = source[1];
    dest[2] = source[0];
    dest += 3;
    source += 3;
  }
}

/**
 * Recopies the given array of pixels, converting from luminance to RGB
 * arrangement.
 */
static void
uchar_l_to_rgb(unsigned char *dest, const unsigned char *source,
               int num_pixels) {
  for (int i = 0; i < num_pixels; i++) {
    dest[0] = source[0];
    dest[1] = source[0];
    dest[2] = source[0];
    dest += 3;
    source += 1;
  }
}

/**
 * Recopies the given array of pixels, converting from BGRA to RGBA
 * arrangement.
 */
static void
uchar_bgra_to_rgba(unsigned char *dest, const unsigned char *source,
                   int num_pixels) {
  for (int i = 0; i < num_pixels; i++) {
    dest[0] = source[2];
    dest[1] = source[1];
    dest[2] = source[0];
    dest[3] = source[3];
    dest += 4;
    source += 4;
  }
}

/**
 * Recopies the given array of pixels, converting from luminance-alpha to RGBA
 * arrangement.
 */
static void
uchar_la_to_rgba(unsigned char *dest, const unsigned char *source,
                 int num_pixels) {
  for (int i = 0; i < num_pixels; i++) {
    dest[0] = source[0];
    dest[1] = source[0];
    dest[2] = source[0];
    dest[3] = source[1];
    dest += 4;
    source += 2;
  }
}

/**
 * Recopies the given array of pixels, converting from BGR to RGB arrangement.
 */
static void
ushort_bgr_to_rgb(unsigned short *dest, const unsigned short *source,
                  int num_pixels) {
  for (int i = 0; i < num_pixels; i++) {
    dest[0] = source[2];
    dest[1] = source[1];
    dest[2] = source[0];
    dest += 3;
    source += 3;
  }
}

/**
 * Recopies the given array of pixels, converting from luminance to RGB
 * arrangement.
 */
static void
ushort_l_to_rgb(unsigned short *dest, const unsigned short *source,
                int num_pixels) {
  for (int i = 0; i < num_pixels; i++) {
    dest[0] = source[0];
    dest[1] = source[0];
    dest[2] = source[0];
    dest += 3;
    source += 1;
  }
}

/**
 * Recopies the given array of pixels, converting from BGRA to RGBA
 * arrangement.
 */
static void
ushort_bgra_to_rgba(unsigned short *dest, const unsigned short *source,
                    int num_pixels) {
  for (int i = 0; i < num_pixels; i++) {
    dest[0] = source[2];
    dest[1] = source[1];
    dest[2] = source[0];
    dest[3] = source[3];
    dest += 4;
    source += 4;
  }
}

/**
 * Recopies the given array of pixels, converting from luminance-alpha to RGBA
 * arrangement.
 */
static void
ushort_la_to_rgba(unsigned short *dest, const unsigned short *source,
                  int num_pixels) {
  for (int i = 0; i < num_pixels; i++) {
    dest[0] = source[0];
    dest[1] = source[0];
    dest[2] = source[0];
    dest[3] = source[1];
    dest += 4;
    source += 2;
  }
}

/**
 * Reverses the order of the components within the image, to convert (for
 * instance) GL_BGR to GL_RGB. Returns the byte pointer representing the
 * converted image, or the original image if it is unchanged.
 *
 * new_image must be supplied; it is the PTA_uchar that will be used to hold
 * the converted image if required.  It will be modified only if the
 * conversion is necessary, in which case the data will be stored there, and
 * this pointer will be returned.  If the conversion is not necessary, this
 * pointer will be left unchanged.
 */
static const unsigned char *
fix_component_ordering(PTA_uchar &new_image,
                       const unsigned char *orig_image, size_t orig_image_size,
                       GLenum external_format, Texture *tex) {
  const unsigned char *result = orig_image;

  switch (external_format) {
  case GL_RGB:
    if (tex->get_num_components() == 1) {
      new_image = PTA_uchar::empty_array(orig_image_size * 3);
      uchar_l_to_rgb(new_image, orig_image, orig_image_size);
      result = new_image;
      break;
    }
    switch (tex->get_component_type()) {
    case Texture::T_unsigned_byte:
    case Texture::T_byte:
      new_image = PTA_uchar::empty_array(orig_image_size);
      uchar_bgr_to_rgb(new_image, orig_image, orig_image_size / 3);
      result = new_image;
      break;

    case Texture::T_unsigned_short:
    case Texture::T_short:
      new_image = PTA_uchar::empty_array(orig_image_size);
      ushort_bgr_to_rgb((unsigned short *)new_image.p(),
                        (const unsigned short *)orig_image,
                        orig_image_size / 6);
      result = new_image;
      break;

    default:
      break;
    }
    break;

  case GL_RGBA:
    if (tex->get_num_components() == 2) {
      new_image = PTA_uchar::empty_array(orig_image_size * 2);
      uchar_la_to_rgba(new_image, orig_image, orig_image_size / 2);
      result = new_image;
      break;
    }
    switch (tex->get_component_type()) {
    case Texture::T_unsigned_byte:
    case Texture::T_byte:
      new_image = PTA_uchar::empty_array(orig_image_size);
      uchar_bgra_to_rgba(new_image, orig_image, orig_image_size / 4);
      result = new_image;
      break;

    case Texture::T_unsigned_short:
    case Texture::T_short:
      new_image = PTA_uchar::empty_array(orig_image_size);
      ushort_bgra_to_rgba((unsigned short *)new_image.p(),
                          (const unsigned short *)orig_image,
                          orig_image_size / 8);
      result = new_image;
      break;

    default:
      break;
    }
    break;

  default:
    break;
  }

  return result;
}

// #--- Zhao Nov2011
string CLP(GraphicsStateGuardian)::get_driver_vendor() { return _gl_vendor; }
string CLP(GraphicsStateGuardian)::get_driver_renderer() { return _gl_renderer; }

string CLP(GraphicsStateGuardian)::get_driver_version() { return _gl_version; }
int CLP(GraphicsStateGuardian)::get_driver_version_major() { return _gl_version_major; }
int CLP(GraphicsStateGuardian)::get_driver_version_minor() { return _gl_version_minor; }
int CLP(GraphicsStateGuardian)::get_driver_shader_version_major() { return _gl_shadlang_ver_major; }
int CLP(GraphicsStateGuardian)::get_driver_shader_version_minor() { return _gl_shadlang_ver_minor; }

/**
 *
 */
CLP(GraphicsStateGuardian)::
CLP(GraphicsStateGuardian)(GraphicsEngine *engine, GraphicsPipe *pipe) :
  GraphicsStateGuardian(gl_coordinate_system, engine, pipe),
  _renderbuffer_residency(get_prepared_objects()->get_name(), "renderbuffer"),
  _active_ppbuffer_memory_pcollector("Graphics memory:" + get_prepared_objects()->get_name() + ":Active:ppbuffer"),
  _inactive_ppbuffer_memory_pcollector("Graphics memory:" + get_prepared_objects()->get_name() + ":Inactive:ppbuffer")
{
  _error_count = 0;
  _last_error_check = -1.0;

  // calling glGetError() forces a sync, this turns it on if you want to.
  _check_errors = gl_check_errors;
  _force_flush = gl_force_flush;

  _gl_shadlang_ver_major = 0;
  _gl_shadlang_ver_minor = 0;

  // Let's say we have a core profile, to be checked later (Otherwise, if we are
  // wrong the user may ask for some non-available functions)
#ifndef OPENGLES
  _core_profile = true;
#endif

  // Hack.  Turn on the flag that we turned off at a higher level, since we
  // know this works properly in OpenGL, and we want the performance benefit
  // it gives us.
  _prepared_objects->_support_released_buffer_cache = true;

  // Assume that we will get a hardware-accelerated context, unless the window
  // tells us otherwise.
  _is_hardware = true;

  _scissor_enabled = false;
  _scissor_attrib_active = false;
  _has_attrib_depth_range = false;

  _white_texture = 0;

#ifndef OPENGLES
  _shader_point_size = false;
#endif

#if defined(HAVE_CG) && !defined(OPENGLES)
  _cg_context = 0;
#endif

#ifdef DO_PSTATS
  if (gl_finish) {
    GLCAT.warning()
      << "The config variable gl-finish is set to true.  This may have a substantial negative impact on your render performance.\n";
  }
#endif  // DO_PSTATS
}

/**
 *
 */
CLP(GraphicsStateGuardian)::
~CLP(GraphicsStateGuardian)() {
  if (GLCAT.is_debug()) {
    GLCAT.debug()
      << "GLGraphicsStateGuardian " << this << " destructing\n";
  }

  close_gsg();
}

/**
 * This is called by the GL if an error occurs, if gl_debug has been enabled
 * (and the driver supports the GL_ARB_debug_output extension).
 */
void CLP(GraphicsStateGuardian)::
debug_callback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *message, GLvoid *userParam) {
  // Determine how to map the severity level.
  NotifySeverity level;
  switch (severity) {
  case GL_DEBUG_SEVERITY_HIGH:
    level = NS_error;
    break;

  case GL_DEBUG_SEVERITY_MEDIUM:
    if (type == GL_DEBUG_TYPE_PERFORMANCE) {
      // Performance warnings should really be "info".
      level = NS_info;
    } else {
      level = NS_warning;
    }
    break;

  case GL_DEBUG_SEVERITY_LOW:
    level = NS_info;
    break;

  case GL_DEBUG_SEVERITY_NOTIFICATION:
    level = NS_debug;
    break;

  default:
    level = NS_fatal; //???
    break;
  }

  string msg_str(message, length);
  GLCAT.out(level) << msg_str << "\n";

#ifndef NDEBUG
  if (level >= gl_debug_abort_level.get_value()) {
    abort();
  }
#endif
}

/**
 * Resets all internal state as if the gsg were newly created.
 */
void CLP(GraphicsStateGuardian)::
reset() {
  _last_error_check = -1.0;
  _white_texture = 0;

  free_pointers();
  GraphicsStateGuardian::reset();

  // Build _inv_state_mask as a mask of 1's where we don't care, and 0's where
  // we do care, about the state.
#ifndef OPENGLES_1
  _inv_state_mask.clear_bit(ShaderAttrib::get_class_slot());
#endif
  _inv_state_mask.clear_bit(AntialiasAttrib::get_class_slot());
  _inv_state_mask.clear_bit(ClipPlaneAttrib::get_class_slot());
  _inv_state_mask.clear_bit(ColorAttrib::get_class_slot());
  _inv_state_mask.clear_bit(ColorScaleAttrib::get_class_slot());
  _inv_state_mask.clear_bit(CullFaceAttrib::get_class_slot());
  _inv_state_mask.clear_bit(DepthBiasAttrib::get_class_slot());
  _inv_state_mask.clear_bit(DepthOffsetAttrib::get_class_slot());
  _inv_state_mask.clear_bit(DepthTestAttrib::get_class_slot());
  _inv_state_mask.clear_bit(DepthWriteAttrib::get_class_slot());
  _inv_state_mask.clear_bit(RenderModeAttrib::get_class_slot());
  _inv_state_mask.clear_bit(TransparencyAttrib::get_class_slot());
  _inv_state_mask.clear_bit(ColorWriteAttrib::get_class_slot());
  _inv_state_mask.clear_bit(ColorBlendAttrib::get_class_slot());
#if !defined(OPENGLES) || defined(OPENGLES_1)
  _inv_state_mask.clear_bit(LogicOpAttrib::get_class_slot());
#endif
  _inv_state_mask.clear_bit(TextureAttrib::get_class_slot());
  _inv_state_mask.clear_bit(TexMatrixAttrib::get_class_slot());
  _inv_state_mask.clear_bit(StencilAttrib::get_class_slot());
  _inv_state_mask.clear_bit(ScissorAttrib::get_class_slot());

  // We only care about this state if we are using the fixed-function pipeline.
#ifdef SUPPORT_FIXED_FUNCTION
  if (has_fixed_function_pipeline()) {
    _inv_state_mask.clear_bit(AlphaTestAttrib::get_class_slot());
    _inv_state_mask.clear_bit(RescaleNormalAttrib::get_class_slot());
    _inv_state_mask.clear_bit(ShadeModelAttrib::get_class_slot());
    _inv_state_mask.clear_bit(TexGenAttrib::get_class_slot());
    _inv_state_mask.clear_bit(MaterialAttrib::get_class_slot());
    _inv_state_mask.clear_bit(LightAttrib::get_class_slot());
    _inv_state_mask.clear_bit(FogAttrib::get_class_slot());
  }
#endif

  // Output the vendor and version strings.
  query_gl_version();

  if (_gl_version_major == 0) {
    // Couldn't get GL.  Fail.
    mark_new();
    return;
  }

  // Save the extensions tokens.
  _extensions.clear();

  // In OpenGL (ES) 3.0 and later, glGetString(GL_EXTENSIONS) is deprecated.
#ifndef OPENGLES_1
  if (_gl_version_major >= 3) {
    PFNGLGETSTRINGIPROC _glGetStringi =
      (PFNGLGETSTRINGIPROC)get_extension_func("glGetStringi");

    if (_glGetStringi != nullptr) {
      GLint n = 0;
      glGetIntegerv(GL_NUM_EXTENSIONS, &n);
      for (GLint i = 0; i < n; ++i) {
        const char *extension = (const char *)_glGetStringi(GL_EXTENSIONS, i);
        _extensions.insert(string(extension));
      }
    } else {
      GLCAT.error() << "glGetStringi is not available!\n";
      save_extensions((const char *)glGetString(GL_EXTENSIONS));
    }
  } else
#endif
  {
    save_extensions((const char *)glGetString(GL_EXTENSIONS));
  }

  get_extra_extensions();

  // This needs access to the extensions, so put this after save_extensions.
  query_glsl_version();

#ifndef OPENGLES
  // Determine whether this OpenGL context has compatibility features.
  bool core_profile = false;

  if (_gl_version_major >= 3) {
    if (_gl_version_major > 3 || _gl_version_minor >= 2) {
      // OpenGL 3.2 has a built-in way to check this.
      GLint profile_mask = 0;
      glGetIntegerv(GL_CONTEXT_PROFILE_MASK, &profile_mask);

      if (profile_mask & GL_CONTEXT_CORE_PROFILE_BIT) {
        core_profile = true;
      } else if (profile_mask & GL_CONTEXT_COMPATIBILITY_PROFILE_BIT) {
        core_profile = false;
      } else {
        core_profile = !has_extension("GL_ARB_compatibility");
      }
    } else {
      // OpenGL 3.0/3.1.
      GLint flags = 0;
      glGetIntegerv(GL_CONTEXT_FLAGS, &flags);
      if (flags & GL_CONTEXT_FLAG_FORWARD_COMPATIBLE_BIT) {
        core_profile = true;
      }
    }
  }

  if (GLCAT.is_debug()) {
    if (core_profile) {
      GLCAT.debug() << "Using core profile\n";
    } else {
      GLCAT.debug() << "Using compatibility profile\n";
    }
  }
  _core_profile = core_profile;
#elif defined(OPENGLES_1)
  static const bool core_profile = false;
#else
  static const bool core_profile = true;
#endif

  // Print out a list of all extensions.
  report_extensions();

  // Check if we are running under a profiling tool such as apitrace.
#if !defined(NDEBUG) && !defined(OPENGLES_1)
  if (has_extension("GL_EXT_debug_marker")) {
    _glPushGroupMarker = (PFNGLPUSHGROUPMARKEREXTPROC)
      get_extension_func("glPushGroupMarkerEXT");
    _glPopGroupMarker = (PFNGLPOPGROUPMARKEREXTPROC)
      get_extension_func("glPopGroupMarkerEXT");

    // Start a group right away.
    push_group_marker("reset");
  } else {
    _glPushGroupMarker = nullptr;
    _glPopGroupMarker = nullptr;
  }
#endif

  // Initialize OpenGL debugging output first, if enabled and supported.
  _supports_debug = false;
  _use_object_labels = false;
  if (gl_debug) {
    PFNGLDEBUGMESSAGECALLBACKPROC_P _glDebugMessageCallback;
    PFNGLDEBUGMESSAGECONTROLPROC _glDebugMessageControl;

    if (is_at_least_gl_version(4, 3) || has_extension("GL_KHR_debug")) {
#ifdef OPENGLES
      _glDebugMessageCallback = (PFNGLDEBUGMESSAGECALLBACKPROC_P)
        get_extension_func("glDebugMessageCallbackKHR");
      _glDebugMessageControl = (PFNGLDEBUGMESSAGECONTROLPROC)
        get_extension_func("glDebugMessageControlKHR");
      _glObjectLabel = (PFNGLOBJECTLABELPROC)
        get_extension_func("glObjectLabelKHR");
#else
      _glDebugMessageCallback = (PFNGLDEBUGMESSAGECALLBACKPROC_P)
        get_extension_func("glDebugMessageCallback");
      _glDebugMessageControl = (PFNGLDEBUGMESSAGECONTROLPROC)
        get_extension_func("glDebugMessageControl");
      _glObjectLabel = (PFNGLOBJECTLABELPROC)
        get_extension_func("glObjectLabel");
#endif
      glEnable(GL_DEBUG_OUTPUT); // Not supported in ARB version
      _supports_debug = true;
      _use_object_labels = gl_debug_object_labels;

#ifndef OPENGLES
    } else if (has_extension("GL_ARB_debug_output")) {
      _glDebugMessageCallback = (PFNGLDEBUGMESSAGECALLBACKPROC_P)
        get_extension_func("glDebugMessageCallbackARB");
      _glDebugMessageControl = (PFNGLDEBUGMESSAGECONTROLPROC)
        get_extension_func("glDebugMessageControlARB");
      _supports_debug = true;
#endif
    } else {
      _supports_debug = false;
    }

    if (_supports_debug) {
      // Set the categories we want to listen to.
      _glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DEBUG_SEVERITY_HIGH,
                             0, nullptr, GLCAT.is_error());
      _glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DEBUG_SEVERITY_MEDIUM,
                             0, nullptr, GLCAT.is_warning());
      _glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DEBUG_SEVERITY_LOW,
                             0, nullptr, GLCAT.is_info());
      _glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DEBUG_SEVERITY_NOTIFICATION,
                             0, nullptr, GLCAT.is_debug());

      // Enable the callback.
      _glDebugMessageCallback((GLDEBUGPROC_P) &debug_callback, (void*)this);
      if (gl_debug_synchronous) {
        glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
      }

      GLCAT.info() << "gl-debug enabled.\n";
    } else {
      GLCAT.warning() << "gl-debug enabled, but NOT supported.\n";
    }
  } else {
    // However, still check if it is supported.
    _supports_debug = is_at_least_gl_version(4, 3)
                   || has_extension("GL_KHR_debug")
                   || has_extension("GL_ARB_debug_output");

    if (GLCAT.is_debug()) {
      if (_supports_debug) {
        GLCAT.debug() << "gl-debug supported, but NOT enabled.\n";
      } else {
        GLCAT.debug() << "gl-debug disabled and unsupported.\n";
      }
    }
  }

  _supported_geom_rendering =
#ifndef OPENGLES
    Geom::GR_render_mode_point |
#endif
    Geom::GR_indexed_point |
    Geom::GR_point | Geom::GR_point_uniform_size |
    Geom::GR_indexed_other |
    Geom::GR_triangle_strip | Geom::GR_triangle_fan |
    Geom::GR_line_strip |
    Geom::GR_flat_last_vertex;

#ifndef OPENGLES
  // Observed bug on PINEBOOK Pro: regular OpenGL 3.3 driver doesn't support
  // wireframe mode, it just renders the solid mesh
  // GL_RENDERER = Mali-T860 (Panfrost)
  // GL_VERSION = 3.3 (Compatibility Profile) Mesa 22.3.6
  if (_gl_vendor != "Panfrost") {
    _supported_geom_rendering |= Geom::GR_render_mode_wireframe;
  }
#endif

  _supports_point_parameters = false;

#ifdef OPENGLES_1
  _glPointParameterfv = glPointParameterfv;
#elif defined(OPENGLES)
  // Other OpenGL ES versions don't support point parameters.
#else
  if (is_at_least_gl_version(1, 4)) {
    _supports_point_parameters = true;
    _glPointParameterfv = (PFNGLPOINTPARAMETERFVPROC)
      get_extension_func("glPointParameterfv");

  } else if (has_extension("GL_ARB_point_parameters")) {
    _supports_point_parameters = true;
    _glPointParameterfv = (PFNGLPOINTPARAMETERFVPROC)
      get_extension_func("glPointParameterfvARB");
  }
  if (_supports_point_parameters) {
    if (_glPointParameterfv == nullptr) {
      GLCAT.warning()
        << "glPointParameterfv advertised as supported by OpenGL runtime, but could not get pointers to extension functions.\n";
      _supports_point_parameters = false;
    }
  }
  if (_supports_point_parameters) {
    _supported_geom_rendering |= Geom::GR_point_perspective | Geom::GR_point_scale;
  } else {
    _glPointParameterfv = null_glPointParameterfv;
  }
#endif  // !OPENGLES_2

#if defined(OPENGLES_2)
  // OpenGL ES 2 doesn't have point sprites.
  _supports_point_sprite = false;
#elif defined(OPENGLES_1)
  _supports_point_sprite = has_extension("GL_OES_point_sprite");
#else
  _supports_point_sprite = is_at_least_gl_version(2, 0) ||
                           has_extension("GL_ARB_point_sprite");
#endif

  if (_supports_point_sprite) {
    // It appears that the point_sprite extension doesn't support texture
    // transforms on the generated texture coordinates.  How inconsistent.
    // Because of this, we don't advertise GR_point_sprite_tex_matrix.
    _supported_geom_rendering |= Geom::GR_point_sprite;
  }

  // Determine whether we support wide lines (and how wide they can be).
  {
    GLfloat aliased_range[2] = {1.0f, 1.0f};
    glGetFloatv(GL_ALIASED_LINE_WIDTH_RANGE, aliased_range);

    _max_line_width = aliased_range[1];

#ifdef SUPPORT_FIXED_FUNCTION
    if (has_fixed_function_pipeline()) {
#ifndef OPENGLES
      GLfloat range[2] = {1.0f, 1.0f};
      glGetFloatv(GL_LINE_WIDTH_RANGE, range);
      _max_line_width = std::max(_max_line_width, range[1]);
#endif

      GLfloat smooth_range[2] = {1.0f, 1.0f};
      glGetFloatv(GL_SMOOTH_LINE_WIDTH_RANGE, smooth_range);
      _max_line_width = std::max(_max_line_width, smooth_range[1]);
    }
#endif
  }

#ifdef OPENGLES_1
  // OpenGL ES 1.0 does not support primitive restart indices.

#elif defined(OPENGLES)
  if (gl_support_primitive_restart_index && is_at_least_gles_version(3, 0)) {
    // In WebGL 2, primitive restart is always enabled.
#ifndef __EMSCRIPTEN__
    glEnable(GL_PRIMITIVE_RESTART_FIXED_INDEX);
#endif
    _supported_geom_rendering |= Geom::GR_strip_cut_index;
  }

#else
  _explicit_primitive_restart = false;
  _glPrimitiveRestartIndex = nullptr;

  if (gl_support_primitive_restart_index) {
    if ((is_at_least_gl_version(4, 3) || has_extension("GL_ARB_ES3_compatibility")) &&
        _gl_renderer.substr(0, 7) != "Gallium") {
      // As long as we enable this, OpenGL will always use the highest
      // possible index for a numeric type as strip cut index, which coincides
      // with our convention.  This saves us a call to glPrimitiveRestartIndex
      // ... of course, though, the Gallium driver bugs out here.  See also:
      // https://www.panda3d.org/forums/viewtopic.php?f=5&t=17512
      glEnable(GL_PRIMITIVE_RESTART_FIXED_INDEX);
      _supported_geom_rendering |= Geom::GR_strip_cut_index;

    } else if (is_at_least_gl_version(3, 1)) {
      // We have to use an explicit primitive restart enableindex.
      _explicit_primitive_restart = true;
      _supported_geom_rendering |= Geom::GR_strip_cut_index;

      _glPrimitiveRestartIndex = (PFNGLPRIMITIVERESTARTINDEXPROC)
        get_extension_func("glPrimitiveRestartIndex");

    }
  }
#endif

#if !defined(OPENGLES) && defined(SUPPORT_FIXED_FUNCTION)
  if (has_fixed_function_pipeline() && is_at_least_gl_version(1, 4)) {
    _glSecondaryColorPointer = (PFNGLSECONDARYCOLORPOINTERPROC)
      get_extension_func("glSecondaryColorPointer");

  } else if (has_extension("GL_EXT_secondary_color")) {
    _glSecondaryColorPointer = (PFNGLSECONDARYCOLORPOINTERPROC)
      get_extension_func("glSecondaryColorPointerEXT");
  }
#endif

#ifndef OPENGLES_1
  _glDrawRangeElements = null_glDrawRangeElements;

  // Temporarily disabled in WebGL due to Firefox bug
#ifndef __EMSCRIPTEN__
#ifdef OPENGLES
  if (is_at_least_gles_version(3, 0)) {
    _glDrawRangeElements = (PFNGLDRAWRANGEELEMENTSPROC)
      get_extension_func("glDrawRangeElements");
  }
#else
  if (is_at_least_gl_version(1, 2)) {
    _glDrawRangeElements = (PFNGLDRAWRANGEELEMENTSPROC)
      get_extension_func("glDrawRangeElements");

  } else if (has_extension("GL_EXT_draw_range_elements")) {
    _glDrawRangeElements = (PFNGLDRAWRANGEELEMENTSPROC)
      get_extension_func("glDrawRangeElementsEXT");
  }
#endif
  if (_glDrawRangeElements == nullptr) {
    GLCAT.warning()
      << "glDrawRangeElements advertised as supported by OpenGL runtime, but could not get pointers to extension functions.\n";
    _glDrawRangeElements = null_glDrawRangeElements;
  }
#endif  // !__EMSCRIPTEN__
#endif  // !OPENGLES_1

  _supports_3d_texture = false;

#ifndef OPENGLES_1
  if (is_at_least_gl_version(1, 2) || is_at_least_gles_version(3, 0)) {
    _supports_3d_texture = true;

    _glTexImage3D = (PFNGLTEXIMAGE3DPROC_P)
      get_extension_func("glTexImage3D");
    _glTexSubImage3D = (PFNGLTEXSUBIMAGE3DPROC)
      get_extension_func("glTexSubImage3D");
    _glCopyTexSubImage3D = (PFNGLCOPYTEXSUBIMAGE3DPROC)
      get_extension_func("glCopyTexSubImage3D");

#ifndef OPENGLES
  } else if (has_extension("GL_EXT_texture3D")) {
    _supports_3d_texture = true;

    _glTexImage3D = (PFNGLTEXIMAGE3DPROC_P)
      get_extension_func("glTexImage3DEXT");
    _glTexSubImage3D = (PFNGLTEXSUBIMAGE3DPROC)
      get_extension_func("glTexSubImage3DEXT");

    _glCopyTexSubImage3D = nullptr;
    if (has_extension("GL_EXT_copy_texture")) {
      _glCopyTexSubImage3D = (PFNGLCOPYTEXSUBIMAGE3DPROC)
        get_extension_func("glCopyTexSubImage3DEXT");
    }
#else
  } else if (has_extension("GL_OES_texture_3D")) {
    _supports_3d_texture = true;

    _glTexImage3D = (PFNGLTEXIMAGE3DPROC_P)
      get_extension_func("glTexImage3DOES");
    _glTexSubImage3D = (PFNGLTEXSUBIMAGE3DPROC)
      get_extension_func("glTexSubImage3DOES");
    _glCopyTexSubImage3D = (PFNGLCOPYTEXSUBIMAGE3DPROC)
      get_extension_func("glCopyTexSubImage3DOES");
    _glFramebufferTexture3D = (PFNGLFRAMEBUFFERTEXTURE3DOES)
      get_extension_func("glFramebufferTexture3DOES");
#endif
  }

  if (_supports_3d_texture) {
    if (_glTexImage3D == nullptr || _glTexSubImage3D == nullptr) {
      GLCAT.warning()
        << "3-D textures advertised as supported by OpenGL runtime, but could not get pointers to extension functions.\n";
      _supports_3d_texture = false;
    }
  }
#endif  // !OPENGLES_1

  _supports_tex_storage = false;

#ifdef OPENGLES
  if (is_at_least_gles_version(3, 0)) {
#else
  if (is_at_least_gl_version(4, 2) || has_extension("GL_ARB_texture_storage")) {
#endif
    _supports_tex_storage = true;

#ifndef OPENGLES
    _glTexStorage1D = (PFNGLTEXSTORAGE1DPROC)
      get_extension_func("glTexStorage1D");
#endif
    _glTexStorage2D = (PFNGLTEXSTORAGE2DPROC)
      get_extension_func("glTexStorage2D");
    _glTexStorage3D = (PFNGLTEXSTORAGE3DPROC)
      get_extension_func("glTexStorage3D");
  }
#ifdef OPENGLES
  else if (has_extension("GL_EXT_texture_storage")) {
    _supports_tex_storage = true;

    _glTexStorage2D = (PFNGLTEXSTORAGE2DPROC)
      get_extension_func("glTexStorage2DEXT");
    _glTexStorage3D = (PFNGLTEXSTORAGE3DPROC)
      get_extension_func("glTexStorage3DEXT");
  }
#endif

  if (_supports_tex_storage) {
    if (
#ifndef OPENGLES
        _glTexStorage1D == nullptr ||
#endif
        _glTexStorage2D == nullptr || _glTexStorage3D == nullptr) {
      GLCAT.warning()
        << "Immutable texture storage advertised as supported by OpenGL runtime, but could not get pointers to extension functions.\n";
      _supports_tex_storage = false;
    }
  }

  _supports_clear_texture = false;
#ifndef OPENGLES
  if (is_at_least_gl_version(4, 4) || has_extension("GL_ARB_clear_texture")) {
    _glClearTexImage = (PFNGLCLEARTEXIMAGEPROC)
      get_extension_func("glClearTexImage");
    _glClearTexSubImage = (PFNGLCLEARTEXSUBIMAGEPROC)
      get_extension_func("glClearTexSubImage");

    if (_glClearTexImage == nullptr || _glClearTexSubImage == nullptr) {
      GLCAT.warning()
        << "GL_ARB_clear_texture advertised as supported by OpenGL runtime, but could not get pointers to extension functions.\n";
    } else {
      _supports_clear_texture = true;
    }
  }
#elif !defined(OPENGLES_1)
  if (has_extension("GL_EXT_clear_texture")) {
    _glClearTexImage = (PFNGLCLEARTEXIMAGEEXTPROC)
      get_extension_func("glClearTexImageEXT");
    _glClearTexSubImage = (PFNGLCLEARTEXSUBIMAGEEXTPROC)
      get_extension_func("glClearTexSubImageEXT");

    if (_glClearTexImage == nullptr || _glClearTexSubImage == nullptr) {
      GLCAT.warning()
        << "GL_EXT_clear_texture advertised as supported by OpenGL runtime, but could not get pointers to extension functions.\n";
    } else {
      _supports_clear_texture = true;
    }
  }
#endif

  _supports_clear_buffer = false;
#ifndef OPENGLES
  if (is_at_least_gl_version(4, 3) || has_extension("GL_ARB_clear_buffer_object")) {
    _glClearBufferData = (PFNGLCLEARBUFFERDATAPROC)
      get_extension_func("glClearBufferData");

    if (_glClearBufferData == nullptr) {
      GLCAT.warning()
        << "GL_ARB_clear_buffer_object advertised as supported by OpenGL runtime, but could not get pointers to extension function.\n";
    } else {
      _supports_clear_buffer = true;
    }
  }
#endif

  _supports_2d_texture_array = false;
#ifndef OPENGLES_1
  if (_gl_version_major >= 3) {
    _supports_2d_texture_array = true;

    _glFramebufferTextureLayer = (PFNGLFRAMEBUFFERTEXTURELAYERPROC)
      get_extension_func("glFramebufferTextureLayer");

#ifndef OPENGLES
  } else if (has_extension("GL_EXT_texture_array")) {
    _supports_2d_texture_array = true;

    _glFramebufferTextureLayer = (PFNGLFRAMEBUFFERTEXTURELAYERPROC)
      get_extension_func("glFramebufferTextureLayerEXT");
#endif
  }

  if (_supports_2d_texture_array && _glFramebufferTextureLayer == nullptr) {
    GLCAT.warning()
      << "Texture arrays advertised as supported by OpenGL runtime, but could not get pointer to glFramebufferTextureLayer function.\n";
  }
#endif  // !OPENGLES_1

#ifdef OPENGLES_2
  _supports_cube_map = true;
#else
  _supports_cube_map =
    is_at_least_gl_version(1, 3) ||
    has_extension("GL_ARB_texture_cube_map") ||
    has_extension("GL_OES_texture_cube_map");
#endif

#ifndef OPENGLES
  if (_supports_cube_map && gl_cube_map_seamless) {
    if (is_at_least_gl_version(3, 2) || has_extension("GL_ARB_seamless_cube_map")) {
      glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS);
    }
  }
#endif

#ifndef OPENGLES
  _supports_cube_map_array = is_at_least_gl_version(4, 0) ||
                             has_extension("GL_ARB_texture_cube_map_array");
#endif

#ifndef OPENGLES
  if (is_at_least_gl_version(3, 1)) {
    _glTexBuffer = (PFNGLTEXBUFFERPROC)get_extension_func("glTexBuffer");
    _supports_buffer_texture = true;

  } else if (has_extension("GL_ARB_texture_buffer_object")) {
    _glTexBuffer = (PFNGLTEXBUFFERPROC)get_extension_func("glTexBufferARB");
    _supports_buffer_texture = true;
  }
#elif !defined(OPENGLES_1)
  if (is_at_least_gles_version(3, 2)) {
    _glTexBuffer = (PFNGLTEXBUFFERPROC)get_extension_func("glTexBuffer");
    _supports_buffer_texture = (_glTexBuffer != nullptr);
  }
  else if (has_extension("GL_OES_texture_buffer")) {
    _glTexBuffer = (PFNGLTEXBUFFERPROC)get_extension_func("glTexBufferOES");
    _supports_buffer_texture = (_glTexBuffer != nullptr);
  }
#endif

#ifdef OPENGLES
  _supports_texture_srgb =
    is_at_least_gles_version(3, 0) || has_extension("GL_EXT_sRGB");
#else
  _supports_texture_srgb =
    is_at_least_gl_version(2, 1) || has_extension("GL_EXT_texture_sRGB");
#endif

#ifdef OPENGLES
  _supports_compressed_texture = true;

  // Supported in the core.  1D textures are not supported by OpenGL ES.
  _glCompressedTexImage1D = nullptr;
  _glCompressedTexImage2D = glCompressedTexImage2D;
  _glCompressedTexSubImage1D = nullptr;
  _glCompressedTexSubImage2D = glCompressedTexSubImage2D;
  _glGetCompressedTexImage = nullptr;

  _glCompressedTexImage3D = nullptr;
  _glCompressedTexSubImage3D = nullptr;
#ifdef OPENGLES_2
  if (_supports_3d_texture) {
    _glCompressedTexImage3D = (PFNGLCOMPRESSEDTEXIMAGE3DPROC)
      get_extension_func("glCompressedTexImage3DOES");
    _glCompressedTexSubImage3D = (PFNGLCOMPRESSEDTEXSUBIMAGE3DPROC)
      get_extension_func("glCompressedTexSubImageOES");
  }
#endif

#else
  if (is_at_least_gl_version(1, 3)) {
    _supports_compressed_texture = true;

    _glCompressedTexImage1D = (PFNGLCOMPRESSEDTEXIMAGE1DPROC)
      get_extension_func("glCompressedTexImage1D");
    _glCompressedTexImage2D = (PFNGLCOMPRESSEDTEXIMAGE2DPROC)
      get_extension_func("glCompressedTexImage2D");
    _glCompressedTexImage3D = (PFNGLCOMPRESSEDTEXIMAGE3DPROC)
      get_extension_func("glCompressedTexImage3D");
    _glCompressedTexSubImage1D = (PFNGLCOMPRESSEDTEXSUBIMAGE1DPROC)
      get_extension_func("glCompressedTexSubImage1D");
    _glCompressedTexSubImage2D = (PFNGLCOMPRESSEDTEXSUBIMAGE2DPROC)
      get_extension_func("glCompressedTexSubImage2D");
    _glCompressedTexSubImage3D = (PFNGLCOMPRESSEDTEXSUBIMAGE3DPROC)
      get_extension_func("glCompressedTexSubImage3D");
    _glGetCompressedTexImage = (PFNGLGETCOMPRESSEDTEXIMAGEPROC)
      get_extension_func("glGetCompressedTexImage");

  } else if (has_extension("GL_ARB_texture_compression")) {
    _supports_compressed_texture = true;

    _glCompressedTexImage1D = (PFNGLCOMPRESSEDTEXIMAGE1DPROC)
      get_extension_func("glCompressedTexImage1DARB");
    _glCompressedTexImage2D = (PFNGLCOMPRESSEDTEXIMAGE2DPROC)
      get_extension_func("glCompressedTexImage2DARB");
    _glCompressedTexImage3D = (PFNGLCOMPRESSEDTEXIMAGE3DPROC)
      get_extension_func("glCompressedTexImage3DARB");
    _glCompressedTexSubImage1D = (PFNGLCOMPRESSEDTEXSUBIMAGE1DPROC)
      get_extension_func("glCompressedTexSubImage1DARB");
    _glCompressedTexSubImage2D = (PFNGLCOMPRESSEDTEXSUBIMAGE2DPROC)
      get_extension_func("glCompressedTexSubImage2DARB");
    _glCompressedTexSubImage3D = (PFNGLCOMPRESSEDTEXSUBIMAGE3DPROC)
      get_extension_func("glCompressedTexSubImage3DARB");
    _glGetCompressedTexImage = (PFNGLGETCOMPRESSEDTEXIMAGEPROC)
      get_extension_func("glGetCompressedTexImageARB");

  } else {
    _supports_compressed_texture = false;
  }

  if (_supports_compressed_texture) {
    if (_glCompressedTexImage1D == nullptr ||
        _glCompressedTexImage2D == nullptr ||
        _glCompressedTexImage3D == nullptr ||
        _glCompressedTexSubImage1D == nullptr ||
        _glCompressedTexSubImage2D == nullptr ||
        _glCompressedTexSubImage3D == nullptr ||
        _glGetCompressedTexImage == nullptr) {
      GLCAT.warning()
        << "Compressed textures advertised as supported by OpenGL runtime, but could not get pointers to extension functions.\n";
      _supports_compressed_texture = false;
    }
  }
#endif

  if (_supports_compressed_texture) {
#ifndef OPENGLES
    _compressed_texture_formats.set_bit(Texture::CM_on);
#endif

    GLint num_compressed_formats = 0;
    glGetIntegerv(GL_NUM_COMPRESSED_TEXTURE_FORMATS, &num_compressed_formats);
    GLint *formats = (GLint *)alloca(num_compressed_formats * sizeof(GLint));
    glGetIntegerv(GL_COMPRESSED_TEXTURE_FORMATS, formats);
    for (int i = 0; i < num_compressed_formats; ++i) {
      switch (formats[i]) {
      case GL_COMPRESSED_RGB_S3TC_DXT1_EXT:
      case GL_COMPRESSED_RGBA_S3TC_DXT1_EXT:
        _compressed_texture_formats.set_bit(Texture::CM_dxt1);
        break;
#ifdef OPENGLES
      case GL_COMPRESSED_RGB_PVRTC_2BPPV1_IMG:
      case GL_COMPRESSED_RGBA_PVRTC_2BPPV1_IMG:
        _compressed_texture_formats.set_bit(Texture::CM_pvr1_2bpp);
        break;
      case GL_COMPRESSED_RGB_PVRTC_4BPPV1_IMG:
      case GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG:
        _compressed_texture_formats.set_bit(Texture::CM_pvr1_4bpp);
        break;
#else
      case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT:
        _compressed_texture_formats.set_bit(Texture::CM_dxt3);
        break;

      case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT:
        _compressed_texture_formats.set_bit(Texture::CM_dxt5);
        break;

      case GL_COMPRESSED_RGB_FXT1_3DFX:
      case GL_COMPRESSED_RGBA_FXT1_3DFX:
        _compressed_texture_formats.set_bit(Texture::CM_fxt1);
        break;
#endif

      case GL_COMPRESSED_R11_EAC:
      case GL_COMPRESSED_RG11_EAC:
        _compressed_texture_formats.set_bit(Texture::CM_eac);
        break;

      case GL_COMPRESSED_RGB8_ETC2:
      case GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2:
      case GL_COMPRESSED_RGBA8_ETC2_EAC:
        _compressed_texture_formats.set_bit(Texture::CM_etc1);
        _compressed_texture_formats.set_bit(Texture::CM_etc2);
        break;

#ifdef OPENGLES
      case GL_ETC1_RGB8_OES:
        _compressed_texture_formats.set_bit(Texture::CM_etc1);
        break;
#endif

      default:
        break;
      }
    }

#ifndef OPENGLES
    // The OpenGL spec states that these are not reported by the above
    // mechanism, so we have to check for the extension ourselves.
    if (is_at_least_gl_version(3, 0) ||
        has_extension("GL_ARB_texture_compression_rgtc") ||
        has_extension("GL_EXT_texture_compression_rgtc")) {
      _compressed_texture_formats.set_bit(Texture::CM_rgtc);
    }
#endif
  }

#ifdef OPENGLES
  // Note that these extensions only offer support for GL_BGRA, not GL_BGR.
  _supports_bgr = has_extension("GL_EXT_texture_format_BGRA8888") ||
                  has_extension("GL_APPLE_texture_format_BGRA8888");
  _supports_bgra_read = has_extension("GL_EXT_read_format_bgra");
#else
  // In regular OpenGL, we have both GL_BGRA and GL_BGR.
  _supports_bgr =
    is_at_least_gl_version(1, 2) || has_extension("GL_EXT_bgra");
  _supports_bgra_read = _supports_bgr;
#endif

#ifdef SUPPORT_FIXED_FUNCTION
#ifdef OPENGLES_1
  _supports_rescale_normal = true;
#else
  _supports_rescale_normal =
    has_fixed_function_pipeline() && gl_support_rescale_normal &&
    (is_at_least_gl_version(1, 2) || has_extension("GL_EXT_rescale_normal"));
#endif

#ifndef OPENGLES
  _use_separate_specular_color =
    has_fixed_function_pipeline() && gl_separate_specular_color &&
    (is_at_least_gl_version(1, 2) || has_extension("GL_EXT_separate_specular_color"));
#endif
#endif  // SUPPORT_FIXED_FUNCTION

#ifdef OPENGLES
  _supports_packed_dabc = false;
  _supports_packed_ufloat = false;
#else
  _supports_packed_dabc = (is_at_least_gl_version(3, 2) ||
                          has_extension("GL_ARB_vertex_array_bgra") ||
                          has_extension("GL_EXT_vertex_array_bgra")) && gl_support_vertex_array_bgra;

  _supports_packed_ufloat = is_at_least_gl_version(4, 4) ||
                            has_extension("GL_ARB_vertex_type_10f_11f_11f_rev");

  if (_supports_packed_dabc) {
    int number = 0;
    if (_gl_renderer.compare(0, 14, "AMD Radeon RX ") == 0) {
      number = atoi(_gl_renderer.c_str() + 14);
    }
    else if (_gl_renderer.compare(0, 10, "Radeon RX ") == 0) {
      number = atoi(_gl_renderer.c_str() + 10);
    }

    // This is buggy for RDNA cards.  Verified on 5700 XT, reportedly also
    // occurs on the entire 5*00 and 6*00 line.
    if (number >= 5000 && number < 8000) {
      _supports_packed_dabc = false;

      if (GLCAT.is_debug()) {
        GLCAT.debug()
          << "Detected AMD Radeon RX " << number
          << ", disabling use of GL_BGRA vertex attributes\n";
      }
    }
  }
#endif

#ifdef OPENGLES
  //TODO
  _supports_multisample = false;
#else
  _supports_multisample =
    is_at_least_gl_version(1, 3) || has_extension("GL_ARB_multisample");
#endif

#ifdef OPENGLES_1
  _supports_generate_mipmap = is_at_least_gles_version(1, 1);
#elif defined(OPENGLES)
  _supports_generate_mipmap = true;
#else
  _supports_generate_mipmap = is_at_least_gl_version(1, 4) ||
                              has_extension("GL_SGIS_generate_mipmap");
#endif

#ifdef OPENGLES_1
  _supports_tex_non_pow2 = false;
#elif defined(OPENGLES)
  _supports_tex_non_pow2 = is_at_least_gles_version(3, 0) ||
    has_extension("GL_OES_texture_npot");
#else
  _supports_tex_non_pow2 = is_at_least_gl_version(2, 0) ||
    has_extension("GL_ARB_texture_non_power_of_two");
#endif

#ifndef OPENGLES_2
  bool supports_multitexture = false;
  if (is_at_least_gl_version(1, 3) || is_at_least_gles_version(1, 1)) {
    supports_multitexture = true;

    _glActiveTexture = (PFNGLACTIVETEXTUREPROC)
      get_extension_func("glActiveTexture");

#ifdef SUPPORT_FIXED_FUNCTION
    if (has_fixed_function_pipeline()) {
      _glClientActiveTexture = (PFNGLACTIVETEXTUREPROC)
        get_extension_func("glClientActiveTexture");
    }
#endif

#ifdef SUPPORT_IMMEDIATE_MODE
    _glMultiTexCoord1f = (PFNGLMULTITEXCOORD1FPROC)
      get_extension_func("glMultiTexCoord1f");
    _glMultiTexCoord2f = (PFNGLMULTITEXCOORD2FPROC)
      get_extension_func("glMultiTexCoord2f");
    _glMultiTexCoord3f = (PFNGLMULTITEXCOORD3FPROC)
      get_extension_func("glMultiTexCoord3f");
    _glMultiTexCoord4f = (PFNGLMULTITEXCOORD4FPROC)
      get_extension_func("glMultiTexCoord4f");
    _glMultiTexCoord1d = (PFNGLMULTITEXCOORD1DPROC)
      get_extension_func("glMultiTexCoord1d");
    _glMultiTexCoord2d = (PFNGLMULTITEXCOORD2DPROC)
      get_extension_func("glMultiTexCoord2d");
    _glMultiTexCoord3d = (PFNGLMULTITEXCOORD3DPROC)
      get_extension_func("glMultiTexCoord3d");
    _glMultiTexCoord4d = (PFNGLMULTITEXCOORD4DPROC)
      get_extension_func("glMultiTexCoord4d");
#endif

  } else if (has_extension("GL_ARB_multitexture")) {
    supports_multitexture = true;

    _glActiveTexture = (PFNGLACTIVETEXTUREPROC)
      get_extension_func("glActiveTextureARB");

#ifdef SUPPORT_FIXED_FUNCTION
    if (has_fixed_function_pipeline()) {
      _glClientActiveTexture = (PFNGLACTIVETEXTUREPROC)
        get_extension_func("glClientActiveTextureARB");
    }
#endif

#ifdef SUPPORT_IMMEDIATE_MODE
    _glMultiTexCoord1f = (PFNGLMULTITEXCOORD1FPROC)
      get_extension_func("glMultiTexCoord1fARB");
    _glMultiTexCoord2f = (PFNGLMULTITEXCOORD2FPROC)
      get_extension_func("glMultiTexCoord2fARB");
    _glMultiTexCoord3f = (PFNGLMULTITEXCOORD3FPROC)
      get_extension_func("glMultiTexCoord3fARB");
    _glMultiTexCoord4f = (PFNGLMULTITEXCOORD4FPROC)
      get_extension_func("glMultiTexCoord4fARB");
    _glMultiTexCoord1d = (PFNGLMULTITEXCOORD1DPROC)
      get_extension_func("glMultiTexCoord1dARB");
    _glMultiTexCoord2d = (PFNGLMULTITEXCOORD2DPROC)
      get_extension_func("glMultiTexCoord2dARB");
    _glMultiTexCoord3d = (PFNGLMULTITEXCOORD3DPROC)
      get_extension_func("glMultiTexCoord3dARB");
    _glMultiTexCoord4d = (PFNGLMULTITEXCOORD4DPROC)
      get_extension_func("glMultiTexCoord4dARB");
#endif
  } else {
    supports_multitexture = false;
  }

  if (supports_multitexture) {
    if (_glActiveTexture == nullptr
#ifdef SUPPORT_FIXED_FUNCTION
        || (has_fixed_function_pipeline() && _glClientActiveTexture == nullptr)
#endif
#ifdef SUPPORT_IMMEDIATE_MODE
        || GLf(_glMultiTexCoord1) == nullptr || GLf(_glMultiTexCoord2) == nullptr
        || GLf(_glMultiTexCoord3) == nullptr || GLf(_glMultiTexCoord4) == nullptr
#endif
        ) {
      GLCAT.warning()
        << "Multitexture advertised as supported by OpenGL runtime, but could not get pointers to extension functions.\n";
      supports_multitexture = false;
    }
  }

  if (!supports_multitexture) {
    // Replace with dummy no-op functions.
    _glActiveTexture = null_glActiveTexture;
  }

#ifdef SUPPORT_FIXED_FUNCTION
  if (!supports_multitexture || !has_fixed_function_pipeline()) {
    _glClientActiveTexture = null_glActiveTexture;
  }
#endif
#endif  // OPENGLES_2

#ifdef OPENGLES_1
  _supports_depth_texture = false;
  _supports_depth_stencil = has_extension("GL_OES_packed_depth_stencil");
  _supports_depth24 = has_extension("GL_OES_depth24");
  _supports_depth32 = has_extension("GL_OES_depth32");
  _supports_luminance_texture = true;

#elif defined(OPENGLES)
  if (is_at_least_gles_version(3, 0)) {
    _supports_depth_texture = true;
    _supports_depth_stencil = true;
    _supports_depth24 = true;
    _supports_depth32 = true;
  } else {
#ifdef __EMSCRIPTEN__
    if (has_extension("WEBGL_depth_texture") ||
        has_extension("GL_ANGLE_depth_texture")) {
#else
    if (has_extension("GL_ANGLE_depth_texture")) {
#endif
      // This extension provides both depth textures and depth-stencil support.
      _supports_depth_texture = true;
      _supports_depth_stencil = true;

    } else if (has_extension("GL_OES_depth_texture")) {
      _supports_depth_texture = true;
      _supports_depth_stencil = has_extension("GL_OES_packed_depth_stencil");
    }
    _supports_depth24 = has_extension("GL_OES_depth24");
    _supports_depth32 = has_extension("GL_OES_depth32");
  }
  _supports_luminance_texture = true;
#else
  _supports_depth_texture = (is_at_least_gl_version(1, 4) ||
                             has_extension("GL_ARB_depth_texture"));
  _supports_depth_stencil = (is_at_least_gl_version(3, 0) ||
                             has_extension("GL_ARB_framebuffer_object") ||
                             has_extension("GL_EXT_packed_depth_stencil"));

  // OpenGL 3 deprecates luminance, luminance-alpha and alpha textures.
  _supports_luminance_texture = !core_profile;
#endif

#ifdef OPENGLES_2
  if (gl_support_shadow_filter && _supports_depth_texture &&
      (is_at_least_gles_version(3, 0) || has_extension("GL_EXT_shadow_samplers"))) {
    _supports_shadow_filter = true;
  }
#else
  if (gl_support_shadow_filter &&
      _supports_depth_texture &&
      (is_at_least_gl_version(1, 4) || has_extension("GL_ARB_shadow")) &&
      (is_at_least_gl_version(2, 0) || has_extension("GL_ARB_fragment_program_shadow"))) {
    _supports_shadow_filter = true;
  }
#endif
  // Actually, we can't keep forever disabling ARB_shadow on ATI cards, since
  // they do work correctly now.  Maybe there is some feature level we can
  // check somewhere?
  /*if (_gl_vendor.substr(0,3)=="ATI") {
    // ATI drivers have never provided correct shadow support.
    _supports_shadow_filter = false;
  }*/

  _supports_texture_combine = false;
  _supports_texture_saved_result = false;
  _supports_texture_dot3 = false;
#ifdef SUPPORT_FIXED_FUNCTION
  if (has_fixed_function_pipeline()) {
    _supports_texture_combine =
      is_at_least_gl_version(1, 3) ||
      is_at_least_gles_version(1, 1) ||
      has_extension("GL_ARB_texture_env_combine");

#ifdef OPENGLES_1
    _supports_texture_saved_result =
      has_extension("GL_OES_texture_env_crossbar");
#else
    _supports_texture_saved_result =
      is_at_least_gl_version(1, 4) ||
      has_extension("GL_ARB_texture_env_crossbar");
#endif

    _supports_texture_dot3 =
      is_at_least_gl_version(1, 3) ||
      is_at_least_gles_version(1, 1) ||
      has_extension("GL_ARB_texture_env_dot3");
  }
#endif  // SUPPORT_FIXED_FUNCTION

#ifdef OPENGLES_2
  _supports_buffers = true;
  _glGenBuffers = glGenBuffers;
  _glBindBuffer = glBindBuffer;
  _glBufferData = glBufferData;
  _glBufferSubData = glBufferSubData;
  _glDeleteBuffers = glDeleteBuffers;

#else
  _supports_buffers = false;
  if (is_at_least_gl_version(1, 5) || is_at_least_gles_version(1, 1)) {
    _supports_buffers = true;

    _glGenBuffers = (PFNGLGENBUFFERSPROC)
      get_extension_func("glGenBuffers");
    _glBindBuffer = (PFNGLBINDBUFFERPROC)
      get_extension_func("glBindBuffer");
    _glBufferData = (PFNGLBUFFERDATAPROC)
      get_extension_func("glBufferData");
    _glBufferSubData = (PFNGLBUFFERSUBDATAPROC)
      get_extension_func("glBufferSubData");
    _glDeleteBuffers = (PFNGLDELETEBUFFERSPROC)
      get_extension_func("glDeleteBuffers");

#ifndef OPENGLES
    _glMapBuffer = (PFNGLMAPBUFFERPROC)
      get_extension_func("glMapBuffer");
    _glUnmapBuffer = (PFNGLUNMAPBUFFERPROC)
      get_extension_func("glUnmapBuffer");
    _glGetBufferSubData = (PFNGLGETBUFFERSUBDATAPROC)
      get_extension_func("glGetBufferSubData");
#endif
  }
#ifndef OPENGLES_1
  else if (has_extension("GL_ARB_vertex_buffer_object")) {
    _supports_buffers = true;

    _glGenBuffers = (PFNGLGENBUFFERSPROC)
      get_extension_func("glGenBuffersARB");
    _glBindBuffer = (PFNGLBINDBUFFERPROC)
      get_extension_func("glBindBufferARB");
    _glBufferData = (PFNGLBUFFERDATAPROC)
      get_extension_func("glBufferDataARB");
    _glBufferSubData = (PFNGLBUFFERSUBDATAPROC)
      get_extension_func("glBufferSubDataARB");
    _glDeleteBuffers = (PFNGLDELETEBUFFERSPROC)
      get_extension_func("glDeleteBuffersARB");
    _glMapBuffer = (PFNGLMAPBUFFERPROC)
      get_extension_func("glMapBufferARB");
    _glUnmapBuffer = (PFNGLUNMAPBUFFERPROC)
      get_extension_func("glUnmapBufferARB");
    _glGetBufferSubData = (PFNGLGETBUFFERSUBDATAPROC)
      get_extension_func("glGetBufferSubDataARB");
  }
#endif  // OPENGLES_1

  if (_supports_buffers) {
    if (_glGenBuffers == nullptr || _glBindBuffer == nullptr ||
        _glBufferData == nullptr || _glBufferSubData == nullptr ||
        _glDeleteBuffers == nullptr) {
      GLCAT.warning()
        << "Buffers advertised as supported by OpenGL runtime, but could not get pointers to extension functions.\n";
      _supports_buffers = false;
    }
  }
#endif

#ifdef OPENGLES
  if (is_at_least_gles_version(3, 0)) {
    _glMapBufferRange = (PFNGLMAPBUFFERRANGEEXTPROC)
      get_extension_func("glMapBufferRange");
    _glUnmapBuffer = (PFNGLUNMAPBUFFERPROC)
      get_extension_func("glUnmapBuffer");
  }
  else if (has_extension("GL_EXT_map_buffer_range")) {
    _glMapBufferRange = (PFNGLMAPBUFFERRANGEEXTPROC)
      get_extension_func("glMapBufferRangeEXT");
    _glUnmapBuffer = (PFNGLUNMAPBUFFERPROC)
      get_extension_func("glUnmapBufferOES");
  }
  else {
    _glMapBufferRange = nullptr;
    _glUnmapBuffer = nullptr;
  }
#else
  // Check for various advanced buffer management features.
  if (is_at_least_gl_version(3, 0) || has_extension("GL_ARB_map_buffer_range")) {
    _glMapBufferRange = (PFNGLMAPBUFFERRANGEPROC)
      get_extension_func("glMapBufferRange");
  } else {
    _glMapBufferRange = nullptr;
  }

  if (is_at_least_gl_version(4, 4) || has_extension("GL_ARB_buffer_storage")) {
    _glBufferStorage = (PFNGLBUFFERSTORAGEPROC)
      get_extension_func("glBufferStorage");

    if (_glBufferStorage != nullptr) {
      _supports_buffer_storage = true;
    } else {
      GLCAT.warning()
        << "Buffer storage advertised as supported by OpenGL runtime, but "
           "could not get pointers to extension function.\n";
    }
  } else {
    _supports_buffer_storage = false;
  }
#endif

  _supports_vao = false;

#ifdef OPENGLES
  if (is_at_least_gles_version(3, 0)) {
#else
  if (is_at_least_gl_version(3, 0) || has_extension("GL_ARB_vertex_array_object")) {
#endif
    _supports_vao = true;

    _glBindVertexArray = (PFNGLBINDVERTEXARRAYPROC)
      get_extension_func("glBindVertexArray");
    _glDeleteVertexArrays = (PFNGLDELETEVERTEXARRAYSPROC)
      get_extension_func("glDeleteVertexArrays");
    _glGenVertexArrays = (PFNGLGENVERTEXARRAYSPROC)
      get_extension_func("glGenVertexArrays");

#ifdef OPENGLES
  } else if (has_extension("GL_OES_vertex_array_object")) {
    _supports_vao = true;

    _glBindVertexArray = (PFNGLBINDVERTEXARRAYPROC)
      get_extension_func("glBindVertexArrayOES");
    _glDeleteVertexArrays = (PFNGLDELETEVERTEXARRAYSPROC)
      get_extension_func("glDeleteVertexArraysOES");
    _glGenVertexArrays = (PFNGLGENVERTEXARRAYSPROC)
      get_extension_func("glGenVertexArraysOES");
#endif
  }

  if (_supports_vao) {
    if (_glBindVertexArray == nullptr || _glDeleteVertexArrays == nullptr ||
        _glGenVertexArrays == nullptr) {
      GLCAT.warning()
        << "Vertex array objects advertised as supported by OpenGL runtime, but could not get pointers to extension functions.\n";
      _supports_vao = false;
    }
  }

  // Check for GLSL support.
#if defined(OPENGLES_1)
  _supports_glsl = false;
  _supports_geometry_shaders = false;
  _supports_tessellation_shaders = false;
#elif defined(OPENGLES)
  _supports_glsl = true;
  _supports_geometry_shaders = false;
  _supports_tessellation_shaders = false;
#else
  _supports_glsl = (_gl_shadlang_ver_major >= 1);
  _supports_tessellation_shaders = is_at_least_gl_version(4, 0) || has_extension("GL_ARB_tessellation_shader");

  if (is_at_least_gl_version(3, 2)) {
    _supports_geometry_shaders = true;
    _glFramebufferTexture = (PFNGLFRAMEBUFFERTEXTUREARBPROC)
      get_extension_func("glFramebufferTexture");

  } else if (has_extension("GL_ARB_geometry_shader4")) {
    _supports_geometry_shaders = true;
    _glFramebufferTexture = (PFNGLFRAMEBUFFERTEXTUREARBPROC)
      get_extension_func("glFramebufferTextureARB");
    _glProgramParameteri = (PFNGLPROGRAMPARAMETERIPROC)
      get_extension_func("glProgramParameteriARB");

  } else if (has_extension("GL_EXT_geometry_shader4")) {
    _supports_geometry_shaders = true;
    _glFramebufferTexture = nullptr;
    _glProgramParameteri = (PFNGLPROGRAMPARAMETERIPROC)
      get_extension_func("glProgramParameteriEXT");

  } else {
    _supports_geometry_shaders = false;
    _glFramebufferTexture = nullptr;
  }

  if (_supports_geometry_shaders) {
    _supported_geom_rendering |= Geom::GR_adjacency;
  }
#endif
  _shader_caps._supports_glsl = _supports_glsl;

  // Check for support for other types of shaders that can be used by Cg.
  _supports_basic_shaders = false;
#if defined(HAVE_CG) && !defined(OPENGLES)
  if (has_extension("GL_ARB_vertex_program") &&
      has_extension("GL_ARB_fragment_program")) {
    _supports_basic_shaders = true;
    _shader_caps._active_vprofile = (int)CG_PROFILE_ARBVP1;
    _shader_caps._active_fprofile = (int)CG_PROFILE_ARBFP1;
    _shader_caps._active_gprofile = (int)CG_PROFILE_UNKNOWN; // No geometry shader if only using basic

    if (basic_shaders_only) {
      // We're happy with ARB programs, thanks.
    } else if (has_extension("GL_NV_gpu_program5")) {
      _shader_caps._active_vprofile = (int)CG_PROFILE_GP5VP;
      _shader_caps._active_fprofile = (int)CG_PROFILE_GP5FP;
      _shader_caps._active_gprofile = (int)CG_PROFILE_GP5GP;

    } else if (has_extension("GL_NV_gpu_program4")) {
      _shader_caps._active_vprofile = (int)CG_PROFILE_GP4VP;
      _shader_caps._active_fprofile = (int)CG_PROFILE_GP4FP;
      _shader_caps._active_gprofile = (int)CG_PROFILE_GP4GP;

    } else if (has_extension("GL_NV_vertex_program3") &&
               has_extension("GL_NV_fragment_program2")) {
      _shader_caps._active_vprofile = (int)CG_PROFILE_VP40;
      _shader_caps._active_fprofile = (int)CG_PROFILE_FP40;
      _shader_caps._active_gprofile = (int)CG_PROFILE_UNKNOWN;

    } else if (has_extension("GL_NV_vertex_program2") &&
               has_extension("GL_NV_fragment_program")) {
      _shader_caps._active_vprofile = (int)CG_PROFILE_VP30;
      _shader_caps._active_fprofile = (int)CG_PROFILE_FP30;
      _shader_caps._active_gprofile = (int)CG_PROFILE_UNKNOWN;

    } else if (has_extension("GL_NV_vertex_program1_1") &&
               has_extension("GL_NV_texture_shader2") &&
               has_extension("GL_NV_register_combiners2")) {
      _shader_caps._active_vprofile = (int)CG_PROFILE_VP20;
      _shader_caps._active_fprofile = (int)CG_PROFILE_FP20;
      _shader_caps._active_gprofile = (int)CG_PROFILE_UNKNOWN;

    } else if (_supports_glsl) {
      // This is what will be available to non-NVIDIA cards.  It is the last
      // option since it is slower to compile GLSL than the other options.
      _shader_caps._active_vprofile = (int)CG_PROFILE_GLSLV;
      _shader_caps._active_fprofile = (int)CG_PROFILE_GLSLF;
      if (_supports_geometry_shaders) {
        _shader_caps._active_gprofile = (int)CG_PROFILE_GLSLG;
      }
    }
    _shader_caps._ultimate_vprofile = (int)CG_PROFILE_GP5VP;
    _shader_caps._ultimate_fprofile = (int)CG_PROFILE_GP5FP;
    _shader_caps._ultimate_gprofile = (int)CG_PROFILE_GP5GP;

    // Bug workaround for radeons.
    if (_shader_caps._active_fprofile == CG_PROFILE_ARBFP1) {
      if (has_extension("GL_ATI_draw_buffers")) {
        _shader_caps._bug_list.insert(Shader::SBUG_ati_draw_buffers);
      }
    }

    Shader::set_default_caps(_shader_caps);

  } else if (_supports_glsl) {
    // No, but we do support GLSL...
    _shader_caps._active_vprofile = (int)CG_PROFILE_GLSLV;
    _shader_caps._active_fprofile = (int)CG_PROFILE_GLSLF;
    if (_supports_geometry_shaders) {
      _shader_caps._active_gprofile = (int)CG_PROFILE_GLSLG;
    } else {
      _shader_caps._active_gprofile = (int)CG_PROFILE_UNKNOWN;
    }
  }
#endif  // HAVE_CG

  _supports_compute_shaders = false;
#ifndef OPENGLES_1
#ifdef OPENGLES
  if (is_at_least_gles_version(3, 1)) {
#else
  if (is_at_least_gl_version(4, 3) || has_extension("GL_ARB_compute_shader")) {
#endif
    _glDispatchCompute = (PFNGLDISPATCHCOMPUTEPROC)
      get_extension_func("glDispatchCompute");

    if (_glDispatchCompute != nullptr) {
      _supports_compute_shaders = true;
    }
  }
#endif  // !OPENGLES_1

#ifndef OPENGLES
  if (_supports_glsl) {
    _glAttachShader = (PFNGLATTACHSHADERPROC)
       get_extension_func("glAttachShader");
    _glBindAttribLocation = (PFNGLBINDATTRIBLOCATIONPROC)
       get_extension_func("glBindAttribLocation");
    _glCompileShader = (PFNGLCOMPILESHADERPROC)
       get_extension_func("glCompileShader");
    _glCreateProgram = (PFNGLCREATEPROGRAMPROC)
       get_extension_func("glCreateProgram");
    _glCreateShader = (PFNGLCREATESHADERPROC)
       get_extension_func("glCreateShader");
    _glDeleteProgram = (PFNGLDELETEPROGRAMPROC)
       get_extension_func("glDeleteProgram");
    _glDeleteShader = (PFNGLDELETESHADERPROC)
       get_extension_func("glDeleteShader");
    _glDetachShader = (PFNGLDETACHSHADERPROC)
       get_extension_func("glDetachShader");
    _glDisableVertexAttribArray = (PFNGLDISABLEVERTEXATTRIBARRAYPROC)
      get_extension_func("glDisableVertexAttribArray");
    _glEnableVertexAttribArray = (PFNGLENABLEVERTEXATTRIBARRAYPROC)
      get_extension_func("glEnableVertexAttribArray");
    _glGetActiveAttrib = (PFNGLGETACTIVEATTRIBPROC)
       get_extension_func("glGetActiveAttrib");
    _glGetActiveUniform = (PFNGLGETACTIVEUNIFORMPROC)
       get_extension_func("glGetActiveUniform");
    _glGetAttribLocation = (PFNGLGETATTRIBLOCATIONPROC)
      get_extension_func("glGetAttribLocation");
    _glGetProgramiv = (PFNGLGETPROGRAMIVPROC)
       get_extension_func("glGetProgramiv");
    _glGetProgramInfoLog = (PFNGLGETPROGRAMINFOLOGPROC)
       get_extension_func("glGetProgramInfoLog");
    _glGetShaderiv = (PFNGLGETSHADERIVPROC)
       get_extension_func("glGetShaderiv");
    _glGetShaderInfoLog = (PFNGLGETSHADERINFOLOGPROC)
       get_extension_func("glGetShaderInfoLog");
    _glGetUniformLocation = (PFNGLGETUNIFORMLOCATIONPROC)
       get_extension_func("glGetUniformLocation");
    _glLinkProgram = (PFNGLLINKPROGRAMPROC)
       get_extension_func("glLinkProgram");
    _glShaderSource = (PFNGLSHADERSOURCEPROC_P)
       get_extension_func("glShaderSource");
    _glUseProgram = (PFNGLUSEPROGRAMPROC)
       get_extension_func("glUseProgram");
    _glUniform4f = (PFNGLUNIFORM4FPROC)
       get_extension_func("glUniform4f");
    _glUniform1i = (PFNGLUNIFORM1IPROC)
       get_extension_func("glUniform1i");
    _glUniform1fv = (PFNGLUNIFORM1FVPROC)
       get_extension_func("glUniform1fv");
    _glUniform2fv = (PFNGLUNIFORM2FVPROC)
       get_extension_func("glUniform2fv");
    _glUniform3fv = (PFNGLUNIFORM3FVPROC)
       get_extension_func("glUniform3fv");
    _glUniform4fv = (PFNGLUNIFORM4FVPROC)
       get_extension_func("glUniform4fv");
    _glUniform1iv = (PFNGLUNIFORM1IVPROC)
       get_extension_func("glUniform1iv");
    _glUniform2iv = (PFNGLUNIFORM2IVPROC)
       get_extension_func("glUniform2iv");
    _glUniform3iv = (PFNGLUNIFORM3IVPROC)
       get_extension_func("glUniform3iv");
    _glUniform4iv = (PFNGLUNIFORM4IVPROC)
       get_extension_func("glUniform4iv");
    _glUniformMatrix3fv = (PFNGLUNIFORMMATRIX3FVPROC)
       get_extension_func("glUniformMatrix3fv");
    _glUniformMatrix4fv = (PFNGLUNIFORMMATRIX4FVPROC)
       get_extension_func("glUniformMatrix4fv");
    _glValidateProgram = (PFNGLVALIDATEPROGRAMPROC)
       get_extension_func("glValidateProgram");
    _glVertexAttrib4fv = (PFNGLVERTEXATTRIB4FVPROC)
       get_extension_func("glVertexAttrib4fv");
    _glVertexAttrib4dv = (PFNGLVERTEXATTRIB4DVPROC)
       get_extension_func("glVertexAttrib4dv");
    _glVertexAttribPointer = (PFNGLVERTEXATTRIBPOINTERPROC)
       get_extension_func("glVertexAttribPointer");

    if (is_at_least_gl_version(3, 0)) {
      _glBindFragDataLocation = (PFNGLBINDFRAGDATALOCATIONPROC)
         get_extension_func("glBindFragDataLocation");
      _glVertexAttribIPointer = (PFNGLVERTEXATTRIBIPOINTERPROC)
         get_extension_func("glVertexAttribIPointer");
      _glUniform1uiv = (PFNGLUNIFORM1UIVPROC)
         get_extension_func("glUniform1uiv");
      _glUniform2uiv = (PFNGLUNIFORM2UIVPROC)
         get_extension_func("glUniform2uiv");
      _glUniform3uiv = (PFNGLUNIFORM3UIVPROC)
         get_extension_func("glUniform3uiv");
      _glUniform4uiv = (PFNGLUNIFORM4UIVPROC)
         get_extension_func("glUniform4uiv");
      _glVertexAttribI4ui = (PFNGLVERTEXATTRIBI4UIPROC)
         get_extension_func("glVertexAttribI4ui");

    } else if (has_extension("GL_EXT_gpu_shader4")) {
      _glBindFragDataLocation = (PFNGLBINDFRAGDATALOCATIONPROC)
         get_extension_func("glBindFragDataLocationEXT");
      _glVertexAttribIPointer = (PFNGLVERTEXATTRIBIPOINTERPROC)
         get_extension_func("glVertexAttribIPointerEXT");
      _glUniform1uiv = (PFNGLUNIFORM1UIVPROC)
         get_extension_func("glUniform1uivEXT");
      _glUniform2uiv = (PFNGLUNIFORM2UIVPROC)
         get_extension_func("glUniform2uivEXT");
      _glUniform3uiv = (PFNGLUNIFORM3UIVPROC)
         get_extension_func("glUniform3uivEXT");
      _glUniform4uiv = (PFNGLUNIFORM4UIVPROC)
         get_extension_func("glUniform4uivEXT");
      _glVertexAttribI4ui = (PFNGLVERTEXATTRIBI4UIPROC)
         get_extension_func("glVertexAttribI4uiEXT");

    } else {
      _glBindFragDataLocation = nullptr;
      _glVertexAttribIPointer = nullptr;
      _glVertexAttribI4ui = nullptr;
    }
    if (is_at_least_gl_version(4, 1) ||
        has_extension("GL_ARB_vertex_attrib_64bit")) {
      _glVertexAttribLPointer = (PFNGLVERTEXATTRIBLPOINTERPROC)
         get_extension_func("glVertexAttribLPointer");
    } else {
      _glVertexAttribLPointer = nullptr;
    }

    if (_supports_tessellation_shaders) {
      _glPatchParameteri = (PFNGLPATCHPARAMETERIPROC)
         get_extension_func("glPatchParameteri");
    }
  } else if (_supports_basic_shaders) {
    // We don't support GLSL, but we support ARB programs.
    _glDisableVertexAttribArray = (PFNGLDISABLEVERTEXATTRIBARRAYPROC)
      get_extension_func("glDisableVertexAttribArrayARB");
    _glEnableVertexAttribArray = (PFNGLENABLEVERTEXATTRIBARRAYPROC)
      get_extension_func("glEnableVertexAttribArrayARB");

    _glVertexAttrib4fv = (PFNGLVERTEXATTRIB4FVPROC)
       get_extension_func("glVertexAttrib4fvARB");
    _glVertexAttrib4dv = (PFNGLVERTEXATTRIB4DVPROC)
       get_extension_func("glVertexAttrib4dvARB");
    _glVertexAttribPointer = (PFNGLVERTEXATTRIBPOINTERPROC)
       get_extension_func("glVertexAttribPointerARB");

    _glBindFragDataLocation = nullptr;
    _glVertexAttribI4ui = nullptr;
    _glVertexAttribIPointer = nullptr;
    _glVertexAttribLPointer = nullptr;
  } else {
    _glVertexAttribI4ui = nullptr;
  }
#endif

#ifdef OPENGLES_2
  _glAttachShader = glAttachShader;
  _glBindAttribLocation = glBindAttribLocation;
  _glCompileShader = glCompileShader;
  _glCreateProgram = glCreateProgram;
  _glCreateShader = glCreateShader;
  _glDeleteProgram = glDeleteProgram;
  _glDeleteShader = glDeleteShader;
  _glDetachShader = glDetachShader;
  _glDisableVertexAttribArray = glDisableVertexAttribArray;
  _glEnableVertexAttribArray = glEnableVertexAttribArray;
  _glGetActiveAttrib = glGetActiveAttrib;
  _glGetActiveUniform = glGetActiveUniform;
  _glGetAttribLocation = glGetAttribLocation;
  _glGetProgramiv = glGetProgramiv;
  _glGetProgramInfoLog = glGetProgramInfoLog;
  _glGetShaderiv = glGetShaderiv;
  _glGetShaderInfoLog = glGetShaderInfoLog;
  _glGetUniformLocation = glGetUniformLocation;
  _glLinkProgram = glLinkProgram;
  _glShaderSource = (PFNGLSHADERSOURCEPROC_P) glShaderSource;
  _glUseProgram = glUseProgram;
  _glUniform4f = glUniform4f;
  _glUniform1i = glUniform1i;
  _glUniform1fv = glUniform1fv;
  _glUniform2fv = glUniform2fv;
  _glUniform3fv = glUniform3fv;
  _glUniform4fv = glUniform4fv;
  _glUniform1iv = glUniform1iv;
  _glUniform2iv = glUniform2iv;
  _glUniform3iv = glUniform3iv;
  _glUniform4iv = glUniform4iv;
  _glUniformMatrix3fv = glUniformMatrix3fv;
  _glUniformMatrix4fv = glUniformMatrix4fv;
  _glValidateProgram = glValidateProgram;
  _glVertexAttrib4fv = glVertexAttrib4fv;
  _glVertexAttrib4dv = null_glVertexAttrib4dv;
  _glVertexAttribPointer = glVertexAttribPointer;
  _glVertexAttribLPointer = nullptr;

  if (is_at_least_gles_version(3, 0)) {
    _glVertexAttribIPointer = (PFNGLVERTEXATTRIBIPOINTERPROC)
      get_extension_func("glVertexAttribIPointer");
    _glVertexAttribI4ui = (PFNGLVERTEXATTRIBI4UIPROC)
      get_extension_func("glVertexAttribI4ui");
  } else {
    _glVertexAttribIPointer = nullptr;
    _glVertexAttribI4ui = nullptr;
  }

  if (has_extension("GL_EXT_blend_func_extended")) {
    _glBindFragDataLocation = (PFNGLBINDFRAGDATALOCATIONPROC)
       get_extension_func("glBindFragDataLocationEXT");
  } else {
    _glBindFragDataLocation = nullptr;
  }
#endif

#ifndef OPENGLES_1
  _use_vertex_attrib_binding = false;
#ifdef OPENGLES
  if (is_at_least_gles_version(3, 1)) {
#else
  if (is_at_least_gl_version(4, 3) || has_extension("GL_ARB_vertex_attrib_binding")) {
#endif
    _glBindVertexBuffer = (PFNGLBINDVERTEXBUFFERPROC)
      get_extension_func("glBindVertexBuffer");
    _glVertexAttribFormat = (PFNGLVERTEXATTRIBFORMATPROC)
      get_extension_func("glVertexAttribFormat");
    _glVertexAttribIFormat = (PFNGLVERTEXATTRIBIFORMATPROC)
      get_extension_func("glVertexAttribIFormat");
    _glVertexAttribBinding = (PFNGLVERTEXATTRIBBINDINGPROC)
      get_extension_func("glVertexAttribBinding");
    _glVertexBindingDivisor = (PFNGLVERTEXBINDINGDIVISORPROC)
      get_extension_func("glVertexBindingDivisor");

#ifndef OPENGLES
    _glVertexAttribLFormat = (PFNGLVERTEXATTRIBLFORMATPROC)
      get_extension_func("glVertexAttribLFormat");
#endif

    if (gl_fixed_vertex_attrib_locations) {
      _use_vertex_attrib_binding = true;
    }
  }
#endif

  // We need to have a default shader to apply in case something didn't happen
  // to have a shader applied, or if it failed to compile.  This default
  // shader just outputs a red color, indicating that something went wrong.
#ifndef OPENGLES_1
  if (_default_shader == nullptr && !has_fixed_function_pipeline()) {
#ifndef OPENGLES
    bool use_float64 = vertices_float64;
    if (use_float64 && is_at_least_gl_version(4, 1)) {
      _default_shader = Shader::make(Shader::SL_GLSL, default_vshader_fp64_gl41, default_fshader);
    } else if (use_float64 && has_extension("GL_ARB_vertex_attrib_64bit")) {
      _default_shader = Shader::make(Shader::SL_GLSL, default_vshader_fp64, default_fshader);
    } else
#endif
    {
      _default_shader = Shader::make(Shader::SL_GLSL, default_vshader, default_fshader);
    }
  }
#endif

#ifndef OPENGLES_1
  // Check for uniform buffers.
#ifdef OPENGLES
  if (is_at_least_gl_version(3, 1) || has_extension("GL_ARB_uniform_buffer_object")) {
#else
  if (is_at_least_gles_version(3, 0)) {
#endif
    _supports_uniform_buffers = true;
    _glGetActiveUniformsiv = (PFNGLGETACTIVEUNIFORMSIVPROC)
       get_extension_func("glGetActiveUniformsiv");
    _glGetActiveUniformBlockiv = (PFNGLGETACTIVEUNIFORMBLOCKIVPROC)
       get_extension_func("glGetActiveUniformBlockiv");
    _glGetActiveUniformBlockName = (PFNGLGETACTIVEUNIFORMBLOCKNAMEPROC)
       get_extension_func("glGetActiveUniformBlockName");
  } else {
    _supports_uniform_buffers = false;
  }

#ifndef OPENGLES
  // Check for SSBOs.
  if (is_at_least_gl_version(4, 3) || has_extension("ARB_shader_storage_buffer_object")) {
    _supports_shader_buffers = true;
    _glGetProgramInterfaceiv = (PFNGLGETPROGRAMINTERFACEIVPROC)
       get_extension_func("glGetProgramInterfaceiv");
    _glGetProgramResourceName = (PFNGLGETPROGRAMRESOURCENAMEPROC)
       get_extension_func("glGetProgramResourceName");
    _glGetProgramResourceiv = (PFNGLGETPROGRAMRESOURCEIVPROC)
       get_extension_func("glGetProgramResourceiv");
    _glShaderStorageBlockBinding = (PFNGLSHADERSTORAGEBLOCKBINDINGPROC)
       get_extension_func("glShaderStorageBlockBinding");
  } else
#endif
  {
    _supports_shader_buffers = false;
  }

  if (_supports_uniform_buffers || _supports_shader_buffers) {
    _glBindBufferBase = (PFNGLBINDBUFFERBASEPROC)
      get_extension_func("glBindBufferBase");
  }
#endif

  // Check whether we support geometry instancing and instanced vertex
  // attribs.
#if defined(OPENGLES_1)
  _supports_vertex_attrib_divisor = false;
  _supports_geometry_instancing = false;

#elif defined(OPENGLES)
  if (is_at_least_gles_version(3, 0)) {
    // OpenGL ES 3 has all of this in the core.
    _glVertexAttribDivisor = (PFNGLVERTEXATTRIBDIVISORPROC)
      get_extension_func("glVertexAttribDivisor");
    _glDrawArraysInstanced = (PFNGLDRAWARRAYSINSTANCEDPROC)
      get_extension_func("glDrawArraysInstanced");
    _glDrawElementsInstanced = (PFNGLDRAWELEMENTSINSTANCEDPROC)
      get_extension_func("glDrawElementsInstanced");

    _supports_vertex_attrib_divisor = true;
    _supports_geometry_instancing = true;

  } else if (has_extension("GL_ANGLE_instanced_arrays")) {
    // This extension has both things in one.
#ifdef __EMSCRIPTEN__
    // Work around bug - it doesn't allow ANGLE suffix in getProcAddress.
    _glVertexAttribDivisor = (PFNGLVERTEXATTRIBDIVISORPROC)
      get_extension_func("glVertexAttribDivisor");
    _glDrawArraysInstanced = (PFNGLDRAWARRAYSINSTANCEDPROC)
      get_extension_func("glDrawArraysInstanced");
    _glDrawElementsInstanced = (PFNGLDRAWELEMENTSINSTANCEDPROC)
      get_extension_func("glDrawElementsInstanced");
#else
    _glVertexAttribDivisor = (PFNGLVERTEXATTRIBDIVISORPROC)
      get_extension_func("glVertexAttribDivisorANGLE");
    _glDrawArraysInstanced = (PFNGLDRAWARRAYSINSTANCEDPROC)
      get_extension_func("glDrawArraysInstancedANGLE");
    _glDrawElementsInstanced = (PFNGLDRAWELEMENTSINSTANCEDPROC)
      get_extension_func("glDrawElementsInstancedANGLE");
#endif

    _supports_vertex_attrib_divisor = true;
    _supports_geometry_instancing = true;

  } else {
    // Check separately for geometry instancing and instanced attribs.
    if (has_extension("GL_EXT_draw_instanced")) {
      _glDrawArraysInstanced = (PFNGLDRAWARRAYSINSTANCEDPROC)
        get_extension_func("glDrawArraysInstancedEXT");
      _glDrawElementsInstanced = (PFNGLDRAWELEMENTSINSTANCEDPROC)
        get_extension_func("glDrawElementsInstancedEXT");
      _supports_geometry_instancing = true;

    } else if (has_extension("GL_NV_draw_instanced")) {
      _glDrawArraysInstanced = (PFNGLDRAWARRAYSINSTANCEDPROC)
        get_extension_func("glDrawArraysInstancedNV");
      _glDrawElementsInstanced = (PFNGLDRAWELEMENTSINSTANCEDPROC)
        get_extension_func("glDrawElementsInstancedNV");
      _supports_geometry_instancing = true;

    } else {
      _supports_geometry_instancing = false;
    }

    if (has_extension("GL_EXT_instanced_arrays")) {
      _glVertexAttribDivisor = (PFNGLVERTEXATTRIBDIVISORPROC)
        get_extension_func("glVertexAttribDivisorEXT");
      _supports_vertex_attrib_divisor = true;

    } else if (has_extension("GL_NV_instanced_arrays")) {
      _glVertexAttribDivisor = (PFNGLVERTEXATTRIBDIVISORPROC)
        get_extension_func("glVertexAttribDivisorNV");
      _supports_vertex_attrib_divisor = true;

    } else {
      _supports_vertex_attrib_divisor = false;
    }
  }

#else
  if (is_at_least_gl_version(3, 3)) {
    // This feature is in OpenGL core as of 3.3.
    _glVertexAttribDivisor = (PFNGLVERTEXATTRIBDIVISORPROC)
      get_extension_func("glVertexAttribDivisor");
    _supports_vertex_attrib_divisor = true;

  } else if (has_extension("GL_ARB_instanced_arrays")) {
    _glVertexAttribDivisor = (PFNGLVERTEXATTRIBDIVISORPROC)
      get_extension_func("glVertexAttribDivisorARB");
    _supports_vertex_attrib_divisor = true;

  } else {
    _supports_vertex_attrib_divisor = false;
  }

  // Some drivers expose one extension, some expose the other.
  if (is_at_least_gl_version(3, 1)) {
    _glDrawArraysInstanced = (PFNGLDRAWARRAYSINSTANCEDPROC)
      get_extension_func("glDrawArraysInstanced");
    _glDrawElementsInstanced = (PFNGLDRAWELEMENTSINSTANCEDPROC)
      get_extension_func("glDrawElementsInstanced");
    _supports_geometry_instancing = true;

  } else if (has_extension("GL_ARB_draw_instanced")) {
    _glDrawArraysInstanced = (PFNGLDRAWARRAYSINSTANCEDPROC)
      get_extension_func("glDrawArraysInstancedARB");
    _glDrawElementsInstanced = (PFNGLDRAWELEMENTSINSTANCEDPROC)
      get_extension_func("glDrawElementsInstancedARB");
    _supports_geometry_instancing = true;

  } else if (has_extension("GL_EXT_draw_instanced")) {
    _glDrawArraysInstanced = (PFNGLDRAWARRAYSINSTANCEDPROC)
      get_extension_func("glDrawArraysInstancedEXT");
    _glDrawElementsInstanced = (PFNGLDRAWELEMENTSINSTANCEDPROC)
      get_extension_func("glDrawElementsInstancedEXT");
    _supports_geometry_instancing = true;

  } else {
    _glDrawElementsInstanced = 0;
    _glDrawArraysInstanced = 0;
    _supports_geometry_instancing = false;
  }
#endif

#ifndef OPENGLES_1
  if (_supports_geometry_instancing) {
    if (_glDrawArraysInstanced == nullptr || _glDrawElementsInstanced == nullptr) {
      GLCAT.warning()
        << "Geometry instancing advertised as supported by OpenGL runtime, but could not get pointers to extension functions.\n";
      _supports_geometry_instancing = false;
    }
  }

  if (_supports_vertex_attrib_divisor) {
    if (_glVertexAttribDivisor == nullptr) {
      GLCAT.warning()
        << "Instanced vertex arrays advertised as supported by OpenGL runtime, but could not get pointers to extension functions.\n";
      _supports_vertex_attrib_divisor = false;
    }
  }
#endif

  // Check if we support indirect draw.
  _supports_indirect_draw = false;
#ifndef OPENGLES_1
#ifdef OPENGLES
  if (is_at_least_gles_version(3, 1)) {
#else
  if (is_at_least_gl_version(4, 0) || has_extension("GL_ARB_draw_indirect")) {
#endif
    _glDrawArraysIndirect = (PFNGLDRAWARRAYSINDIRECTPROC)
      get_extension_func("glDrawArraysIndirect");
    _glDrawElementsIndirect = (PFNGLDRAWELEMENTSINDIRECTPROC)
      get_extension_func("glDrawElementsIndirect");

    if (_glDrawArraysIndirect == nullptr || _glDrawElementsIndirect == nullptr) {
      GLCAT.warning()
        << "Indirect draw advertised as supported by OpenGL runtime, but could not get pointers to extension functions.\n";
    } else {
      _supports_indirect_draw = true;
    }
  }
#endif

#ifdef OPENGLES_1
  _supports_framebuffer_multisample = false;
  _supports_framebuffer_blit = false;

  if (has_extension("GL_OES_framebuffer_object")) {
    _supports_framebuffer_object = true;
    _glIsRenderbuffer = (PFNGLISRENDERBUFFEROESPROC)
      get_extension_func("glIsRenderbufferOES");
    _glBindRenderbuffer = (PFNGLBINDRENDERBUFFEROESPROC)
      get_extension_func("glBindRenderbufferOES");
    _glDeleteRenderbuffers = (PFNGLDELETERENDERBUFFERSOESPROC)
      get_extension_func("glDeleteRenderbuffersOES");
    _glGenRenderbuffers = (PFNGLGENRENDERBUFFERSOESPROC)
      get_extension_func("glGenRenderbuffersOES");
    _glRenderbufferStorage = (PFNGLRENDERBUFFERSTORAGEOESPROC)
      get_extension_func("glRenderbufferStorageOES");
    _glGetRenderbufferParameteriv = (PFNGLGETRENDERBUFFERPARAMETERIVOESPROC)
      get_extension_func("glGetRenderbufferParameterivOES");
    _glIsFramebuffer = (PFNGLISFRAMEBUFFEROESPROC)
      get_extension_func("glIsFramebufferOES");
    _glBindFramebuffer = (PFNGLBINDFRAMEBUFFEROESPROC)
      get_extension_func("glBindFramebufferOES");
    _glDeleteFramebuffers = (PFNGLDELETEFRAMEBUFFERSOESPROC)
      get_extension_func("glDeleteFramebuffersOES");
    _glGenFramebuffers = (PFNGLGENFRAMEBUFFERSOESPROC)
      get_extension_func("glGenFramebuffersOES");
    _glCheckFramebufferStatus = (PFNGLCHECKFRAMEBUFFERSTATUSOESPROC)
      get_extension_func("glCheckFramebufferStatusOES");
    _glFramebufferTexture1D = nullptr;
    _glFramebufferTexture2D = (PFNGLFRAMEBUFFERTEXTURE2DOESPROC)
      get_extension_func("glFramebufferTexture2DOES");
    _glFramebufferRenderbuffer = (PFNGLFRAMEBUFFERRENDERBUFFEROESPROC)
      get_extension_func("glFramebufferRenderbufferOES");
    _glGetFramebufferAttachmentParameteriv = (PFNGLGETFRAMEBUFFERATTACHMENTPARAMETERIVOESPROC)
      get_extension_func("glGetFramebufferAttachmentParameterivOES");
    _glGenerateMipmap = (PFNGLGENERATEMIPMAPOESPROC)
      get_extension_func("glGenerateMipmapOES");
  } else {
    _supports_framebuffer_object = false;
    _glGenerateMipmap = nullptr;
  }
#elif defined(OPENGLES)
  // In OpenGL ES 2.x, FBO's are supported in the core.
  _supports_framebuffer_object = true;
  _glIsRenderbuffer = glIsRenderbuffer;
  _glBindRenderbuffer = glBindRenderbuffer;
  _glDeleteRenderbuffers = glDeleteRenderbuffers;
  _glGenRenderbuffers = glGenRenderbuffers;
  _glRenderbufferStorage = glRenderbufferStorage;
  _glGetRenderbufferParameteriv = glGetRenderbufferParameteriv;
  _glIsFramebuffer = glIsFramebuffer;
  _glBindFramebuffer = glBindFramebuffer;
  _glDeleteFramebuffers = glDeleteFramebuffers;
  _glGenFramebuffers = glGenFramebuffers;
  _glCheckFramebufferStatus = glCheckFramebufferStatus;
  _glFramebufferTexture1D = nullptr;
  _glFramebufferTexture2D = glFramebufferTexture2D;
  _glFramebufferRenderbuffer = glFramebufferRenderbuffer;
  _glGetFramebufferAttachmentParameteriv = glGetFramebufferAttachmentParameteriv;
  _glGenerateMipmap = glGenerateMipmap;

  if (is_at_least_gles_version(3, 0)) {
    _supports_framebuffer_multisample = true;
    _supports_framebuffer_blit = true;

    _glRenderbufferStorageMultisample = (PFNGLRENDERBUFFERSTORAGEMULTISAMPLEEXTPROC)
      get_extension_func("glRenderbufferStorageMultisample");
    _glBlitFramebuffer = (PFNGLBLITFRAMEBUFFEREXTPROC)
      get_extension_func("glBlitFramebuffer");
  } else {
    if (has_extension("GL_ANGLE_framebuffer_multisample")) {
      _supports_framebuffer_multisample = true;
      _glRenderbufferStorageMultisample = (PFNGLRENDERBUFFERSTORAGEMULTISAMPLEANGLEPROC)
        get_extension_func("glRenderbufferStorageMultisampleANGLE");
    } else {
      _supports_framebuffer_multisample = false;
    }
    if (has_extension("GL_ANGLE_framebuffer_blit")) {
      _supports_framebuffer_blit = true;
      _glBlitFramebuffer = (PFNGLBLITFRAMEBUFFERANGLEPROC)
        get_extension_func("glBlitFramebufferANGLE");
    } else {
      _supports_framebuffer_blit = false;
    }
  }
#else  // Desktop OpenGL case.
  if (is_at_least_gl_version(3, 0) || has_extension("GL_ARB_framebuffer_object")) {
    _supports_framebuffer_object = true;
    _supports_framebuffer_multisample = true;
    _supports_framebuffer_blit = true;

    _glIsRenderbuffer = (PFNGLISRENDERBUFFERPROC)
      get_extension_func("glIsRenderbuffer");
    _glBindRenderbuffer = (PFNGLBINDRENDERBUFFERPROC)
      get_extension_func("glBindRenderbuffer");
    _glDeleteRenderbuffers = (PFNGLDELETERENDERBUFFERSPROC)
      get_extension_func("glDeleteRenderbuffers");
    _glGenRenderbuffers = (PFNGLGENRENDERBUFFERSPROC)
      get_extension_func("glGenRenderbuffers");
    _glRenderbufferStorage = (PFNGLRENDERBUFFERSTORAGEPROC)
      get_extension_func("glRenderbufferStorage");
    _glGetRenderbufferParameteriv = (PFNGLGETRENDERBUFFERPARAMETERIVPROC)
      get_extension_func("glGetRenderbufferParameteriv");
    _glIsFramebuffer = (PFNGLISFRAMEBUFFERPROC)
      get_extension_func("glIsFramebuffer");
    _glBindFramebuffer = (PFNGLBINDFRAMEBUFFERPROC)
      get_extension_func("glBindFramebuffer");
    _glDeleteFramebuffers = (PFNGLDELETEFRAMEBUFFERSPROC)
      get_extension_func("glDeleteFramebuffers");
    _glGenFramebuffers = (PFNGLGENFRAMEBUFFERSPROC)
      get_extension_func("glGenFramebuffers");
    _glCheckFramebufferStatus = (PFNGLCHECKFRAMEBUFFERSTATUSPROC)
      get_extension_func("glCheckFramebufferStatus");
    _glFramebufferTexture1D = (PFNGLFRAMEBUFFERTEXTURE1DPROC)
      get_extension_func("glFramebufferTexture1D");
    _glFramebufferTexture2D = (PFNGLFRAMEBUFFERTEXTURE2DPROC)
      get_extension_func("glFramebufferTexture2D");
    _glFramebufferTexture3D = (PFNGLFRAMEBUFFERTEXTURE3DPROC)
      get_extension_func("glFramebufferTexture3D");
    _glFramebufferRenderbuffer = (PFNGLFRAMEBUFFERRENDERBUFFERPROC)
      get_extension_func("glFramebufferRenderbuffer");
    _glGetFramebufferAttachmentParameteriv = (PFNGLGETFRAMEBUFFERATTACHMENTPARAMETERIVPROC)
      get_extension_func("glGetFramebufferAttachmentParameteriv");
    _glGenerateMipmap = (PFNGLGENERATEMIPMAPPROC)
      get_extension_func("glGenerateMipmap");
    _glRenderbufferStorageMultisample = (PFNGLRENDERBUFFERSTORAGEMULTISAMPLEPROC)
      get_extension_func("glRenderbufferStorageMultisample");
    _glBlitFramebuffer = (PFNGLBLITFRAMEBUFFERPROC)
      get_extension_func("glBlitFramebuffer");

  } else if (has_extension("GL_EXT_framebuffer_object")) {
    _supports_framebuffer_object = true;
    _glIsRenderbuffer = (PFNGLISRENDERBUFFEREXTPROC)
      get_extension_func("glIsRenderbufferEXT");
    _glBindRenderbuffer = (PFNGLBINDRENDERBUFFEREXTPROC)
      get_extension_func("glBindRenderbufferEXT");
    _glDeleteRenderbuffers = (PFNGLDELETERENDERBUFFERSEXTPROC)
      get_extension_func("glDeleteRenderbuffersEXT");
    _glGenRenderbuffers = (PFNGLGENRENDERBUFFERSEXTPROC)
      get_extension_func("glGenRenderbuffersEXT");
    _glRenderbufferStorage = (PFNGLRENDERBUFFERSTORAGEEXTPROC)
      get_extension_func("glRenderbufferStorageEXT");
    _glGetRenderbufferParameteriv = (PFNGLGETRENDERBUFFERPARAMETERIVEXTPROC)
      get_extension_func("glGetRenderbufferParameterivEXT");
    _glIsFramebuffer = (PFNGLISFRAMEBUFFEREXTPROC)
      get_extension_func("glIsFramebufferEXT");
    _glBindFramebuffer = (PFNGLBINDFRAMEBUFFEREXTPROC)
      get_extension_func("glBindFramebufferEXT");
    _glDeleteFramebuffers = (PFNGLDELETEFRAMEBUFFERSEXTPROC)
      get_extension_func("glDeleteFramebuffersEXT");
    _glGenFramebuffers = (PFNGLGENFRAMEBUFFERSEXTPROC)
      get_extension_func("glGenFramebuffersEXT");
    _glCheckFramebufferStatus = (PFNGLCHECKFRAMEBUFFERSTATUSEXTPROC)
      get_extension_func("glCheckFramebufferStatusEXT");
    _glFramebufferTexture1D = (PFNGLFRAMEBUFFERTEXTURE1DEXTPROC)
      get_extension_func("glFramebufferTexture1DEXT");
    _glFramebufferTexture2D = (PFNGLFRAMEBUFFERTEXTURE2DEXTPROC)
      get_extension_func("glFramebufferTexture2DEXT");
    _glFramebufferTexture3D = (PFNGLFRAMEBUFFERTEXTURE3DEXTPROC)
      get_extension_func("glFramebufferTexture3DEXT");
    _glFramebufferRenderbuffer = (PFNGLFRAMEBUFFERRENDERBUFFEREXTPROC)
      get_extension_func("glFramebufferRenderbufferEXT");
    _glGetFramebufferAttachmentParameteriv = (PFNGLGETFRAMEBUFFERATTACHMENTPARAMETERIVEXTPROC)
      get_extension_func("glGetFramebufferAttachmentParameterivEXT");
    _glGenerateMipmap = (PFNGLGENERATEMIPMAPEXTPROC)
      get_extension_func("glGenerateMipmapEXT");

    if (has_extension("GL_EXT_framebuffer_multisample")) {
      _supports_framebuffer_multisample = true;
      _glRenderbufferStorageMultisample = (PFNGLRENDERBUFFERSTORAGEMULTISAMPLEEXTPROC)
        get_extension_func("glRenderbufferStorageMultisampleEXT");
    } else {
      _supports_framebuffer_multisample = false;
    }
    if (has_extension("GL_EXT_framebuffer_blit")) {
      _supports_framebuffer_blit = true;
      _glBlitFramebuffer = (PFNGLBLITFRAMEBUFFEREXTPROC)
        get_extension_func("glBlitFramebufferEXT");
    } else {
      _supports_framebuffer_blit = false;
    }

  } else {
    _supports_framebuffer_object = false;
    _supports_framebuffer_multisample = false;
    _supports_framebuffer_blit = false;
    _glGenerateMipmap = nullptr;
  }
#endif

#ifndef OPENGLES
  if (is_at_least_gl_version(4, 5) || has_extension("GL_ARB_direct_state_access")) {
    _glCreateTextures = (PFNGLCREATETEXTURESPROC)
      get_extension_func("glCreateTextures");
    _glTextureStorage2D = (PFNGLTEXTURESTORAGE2DPROC)
      get_extension_func("glTextureStorage2D");
    _glTextureSubImage2D = (PFNGLTEXTURESUBIMAGE2DPROC)
      get_extension_func("glTextureSubImage2D");
    _glTextureParameteri = (PFNGLTEXTUREPARAMETERIPROC)
      get_extension_func("glTextureParameteri");
    _glGenerateTextureMipmap = (PFNGLGENERATETEXTUREMIPMAPPROC)
      get_extension_func("glGenerateTextureMipmap");
    _glBindTextureUnit = (PFNGLBINDTEXTUREUNITPROC)
      get_extension_func("glBindTextureUnit");

    _supports_dsa = true;
  } else {
    _supports_dsa = false;
  }
#endif

#ifndef OPENGLES_1
  // Do we support empty framebuffer objects?
#ifdef OPENGLES
  if (is_at_least_gles_version(3, 1)) {
#else
  if (is_at_least_gl_version(4, 3) || has_extension("GL_ARB_framebuffer_no_attachments")) {
#endif
    _glFramebufferParameteri = (PFNGLFRAMEBUFFERPARAMETERIPROC)
      get_extension_func("glFramebufferParameteri");
    _supports_empty_framebuffer = true;
  } else {
    _supports_empty_framebuffer = false;
  }
#endif  // !OPENGLES_1

#ifndef OPENGLES
  _supports_framebuffer_multisample_coverage_nv = false;
  if (_supports_framebuffer_multisample &&
      has_extension("GL_NV_framebuffer_multisample_coverage")) {
    _supports_framebuffer_multisample_coverage_nv = true;
    _glRenderbufferStorageMultisampleCoverage = (PFNGLRENDERBUFFERSTORAGEMULTISAMPLECOVERAGENVPROC)
      get_extension_func("glRenderbufferStorageMultisampleCoverageNV");
  }
#endif

#if defined(OPENGLES_1)
  _glDrawBuffers = nullptr;
  _max_color_targets = 1;

#elif defined(OPENGLES_2)
  if (is_at_least_gles_version(3, 0)) {
    _glDrawBuffers = (PFNGLDRAWBUFFERSPROC)
      get_extension_func("glDrawBuffers");

#ifdef __EMSCRIPTEN__
  } else if (has_extension("WEBGL_draw_buffers")) {
    _glDrawBuffers = (PFNGLDRAWBUFFERSPROC)
      get_extension_func("glDrawBuffers");
#endif  // EMSCRIPTEN

  } else if (has_extension("GL_EXT_draw_buffers")) {
    _glDrawBuffers = (PFNGLDRAWBUFFERSPROC)
      get_extension_func("glDrawBuffersEXT");

  } else if (has_extension("GL_NV_draw_buffers")) {
    _glDrawBuffers = (PFNGLDRAWBUFFERSPROC)
      get_extension_func("glDrawBuffersNV");

  } else {
    _glDrawBuffers = nullptr;
  }

#else
  if (is_at_least_gl_version(2, 0)) {
    _glDrawBuffers = (PFNGLDRAWBUFFERSPROC)
      get_extension_func("glDrawBuffers");

  } else if (has_extension("GL_ARB_draw_buffers")) {
    _glDrawBuffers = (PFNGLDRAWBUFFERSPROC)
      get_extension_func("glDrawBuffersARB");

  } else {
    _glDrawBuffers = nullptr;
  }
#endif

#if defined(OPENGLES) && !defined(OPENGLES_1)
  if (is_at_least_gles_version(3, 0)) {
    _glReadBuffer = (PFNGLREADBUFFERPROC)
      get_extension_func("glReadBuffer");

  } else if (has_extension("GL_NV_read_buffer")) {
    _glReadBuffer = (PFNGLREADBUFFERPROC)
      get_extension_func("glReadBufferNV");

  } else {
    _glReadBuffer = nullptr;
  }
#endif

#ifndef OPENGLES_1
  _max_color_targets = 1;
  if (_glDrawBuffers != nullptr) {
    GLint max_draw_buffers = 0;
    glGetIntegerv(GL_MAX_DRAW_BUFFERS, &max_draw_buffers);
    _max_color_targets = max_draw_buffers;
  }
#endif  // !OPENGLES_1

#ifndef OPENGLES_1
  if (_gl_version_major >= 3) {
    _glClearBufferfv = (PFNGLCLEARBUFFERFVPROC)
      get_extension_func("glClearBufferfv");
    _glClearBufferiv = (PFNGLCLEARBUFFERIVPROC)
      get_extension_func("glClearBufferiv");
    _glClearBufferfi = (PFNGLCLEARBUFFERFIPROC)
      get_extension_func("glClearBufferfi");

  } else {
    _glClearBufferfv = nullptr;
    _glClearBufferiv = nullptr;
    _glClearBufferfi = nullptr;
  }
#endif  // !OPENGLES

#ifndef OPENGLES
  _supports_viewport_arrays = false;

  if (is_at_least_gl_version(4, 1) || has_extension("GL_ARB_viewport_array")) {
    _glViewportArrayv = (PFNGLVIEWPORTARRAYVPROC)
      get_extension_func("glViewportArrayv");
    _glScissorArrayv = (PFNGLSCISSORARRAYVPROC)
      get_extension_func("glScissorArrayv");
    _glDepthRangeArrayv = (PFNGLDEPTHRANGEARRAYVPROC)
      get_extension_func("glDepthRangeArrayv");

    if (_glViewportArrayv == nullptr || _glScissorArrayv == nullptr || _glDepthRangeArrayv == nullptr) {
      GLCAT.warning()
          << "Viewport arrays advertised as supported by OpenGL runtime, but could not get pointers to extension functions.\n";
    } else {
      _supports_viewport_arrays = true;
    }
  }
#endif  // !OPENGLES

  _max_fb_samples = 0;
  if (_supports_framebuffer_multisample) {
    GLint max_samples;
    glGetIntegerv(GL_MAX_SAMPLES_EXT, &max_samples);
    _max_fb_samples = max_samples;
  }

  _supports_occlusion_query = false;
#ifndef OPENGLES
  if (gl_support_occlusion_query) {
    if (is_at_least_gl_version(1, 5)) {
      _supports_occlusion_query = true;

      _glGenQueries = (PFNGLGENQUERIESPROC)
        get_extension_func("glGenQueries");
      _glBeginQuery = (PFNGLBEGINQUERYPROC)
        get_extension_func("glBeginQuery");
      _glEndQuery = (PFNGLENDQUERYPROC)
        get_extension_func("glEndQuery");
      _glDeleteQueries = (PFNGLDELETEQUERIESPROC)
        get_extension_func("glDeleteQueries");
      _glGetQueryiv = (PFNGLGETQUERYIVPROC)
        get_extension_func("glGetQueryiv");
      _glGetQueryObjectuiv = (PFNGLGETQUERYOBJECTUIVPROC)
        get_extension_func("glGetQueryObjectuiv");

    } else if (has_extension("GL_ARB_occlusion_query")) {
      _supports_occlusion_query = true;
      _glGenQueries = (PFNGLGENQUERIESPROC)
        get_extension_func("glGenQueriesARB");
      _glBeginQuery = (PFNGLBEGINQUERYPROC)
        get_extension_func("glBeginQueryARB");
      _glEndQuery = (PFNGLENDQUERYPROC)
        get_extension_func("glEndQueryARB");
      _glDeleteQueries = (PFNGLDELETEQUERIESPROC)
        get_extension_func("glDeleteQueriesARB");
      _glGetQueryiv = (PFNGLGETQUERYIVPROC)
        get_extension_func("glGetQueryivARB");
      _glGetQueryObjectuiv = (PFNGLGETQUERYOBJECTUIVPROC)
        get_extension_func("glGetQueryObjectuivARB");
    }
  }

  if (_supports_occlusion_query) {
    if (_glGenQueries == nullptr || _glBeginQuery == nullptr ||
        _glEndQuery == nullptr || _glDeleteQueries == nullptr ||
        _glGetQueryiv == nullptr || _glGetQueryObjectuiv == nullptr) {
      GLCAT.warning()
        << "Occlusion queries advertised as supported by OpenGL runtime, but could not get pointers to extension functions.\n";
      _supports_occlusion_query = false;
    } else {
      GLint num_bits;
      _glGetQueryiv(GL_SAMPLES_PASSED, GL_QUERY_COUNTER_BITS, &num_bits);
      if (num_bits == 0) {
        _supports_occlusion_query = false;
      }
      if (GLCAT.is_debug()) {
        GLCAT.debug()
          << "Occlusion query counter provides " << num_bits << " bits.\n";
      }
    }
  }
#endif  // !OPENGLES

  _supports_timer_query = false;
#if defined(DO_PSTATS) && !defined(OPENGLES)
  if (is_at_least_gl_version(3, 3) || has_extension("GL_ARB_timer_query")) {
    _supports_timer_query = true;

    _glQueryCounter = (PFNGLQUERYCOUNTERPROC)
      get_extension_func("glQueryCounter");
    _glGetQueryObjecti64v = (PFNGLGETQUERYOBJECTI64VPROC)
      get_extension_func("glGetQueryObjecti64v");
    _glGetQueryObjectui64v = (PFNGLGETQUERYOBJECTUI64VPROC)
      get_extension_func("glGetQueryObjectui64v");

    _glGetInteger64v = (PFNGLGETINTEGER64VPROC)
      get_extension_func("glGetInteger64v");
  }
#endif

#ifdef OPENGLES_1
  // In OpenGL ES 1, blending is supported via extensions.
  if (has_extension("GL_OES_blend_subtract")) {
    _glBlendEquation = (PFNGLBLENDEQUATIONPROC)
      get_extension_func("glBlendEquationOES");

    if (_glBlendEquation == nullptr) {
      _glBlendEquation = null_glBlendEquation;
      GLCAT.warning()
        << "BlendEquationOES advertised as supported by OpenGL ES runtime, but "
           "could not get pointer to extension function.\n";
    }
  } else {
    _glBlendEquation = null_glBlendEquation;
  }

  if (has_extension("GL_OES_blend_equation_separate")) {
    _glBlendEquationSeparate = (PFNGLBLENDEQUATIONSEPARATEOESPROC)
      get_extension_func("glBlendEquationSeparateOES");

    if (_glBlendEquation == nullptr) {
      _supports_blend_equation_separate = false;
      GLCAT.warning()
        << "BlendEquationSeparateOES advertised as supported by OpenGL ES "
           "runtime, but could not get pointer to extension function.\n";
    } else {
      _supports_blend_equation_separate = true;
    }
  } else {
    _supports_blend_equation_separate = false;
    _glBlendEquationSeparate = nullptr;
  }

  if (has_extension("GL_OES_blend_func_separate")) {
    _glBlendFuncSeparate = (PFNGLBLENDFUNCSEPARATEOESPROC)
      get_extension_func("glBlendFuncSeparateOES");

    if (_glBlendFuncSeparate == nullptr) {
      _glBlendFuncSeparate = null_glBlendFuncSeparate;
      GLCAT.warning()
        << "BlendFuncSeparateOES advertised as supported by OpenGL ES runtime, but "
           "could not get pointer to extension function.\n";
    }
  } else {
    _glBlendFuncSeparate = null_glBlendFuncSeparate;
  }

#elif defined(OPENGLES)
  // In OpenGL ES 2.x and above, this is supported in the core.
  _supports_blend_equation_separate = true;

#else
  if (is_at_least_gl_version(1, 2)) {
    _glBlendEquation = (PFNGLBLENDEQUATIONPROC)
      get_extension_func("glBlendEquation");

  } else if (has_extension("GL_EXT_blend_minmax")) {
    _glBlendEquation = (PFNGLBLENDEQUATIONPROC)
      get_extension_func("glBlendEquationEXT");

  } else {
    _glBlendEquation = null_glBlendEquation;
  }

  if (_glBlendEquation == nullptr) {
    _glBlendEquation = null_glBlendEquation;
    GLCAT.warning()
      << "BlendEquation advertised as supported by OpenGL runtime, but could "
         "not get pointer to extension function.\n";
  }

  if (is_at_least_gl_version(2, 0)) {
    _supports_blend_equation_separate = true;
    _glBlendEquationSeparate = (PFNGLBLENDEQUATIONSEPARATEPROC)
      get_extension_func("glBlendEquationSeparate");

  } else if (has_extension("GL_EXT_blend_equation_separate")) {
    _supports_blend_equation_separate = true;
    _glBlendEquationSeparate = (PFNGLBLENDEQUATIONSEPARATEEXTPROC)
      get_extension_func("glBlendEquationSeparateEXT");

  } else {
    _supports_blend_equation_separate = false;
    _glBlendEquationSeparate = nullptr;
  }

  if (_supports_blend_equation_separate && _glBlendEquationSeparate == nullptr) {
    _supports_blend_equation_separate = false;
    GLCAT.warning()
      << "BlendEquationSeparate advertised as supported by OpenGL runtime, "
         "but could not get pointer to extension function.\n";
  }

  if (is_at_least_gl_version(1, 4)) {
    _glBlendFuncSeparate = (PFNGLBLENDFUNCSEPARATEPROC)
      get_extension_func("glBlendFuncSeparate");

  } else if (has_extension("GL_EXT_blend_func_separate")) {
    _glBlendFuncSeparate = (PFNGLBLENDFUNCSEPARATEEXTPROC)
      get_extension_func("glBlendFuncSeparateEXT");

  } else {
    _glBlendFuncSeparate = null_glBlendFuncSeparate;
  }

  if (_glBlendFuncSeparate == nullptr) {
    _glBlendFuncSeparate = null_glBlendFuncSeparate;
    GLCAT.warning()
      << "BlendFuncSeparate advertised as supported by OpenGL runtime, but could not get pointers to extension function.\n";
  }
#endif

  // In OpenGL ES 2.x, this is supported in the core.  In 1.x, not at all.
#ifndef OPENGLES
  _glBlendColor = nullptr;
  bool supports_blend_color = false;
  if (is_at_least_gl_version(1, 2)) {
    supports_blend_color = true;
    _glBlendColor = (PFNGLBLENDCOLORPROC)
      get_extension_func("glBlendColor");
  } else if (has_extension("GL_EXT_blend_color")) {
    supports_blend_color = true;
    _glBlendColor = (PFNGLBLENDCOLORPROC)
      get_extension_func("glBlendColorEXT");
  }
  if (supports_blend_color && _glBlendColor == nullptr) {
    GLCAT.warning()
      << "BlendColor advertised as supported by OpenGL runtime, but could not get pointers to extension function.\n";
  }
  if (_glBlendColor == nullptr) {
    _glBlendColor = null_glBlendColor;
  }
#endif

#ifdef OPENGLES_1
  // OpenGL ES 1 doesn't support dual-source blending.
#elif defined(OPENGLES)
  _supports_dual_source_blending = has_extension("GL_EXT_blend_func_extended");
#else
  _supports_dual_source_blending =
    is_at_least_gl_version(3, 3) || has_extension("GL_ARB_blend_func_extended");
#endif

#ifndef OPENGLES
  if (is_at_least_gl_version(3, 2) || has_extension("GL_ARB_sync")) {
    _glFenceSync = (PFNGLFENCESYNCPROC)get_extension_func("glFenceSync");
    _glDeleteSync = (PFNGLDELETESYNCPROC)get_extension_func("glDeleteSync");
    _glClientWaitSync = (PFNGLCLIENTWAITSYNCPROC)get_extension_func("glClientWaitSync");
    _glGetSynciv = (PFNGLGETSYNCIVPROC)get_extension_func("glGetSynciv");
  }
#elif !defined(OPENGLES_1)
  if (is_at_least_gles_version(3, 0)) {
    _glFenceSync = (PFNGLFENCESYNCPROC)get_extension_func("glFenceSync");
    _glDeleteSync = (PFNGLDELETESYNCPROC)get_extension_func("glDeleteSync");
    _glClientWaitSync = (PFNGLCLIENTWAITSYNCPROC)get_extension_func("glClientWaitSync");
    _glGetSynciv = (PFNGLGETSYNCIVPROC)get_extension_func("glGetSynciv");
  }
  else if (has_extension("GL_APPLE_sync")) {
    _glFenceSync = (PFNGLFENCESYNCPROC)get_extension_func("glFenceSyncAPPLE");
    _glDeleteSync = (PFNGLDELETESYNCPROC)get_extension_func("glDeleteSyncAPPLE");
    _glClientWaitSync = (PFNGLCLIENTWAITSYNCPROC)get_extension_func("glClientWaitSyncAPPLE");
    _glGetSynciv = (PFNGLGETSYNCIVPROC)get_extension_func("glGetSyncivAPPLE");
  }
#endif

#ifdef OPENGLES
  _edge_clamp = GL_CLAMP_TO_EDGE;
#else
  _edge_clamp = GL_CLAMP;
  if (is_at_least_gl_version(1, 2) || is_at_least_gles_version(1, 1) ||
      has_extension("GL_SGIS_texture_edge_clamp")) {
    _edge_clamp = GL_CLAMP_TO_EDGE;
  }
#endif

  _border_clamp = _edge_clamp;
#ifndef OPENGLES
  if (gl_support_clamp_to_border &&
      (is_at_least_gl_version(1, 3) ||
       has_extension("GL_ARB_texture_border_clamp"))) {
    _border_clamp = GL_CLAMP_TO_BORDER;
  }
#endif

#ifdef OPENGLES_1
  _mirror_repeat = GL_REPEAT;
  if (has_extension("GL_OES_texture_mirrored_repeat")) {
    _mirror_repeat = GL_MIRRORED_REPEAT;
  }
#elif defined(OPENGLES)
  // OpenGL 2.x and above support this in the core.
  _mirror_repeat = GL_MIRRORED_REPEAT;
#else
  _mirror_repeat = GL_REPEAT;
  if (is_at_least_gl_version(1, 4) ||
      has_extension("GL_ARB_texture_mirrored_repeat")) {
    _mirror_repeat = GL_MIRRORED_REPEAT;
  }
#endif

  _mirror_clamp = _edge_clamp;
  _mirror_edge_clamp = _edge_clamp;
  _mirror_border_clamp = _border_clamp;
#ifndef OPENGLES
  if (has_extension("GL_EXT_texture_mirror_clamp")) {
    _mirror_clamp = GL_MIRROR_CLAMP_EXT;
    _mirror_edge_clamp = GL_MIRROR_CLAMP_TO_EDGE_EXT;
    _mirror_border_clamp = GL_MIRROR_CLAMP_TO_BORDER_EXT;

  } else if (is_at_least_gl_version(4, 4) ||
             has_extension("GL_ARB_texture_mirror_clamp_to_edge")) {
    _mirror_clamp = GL_MIRROR_CLAMP_TO_EDGE;
    _mirror_edge_clamp = GL_MIRROR_CLAMP_TO_EDGE;
  }
#endif

#ifdef OPENGLES
  _supports_texture_lod = is_at_least_gles_version(3, 0);
  _supports_texture_lod_bias = false;
#else
  _supports_texture_lod = false;
  _supports_texture_lod_bias = false;

  if (gl_support_texture_lod &&
      (is_at_least_gl_version(1, 2) || has_extension("GL_SGIS_texture_lod"))) {
    _supports_texture_lod = true;

    if (is_at_least_gl_version(1, 4) || has_extension("GL_EXT_texture_lod_bias")) {
      _supports_texture_lod_bias = true;
    }
  }
#endif

#ifdef OPENGLES
  _supports_texture_max_level = is_at_least_gles_version(3, 0) ||
                                has_extension("GL_APPLE_texture_max_level");
#else
  _supports_texture_max_level = is_at_least_gl_version(1, 2);
#endif

  if (_supports_multisample) {
    GLint sample_buffers = 0;
    glGetIntegerv(GL_SAMPLE_BUFFERS, &sample_buffers);
    if (sample_buffers != 1) {
      // Even if the API supports multisample, we might have ended up with a
      // framebuffer that doesn't have any multisample bits.  (It's also
      // possible the graphics card doesn't provide any framebuffers with
      // multisample.)  In this case, we don't really support the multisample
      // API's, since they won't do anything.
      _supports_multisample = false;
    }
  }

  GLint max_texture_size = 0;
  GLint max_3d_texture_size = 0;
  GLint max_2d_texture_array_layers = 0;
  GLint max_cube_map_size = 0;

  glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max_texture_size);
  _max_texture_dimension = max_texture_size;

  if (_supports_3d_texture) {
#ifndef OPENGLES_1
    glGetIntegerv(GL_MAX_3D_TEXTURE_SIZE, &max_3d_texture_size);
#endif
    _max_3d_texture_dimension = max_3d_texture_size;
  } else {
    _max_3d_texture_dimension = 0;
  }
#ifndef OPENGLES_1
  if (_supports_2d_texture_array) {
    glGetIntegerv(GL_MAX_ARRAY_TEXTURE_LAYERS, &max_2d_texture_array_layers);
    _max_2d_texture_array_layers = max_2d_texture_array_layers;
  }
#endif
  if (_supports_cube_map) {
    glGetIntegerv(GL_MAX_CUBE_MAP_TEXTURE_SIZE, &max_cube_map_size);
    _max_cube_map_dimension = max_cube_map_size;
  } else {
    _max_cube_map_dimension = 0;
  }

#ifndef OPENGLES
  if (_supports_buffer_texture) {
    GLint max_buffer_texture_size = 0;
    glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE, &max_buffer_texture_size);
    _max_buffer_texture_size = max_buffer_texture_size;
  } else {
    _max_buffer_texture_size = 0;
  }
#endif  // !OPENGLES

#ifndef OPENGLES
  GLint max_elements_vertices = 0, max_elements_indices = 0;
  if (is_at_least_gl_version(1, 2) || has_extension("GL_EXT_draw_range_elements")) {
    glGetIntegerv(GL_MAX_ELEMENTS_VERTICES, &max_elements_vertices);
    glGetIntegerv(GL_MAX_ELEMENTS_INDICES, &max_elements_indices);

    if (max_elements_vertices > 0) {
      _max_vertices_per_array = max_elements_vertices;
    }
    if (max_elements_indices > 0) {
      _max_vertices_per_primitive = max_elements_indices;
    }
  }
#endif  // OPENGLES

  if (GLCAT.is_debug()) {
    GLCAT.debug()
      << "max texture dimension = " << _max_texture_dimension
      << ", max 3d texture = " << _max_3d_texture_dimension
      << ", max 2d texture array = " << max_2d_texture_array_layers
      << ", max cube map = " << _max_cube_map_dimension << "\n";
#ifndef OPENGLES
    GLCAT.debug()
      << "max_elements_vertices = " << max_elements_vertices
      << ", max_elements_indices = " << max_elements_indices << "\n";
#endif
    if (_supports_buffers) {
      if (vertex_buffers) {
        GLCAT.debug()
          << "vertex buffer objects are supported.\n";
      } else {
        GLCAT.debug()
          << "vertex buffer objects are supported (but not enabled).\n";
      }
    } else {
      GLCAT.debug()
        << "vertex buffer objects are NOT supported.\n";
    }

#ifdef SUPPORT_IMMEDIATE_MODE
    if (!vertex_arrays) {
      GLCAT.debug()
        << "immediate mode commands will be used instead of vertex arrays.\n";
    }
#endif

    if (!_supports_compressed_texture) {
      GLCAT.debug()
        << "Texture compression is not supported.\n";

    } else {
      GLint num_compressed_formats = 0;
      glGetIntegerv(GL_NUM_COMPRESSED_TEXTURE_FORMATS, &num_compressed_formats);
      if (num_compressed_formats == 0) {
        GLCAT.debug()
          << "No specific compressed texture formats are supported.\n";
      } else {
#ifndef NDEBUG
        GLCAT.debug()
          << "Supported compressed texture formats:\n";
        GLint *formats = (GLint *)alloca(num_compressed_formats * sizeof(GLint));
        glGetIntegerv(GL_COMPRESSED_TEXTURE_FORMATS, formats);
        for (int i = 0; i < num_compressed_formats; ++i) {
          const char *format_str = get_compressed_format_string(formats[i]);
          if (format_str != nullptr) {
            GLCAT.debug(false) << "  " << format_str << '\n';
          } else {
            GLCAT.debug(false)
              << "  Unknown compressed format 0x" << hex << formats[i]
              << dec << "\n";
          }
        }
#endif
      }
    }
  }

  _active_texture_stage = -1;
  _num_active_texture_stages = 0;

  // Check availability of anisotropic texture filtering.
  _supports_anisotropy = false;
  _max_anisotropy = 1.0;
  if (is_at_least_gl_version(4, 6) ||
      has_extension("GL_EXT_texture_filter_anisotropic") ||
      has_extension("GL_ARB_texture_filter_anisotropic")) {
    GLfloat max_anisotropy;
    glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &max_anisotropy);
    _max_anisotropy = (PN_stdfloat)max_anisotropy;
    _supports_anisotropy = true;
  }

  // Check availability of image read/write functionality in shaders.
  _max_image_units = 0;
#ifndef OPENGLES_1
#ifdef OPENGLES
  if (is_at_least_gles_version(3, 1) && gl_immutable_texture_storage) {
#else
  if (is_at_least_gl_version(4, 2) || has_extension("GL_ARB_shader_image_load_store")) {
#endif
    _glBindImageTexture = (PFNGLBINDIMAGETEXTUREPROC)
      get_extension_func("glBindImageTexture");
    _glMemoryBarrier = (PFNGLMEMORYBARRIERPROC)
      get_extension_func("glMemoryBarrier");

    glGetIntegerv(GL_MAX_IMAGE_UNITS, &_max_image_units);

#ifndef OPENGLES
  } else if (has_extension("GL_EXT_shader_image_load_store")) {
    _glBindImageTexture = (PFNGLBINDIMAGETEXTUREPROC)
      get_extension_func("glBindImageTextureEXT");
    _glMemoryBarrier = (PFNGLMEMORYBARRIERPROC)
      get_extension_func("glMemoryBarrierEXT");

    glGetIntegerv(GL_MAX_IMAGE_UNITS_EXT, &_max_image_units);
#endif

  } else {
    _glBindImageTexture = nullptr;
    _glMemoryBarrier = null_glMemoryBarrier;
  }
#endif  // !OPENGLES_1

  _supports_sampler_objects = false;

#ifndef OPENGLES_1
  if (gl_support_sampler_objects &&
#ifdef OPENGLES
      is_at_least_gles_version(3, 0)) {
#else
      (is_at_least_gl_version(3, 3) || has_extension("GL_ARB_sampler_objects"))) {
#endif
    _glGenSamplers = (PFNGLGENSAMPLERSPROC) get_extension_func("glGenSamplers");
    _glDeleteSamplers = (PFNGLDELETESAMPLERSPROC) get_extension_func("glDeleteSamplers");
    _glBindSampler = (PFNGLBINDSAMPLERPROC) get_extension_func("glBindSampler");
    _glSamplerParameteri = (PFNGLSAMPLERPARAMETERIPROC) get_extension_func("glSamplerParameteri");
    _glSamplerParameteriv = (PFNGLSAMPLERPARAMETERIVPROC) get_extension_func("glSamplerParameteriv");
    _glSamplerParameterf = (PFNGLSAMPLERPARAMETERFPROC) get_extension_func("glSamplerParameterf");
    _glSamplerParameterfv = (PFNGLSAMPLERPARAMETERFVPROC) get_extension_func("glSamplerParameterfv");

    if (_glGenSamplers == nullptr || _glDeleteSamplers == nullptr ||
        _glBindSampler == nullptr || _glSamplerParameteri == nullptr ||
        _glSamplerParameteriv == nullptr || _glSamplerParameterf == nullptr ||
        _glSamplerParameterfv == nullptr) {
      GLCAT.warning()
        << "GL_ARB_sampler_objects advertised as supported by OpenGL runtime, but could not get pointers to extension function.\n";
    } else {
      _supports_sampler_objects = true;
    }
  }
#endif  // !OPENGLES_1

  // Check availability of multi-bind functions.
  _supports_multi_bind = false;
#ifndef OPENGLES
  if (is_at_least_gl_version(4, 4) || has_extension("GL_ARB_multi_bind")) {
    _glBindTextures = (PFNGLBINDTEXTURESPROC)
      get_extension_func("glBindTextures");
    _glBindImageTextures = (PFNGLBINDIMAGETEXTURESPROC)
      get_extension_func("glBindImageTextures");

    if (_supports_sampler_objects) {
      _glBindSamplers = (PFNGLBINDSAMPLERSPROC)
        get_extension_func("glBindSamplers");
    }

    if (_use_vertex_attrib_binding) {
      _glBindVertexBuffers = (PFNGLBINDVERTEXBUFFERSPROC)
        get_extension_func("glBindVertexBuffers");
    }

    if (_glBindTextures != nullptr && _glBindImageTextures != nullptr) {
      _supports_multi_bind = true;
    } else {
      GLCAT.warning()
        << "ARB_multi_bind advertised as supported by OpenGL runtime, but could not get pointers to extension function.\n";
    }
  }
#endif  // !OPENGLES

#ifndef OPENGLES_1
#ifdef OPENGLES
  if (is_at_least_gl_version(3, 0)) {
#else
  if (is_at_least_gl_version(4, 3) || has_extension("GL_ARB_internalformat_query2")) {
#endif
    _glGetInternalformativ = (PFNGLGETINTERNALFORMATIVPROC)
      get_extension_func("glGetInternalformativ");

    if (_glGetInternalformativ == nullptr) {
      GLCAT.warning()
        << "ARB_internalformat_query2 advertised as supported by OpenGL runtime, but could not get pointers to extension function.\n";
    }
  }
#endif  // !OPENGLES_1

  _supports_bindless_texture = false;
#ifndef OPENGLES
  if (has_extension("GL_ARB_bindless_texture")) {
    _glGetTextureHandle = (PFNGLGETTEXTUREHANDLEPROC)
      get_extension_func("glGetTextureHandleARB");
    _glGetTextureSamplerHandle = (PFNGLGETTEXTURESAMPLERHANDLEPROC)
      get_extension_func("glGetTextureSamplerHandleARB");
    _glMakeTextureHandleResident = (PFNGLMAKETEXTUREHANDLERESIDENTPROC)
      get_extension_func("glMakeTextureHandleResidentARB");
    _glUniformHandleui64 = (PFNGLUNIFORMHANDLEUI64PROC)
      get_extension_func("glUniformHandleui64ARB");

    if (_glGetTextureHandle == nullptr || _glMakeTextureHandleResident == nullptr ||
       _glUniformHandleui64 == nullptr) {
      GLCAT.warning()
        << "GL_ARB_bindless_texture advertised as supported by OpenGL runtime, but could not get pointers to extension function.\n";
    } else {
      _supports_bindless_texture = true;
    }
  }
#endif  // !OPENGLES

#if !defined(OPENGLES_1) && !defined(__EMSCRIPTEN__)
  _supports_get_program_binary = false;
  _program_binary_formats.clear();

#ifdef OPENGLES
  if (is_at_least_gles_version(3, 0)) {
#else
  if (is_at_least_gl_version(4, 1) || has_extension("GL_ARB_get_program_binary")) {
#endif
    _glGetProgramBinary = (PFNGLGETPROGRAMBINARYPROC)
      get_extension_func("glGetProgramBinary");
    _glProgramBinary = (PFNGLPROGRAMBINARYPROC)
      get_extension_func("glProgramBinary");
    _glProgramParameteri = (PFNGLPROGRAMPARAMETERIPROC)
      get_extension_func("glProgramParameteri");

    GLint num_binary_formats = 0;
    if (_glGetProgramBinary != nullptr &&
        _glProgramBinary != nullptr &&
        _glProgramParameteri != nullptr) {
      glGetIntegerv(GL_NUM_PROGRAM_BINARY_FORMATS, &num_binary_formats);
    }

    if (num_binary_formats > 0) {
      _supports_get_program_binary = true;

      GLenum *binary_formats = (GLenum *)alloca(sizeof(GLenum) * num_binary_formats);
      glGetIntegerv(GL_PROGRAM_BINARY_FORMATS, (GLint *)binary_formats);

      for (int i = 0; i < num_binary_formats; ++i) {
        _program_binary_formats.insert(binary_formats[i]);
      }
    }
  }
#endif  // !OPENGLES_1

  report_my_gl_errors();

  if (core_profile) {
    // TODO: better detection mechanism?
    _supports_stencil = support_stencil;
  }
#ifdef SUPPORT_FIXED_FUNCTION
  else if (support_stencil) {
    GLint num_stencil_bits;
    glGetIntegerv(GL_STENCIL_BITS, &num_stencil_bits);
    _supports_stencil = (num_stencil_bits != 0);
  }
#endif

#ifdef OPENGLES_1
  _supports_stencil_wrap = has_extension("GL_OES_stencil_wrap");
#elif defined(OPENGLES)
  _supports_stencil_wrap = true;
#else
  _supports_stencil_wrap = is_at_least_gl_version(1, 4) ||
                           has_extension("GL_EXT_stencil_wrap");
#endif

  _supports_two_sided_stencil = false;
#ifndef OPENGLES
  //TODO: support the two-sided stencil functions that ended up in core.
  if (has_extension("GL_EXT_stencil_two_side")) {
    _glActiveStencilFaceEXT = (PFNGLACTIVESTENCILFACEEXTPROC)
      get_extension_func("glActiveStencilFaceEXT");
    _supports_two_sided_stencil = true;
  } else {
    _glActiveStencilFaceEXT = 0;
  }
#endif

  // Ensure the initial state is what we say it should be (in some cases, we
  // don't want the GL default settings; in others, we have to force the point
  // with some drivers that aren't strictly compliant w.r.t.  initial
  // settings).
  glFrontFace(GL_CCW);
#ifndef OPENGLES_2
  glDisable(GL_LINE_SMOOTH);
#endif
#ifdef SUPPORT_FIXED_FUNCTION
  if (has_fixed_function_pipeline()) {
    glDisable(GL_POINT_SMOOTH);
  }
#endif
#ifndef OPENGLES
  glDisable(GL_POLYGON_SMOOTH);
#endif  // OPENGLES

#ifndef OPENGLES_2
  if (_supports_multisample) {
    glDisable(GL_MULTISAMPLE);
  }
#endif

  // Set depth range from zero to one if requested.
#ifndef OPENGLES_1
  _use_depth_zero_to_one = false;
  _use_remapped_depth_range = false;

  if (gl_depth_zero_to_one) {
#ifndef OPENGLES
    PFNGLCLIPCONTROLPROC pglClipControl = nullptr;
    if (is_at_least_gl_version(4, 5) || has_extension("GL_ARB_clip_control")) {
      pglClipControl = (PFNGLCLIPCONTROLPROC)get_extension_func("glClipControl");
    }
#else
    PFNGLCLIPCONTROLEXTPROC pglClipControl = nullptr;
    if (has_extension("GL_EXT_clip_control")) {
      pglClipControl = (PFNGLCLIPCONTROLEXTPROC)get_extension_func("glClipControlEXT");
    }
#endif

    if (pglClipControl != nullptr) {
      pglClipControl(GL_LOWER_LEFT, GL_ZERO_TO_ONE);
      _use_depth_zero_to_one = true;

      if (GLCAT.is_debug()) {
        GLCAT.debug()
          << "Set zero-to-one depth using glClipControl\n";
      }
    }/* else if (has_extension("GL_NV_depth_buffer_float")) {
      // Alternatively, all GeForce 8+ and even some AMD drivers support this
      // extension, which (unlike the core glDepthRange, which clamps its
      // input parameters) can compensate for the built-in depth remapping.
      _glDepthRangedNV = (PFNGLDEPTHRANGEDNVPROC)get_extension_func("glDepthRangedNV");

      if (_glDepthRangedNV != nullptr) {
        _glDepthRangedNV(-1.0, 1.0);
        _use_depth_zero_to_one = true;
        _use_remapped_depth_range = true;
        _has_attrib_depth_range = false;

        if (GLCAT.is_debug()) {
          GLCAT.debug()
            << "Set zero-to-one depth using glDepthRangedNV\n";
        }
      }
    }*/

    if (!_use_depth_zero_to_one) {
      GLCAT.warning()
        << "Zero-to-one depth was requested, but driver does not support it.\n";
    }
  }
#endif

  if (_has_attrib_depth_range) {
#ifdef OPENGLES
    glDepthRangef(0.0f, 1.0f);
#else
    glDepthRange(0.0, 1.0);
#endif
    _depth_range_near = 0;
    _depth_range_far = 1;
    _has_attrib_depth_range = false;
  }

#ifndef OPENGLES_1
#ifndef OPENGLES
  if (is_at_least_gl_version(4, 6) || has_extension("GL_ARB_polygon_offset_clamp")) {
    _glPolygonOffsetClamp = (PFNGLPOLYGONOFFSETCLAMPEXTPROC)get_extension_func("glPolygonOffsetClamp");
  }
  else
#endif
  if (has_extension("GL_EXT_polygon_offset_clamp")) {
    _glPolygonOffsetClamp = (PFNGLPOLYGONOFFSETCLAMPEXTPROC)get_extension_func("glPolygonOffsetClampEXT");
  }
  else {
    _glPolygonOffsetClamp = null_glPolygonOffsetClamp;
  }
#endif

  // Set up all the enableddisabled flags to GL's known initial values:
  // everything off.
  _multisample_mode = 0;
  _line_smooth_enabled = false;
  _point_smooth_enabled = false;
  _polygon_smooth_enabled = false;
  _stencil_test_enabled = false;
  _blend_enabled = false;
  _depth_test_enabled = false;
  _fog_enabled = false;
  _alpha_test_enabled = false;
  _polygon_offset_enabled = false;
  _flat_shade_model = false;
  _decal_level = 0;
  _active_color_write_mask = ColorWriteAttrib::C_all;
  _tex_gen_point_sprite = false;

#ifndef OPENGLES_1
  _enabled_vertex_attrib_arrays.clear();
  memset(_vertex_attrib_divisors, 0, sizeof(GLuint) * 32);
#endif

  // Dither is on by default in GL; let's turn it off
  glDisable(GL_DITHER);
  _dithering_enabled = false;

#ifndef OPENGLES_1
  _current_shader = nullptr;
  _current_shader_context = nullptr;
  _vertex_array_shader = nullptr;
  _vertex_array_shader_context = nullptr;
  _texture_binding_shader = nullptr;
  _texture_binding_shader_context = nullptr;
#endif

  // Count the max number of lights
  _max_lights = 0;
#ifdef SUPPORT_FIXED_FUNCTION
  if (has_fixed_function_pipeline()) {
    GLint max_lights = 0;
    glGetIntegerv(GL_MAX_LIGHTS, &max_lights);
    _max_lights = max_lights;

    if (GLCAT.is_debug()) {
      GLCAT.debug()
        << "max lights = " << _max_lights << "\n";
    }
  }
#endif

  // Count the max number of clipping planes
  _max_clip_planes = 0;
#ifdef SUPPORT_FIXED_FUNCTION
  if (has_fixed_function_pipeline()) {
    GLint max_clip_planes = 0;
    glGetIntegerv(GL_MAX_CLIP_PLANES, &max_clip_planes);
    _max_clip_planes = max_clip_planes;

    if (GLCAT.is_debug()) {
      GLCAT.debug()
        << "max clip planes = " << _max_clip_planes << "\n";
    }
  }
#endif

  _max_texture_stages = 1;
#ifdef SUPPORT_FIXED_FUNCTION
  if (supports_multitexture && has_fixed_function_pipeline()) {
    GLint max_texture_stages = 0;
    glGetIntegerv(GL_MAX_TEXTURE_UNITS, &max_texture_stages);
    _max_texture_stages = max_texture_stages;

    if (GLCAT.is_debug()) {
      GLCAT.debug()
        << "max texture stages = " << _max_texture_stages << "\n";
    }
  }
#endif

#if !defined(OPENGLES_1) && !defined(__EMSCRIPTEN__)
  _max_vertex_attrib_stride = -1;
#ifdef OPENGLES
  if (is_at_least_gles_version(3, 1))
#else
  if (is_at_least_gl_version(4, 4))
#endif
  {
    glGetIntegerv(GL_MAX_VERTEX_ATTRIB_STRIDE, &_max_vertex_attrib_stride);

    if (_max_vertex_attrib_stride < 0) {
      GLCAT.warning()
        << "Failed to query GL_MAX_VERTEX_ATTRIB_STRIDE.\n";
    }
    else if (GLCAT.is_debug()) {
      GLCAT.debug()
        << "max vertex attrib stride = " << _max_vertex_attrib_stride << "\n";
    }
  }
  if (_max_vertex_attrib_stride < 0) {
    // OpenGL doesn't specify a maximum before version 4.4 / ES 3.1, but
    // drivers really do have one.  Make an educated guess.
#ifdef OPENGLES
    _max_vertex_attrib_stride = (_gl_vendor == "Qualcomm") ? INT_MAX : 2048;
#elif defined(_WIN32)
    _max_vertex_attrib_stride = (_gl_vendor == "Intel") ? 4095 : 2048;
#else
    _max_vertex_attrib_stride = 2048;
#endif
    if (GLCAT.is_debug()) {
      GLCAT.debug()
        << "max vertex attrib stride = " << _max_vertex_attrib_stride
        << " (guessed)\n";
    }
  }
#endif  // !OPENGLES_1

  _current_vbuffer_index = 0;
  _current_ibuffer_index = 0;
  _current_vao_index = 0;
  _current_fbo = 0;
  _auto_antialias_mode = false;
  _render_mode = RenderModeAttrib::M_filled;
  _point_size = 1.0f;
  _point_perspective = false;

#ifndef OPENGLES
  _current_vertex_buffers.clear();
  _current_vertex_format.clear();
  memset(_vertex_attrib_columns, 0, sizeof(const GeomVertexColumn *) * 32);

  _current_sbuffer_index = 0;
  _current_sbuffer_base.clear();
#endif

  report_my_gl_errors();

#ifdef SUPPORT_FIXED_FUNCTION
  if (has_fixed_function_pipeline()) {
    if (gl_cheap_textures) {
      GLCAT.info()
        << "Setting glHint() for fastest textures.\n";
      glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_FASTEST);
    }

    // Use per-vertex fog if per-pixel fog requires SW renderer
    glHint(GL_FOG_HINT, GL_DONT_CARE);
  }
#endif

#ifdef SUPPORT_FIXED_FUNCTION
  if (has_fixed_function_pipeline()) {
    GLint num_red_bits = 0;
    glGetIntegerv(GL_RED_BITS, &num_red_bits);
    if (num_red_bits < 8) {
      glEnable(GL_DITHER);
      _dithering_enabled = true;
      if (GLCAT.is_debug()) {
        GLCAT.debug()
          << "frame buffer depth = " << num_red_bits
          << " bits/channel, enabling dithering\n";
      }
    }
  }
#endif

  _error_count = 0;

  report_my_gl_errors();

#if !defined(OPENGLES_1) && !defined(__EMSCRIPTEN__)
  if (GLCAT.is_debug()) {
    if (_supports_get_program_binary) {
      GLCAT.debug()
        << "Supported program binary formats:\n";
      GLCAT.debug() << " ";

      pset<GLenum>::const_iterator it;
      for (it = _program_binary_formats.begin();
           it != _program_binary_formats.end(); ++it) {
        char number[16];
        sprintf(number, "0x%04X", *it);
        GLCAT.debug(false) << " " << number << "";
      }
      GLCAT.debug(false) << "\n";
    } else {
      GLCAT.debug() << "No program binary formats supported.\n";
    }
  }
#endif

  // Do we guarantee that we can apply the color scale via a shader?  We set
  // this false if there is a chance that the fixed-function pipeline is used.
  _runtime_color_scale = !has_fixed_function_pipeline();

#ifndef OPENGLES
  if (_gl_shadlang_ver_major >= 4 || has_extension("GL_NV_gpu_program5")) {
    // gp5fp - OpenGL fragment profile for GeForce 400 Series and up
    _shader_model = SM_50;

  } else if (_gl_shadlang_ver_major >= 3 ||
             has_extension("GL_NV_gpu_program4")) {
    // gp4fp - OpenGL fragment profile for G8x (GeForce 8xxx and up)
    _shader_model = SM_40;

  } else if (has_extension("GL_NV_fragment_program2")) {
    // fp40 - OpenGL fragment profile for NV4x (GeForce 6xxx and 7xxx Series,
    // NV4x-based Quadro FX, etc.)
    _shader_model = SM_30;

  } else if (has_extension("GL_NV_fragment_program")) {
    // fp30 - OpenGL fragment profile for NV3x (GeForce FX, Quadro FX, etc.)
    _shader_model = SM_2X;

  } else if (_gl_shadlang_ver_major >= 1 ||
             has_extension("GL_ARB_fragment_program")) {
    // This OpenGL profile corresponds to the per-fragment functionality
    // introduced by GeForce FX and other DirectX 9 GPUs.
    _shader_model = SM_20;

  } else if (has_extension("GL_NV_texture_shader2")) {
    // fp20 - OpenGL fragment profile for NV2x (GeForce3, GeForce4 Ti, Quadro
    // DCC, etc.)
    _shader_model = SM_11;

  } else {
    // No shader support
    _shader_model = SM_00;
  }

  // DisplayInformation may have better shader model detection
  {
    GraphicsPipe *pipe;
    DisplayInformation *display_information;

    pipe = this->get_pipe();
    if (pipe) {
      display_information = pipe->get_display_information ();
      if (display_information) {
        if (display_information->get_shader_model() > _shader_model) {
          _shader_model = display_information->get_shader_model();
        }
      }
    }
  }
  _auto_detect_shader_model = _shader_model;

  if (GLCAT.is_debug()) {
#ifdef HAVE_CG
#if CG_VERSION_NUM >= 2200
    GLCAT.debug() << "Supported Cg profiles:\n";
    int num_profiles = cgGetNumSupportedProfiles();
    for (int i = 0; i < num_profiles; ++i) {
      CGprofile profile = cgGetSupportedProfile(i);
      if (cgGLIsProfileSupported(profile)) {
        GLCAT.debug() << "  " << cgGetProfileString(profile) << "\n";
      }
    }
#endif  // CG_VERSION_NUM >= 2200

#if CG_VERSION_NUM >= 3100
    GLCAT.debug() << "Cg GLSL version = "
      << cgGLGetGLSLVersionString(cgGLDetectGLSLVersion()) << "\n";
#endif

    GLCAT.debug()
      << "Cg latest vertex profile = "
      << cgGetProfileString(cgGLGetLatestProfile(CG_GL_VERTEX)) << "\n";
    GLCAT.debug()
      << "Cg latest fragment profile = "
      << cgGetProfileString(cgGLGetLatestProfile(CG_GL_FRAGMENT)) << "\n";
#if CG_VERSION_NUM >= 2000
    GLCAT.debug()
      << "Cg latest geometry profile = "
      << cgGetProfileString(cgGLGetLatestProfile(CG_GL_GEOMETRY)) << "\n";
#endif
    GLCAT.debug() << "basic-shaders-only " << basic_shaders_only << "\n";
    GLCAT.debug()
      << "Cg active vertex profile = "
      << cgGetProfileString((CGprofile)_shader_caps._active_vprofile) << "\n";
    GLCAT.debug()
      << "Cg active fragment profile = "
      << cgGetProfileString((CGprofile)_shader_caps._active_fprofile) << "\n";
    GLCAT.debug()
      << "Cg active geometry profile = "
      << cgGetProfileString((CGprofile)_shader_caps._active_gprofile) << "\n";
#endif  // HAVE_CG

    GLCAT.debug() << "shader model = " << _shader_model << "\n";
  }
#endif  // !OPENGLES

  // OpenGL core profile requires a VAO to be bound.  It's a bit silly,
  // because we can just bind a VAO and then forget about it.
#if !defined(OPENGLES)
  if (core_profile) {
    if (_supports_vao) {
      _glGenVertexArrays(1, &_current_vao_index);
      _glBindVertexArray(_current_vao_index);
    } else {
      GLCAT.error()
        << "Core profile enabled, but vertex array objects not supported!\n";
    }
  }
#endif

  pop_group_marker();

  // Now that the GSG has been initialized, make it available for
  // optimizations.
  add_gsg(this);
}

/**
 * Force the graphics card to finish drawing before returning.  !!!!!HACK
 * WARNING!!!! glfinish does not actually wait for the graphics card to finish
 * drawing only for draw calls to finish.  Thus flip may not happene
 * immediately.  Instead we read a single pixel from the framebuffer.  This
 * forces the graphics card to finish drawing the frame before returning.
 */
void CLP(GraphicsStateGuardian)::
finish() {
  // Rather than call glfinish which returns immediately if draw commands have
  // been submitted, we will read a single pixel from the frame.  That will
  // force the graphics card to finish drawing before it is called
  char data[4];
  glReadPixels(0,0,1,1,GL_RGBA,GL_UNSIGNED_BYTE,&data);
  // glFinish();
}

/**
 * Clears the framebuffer within the current DisplayRegion, according to the
 * flags indicated by the given DrawableRegion object.
 *
 * This does not set the DisplayRegion first.  You should call
 * prepare_display_region() to specify the region you wish the clear operation
 * to apply to.
 */
void CLP(GraphicsStateGuardian)::
clear(DrawableRegion *clearable) {
  report_my_gl_errors();

  if (!clearable->is_any_clear_active()) {
    return;
  }

  // XXX rdb: Is this line really necessary?  Could we perhaps just reset the
  // color write mask and other relevant attributes?
  set_state_and_transform(RenderState::make_empty(), _internal_transform);

  int mask = 0;

#ifndef OPENGLES_1
  if (_current_fbo != 0 && _glClearBufferfv != nullptr) {
    // We can use glClearBuffer to clear all the color attachments, which
    // protects us from the overhead of having to call set_draw_buffer for
    // every single attachment.
    int index = 0;

    if (_current_properties->get_color_bits() > 0) {
      if (_current_properties->is_stereo()) {
        // Clear both left and right attachments.
        if (clearable->get_clear_active(GraphicsOutput::RTP_color)) {
          LColorf v = LCAST(float, clearable->get_clear_value(GraphicsOutput::RTP_color));
          _glClearBufferfv(GL_COLOR, index, v.get_data());
          _glClearBufferfv(GL_COLOR, index + 1, v.get_data());
        }
        index += 2;
      } else {
        if (clearable->get_clear_active(GraphicsOutput::RTP_color)) {
          LColorf v = LCAST(float, clearable->get_clear_value(GraphicsOutput::RTP_color));
          _glClearBufferfv(GL_COLOR, index, v.get_data());
        }
        ++index;
      }
    }
    for (int i = 0; i < _current_properties->get_aux_rgba(); ++i) {
      int layerid = GraphicsOutput::RTP_aux_rgba_0 + i;
      if (clearable->get_clear_active(layerid)) {
        LColorf v = LCAST(float, clearable->get_clear_value(layerid));
        _glClearBufferfv(GL_COLOR, index, v.get_data());
      }
      ++index;
    }
    for (int i = 0; i < _current_properties->get_aux_hrgba(); ++i) {
      int layerid = GraphicsOutput::RTP_aux_hrgba_0 + i;
      if (clearable->get_clear_active(layerid)) {
        LColorf v = LCAST(float, clearable->get_clear_value(layerid));
        _glClearBufferfv(GL_COLOR, index, v.get_data());
      }
      ++index;
    }
    for (int i = 0; i < _current_properties->get_aux_float(); ++i) {
      int layerid = GraphicsOutput::RTP_aux_float_0 + i;
      if (clearable->get_clear_active(layerid)) {
        LColorf v = LCAST(float, clearable->get_clear_value(layerid));
        _glClearBufferfv(GL_COLOR, index, v.get_data());
      }
      ++index;
    }
  } else
#endif
  {
    if (_current_properties->get_aux_mask() != 0) {
      for (int i = 0; i < _current_properties->get_aux_rgba(); ++i) {
        int layerid = GraphicsOutput::RTP_aux_rgba_0 + i;
        int layerbit = RenderBuffer::T_aux_rgba_0 << i;
        if (clearable->get_clear_active(layerid)) {
          LColor v = clearable->get_clear_value(layerid);
          glClearColor(v[0], v[1], v[2], v[3]);
          set_draw_buffer(layerbit);
          glClear(GL_COLOR_BUFFER_BIT);
        }
      }
      for (int i = 0; i < _current_properties->get_aux_hrgba(); ++i) {
        int layerid = GraphicsOutput::RTP_aux_hrgba_0 + i;
        int layerbit = RenderBuffer::T_aux_hrgba_0 << i;
        if (clearable->get_clear_active(layerid)) {
          LColor v = clearable->get_clear_value(layerid);
          glClearColor(v[0], v[1], v[2], v[3]);
          set_draw_buffer(layerbit);
          glClear(GL_COLOR_BUFFER_BIT);
        }
      }
      for (int i = 0; i < _current_properties->get_aux_float(); ++i) {
        int layerid = GraphicsOutput::RTP_aux_float_0 + i;
        int layerbit = RenderBuffer::T_aux_float_0 << i;
        if (clearable->get_clear_active(layerid)) {
          LColor v = clearable->get_clear_value(layerid);
          glClearColor(v[0], v[1], v[2], v[3]);
          set_draw_buffer(layerbit);
          glClear(GL_COLOR_BUFFER_BIT);
        }
      }

      // In the past, it was possible to set the draw buffer once in
      // prepare_display_region and then forget about it.  Now, with aux
      // layers, it is necessary to occasionally change the draw buffer.  In
      // time, I think there will need to be a draw buffer attrib.  Until
      // then, this little hack to put things back the way they were after
      // prepare_display_region will do.

      set_draw_buffer(_draw_buffer_type);
    }

    if (_current_properties->get_color_bits() > 0) {
      if (clearable->get_clear_color_active()) {
        LColor v = clearable->get_clear_color();
        glClearColor(v[0], v[1], v[2], v[3]);
        clear_color_write_mask();
        _state_mask.clear_bit(ColorWriteAttrib::get_class_slot());
        mask |= GL_COLOR_BUFFER_BIT;
      }
    }
  }

  if (clearable->get_clear_depth_active()) {
#ifdef OPENGLES
    glClearDepthf(clearable->get_clear_depth());
#else
    glClearDepth(clearable->get_clear_depth());
#endif  // OPENGLES
#ifdef GSG_VERBOSE
    GLCAT.spam()
      << "glDepthMask(GL_TRUE)" << endl;
#endif
    glDepthMask(GL_TRUE);
    _state_mask.clear_bit(DepthWriteAttrib::get_class_slot());
    mask |= GL_DEPTH_BUFFER_BIT;
  }

  if (_supports_stencil && clearable->get_clear_stencil_active()) {
    glStencilMask(~0);
    glClearStencil(clearable->get_clear_stencil());
    mask |= GL_STENCIL_BUFFER_BIT;
  }

  if (mask != 0) {
    glClear(mask);

    if (GLCAT.is_spam()) {
      string clear_flags;
      if (mask & GL_COLOR_BUFFER_BIT) {
        clear_flags += " | GL_COLOR_BUFFER_BIT";
      }
      if (mask & GL_DEPTH_BUFFER_BIT) {
        clear_flags += " | GL_DEPTH_BUFFER_BIT";
      }
      if (mask & GL_STENCIL_BUFFER_BIT) {
        clear_flags += " | GL_STENCIL_BUFFER_BIT";
      }
  #ifndef OPENGLES
      if (mask & GL_ACCUM_BUFFER_BIT) {
        clear_flags += " | GL_ACCUM_BUFFER_BIT";
      }
  #endif
      GLCAT.spam() << "glClear(" << (clear_flags.c_str() + 3) << ")\n";
    }
  }

  report_my_gl_errors();
}

/**
 * Prepare a display region for rendering (set up scissor region and viewport)
 */
void CLP(GraphicsStateGuardian)::
prepare_display_region(DisplayRegionPipelineReader *dr) {
  nassertv(dr != nullptr);
  GraphicsStateGuardian::prepare_display_region(dr);

  int l, b, w, h;
  dr->get_region_pixels(l, b, w, h);
  _viewport_x = l;
  _viewport_y = b;
  _viewport_width = w;
  _viewport_height = h;
  GLint x = GLint(l);
  GLint y = GLint(b);
  GLsizei width = GLsizei(w);
  GLsizei height = GLsizei(h);

  _draw_buffer_type = dr->get_object()->get_draw_buffer_type() & _current_properties->get_buffer_mask() & _stereo_buffer_mask;
  _draw_buffer_type |= _current_properties->get_aux_mask();
  set_draw_buffer(_draw_buffer_type);

  int count = dr->get_num_regions();

  if (dr->get_scissor_enabled()) {
    if (GLCAT.is_spam()) {
      GLCAT.spam()
        << "glEnable(GL_SCISSOR_TEST)\n";
    }
    glEnable(GL_SCISSOR_TEST);
    _scissor_enabled = true;
    _scissor_array.resize(count);
  } else {
    if (GLCAT.is_spam()) {
      GLCAT.spam()
        << "glDisable(GL_SCISSOR_TEST)\n";
    }
    glDisable(GL_SCISSOR_TEST);
    _scissor_enabled = false;
    _scissor_array.clear();
  }

  _scissor_attrib_active = false;

#ifndef OPENGLES
  if (_supports_viewport_arrays) {

    GLfloat *viewports = (GLfloat *)alloca(sizeof(GLfloat) * 4 * count);

    // We store the scissor regions in a vector since we may need to switch
    // back to it in do_issue_scissor.
    for (int i = 0; i < count; ++i) {
      LVecBase4i sr;
      dr->get_region_pixels(i, sr[0], sr[1], sr[2], sr[3]);
      GLfloat *vr = viewports + i * 4;
      vr[0] = (GLfloat) sr[0];
      vr[1] = (GLfloat) sr[1];
      vr[2] = (GLfloat) sr[2];
      vr[3] = (GLfloat) sr[3];
      if (_scissor_enabled) {
        _scissor_array[i] = sr;
      }
    }
    _glViewportArrayv(0, count, viewports);
    if (_scissor_enabled) {
      _glScissorArrayv(0, count, _scissor_array[0].get_data());
    }

    if (GLCAT.is_spam()) {
      GLCAT.spam()
        << "glViewportArrayv(0, " << count << ",";
      for (int i = 0; i < count; ++i) {
        GLfloat *vr = viewports + i * 4;
        GLCAT.spam(false) << " [" << vr[0] << " " << vr[1] << " " << vr[2] << " " << vr[3] << "]";
      }
      GLCAT.spam(false) << ")\n";
      if (_scissor_enabled) {
        GLCAT.spam()
          << "glScissorArrayv(0, " << count << ",";
        for (int i = 0; i < count; ++i) {
          const LVecBase4i &sr = _scissor_array[i];
          GLCAT.spam(false) << " [" << sr << "]";
        }
        GLCAT.spam(false) << ")\n";
      }
    }

  } else
#endif  // OPENGLES
  {
    glViewport(x, y, width, height);
    if (_scissor_enabled) {
      glScissor(x, y, width, height);

      _scissor_array.resize(1);
      _scissor_array[0].set(x, y, width, height);
    }

    if (GLCAT.is_spam()) {
      GLCAT.spam()
        << "glViewport(" << x << ", " << y << ", " << width << ", " << height << ")\n";
      if (dr->get_scissor_enabled()) {
        GLCAT.spam()
          << "glScissor(" << x << ", " << y << ", " << width << ", " << height << ")\n";
      }
    }
  }

  PN_stdfloat nearv;
  PN_stdfloat farv;
  dr->get_depth_range(nearv, farv);
#ifdef GSG_VERBOSE
  if (GLCAT.is_spam()) {
    GLCAT.spam()
      << "glDepthRange(" << nearv << ", " << farv << ")" << endl;
  }
#endif

#ifdef OPENGLES
  // OpenGL ES uses a single-precision call.
  glDepthRangef((GLclampf)nearv, (GLclampf)farv);
#else
  // Mainline OpenGL uses a double-precision call.
  if (!_use_remapped_depth_range) {
    glDepthRange((GLclampd)nearv, (GLclampd)farv);
  } else {
    // If we have a remapped depth range, we should adjust the values to range
    // from -1 to 1.  We need to use an NV extension to pass unclamped values.
    _glDepthRangedNV(nearv * 2.0 - 1.0, farv * 2.0 - 1.0);
  }
#endif  // OPENGLES
  _has_attrib_depth_range = false;
  _depth_range_near = nearv;
  _depth_range_far = farv;

  report_my_gl_errors();
}

/**
 * Resets any non-standard graphics state that might give a callback apoplexy.
 * Some drivers require that the graphics state be restored to neutral before
 * performing certain operations.  In OpenGL, for instance, this closes any
 * open vertex buffers.
 */
void CLP(GraphicsStateGuardian)::
clear_before_callback() {
#ifdef SUPPORT_FIXED_FUNCTION
  if (has_fixed_function_pipeline()) {
    disable_standard_vertex_arrays();
  }
#endif
#ifndef OPENGLES_1
  if (_vertex_array_shader_context != 0) {
    _vertex_array_shader_context->disable_shader_vertex_arrays();
    _vertex_array_shader = nullptr;
    _vertex_array_shader_context = nullptr;
  }
#endif
  unbind_buffers();

  // Some callbacks may quite reasonably assume that the active texture stage
  // is still set to stage 0.  CEGUI, in particular, makes this assumption.
  set_active_texture_stage(0);
#ifdef SUPPORT_FIXED_FUNCTION
  _glClientActiveTexture(GL_TEXTURE0);
#endif

  // It's also quite reasonable to presume there aren't any funny color write
  // mask settings active.
  clear_color_write_mask();

  // Clear the bound sampler object, so that we do not inadvertently override
  // the callback's desired sampler settings.
#ifndef OPENGLES_1
  if (_supports_sampler_objects) {
    _glBindSampler(0, 0);

    if (GLCAT.is_spam()) {
      GLCAT.spam()
        << "glBindSampler(0, 0)\n";
    }
  }
#endif
}

/**
 * Given a lens, calculates the appropriate projection matrix for use with
 * this gsg.  Note that the projection matrix depends a lot upon the
 * coordinate system of the rendering API.
 *
 * The return value is a TransformState if the lens is acceptable, NULL if it
 * is not.
 */
CPT(TransformState) CLP(GraphicsStateGuardian)::
calc_projection_mat(const Lens *lens) {
  if (lens == nullptr) {
    return nullptr;
  }

  if (!lens->is_linear()) {
    return nullptr;
  }

  // The projection matrix must always be right-handed Y-up, even if our
  // coordinate system of choice is otherwise, because certain GL calls
  // (specifically glTexGen(GL_SPHERE_MAP)) assume this kind of a coordinate
  // system.  Sigh.  In order to implement a Z-up (or other arbitrary)
  // coordinate system, we'll use a Y-up projection matrix, and store the
  // conversion to our coordinate system of choice in the modelview matrix.

  LMatrix4 result =
    LMatrix4::convert_mat(_internal_coordinate_system,
                          lens->get_coordinate_system()) *
    lens->get_projection_mat(_current_stereo_channel);

#ifndef OPENGLES
  if (_use_depth_zero_to_one) {
    // If we requested that the OpenGL NDC Z goes from zero to one like in
    // Direct3D, we need to scale the projection matrix, which assumes -1..1.
    static const LMatrix4 rescale_mat
      (1, 0, 0, 0,
       0, 1, 0, 0,
       0, 0, 0.5, 0,
       0, 0, 0.5, 1);
    result *= rescale_mat;
  }
#endif

  if (_scene_setup->get_inverted()) {
    // If the scene is supposed to be inverted, then invert the projection
    // matrix.
    result *= LMatrix4::scale_mat(1.0f, -1.0f, 1.0f);
  }

  return TransformState::make_mat(result);
}

/**
 * Makes the current lens (whichever lens was most recently specified with
 * set_scene()) active, so that it will transform future rendered geometry.
 * Normally this is only called from the draw process, and usually it is
 * called by set_scene().
 *
 * The return value is true if the lens is acceptable, false if it is not.
 */
bool CLP(GraphicsStateGuardian)::
prepare_lens() {
#ifdef SUPPORT_FIXED_FUNCTION
  if (has_fixed_function_pipeline()) {
    if (GLCAT.is_spam()) {
      GLCAT.spam()
        << "glMatrixMode(GL_PROJECTION): " << _projection_mat->get_mat() << endl;
    }

    glMatrixMode(GL_PROJECTION);
    call_glLoadMatrix(_projection_mat->get_mat());
    report_my_gl_errors();

    do_point_size();
  }
#endif

  return true;
}

/**
 * Called before each frame is rendered, to allow the GSG a chance to do any
 * internal cleanup before beginning the frame.
 *
 * The return value is true if successful (in which case the frame will be
 * drawn and end_frame() will be called later), or false if unsuccessful (in
 * which case nothing will be drawn and end_frame() will not be called).
 */
bool CLP(GraphicsStateGuardian)::
begin_frame(Thread *current_thread) {
  if (!GraphicsStateGuardian::begin_frame(current_thread)) {
    return false;
  }
  _renderbuffer_residency.begin_frame(current_thread);

  report_my_gl_errors();

#ifdef DO_PSTATS
  _vertices_display_list_pcollector.clear_level();
  _vertices_immediate_pcollector.clear_level();
  _primitive_batches_display_list_pcollector.clear_level();
#endif

  if (!_async_ram_copies.empty()) {
    finish_async_framebuffer_ram_copies();
  }

#if defined(DO_PSTATS) && !defined(OPENGLES)
  int frame_number = ClockObject::get_global_clock()->get_frame_count(current_thread);
  if (_current_frame_timing == nullptr ||
      frame_number != _current_frame_timing->_frame_number) {

    _current_frame_timing = begin_frame_timing(frame_number);
  }
#endif

#ifndef NDEBUG
  _show_texture_usage = false;
  if (gl_show_texture_usage) {
    // When this is true, then every other second, we show the usage textures
    // instead of the real textures.
    double now = ClockObject::get_global_clock()->get_frame_time(current_thread);
    int this_second = (int)floor(now);
    if (this_second & 1) {
      _show_texture_usage = true;
      _show_texture_usage_index = this_second >> 1;

      int max_size = gl_show_texture_usage_max_size;
      if (max_size != _show_texture_usage_max_size) {
        // Remove the cache of usage textures; we've changed the max size.
        UsageTextures::iterator ui;
        for (ui = _usage_textures.begin();
             ui != _usage_textures.end();
             ++ui) {
          GLuint index = (*ui).second;
          glDeleteTextures(1, &index);
        }
        _usage_textures.clear();
        _show_texture_usage_max_size = max_size;
      }
    }
  }
#endif  // NDEBUG

#ifndef OPENGLES
  if (_current_properties->get_srgb_color()) {
    glEnable(GL_FRAMEBUFFER_SRGB);
  }
#endif

  report_my_gl_errors();
  return true;
}

/**
 * Called between begin_frame() and end_frame() to mark the beginning of
 * drawing commands for a "scene" (usually a particular DisplayRegion) within
 * a frame.  All 3-D drawing commands, except the clear operation, must be
 * enclosed within begin_scene() .. end_scene().
 *
 * The return value is true if successful (in which case the scene will be
 * drawn and end_scene() will be called later), or false if unsuccessful (in
 * which case nothing will be drawn and end_scene() will not be called).
 */
bool CLP(GraphicsStateGuardian)::
begin_scene() {
  return GraphicsStateGuardian::begin_scene();
}

/**
 * Called between begin_frame() and end_frame() to mark the end of drawing
 * commands for a "scene" (usually a particular DisplayRegion) within a frame.
 * All 3-D drawing commands, except the clear operation, must be enclosed
 * within begin_scene() .. end_scene().
 */
void CLP(GraphicsStateGuardian)::
end_scene() {
  GraphicsStateGuardian::end_scene();

  _dlights.clear();
  report_my_gl_errors();
}

/**
 * Called after each frame is rendered, to allow the GSG a chance to do any
 * internal cleanup after rendering the frame, and before the window flips.
 */
void CLP(GraphicsStateGuardian)::
end_frame(Thread *current_thread) {
  report_my_gl_errors();

#ifndef OPENGLES
  if (_current_properties->get_srgb_color()) {
    glDisable(GL_FRAMEBUFFER_SRGB);
  }
#endif

#ifdef DO_PSTATS
  // Check for textures, etc., that are no longer resident.  These calls might
  // be measurably expensive, and they don't have any benefit unless we are
  // actually viewing PStats, so don't do them unless we're connected.  That
  // will just mean that we'll count everything as resident until the user
  // connects PStats, at which point it will then correct the assessment.  No
  // harm done.
  if (has_fixed_function_pipeline() && PStatClient::is_connected()) {
    PStatTimer timer(_check_residency_pcollector);
    check_nonresident_texture(_prepared_objects->_texture_residency.get_inactive_resident());
    check_nonresident_texture(_prepared_objects->_texture_residency.get_active_resident());

    // OpenGL provides no methods for querying whether a buffer object (vertex
    // buffer) is resident.  In fact, the API appears geared towards the
    // assumption that such buffers are always resident.  OK.
  }
#endif

#ifndef OPENGLES_1
  // This breaks shaders across multiple regions.
  if (_vertex_array_shader_context != 0) {
    _vertex_array_shader_context->disable_shader_vertex_arrays();
    _vertex_array_shader = nullptr;
    _vertex_array_shader_context = nullptr;
  }
  if (_texture_binding_shader_context != 0) {
    _texture_binding_shader_context->disable_shader_texture_bindings();
    _texture_binding_shader = nullptr;
    _texture_binding_shader_context = nullptr;
  }
  if (_current_shader_context != 0) {
    _current_shader_context->unbind();
    _current_shader = nullptr;
    _current_shader_context = nullptr;
  }
  _state_shader = nullptr;
#endif

  // Respecify the active texture next frame, for good measure.
  _active_texture_stage = -1;

  // Calling glFlush() at the end of the frame is particularly necessary if
  // this is a single-buffered visual, so that the frame will be finished
  // drawing before we return to the application.  It's not clear what effect
  // this has on our total frame time.  if (_force_flush ||
  // _current_properties->is_single_buffered()) { gl_flush(); }
  maybe_gl_finish();

  GraphicsStateGuardian::end_frame(current_thread);

  _renderbuffer_residency.end_frame(current_thread);

  // Flush any PCollectors specific to this kind of GSG.
  _primitive_batches_display_list_pcollector.flush_level();
  _vertices_display_list_pcollector.flush_level();
  _vertices_immediate_pcollector.flush_level();

  // Now is a good time to delete any pending display lists.
#ifndef OPENGLES
#ifdef SUPPORT_FIXED_FUNCTION
  if (has_fixed_function_pipeline() && display_lists) {
    LightMutexHolder holder(_lock);
    if (!_deleted_display_lists.empty()) {
      DeletedNames::iterator ddli;
      for (ddli = _deleted_display_lists.begin();
           ddli != _deleted_display_lists.end();
           ++ddli) {
        if (GLCAT.is_debug()) {
          GLCAT.debug()
            << "releasing display list index " << (int)(*ddli) << "\n";
        }
        glDeleteLists((*ddli), 1);
      }
      _deleted_display_lists.clear();
    }
  }
#endif

  // And deleted queries, too, unless we're using query timers in which case
  // we'll need to reuse lots of them.
  if (_supports_occlusion_query && !get_timer_queries_active()) {
    LightMutexHolder holder(_lock);
    if (!_deleted_queries.empty()) {
      if (GLCAT.is_spam()) {
        DeletedNames::iterator dqi;
        for (dqi = _deleted_queries.begin();
             dqi != _deleted_queries.end();
             ++dqi) {
          GLCAT.spam()
            << "releasing query index " << (int)(*dqi) << "\n";
        }
      }
      _glDeleteQueries(_deleted_queries.size(), &_deleted_queries[0]);
      _deleted_queries.clear();
    }
  }
#endif  // OPENGLES

#ifndef OPENGLES_1
  if (!_deleted_buffers.empty()) {
    GLuint *indices = (GLuint *)alloca(sizeof(GLuint *) * _deleted_buffers.size());
    size_t num_indices = 0;
    DeletedBuffers::iterator it = _deleted_buffers.begin();
    while (it != _deleted_buffers.end()) {
      DeletedBuffer &buffer = *it;
      if (!_supports_buffer_storage && buffer._mapped_pointer != nullptr) {
        _glBindBuffer(GL_PIXEL_PACK_BUFFER, buffer._index);
        _glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
        buffer._mapped_pointer = nullptr;
      }
      if (++buffer._age > 2) {
        indices[num_indices++] = buffer._index;
        it = _deleted_buffers.erase(it);
        _inactive_ppbuffer_memory_pcollector.sub_level(buffer._size);
      } else {
        ++it;
      }
    }
    if (!_supports_buffer_storage) {
      _glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
    }
    if (num_indices > 0) {
      _glDeleteBuffers(num_indices, indices);
    }
  }

  _active_ppbuffer_memory_pcollector.flush_level();
  _inactive_ppbuffer_memory_pcollector.flush_level();
#endif

#ifndef NDEBUG
  if (_check_errors || (_supports_debug && gl_debug)) {
    report_my_gl_errors();
  } else {
    // If _check_errors is false, we still want to check for errors once every
    // second, so that we know if anything went wrong at all.
    double current = ClockObject::get_global_clock()->get_frame_time();

    if (current - _last_error_check >= 1.0) {
      _last_error_check = current;
      PStatTimer timer(_check_error_pcollector);

      GLenum error_code = glGetError();
      if (error_code != GL_NO_ERROR) {
        int error_count = 0;

        do {
          ++error_count;
          GLCAT.error()
            << "GL error 0x" << hex << error_code << dec << " : "
            << get_error_string(error_code) << "\n";
          error_code = glGetError();
        } while (error_code != GL_NO_ERROR);

        if (error_count == 1) {
          GLCAT.error()
            << "An OpenGL error has occurred.";
        } else {
          GLCAT.error()
            << error_count << " OpenGL errors have occurred.";
        }

        if (_supports_debug) {
          GLCAT.error(false) << "  Set gl-debug #t "
            << "in your PRC file to display more information.\n";
        } else {
          GLCAT.error(false) << "  Set gl-check-errors #t "
            << "in your PRC file to display more information.\n";
        }

        _error_count += error_count;
        if (_error_count >= gl_max_errors) {
          panic_deactivate();
        }
      }
    }
  }
#endif

  // Add in a newline to the spam output for improved legibility.
  if (GLCAT.is_spam()) {
    GLCAT.spam(false) << endl;
  }
}

/**
 *
 */
CLP(GraphicsStateGuardian)::FrameTiming *CLP(GraphicsStateGuardian)::
begin_frame_timing(int frame_number) {
#if defined(DO_PSTATS) && !defined(OPENGLES)
  if (!_timer_queries_active) {
    if (pstats_gpu_timing && _supports_timer_query && PStatClient::is_connected()) {
      _timer_queries_active = true;
    } else {
      return nullptr;
    }
  }

  PStatClient *client = PStatClient::get_global_pstats();
  PStatTimer timer(_wait_timer_pcollector);

  _timer_queries_pcollector.clear_level();

  if (_deleted_queries.size() < 128) {
    // We'll need a lot of timer queries, so allocate a whole bunch up front.
    size_t alloc_count = 128 - _deleted_queries.size();
    _deleted_queries.resize(_deleted_queries.size() + alloc_count);
    _glGenQueries(alloc_count, _deleted_queries.data() + _deleted_queries.size() - alloc_count);
  }

  // Issue a start query for collector 0, marking the start of this frame.
  GLuint frame_query = _deleted_queries.back();
  _deleted_queries.pop_back();
  _glQueryCounter(frame_query, GL_TIMESTAMP);

  // Synchronize the GL time with the PStats clock.  Note that the driver may
  // arbitrarily decide to wait a long time on this call if the queue is
  // saturated with work.  Experimentally, it seems that it queries the time
  // *after* the wait, so we take the CPU time after it returns.  If we find
  // that some drivers do it differently, we may have to do multiple calls.
  GLint64 gl_time;
  _glGetInteger64v(GL_TIMESTAMP, &gl_time);
  double cpu_time = client->get_real_time();

  // Check if the results from the previous frame are available.  We just need
  // to check whether the last query for each frame is available.
  while (!_frame_timings.empty()) {
    const FrameTiming &frame = _frame_timings.front();
    GLuint last_query = frame._queries.back().first;
    GLuint result;
    _glGetQueryObjectuiv(last_query, GL_QUERY_RESULT_AVAILABLE, &result);
    if (result == 0) {
      // Not ready, so subsequent frames won't be, either.
      break;
    }
    // We've got a frame whose timer queries are ready.
    end_frame_timing(frame);
    _frame_timings.pop_front();
  }

  FrameTiming frame;
  frame._frame_number = frame_number;
  frame._gpu_sync_time = gl_time;
  frame._cpu_sync_time = cpu_time;
  frame._queries.push_back(std::make_pair(frame_query, 0));
  _frame_timings.push_back(std::move(frame));

  return &_frame_timings.back();
#else
  return nullptr;
#endif
}

/**
 * Gets the timer query results for the given frame and sends them to the
 * PStats server.
 */
void CLP(GraphicsStateGuardian)::
end_frame_timing(const FrameTiming &frame) {
#if defined(DO_PSTATS) && !defined(OPENGLES)
  // This uses the lower-level PStats interfaces for now because of all the
  // unnecessary overhead that would otherwise be incurred when adding such a
  // large amount of data at once.
  if (!PStatClient::is_connected()) {
    _timer_queries_active = false;
    return;
  }

  // We represent each GSG as one thread.  In the future we may change this to
  // representing each graphics device as one thread, but OpenGL doesn't really
  // expose this information to us.
  PStatThread gpu_thread = get_pstats_thread();

  PStatFrameData frame_data;
  size_t latency_ref_i = 0;

  for (auto &query : frame._queries) {
    GLuint64 time_ns;
    _glGetQueryObjectui64v(query.first, GL_QUERY_RESULT, &time_ns);

    if (query.second & 0x10000) {
      // Latency query.
      GLint64 ref = frame._latency_refs[latency_ref_i++];
      double time = ((GLint64)time_ns - ref) * 0.000001;
      frame_data.add_level(query.second & 0x7fff, time);
    }
    else {
      // Convert GL time to Panda time.
      double time = ((GLint64)time_ns - frame._gpu_sync_time) * 0.000000001 + frame._cpu_sync_time;
      if (query.second & 0x8000) {
        frame_data.add_stop(query.second & 0x7fff, time);
      }
      else {
        frame_data.add_start(query.second & 0x7fff, time);
      }
    }
    _deleted_queries.push_back(query.first);
  }

  // The end time of the last collector is implicitly the frame's end time.
  frame_data.add_stop(0, frame_data.get_end());
  gpu_thread.add_frame(frame._frame_number, std::move(frame_data));

  _timer_queries_pcollector.add_level_now(frame._queries.size());
#endif
}

/**
 * Called before a sequence of draw_primitive() functions are called, this
 * should prepare the vertex data for rendering.  It returns true if the
 * vertices are ok, false to abort this group of primitives.
 */
bool CLP(GraphicsStateGuardian)::
begin_draw_primitives(const GeomPipelineReader *geom_reader,
                      const GeomVertexDataPipelineReader *data_reader,
                      size_t num_instances, bool force) {
#ifndef NDEBUG
  if (GLCAT.is_spam()) {
    GLCAT.spam() << "begin_draw_primitives: " << *(data_reader->get_object()) << "\n";
  }
#endif  // NDEBUG

#ifndef OPENGLES_1
  if (!has_fixed_function_pipeline()) {
    // We can't draw without a shader bound in OpenGL ES 2.  This shouldn't
    // happen anyway unless the default shader failed to compile somehow.
    if (_current_shader_context == nullptr) {
      return false;
    }
  }
#endif

  if (!GraphicsStateGuardian::begin_draw_primitives(geom_reader, data_reader, num_instances, force)) {
    return false;
  }
  nassertr(_data_reader != nullptr, false);

#ifndef OPENGLES_1
  _instance_count = _supports_geometry_instancing ? num_instances : 1;
#endif

  _geom_display_list = 0;

  if (_auto_antialias_mode) {
    switch (geom_reader->get_primitive_type()) {
    case GeomPrimitive::PT_polygons:
    case GeomPrimitive::PT_patches:
      setup_antialias_polygon();
      break;
    case GeomPrimitive::PT_points:
      setup_antialias_point();
      break;
    case GeomPrimitive::PT_lines:
      setup_antialias_line();
      break;
    case GeomPrimitive::PT_none:
      break;
    }

    int transparency_slot = TransparencyAttrib::get_class_slot();
    int color_write_slot = ColorWriteAttrib::get_class_slot();
    int color_blend_slot = ColorBlendAttrib::get_class_slot();
    if (!_state_mask.get_bit(transparency_slot) ||
        !_state_mask.get_bit(color_write_slot) ||
        !_state_mask.get_bit(color_blend_slot)) {
      do_issue_blending();
      _state_mask.set_bit(transparency_slot);
      _state_mask.set_bit(color_write_slot);
      _state_mask.set_bit(color_blend_slot);
    }
  }

#ifdef SUPPORT_FIXED_FUNCTION
  if (has_fixed_function_pipeline() && _data_reader->is_vertex_transformed()) {
    // If the vertex data claims to be already transformed into clip
    // coordinates, wipe out the current projection and modelview matrix (so
    // we don't attempt to transform it again).
    glMatrixMode(GL_PROJECTION);
    glPushMatrix();
    glLoadIdentity();
    glMatrixMode(GL_MODELVIEW);
    glPushMatrix();
    glLoadIdentity();
  }
#endif

#if !defined(OPENGLES) && defined(SUPPORT_FIXED_FUNCTION)  // Display lists not supported by OpenGL ES.
  if (has_fixed_function_pipeline() &&
      /*geom_reader->get_usage_hint() == Geom::UH_static &&*/
      _data_reader->get_usage_hint() == Geom::UH_static &&
      display_lists) {
    // If the geom claims to be totally static, try to build it into a display
    // list.

    // Before we compile or call a display list, make sure the current buffers
    // are unbound, or the nVidia drivers may crash.
    unbind_buffers();

    GeomContext *gc = geom_reader->prepare_now(get_prepared_objects(), this);
    nassertr(gc != nullptr, false);
    CLP(GeomContext) *ggc = DCAST(CLP(GeomContext), gc);
    //const CLP(GeomMunger) *gmunger = DCAST(CLP(GeomMunger), _munger);

    UpdateSeq modified = max(geom_reader->get_modified(), _data_reader->get_modified());
    if (ggc->get_display_list(_geom_display_list, nullptr, modified)) {
      // If it hasn't been modified, just play the display list again.
      if (GLCAT.is_spam()) {
        GLCAT.spam()
          << "calling display list " << (int)_geom_display_list << "\n";
      }

      glCallList(_geom_display_list);
#ifdef DO_PSTATS
      _vertices_display_list_pcollector.add_level(ggc->_num_verts);
      _primitive_batches_display_list_pcollector.add_level(1);
#endif

      // And now we don't need to do anything else for this geom.
      _geom_display_list = 0;
      end_draw_primitives();
      return false;
    }

    // Since we start this collector explicitly, we have to be sure to stop it
    // again.
    _load_display_list_pcollector.start();

    if (GLCAT.is_debug()) {
      GLCAT.debug()
        << "compiling display list " << (int)_geom_display_list << "\n";
    }

    // If it has been modified, or this is the first time, then we need to
    // build the display list up.
    if (gl_compile_and_execute) {
      glNewList(_geom_display_list, GL_COMPILE_AND_EXECUTE);
    } else {
      glNewList(_geom_display_list, GL_COMPILE);
    }

#ifdef DO_PSTATS
    // Count up the number of vertices used by primitives in the Geom, for
    // PStats reporting.
    ggc->_num_verts = 0;
    for (int i = 0; i < geom_reader->get_num_primitives(); i++) {
      ggc->_num_verts += geom_reader->get_primitive(i)->get_num_vertices();
    }
#endif
  }
#endif  // OPENGLES

  // Enable the appropriate vertex arrays, and disable any extra vertex arrays
  // used by the previous rendering mode.
#ifdef SUPPORT_IMMEDIATE_MODE
  _use_sender = !vertex_arrays;
#endif

#ifndef OPENGLES_1
  if (_use_vertex_attrib_binding) {
    const GeomVertexFormat *format = data_reader->get_format();
    if (format != _current_vertex_format) {
      update_shader_vertex_format(format);
    }
  }
#endif

  {
    // PStatGPUTimer timer(this, _vertex_array_update_pcollector);
#ifdef OPENGLES_1
    if (!update_standard_vertex_arrays(force)) {
      return false;
    }
#else
    if (_current_shader_context == 0) {
      // No shader.
      if (_vertex_array_shader_context != 0) {
        _vertex_array_shader_context->disable_shader_vertex_arrays();
      }
#ifdef SUPPORT_FIXED_FUNCTION
      if (has_fixed_function_pipeline() && !update_standard_vertex_arrays(force)) {
        return false;
      }
#endif
    } else {
#ifdef SUPPORT_FIXED_FUNCTION
      if (has_fixed_function_pipeline()) {
        // Shader.
        if (_vertex_array_shader_context == 0 ||
            _vertex_array_shader_context->uses_standard_vertex_arrays()) {
          // Previous shader used standard arrays.
          if (_current_shader_context->uses_standard_vertex_arrays()) {
            // So does the current, so update them.
            if (!update_standard_vertex_arrays(force)) {
              return false;
            }
          } else {
            // The current shader does not, so disable them entirely.
            disable_standard_vertex_arrays();
          }
        }
#ifdef HAVE_CG
        else if (_vertex_array_shader_context->is_of_type(CLP(CgShaderContext)::get_class_type())) {
          // The previous shader was a Cg shader, which can leave a messy
          // situation.
          _vertex_array_shader_context->disable_shader_vertex_arrays();
        }
#endif
      }
#endif  // SUPPORT_FIXED_FUNCTION
      // Now update the vertex arrays for the current shader.
      if (!_current_shader_context->
          update_shader_vertex_arrays(_vertex_array_shader_context, force)) {
        return false;
      }
    }

    _vertex_array_shader = _current_shader;
    _vertex_array_shader_context = _current_shader_context;
#endif  // OPENGLES_1
  }

  report_my_gl_errors();
  return true;
}

#ifdef SUPPORT_FIXED_FUNCTION
/**
 * Disables any unneeded vertex arrays that were previously enabled, and
 * enables any vertex arrays that are needed that were not previously enabled
 * (or, sets up an immediate-mode sender). Called only from
 * begin_draw_primitives.  Used only when the standard (non-shader) pipeline
 * is about to be used - glShaderContexts are responsible for setting up their
 * own vertex arrays.
 */
bool CLP(GraphicsStateGuardian)::
update_standard_vertex_arrays(bool force) {
#ifdef SUPPORT_IMMEDIATE_MODE
  if (_use_sender) {
    // We must use immediate mode to render primitives.
    _sender.clear();

    _sender.add_column(_data_reader, InternalName::get_normal(),
                       nullptr, nullptr, GLPf(Normal3), nullptr);
#ifndef NDEBUG
    if (_show_texture_usage) {
      // In show_texture_usage mode, all colors are white, so as not to
      // contaminate the texture color.
      GLPf(Color4)(1.0f, 1.0f, 1.0f, 1.0f);
    } else
#endif // NDEBUG
      if (!_sender.add_column(_data_reader, InternalName::get_color(),
                              nullptr, nullptr, GLPf(Color3), GLPf(Color4))) {
        // If we didn't have a color column, the item color is white.
        GLPf(Color4)(1.0f, 1.0f, 1.0f, 1.0f);
      }

    // Now set up each of the active texture coordinate stages--or at least
    // those for which we're not generating texture coordinates automatically.
    int max_stage_index = _target_texture->get_num_on_ff_stages();
    int stage_index = 0;
    while (stage_index < max_stage_index) {
      TextureStage *stage = _target_texture->get_on_ff_stage(stage_index);
      if (!_target_tex_gen->has_gen_texcoord_stage(stage)) {
        // This stage is not one of the stages that doesn't need texcoords
        // issued for it.
        const InternalName *name = stage->get_texcoord_name();
        if (stage_index == 0) {
          // Use the original functions for stage 0, in case we don't support
          // multitexture.
          _sender.add_column(_data_reader, name,
                             GLPf(TexCoord1), GLPf(TexCoord2),
                             GLPf(TexCoord3), GLPf(TexCoord4));

        } else {
          // Other stages require the multitexture functions.
          _sender.add_texcoord_column(_data_reader, name, stage_index,
                                      GLf(_glMultiTexCoord1), GLf(_glMultiTexCoord2),
                                      GLf(_glMultiTexCoord3), GLf(_glMultiTexCoord4));
        }
      }

      ++stage_index;
    }

    // Be sure also to disable any texture stages we had enabled before.
    while (stage_index < _last_max_stage_index) {
      _glClientActiveTexture(GL_TEXTURE0 + stage_index);
      glDisableClientState(GL_TEXTURE_COORD_ARRAY);
      ++stage_index;
    }
    _last_max_stage_index = max_stage_index;

    // We must add vertex last, because glVertex3f() is the key function call
    // that actually issues the vertex.
    _sender.add_column(_data_reader, InternalName::get_vertex(),
                       nullptr, GLPf(Vertex2), GLPf(Vertex3), GLPf(Vertex4));

  } else
#endif  // SUPPORT_IMMEDIATE_MODE
  {
    // We may use vertex arrays or buffers to render primitives.
    const GeomVertexArrayDataHandle *array_reader;
    const unsigned char *client_pointer;
    int num_values;
    Geom::NumericType numeric_type;
    int start;
    int stride;

    if (_data_reader->get_normal_info(array_reader, numeric_type,
                                      start, stride)) {
      if (!setup_array_data(client_pointer, array_reader, force)) {
        return false;
      }
      glNormalPointer(get_numeric_type(numeric_type), stride,
                      client_pointer + start);
      glEnableClientState(GL_NORMAL_ARRAY);
    } else {
      glDisableClientState(GL_NORMAL_ARRAY);
    }

#ifndef NDEBUG
    if (_show_texture_usage) {
      // In show_texture_usage mode, all colors are white, so as not to
      // contaminate the texture color.
      glDisableClientState(GL_COLOR_ARRAY);
      GLPf(Color4)(1.0f, 1.0f, 1.0f, 1.0f);
    } else
#endif // NDEBUG
      if (_vertex_colors_enabled &&
          _data_reader->get_color_info(array_reader, num_values, numeric_type,
                                       start, stride)) {
        if (!setup_array_data(client_pointer, array_reader, force)) {
          return false;
        }
        if (numeric_type == Geom::NT_packed_dabc) {
          glColorPointer(GL_BGRA, GL_UNSIGNED_BYTE,
                         stride, client_pointer + start);
        } else {
          glColorPointer(num_values, get_numeric_type(numeric_type),
                         stride, client_pointer + start);
        }
        glEnableClientState(GL_COLOR_ARRAY);
      } else {
        glDisableClientState(GL_COLOR_ARRAY);

        // Since we don't have per-vertex color, the implicit color is white.
        if (_color_scale_via_lighting) {
          GLPf(Color4)(1.0f, 1.0f, 1.0f, 1.0f);
        } else {
          LColor color = _scene_graph_color;
          color.componentwise_mult(_current_color_scale);
          GLPf(Color4)(color[0], color[1], color[2], color[3]);
        }
      }

    // Now set up each of the active texture coordinate stages--or at least
    // those for which we're not generating texture coordinates automatically.
    int max_stage_index = _target_texture->get_num_on_ff_stages();
    int stage_index = 0;
    while (stage_index < max_stage_index) {
      _glClientActiveTexture(GL_TEXTURE0 + stage_index);
      TextureStage *stage = _target_texture->get_on_ff_stage(stage_index);
      if (!_target_tex_gen->has_gen_texcoord_stage(stage)) {
        // This stage is not one of the stages that doesn't need texcoords
        // issued for it.
        const InternalName *name = stage->get_texcoord_name();

        if (_data_reader->get_array_info(name, array_reader, num_values,
                                         numeric_type, start, stride)) {
          // The vertex data does have texcoords for this stage.
          if (!setup_array_data(client_pointer, array_reader, force)) {
            return false;
          }
          glTexCoordPointer(num_values, get_numeric_type(numeric_type),
                               stride, client_pointer + start);
          glEnableClientState(GL_TEXTURE_COORD_ARRAY);

        } else {
          // The vertex data doesn't have texcoords for this stage (even
          // though they're needed).
          glDisableClientState(GL_TEXTURE_COORD_ARRAY);
        }
      } else {
        // No texcoords are needed for this stage.
        glDisableClientState(GL_TEXTURE_COORD_ARRAY);
      }

      ++stage_index;
    }

    // Be sure also to disable any texture stages we had enabled before.
    while (stage_index < _last_max_stage_index) {
      _glClientActiveTexture(GL_TEXTURE0 + stage_index);
      glDisableClientState(GL_TEXTURE_COORD_ARRAY);
      ++stage_index;
    }
    _last_max_stage_index = max_stage_index;

    // There's no requirement that we add vertices last, but we do anyway.
    if (_data_reader->get_vertex_info(array_reader, num_values, numeric_type,
                                      start, stride)) {
      if (!setup_array_data(client_pointer, array_reader, force)) {
        return false;
      }
      glVertexPointer(num_values, get_numeric_type(numeric_type),
                      stride, client_pointer + start);
      glEnableClientState(GL_VERTEX_ARRAY);
    }
  }
  return true;
}
#endif  // SUPPORT_FIXED_FUNCTION

/**
 * Ensures the vertex and array buffers are no longer bound.  Some graphics
 * drivers crash if these are left bound indiscriminantly.
 */
void CLP(GraphicsStateGuardian)::
unbind_buffers() {
  if (_current_vbuffer_index != 0) {
    if (GLCAT.is_spam() && gl_debug_buffers) {
      GLCAT.spam()
        << "unbinding vertex buffer\n";
    }
    _glBindBuffer(GL_ARRAY_BUFFER, 0);
    _current_vbuffer_index = 0;
  }
  if (_current_ibuffer_index != 0) {
    if (GLCAT.is_spam() && gl_debug_buffers) {
      GLCAT.spam()
        << "unbinding index buffer\n";
    }
    _glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    _current_ibuffer_index = 0;
  }

#ifndef OPENGLES
  if (_current_vertex_buffers.size() > 1 && _supports_multi_bind) {
    _glBindVertexBuffers(0, _current_vertex_buffers.size(), nullptr, nullptr, nullptr);
  } else {
    for (size_t i = 0; i < _current_vertex_buffers.size(); ++i) {
      if (_current_vertex_buffers[i] != 0) {
        _glBindVertexBuffer((GLuint)i, 0, 0, 0);
      }
    }
  }
  _current_vertex_buffers.clear();
#endif

#ifdef SUPPORT_FIXED_FUNCTION
  if (has_fixed_function_pipeline()) {
    disable_standard_vertex_arrays();
  }
#endif
}

#ifdef SUPPORT_FIXED_FUNCTION
/**
 * Used to disable all the standard vertex arrays that are currently enabled.
 * glShaderContexts are responsible for setting up their own vertex arrays,
 * but before they can do so, the standard vertex arrays need to be disabled
 * to get them "out of the way."  Called only from begin_draw_primitives.
 */
void CLP(GraphicsStateGuardian)::
disable_standard_vertex_arrays() {
#ifdef SUPPORT_IMMEDIATE_MODE
  if (_use_sender) return;
#endif

  glDisableClientState(GL_NORMAL_ARRAY);
  glDisableClientState(GL_COLOR_ARRAY);
  GLPf(Color4)(1.0f, 1.0f, 1.0f, 1.0f);

  for (int stage_index=0; stage_index < _last_max_stage_index; stage_index++) {
    _glClientActiveTexture(GL_TEXTURE0 + stage_index);
    glDisableClientState(GL_TEXTURE_COORD_ARRAY);
  }
  _last_max_stage_index = 0;

  glDisableClientState(GL_VERTEX_ARRAY);
  report_my_gl_errors();
}
#endif  // SUPPORT_FIXED_FUNCTION

#ifndef OPENGLES_1
/**
 * Updates the vertex format used by the shader.  This is still an
 * experimental feature.
 */
void CLP(GraphicsStateGuardian)::
update_shader_vertex_format(const GeomVertexFormat *format) {
  size_t num_columns = format->get_num_columns();
  for (size_t ci = 0; ci < num_columns; ++ci) {
    GLuint binding = format->get_array_with(ci);
    const GeomVertexColumn *column = format->get_column(ci);

    // Needs improvement, obviously.
    const InternalName *name = column->get_name();
    GLuint loc;
    if (name == InternalName::get_vertex()) {
      loc = 0;
    } else if (name == InternalName::get_transform_weight()) {
      loc = 1;
    } else if (name == InternalName::get_normal()) {
      loc = 2;
    } else if (name == InternalName::get_color()) {
      loc = 3;
    } else if (name == InternalName::get_transform_index()) {
      loc = 7;
    } else if (name == InternalName::get_texcoord()) {
      loc = 8;
    } else {
      // Not yet supported, ignore for now.  This system will be improved.
      continue;
    }

    if (_vertex_attrib_columns[loc] != nullptr &&
        _vertex_attrib_columns[loc]->compare_to(*column) == 0) {
      continue;
    }
    _vertex_attrib_columns[loc] = column;

    GLuint offset = column->get_start();
    GLenum type = get_numeric_type(column->get_numeric_type());
    GLboolean normalized = (column->get_contents() == GeomEnums::C_color);
    GLint size = column->get_num_values();

    if (column->get_numeric_type() == GeomEnums::NT_packed_dabc) {
      // GL_BGRA is a special accepted value available since OpenGL 3.2. It
      // requires us to pass GL_TRUE for normalized.
      size = GL_BGRA;
      normalized = GL_TRUE;
    }

    for (int i = 0; i < column->get_num_elements(); ++i) {
      if (loc == 7) { // Temp hack
        _glVertexAttribIFormat(loc, size, type, offset);
      } else {
        _glVertexAttribFormat(loc, size, type, normalized, offset);
      }
      _glVertexAttribBinding(loc, binding);

      offset += column->get_element_stride();
      ++loc;
    }
  }

  size_t num_arrays = format->get_num_arrays();
  for (size_t ai = 0; ai < num_arrays; ++ai) {
    _glVertexBindingDivisor(ai, format->get_array(ai)->get_divisor());
  }

  _current_vertex_format = format;
}
#endif

/**
 * Draws a series of disconnected triangles.
 */
bool CLP(GraphicsStateGuardian)::
draw_triangles(const GeomPrimitivePipelineReader *reader, bool force) {
  // PStatGPUTimer timer(this, _draw_primitive_pcollector,
  // reader->get_current_thread());

#ifndef NDEBUG
  if (GLCAT.is_spam()) {
    GLCAT.spam() << "draw_triangles: " << *(reader->get_object()) << "\n";
  }
#endif  // NDEBUG

#ifdef SUPPORT_IMMEDIATE_MODE
  if (_use_sender) {
    draw_immediate_simple_primitives(reader, GL_TRIANGLES);

  } else
#endif  // SUPPORT_IMMEDIATE_MODE
  {
    int num_vertices = reader->get_num_vertices();
    _vertices_tri_pcollector.add_level(num_vertices);
    _primitive_batches_tri_pcollector.add_level(1);

    if (reader->is_indexed()) {
      const unsigned char *client_pointer;
      if (!setup_primitive(client_pointer, reader, force)) {
        return false;
      }

#ifndef OPENGLES_1
      if (_instance_count != 1) {
        _glDrawElementsInstanced(GL_TRIANGLES, num_vertices,
                                 get_numeric_type(reader->get_index_type()),
                                 client_pointer, _instance_count);
      } else
#endif
      {
        _glDrawRangeElements(GL_TRIANGLES,
                             reader->get_min_vertex(),
                             reader->get_max_vertex(),
                             num_vertices,
                             get_numeric_type(reader->get_index_type()),
                             client_pointer);
      }
    } else {
#ifndef OPENGLES_1
      if (_instance_count != 1) {
        _glDrawArraysInstanced(GL_TRIANGLES,
                               reader->get_first_vertex(),
                               num_vertices, _instance_count);
      } else
#endif
      {
        glDrawArrays(GL_TRIANGLES,
                        reader->get_first_vertex(),
                        num_vertices);
      }
    }
  }

  report_my_gl_errors();
  return true;
}

/**
 * Draws a series of disconnected triangles with adjacency information.
 */
#ifndef OPENGLES
bool CLP(GraphicsStateGuardian)::
draw_triangles_adj(const GeomPrimitivePipelineReader *reader, bool force) {
  // PStatGPUTimer timer(this, _draw_primitive_pcollector,
  // reader->get_current_thread());

#ifndef NDEBUG
  if (GLCAT.is_spam()) {
    GLCAT.spam() << "draw_triangles_adj: " << *(reader->get_object()) << "\n";
  }
#endif  // NDEBUG

#ifdef SUPPORT_IMMEDIATE_MODE
  if (_use_sender) {
    draw_immediate_simple_primitives(reader, GL_TRIANGLES_ADJACENCY);

  } else
#endif  // SUPPORT_IMMEDIATE_MODE
  {
    int num_vertices = reader->get_num_vertices();
    _vertices_tri_pcollector.add_level(num_vertices);
    _primitive_batches_tri_pcollector.add_level(1);

    if (reader->is_indexed()) {
      const unsigned char *client_pointer;
      if (!setup_primitive(client_pointer, reader, force)) {
        return false;
      }

      if (_instance_count != 1) {
        _glDrawElementsInstanced(GL_TRIANGLES_ADJACENCY, num_vertices,
                                 get_numeric_type(reader->get_index_type()),
                                 client_pointer, _instance_count);
      } else {
        _glDrawRangeElements(GL_TRIANGLES_ADJACENCY,
                             reader->get_min_vertex(),
                             reader->get_max_vertex(),
                             num_vertices,
                             get_numeric_type(reader->get_index_type()),
                             client_pointer);
      }
    } else {
      if (_instance_count != 1) {
        _glDrawArraysInstanced(GL_TRIANGLES_ADJACENCY,
                               reader->get_first_vertex(),
                               num_vertices, _instance_count);
      } else {
        glDrawArrays(GL_TRIANGLES_ADJACENCY,
                     reader->get_first_vertex(),
                     num_vertices);
      }
    }
  }

  report_my_gl_errors();
  return true;
}
#endif  // OPENGLES

/**
 * Draws a series of triangle strips.
 */
bool CLP(GraphicsStateGuardian)::
draw_tristrips(const GeomPrimitivePipelineReader *reader, bool force) {
  // PStatGPUTimer timer(this, _draw_primitive_pcollector,
  // reader->get_current_thread());

  report_my_gl_errors();

#ifndef NDEBUG
  if (GLCAT.is_spam()) {
    GLCAT.spam() << "draw_tristrips: " << *(reader->get_object()) << "\n";
  }
#endif  // NDEBUG

#ifdef SUPPORT_IMMEDIATE_MODE
  if (_use_sender) {
    draw_immediate_composite_primitives(reader, GL_TRIANGLE_STRIP);

  } else
#endif  // SUPPORT_IMMEDIATE_MODE
  {
    if (connect_triangle_strips && _render_mode != RenderModeAttrib::M_wireframe) {
      // One long triangle strip, connected by the degenerate vertices that
      // have already been set up within the primitive.
      int num_vertices = reader->get_num_vertices();
      _vertices_tristrip_pcollector.add_level(num_vertices);
      _primitive_batches_tristrip_pcollector.add_level(1);
      if (reader->is_indexed()) {
        const unsigned char *client_pointer;
        if (!setup_primitive(client_pointer, reader, force)) {
          return false;
        }
#ifndef OPENGLES_1
        if (_instance_count != 1) {
          _glDrawElementsInstanced(GL_TRIANGLE_STRIP, num_vertices,
                                   get_numeric_type(reader->get_index_type()),
                                   client_pointer, _instance_count);
        } else
#endif
        {
          _glDrawRangeElements(GL_TRIANGLE_STRIP,
                               reader->get_min_vertex(),
                               reader->get_max_vertex(),
                               num_vertices,
                               get_numeric_type(reader->get_index_type()),
                               client_pointer);
        }
      } else {
#ifndef OPENGLES_1
        if (_instance_count != 1) {
          _glDrawArraysInstanced(GL_TRIANGLE_STRIP,
                                 reader->get_first_vertex(),
                                 num_vertices, _instance_count);
        } else
#endif
        {
          glDrawArrays(GL_TRIANGLE_STRIP,
                          reader->get_first_vertex(),
                          num_vertices);
        }
      }

    } else {
      // Send the individual triangle strips, stepping over the degenerate
      // vertices.
      CPTA_int ends = reader->get_ends();

      _primitive_batches_tristrip_pcollector.add_level(ends.size());
      if (reader->is_indexed()) {
        const unsigned char *client_pointer;
        if (!setup_primitive(client_pointer, reader, force)) {
          return false;
        }
        int index_stride = reader->get_index_stride();
        GeomVertexReader mins(reader->get_mins(), 0);
        GeomVertexReader maxs(reader->get_maxs(), 0);
        nassertr(reader->get_mins()->get_num_rows() == (int)ends.size() &&
                 reader->get_maxs()->get_num_rows() == (int)ends.size(), false);

        unsigned int start = 0;
        for (size_t i = 0; i < ends.size(); i++) {
          _vertices_tristrip_pcollector.add_level(ends[i] - start);
#ifndef OPENGLES_1
          if (_instance_count != 1) {
            _glDrawElementsInstanced(GL_TRIANGLE_STRIP, ends[i] - start,
                                     get_numeric_type(reader->get_index_type()),
                                     client_pointer + start * index_stride,
                                     _instance_count);
          } else
#endif
          {
            _glDrawRangeElements(GL_TRIANGLE_STRIP,
                                 mins.get_data1i(), maxs.get_data1i(),
                                 ends[i] - start,
                                 get_numeric_type(reader->get_index_type()),
                                 client_pointer + start * index_stride);
          }
          start = ends[i] + 2;
        }
      } else {
        unsigned int start = 0;
        int first_vertex = reader->get_first_vertex();
        for (size_t i = 0; i < ends.size(); i++) {
          _vertices_tristrip_pcollector.add_level(ends[i] - start);
#ifndef OPENGLES_1
          if (_instance_count != 1) {
            _glDrawArraysInstanced(GL_TRIANGLE_STRIP, first_vertex + start,
                                   ends[i] - start, _instance_count);
          } else
#endif
          {
            glDrawArrays(GL_TRIANGLE_STRIP, first_vertex + start,
                            ends[i] - start);
          }
          start = ends[i] + 2;
        }
      }
    }
  }

  report_my_gl_errors();
  return true;
}

/**
 * Draws a series of triangle strips with adjacency information.
 */
#ifndef OPENGLES
bool CLP(GraphicsStateGuardian)::
draw_tristrips_adj(const GeomPrimitivePipelineReader *reader, bool force) {
  // PStatGPUTimer timer(this, _draw_primitive_pcollector,
  // reader->get_current_thread());

  report_my_gl_errors();

#ifndef NDEBUG
  if (GLCAT.is_spam()) {
    GLCAT.spam() << "draw_tristrips_adj: " << *(reader->get_object()) << "\n";
  }
#endif  // NDEBUG

#ifdef SUPPORT_IMMEDIATE_MODE
  if (_use_sender) {
    draw_immediate_composite_primitives(reader, GL_TRIANGLE_STRIP_ADJACENCY);

  } else
#endif  // SUPPORT_IMMEDIATE_MODE
  {
    if (reader->is_indexed() &&
        (_supported_geom_rendering & GeomEnums::GR_strip_cut_index) != 0) {
      // One long line strip, connected by strip cut indices.
      if (_explicit_primitive_restart) {
        glEnable(GL_PRIMITIVE_RESTART);
        _glPrimitiveRestartIndex(reader->get_strip_cut_index());
      }
      int num_vertices = reader->get_num_vertices();
      _vertices_tristrip_pcollector.add_level(num_vertices);
      _primitive_batches_tristrip_pcollector.add_level(1);
      if (reader->is_indexed()) {
        const unsigned char *client_pointer;
        if (!setup_primitive(client_pointer, reader, force)) {
          return false;
        }
        if (_instance_count != 1) {
          _glDrawElementsInstanced(GL_TRIANGLE_STRIP_ADJACENCY, num_vertices,
                                   get_numeric_type(reader->get_index_type()),
                                   client_pointer, _instance_count);
        } else {
          _glDrawRangeElements(GL_TRIANGLE_STRIP_ADJACENCY,
                               reader->get_min_vertex(),
                               reader->get_max_vertex(),
                               num_vertices,
                               get_numeric_type(reader->get_index_type()),
                               client_pointer);
        }
      } else {
        if (_instance_count != 1) {
          _glDrawArraysInstanced(GL_TRIANGLE_STRIP_ADJACENCY,
                                 reader->get_first_vertex(),
                                 num_vertices, _instance_count);
        } else {
          glDrawArrays(GL_TRIANGLE_STRIP_ADJACENCY,
                          reader->get_first_vertex(),
                          num_vertices);
        }
      }
      if (_explicit_primitive_restart) {
        glDisable(GL_PRIMITIVE_RESTART);
      }
    } else {
      // Send the individual triangle strips, stepping over the degenerate
      // vertices.
      CPTA_int ends = reader->get_ends();

      _primitive_batches_tristrip_pcollector.add_level(ends.size());
      if (reader->is_indexed()) {
        const unsigned char *client_pointer;
        if (!setup_primitive(client_pointer, reader, force)) {
          return false;
        }
        int index_stride = reader->get_index_stride();
        GeomVertexReader mins(reader->get_mins(), 0);
        GeomVertexReader maxs(reader->get_maxs(), 0);
        nassertr(reader->get_mins()->get_num_rows() == (int)ends.size() &&
                 reader->get_maxs()->get_num_rows() == (int)ends.size(), false);

        unsigned int start = 0;
        for (size_t i = 0; i < ends.size(); i++) {
          _vertices_tristrip_pcollector.add_level(ends[i] - start);
          if (_instance_count != 1) {
            _glDrawElementsInstanced(GL_TRIANGLE_STRIP_ADJACENCY, ends[i] - start,
                                     get_numeric_type(reader->get_index_type()),
                                     client_pointer + start * index_stride,
                                     _instance_count);
          } else {
            _glDrawRangeElements(GL_TRIANGLE_STRIP_ADJACENCY,
                                 mins.get_data1i(), maxs.get_data1i(),
                                 ends[i] - start,
                                 get_numeric_type(reader->get_index_type()),
                                 client_pointer + start * index_stride);
          }
          start = ends[i] + 1;
        }
      } else {
        unsigned int start = 0;
        int first_vertex = reader->get_first_vertex();
        for (size_t i = 0; i < ends.size(); i++) {
          _vertices_tristrip_pcollector.add_level(ends[i] - start);
          if (_instance_count != 1) {
            _glDrawArraysInstanced(GL_TRIANGLE_STRIP_ADJACENCY, first_vertex + start,
                                   ends[i] - start, _instance_count);
          } else {
            glDrawArrays(GL_TRIANGLE_STRIP_ADJACENCY, first_vertex + start,
                         ends[i] - start);
          }
          start = ends[i] + 1;
        }
      }
    }
  }

  report_my_gl_errors();
  return true;
}
#endif  // OPENGLES

/**
 * Draws a series of triangle fans.
 */
bool CLP(GraphicsStateGuardian)::
draw_trifans(const GeomPrimitivePipelineReader *reader, bool force) {
  // PStatGPUTimer timer(this, _draw_primitive_pcollector,
  // reader->get_current_thread());

#ifndef NDEBUG
  if (GLCAT.is_spam()) {
    GLCAT.spam() << "draw_trifans: " << *(reader->get_object()) << "\n";
  }
#endif  // NDEBUG

#ifdef SUPPORT_IMMEDIATE_MODE
  if (_use_sender) {
    draw_immediate_composite_primitives(reader, GL_TRIANGLE_FAN);
  } else
#endif  // SUPPORT_IMMEDIATE_MODE
  {
    // Send the individual triangle fans.  There's no connecting fans with
    // degenerate vertices, so no worries about that.
    CPTA_int ends = reader->get_ends();

    _primitive_batches_trifan_pcollector.add_level(ends.size());
    if (reader->is_indexed()) {
      const unsigned char *client_pointer;
      if (!setup_primitive(client_pointer, reader, force)) {
        return false;
      }
      int index_stride = reader->get_index_stride();
      GeomVertexReader mins(reader->get_mins(), 0);
      GeomVertexReader maxs(reader->get_maxs(), 0);
      nassertr(reader->get_mins()->get_num_rows() == (int)ends.size() &&
               reader->get_maxs()->get_num_rows() == (int)ends.size(), false);

      unsigned int start = 0;
      for (size_t i = 0; i < ends.size(); i++) {
        _vertices_trifan_pcollector.add_level(ends[i] - start);
#ifndef OPENGLES_1
        if (_instance_count != 1) {
          _glDrawElementsInstanced(GL_TRIANGLE_FAN, ends[i] - start,
                                   get_numeric_type(reader->get_index_type()),
                                   client_pointer + start * index_stride,
                                   _instance_count);
        } else
#endif
        {
          _glDrawRangeElements(GL_TRIANGLE_FAN,
                               mins.get_data1i(), maxs.get_data1i(), ends[i] - start,
                               get_numeric_type(reader->get_index_type()),
                               client_pointer + start * index_stride);
        }
        start = ends[i];
      }
    } else {
      unsigned int start = 0;
      int first_vertex = reader->get_first_vertex();
      for (size_t i = 0; i < ends.size(); i++) {
        _vertices_trifan_pcollector.add_level(ends[i] - start);
#ifndef OPENGLES_1
        if (_instance_count != 1) {
          _glDrawArraysInstanced(GL_TRIANGLE_FAN, first_vertex + start,
                                 ends[i] - start, _instance_count);
        } else
#endif
        {
          glDrawArrays(GL_TRIANGLE_FAN, first_vertex + start,
                          ends[i] - start);
        }
        start = ends[i];
      }
    }
  }

  report_my_gl_errors();
  return true;
}

/**
 * Draws a series of "patches", which can only be processed by a tessellation
 * shader.
 */
bool CLP(GraphicsStateGuardian)::
draw_patches(const GeomPrimitivePipelineReader *reader, bool force) {
  // PStatGPUTimer timer(this, _draw_primitive_pcollector,
  // reader->get_current_thread());

#ifndef NDEBUG
  if (GLCAT.is_spam()) {
    GLCAT.spam() << "draw_patches: " << *(reader->get_object()) << "\n";
  }
#endif  // NDEBUG

  if (!get_supports_tessellation_shaders()) {
    return false;
  }

#ifndef OPENGLES
  _glPatchParameteri(GL_PATCH_VERTICES, reader->get_object()->get_num_vertices_per_primitive());

#ifdef SUPPORT_IMMEDIATE_MODE
  if (_use_sender) {
    draw_immediate_simple_primitives(reader, GL_PATCHES);

  } else
#endif  // SUPPORT_IMMEDIATE_MODE
  {
    int num_vertices = reader->get_num_vertices();
    _vertices_patch_pcollector.add_level(num_vertices);
    _primitive_batches_patch_pcollector.add_level(1);

    if (reader->is_indexed()) {
      const unsigned char *client_pointer;
      if (!setup_primitive(client_pointer, reader, force)) {
        return false;
      }

#ifndef OPENGLES_1
      if (_instance_count != 1) {
        _glDrawElementsInstanced(GL_PATCHES, num_vertices,
                                 get_numeric_type(reader->get_index_type()),
                                 client_pointer, _instance_count);
      } else
#endif
      {
        _glDrawRangeElements(GL_PATCHES,
                             reader->get_min_vertex(),
                             reader->get_max_vertex(),
                             num_vertices,
                             get_numeric_type(reader->get_index_type()),
                             client_pointer);
      }
    } else {
#ifndef OPENGLES_1
      if (_instance_count != 1) {
        _glDrawArraysInstanced(GL_PATCHES,
                               reader->get_first_vertex(),
                               num_vertices, _instance_count);
      } else
#endif
      {
        glDrawArrays(GL_PATCHES,
                        reader->get_first_vertex(),
                        num_vertices);
      }
    }
  }

#endif // OPENGLES

  report_my_gl_errors();
  return true;
}

/**
 * Draws a series of disconnected line segments.
 */
bool CLP(GraphicsStateGuardian)::
draw_lines(const GeomPrimitivePipelineReader *reader, bool force) {
  // PStatGPUTimer timer(this, _draw_primitive_pcollector,
  // reader->get_current_thread());

#ifndef NDEBUG
  if (GLCAT.is_spam()) {
    GLCAT.spam() << "draw_lines: " << *(reader->get_object()) << "\n";
  }
#endif  // NDEBUG

#ifdef SUPPORT_IMMEDIATE_MODE
  if (_use_sender) {
    draw_immediate_simple_primitives(reader, GL_LINES);
  } else
#endif  // SUPPORT_IMMEDIATE_MODE
  {
    int num_vertices = reader->get_num_vertices();
    _vertices_other_pcollector.add_level(num_vertices);
    _primitive_batches_other_pcollector.add_level(1);

    if (reader->is_indexed()) {
      const unsigned char *client_pointer;
      if (!setup_primitive(client_pointer, reader, force)) {
        return false;
      }
#ifndef OPENGLES_1
      if (_instance_count != 1) {
        _glDrawElementsInstanced(GL_LINES, num_vertices,
                                 get_numeric_type(reader->get_index_type()),
                                 client_pointer, _instance_count);
      } else
#endif
      {
        _glDrawRangeElements(GL_LINES,
                             reader->get_min_vertex(),
                             reader->get_max_vertex(),
                             num_vertices,
                             get_numeric_type(reader->get_index_type()),
                             client_pointer);
      }
    } else {
#ifndef OPENGLES_1
      if (_instance_count != 1) {
        _glDrawArraysInstanced(GL_LINES,
                               reader->get_first_vertex(),
                               num_vertices, _instance_count);
      } else
#endif
      {
        glDrawArrays(GL_LINES,
                        reader->get_first_vertex(),
                        num_vertices);
      }
    }
  }

  report_my_gl_errors();
  return true;
}

/**
 * Draws a series of disconnected line segments with adjacency information.
 */
#ifndef OPENGLES
bool CLP(GraphicsStateGuardian)::
draw_lines_adj(const GeomPrimitivePipelineReader *reader, bool force) {
  // PStatGPUTimer timer(this, _draw_primitive_pcollector,
  // reader->get_current_thread());

#ifndef NDEBUG
  if (GLCAT.is_spam()) {
    GLCAT.spam() << "draw_lines_adj: " << *(reader->get_object()) << "\n";
  }
#endif  // NDEBUG

#ifdef SUPPORT_IMMEDIATE_MODE
  if (_use_sender) {
    draw_immediate_simple_primitives(reader, GL_LINES_ADJACENCY);
  } else
#endif  // SUPPORT_IMMEDIATE_MODE
  {
    int num_vertices = reader->get_num_vertices();
    _vertices_other_pcollector.add_level(num_vertices);
    _primitive_batches_other_pcollector.add_level(1);

    if (reader->is_indexed()) {
      const unsigned char *client_pointer;
      if (!setup_primitive(client_pointer, reader, force)) {
        return false;
      }
      if (_instance_count != 1) {
        _glDrawElementsInstanced(GL_LINES_ADJACENCY, num_vertices,
                                 get_numeric_type(reader->get_index_type()),
                                 client_pointer, _instance_count);
      } else {
        _glDrawRangeElements(GL_LINES_ADJACENCY,
                             reader->get_min_vertex(),
                             reader->get_max_vertex(),
                             num_vertices,
                             get_numeric_type(reader->get_index_type()),
                             client_pointer);
      }
    } else {
      if (_instance_count != 1) {
        _glDrawArraysInstanced(GL_LINES_ADJACENCY,
                               reader->get_first_vertex(),
                               num_vertices, _instance_count);
      } else {
        glDrawArrays(GL_LINES_ADJACENCY,
                     reader->get_first_vertex(),
                     num_vertices);
      }
    }
  }

  report_my_gl_errors();
  return true;
}
#endif  // OPENGLES

/**
 * Draws a series of line strips.
 */
bool CLP(GraphicsStateGuardian)::
draw_linestrips(const GeomPrimitivePipelineReader *reader, bool force) {
  // PStatGPUTimer timer(this, _draw_primitive_pcollector,
  // reader->get_current_thread());

  report_my_gl_errors();

#ifndef NDEBUG
  if (GLCAT.is_spam()) {
    GLCAT.spam() << "draw_linestrips: " << *(reader->get_object()) << "\n";
  }
#endif  // NDEBUG

#ifdef SUPPORT_IMMEDIATE_MODE
  if (_use_sender) {
    draw_immediate_composite_primitives(reader, GL_LINE_STRIP);

  } else
#endif  // SUPPORT_IMMEDIATE_MODE
  {
    if (reader->is_indexed() &&
        (_supported_geom_rendering & GeomEnums::GR_strip_cut_index) != 0) {
      // One long triangle strip, connected by strip cut indices.
#ifndef OPENGLES
      if (_explicit_primitive_restart) {
        glEnable(GL_PRIMITIVE_RESTART);
        _glPrimitiveRestartIndex(reader->get_strip_cut_index());
      }
#endif  // !OPENGLES

      int num_vertices = reader->get_num_vertices();
      _vertices_other_pcollector.add_level(num_vertices);
      _primitive_batches_other_pcollector.add_level(1);

      const unsigned char *client_pointer;
      if (!setup_primitive(client_pointer, reader, force)) {
        return false;
      }
#ifndef OPENGLES_1
      if (_instance_count != 1) {
        _glDrawElementsInstanced(GL_LINE_STRIP, num_vertices,
                                 get_numeric_type(reader->get_index_type()),
                                 client_pointer, _instance_count);
      } else
#endif  // !OPENGLES
      {
        _glDrawRangeElements(GL_LINE_STRIP,
                             reader->get_min_vertex(),
                             reader->get_max_vertex(),
                             num_vertices,
                             get_numeric_type(reader->get_index_type()),
                             client_pointer);
      }

#ifndef OPENGLES
      if (_explicit_primitive_restart) {
        glDisable(GL_PRIMITIVE_RESTART);
      }
#endif  // !OPENGLES
    } else {
      // Send the individual line strips, stepping over the strip-cut indices.
      CPTA_int ends = reader->get_ends();

      _primitive_batches_other_pcollector.add_level(ends.size());
      if (reader->is_indexed()) {
        const unsigned char *client_pointer;
        if (!setup_primitive(client_pointer, reader, force)) {
          return false;
        }
        int index_stride = reader->get_index_stride();
        GeomVertexReader mins(reader->get_mins(), 0);
        GeomVertexReader maxs(reader->get_maxs(), 0);
        nassertr(reader->get_mins()->get_num_rows() == (int)ends.size() &&
                 reader->get_maxs()->get_num_rows() == (int)ends.size(), false);

        unsigned int start = 0;
        for (size_t i = 0; i < ends.size(); i++) {
          _vertices_other_pcollector.add_level(ends[i] - start);
#ifndef OPENGLES_1
          if (_instance_count != 1) {
            _glDrawElementsInstanced(GL_LINE_STRIP, ends[i] - start,
                                     get_numeric_type(reader->get_index_type()),
                                     client_pointer + start * index_stride,
                                     _instance_count);
          } else
#endif
          {
            _glDrawRangeElements(GL_LINE_STRIP,
                                 mins.get_data1i(), maxs.get_data1i(),
                                 ends[i] - start,
                                 get_numeric_type(reader->get_index_type()),
                                 client_pointer + start * index_stride);
          }
          start = ends[i] + 1;
        }
      } else {
        unsigned int start = 0;
        int first_vertex = reader->get_first_vertex();
        for (size_t i = 0; i < ends.size(); i++) {
          _vertices_other_pcollector.add_level(ends[i] - start);
#ifndef OPENGLES_1
          if (_instance_count != 1) {
            _glDrawArraysInstanced(GL_LINE_STRIP, first_vertex + start,
                                   ends[i] - start, _instance_count);
          } else
#endif
          {
            glDrawArrays(GL_LINE_STRIP, first_vertex + start, ends[i] - start);
          }
          start = ends[i] + 1;
        }
      }
    }
  }

  report_my_gl_errors();
  return true;
}

/**
 * Draws a series of line strips with adjacency information.
 */
#ifndef OPENGLES
bool CLP(GraphicsStateGuardian)::
draw_linestrips_adj(const GeomPrimitivePipelineReader *reader, bool force) {
  // PStatGPUTimer timer(this, _draw_primitive_pcollector,
  // reader->get_current_thread());

  report_my_gl_errors();

#ifndef NDEBUG
  if (GLCAT.is_spam()) {
    GLCAT.spam() << "draw_linestrips_adj: " << *(reader->get_object()) << "\n";
  }
#endif  // NDEBUG

#ifdef SUPPORT_IMMEDIATE_MODE
  if (_use_sender) {
    draw_immediate_composite_primitives(reader, GL_LINE_STRIP_ADJACENCY);

  } else
#endif  // SUPPORT_IMMEDIATE_MODE
  {
    if (reader->is_indexed() &&
        (_supported_geom_rendering & GeomEnums::GR_strip_cut_index) != 0) {
      // One long line strip, connected by strip cut indices.
      if (_explicit_primitive_restart) {
        glEnable(GL_PRIMITIVE_RESTART);
        _glPrimitiveRestartIndex(reader->get_strip_cut_index());
      }

      int num_vertices = reader->get_num_vertices();
      _vertices_other_pcollector.add_level(num_vertices);
      _primitive_batches_other_pcollector.add_level(1);

      const unsigned char *client_pointer;
      if (!setup_primitive(client_pointer, reader, force)) {
        return false;
      }
      if (_instance_count != 1) {
        _glDrawElementsInstanced(GL_LINE_STRIP_ADJACENCY, num_vertices,
                                 get_numeric_type(reader->get_index_type()),
                                 client_pointer, _instance_count);
      } else {
        _glDrawRangeElements(GL_LINE_STRIP_ADJACENCY,
                             reader->get_min_vertex(),
                             reader->get_max_vertex(),
                             num_vertices,
                             get_numeric_type(reader->get_index_type()),
                             client_pointer);
      }

      if (_explicit_primitive_restart) {
        glDisable(GL_PRIMITIVE_RESTART);
      }
    } else {
      // Send the individual line strips, stepping over the strip-cut indices.
      CPTA_int ends = reader->get_ends();

      _primitive_batches_other_pcollector.add_level(ends.size());
      if (reader->is_indexed()) {
        const unsigned char *client_pointer;
        if (!setup_primitive(client_pointer, reader, force)) {
          return false;
        }
        int index_stride = reader->get_index_stride();
        GeomVertexReader mins(reader->get_mins(), 0);
        GeomVertexReader maxs(reader->get_maxs(), 0);
        nassertr(reader->get_mins()->get_num_rows() == (int)ends.size() &&
                 reader->get_maxs()->get_num_rows() == (int)ends.size(), false);

        unsigned int start = 0;
        for (size_t i = 0; i < ends.size(); i++) {
          _vertices_other_pcollector.add_level(ends[i] - start);
          if (_instance_count != 1) {
            _glDrawElementsInstanced(GL_LINE_STRIP_ADJACENCY, ends[i] - start,
                                     get_numeric_type(reader->get_index_type()),
                                     client_pointer + start * index_stride,
                                     _instance_count);
          } else {
            _glDrawRangeElements(GL_LINE_STRIP_ADJACENCY,
                                 mins.get_data1i(), maxs.get_data1i(),
                                 ends[i] - start,
                                 get_numeric_type(reader->get_index_type()),
                                 client_pointer + start * index_stride);
          }
          start = ends[i] + 1;
        }
      } else {
        unsigned int start = 0;
        int first_vertex = reader->get_first_vertex();
        for (size_t i = 0; i < ends.size(); i++) {
          _vertices_other_pcollector.add_level(ends[i] - start);
          if (_instance_count != 1) {
            _glDrawArraysInstanced(GL_LINE_STRIP_ADJACENCY, first_vertex + start,
                                   ends[i] - start, _instance_count);
          } else {
            glDrawArrays(GL_LINE_STRIP_ADJACENCY, first_vertex + start, ends[i] - start);
          }
          start = ends[i] + 1;
        }
      }
    }
  }

  report_my_gl_errors();
  return true;
}
#endif  // OPENGLES

/**
 * Draws a series of disconnected points.
 */
bool CLP(GraphicsStateGuardian)::
draw_points(const GeomPrimitivePipelineReader *reader, bool force) {
  // PStatGPUTimer timer(this, _draw_primitive_pcollector,
  // reader->get_current_thread());

#ifndef NDEBUG
  if (GLCAT.is_spam()) {
    GLCAT.spam() << "draw_points: " << *(reader->get_object()) << "\n";
  }
#endif  // NDEBUG

#ifdef SUPPORT_IMMEDIATE_MODE
  if (_use_sender) {
    draw_immediate_simple_primitives(reader, GL_POINTS);
  } else
#endif  // SUPPORT_IMMEDIATE_MODE
  {
    int num_vertices = reader->get_num_vertices();
    _vertices_other_pcollector.add_level(num_vertices);
    _primitive_batches_other_pcollector.add_level(1);

    if (reader->is_indexed()) {
      const unsigned char *client_pointer;
      if (!setup_primitive(client_pointer, reader, force)) {
        return false;
      }
#ifndef OPENGLES_1
      if (_instance_count != 1) {
        _glDrawElementsInstanced(GL_POINTS, num_vertices,
                                 get_numeric_type(reader->get_index_type()),
                                 client_pointer, _instance_count);
      } else
#endif
      {
        _glDrawRangeElements(GL_POINTS,
                             reader->get_min_vertex(),
                             reader->get_max_vertex(),
                             num_vertices,
                             get_numeric_type(reader->get_index_type()),
                             client_pointer);
      }
    } else {
#ifndef OPENGLES_1
      if (_instance_count != 1) {
        _glDrawArraysInstanced(GL_POINTS,
                               reader->get_first_vertex(),
                               num_vertices, _instance_count);
      } else
#endif
      {
        glDrawArrays(GL_POINTS, reader->get_first_vertex(), num_vertices);
      }
    }
  }

  report_my_gl_errors();
  return true;
}

/**
 * Called after a sequence of draw_primitive() functions are called, this
 * should do whatever cleanup is appropriate.
 */
void CLP(GraphicsStateGuardian)::
end_draw_primitives() {
#if !defined(OPENGLES) && defined(SUPPORT_FIXED_FUNCTION)  // Display lists not supported by OpenGL ES.
  if (has_fixed_function_pipeline() && _geom_display_list != 0) {
    // If we were building a display list, close it now.
    glEndList();
    _load_display_list_pcollector.stop();

    if (!gl_compile_and_execute) {
      glCallList(_geom_display_list);
    }
    _primitive_batches_display_list_pcollector.add_level(1);
  }
  _geom_display_list = 0;
#endif

#ifdef SUPPORT_FIXED_FUNCTION
  if (has_fixed_function_pipeline() && _transform_stale) {
    glMatrixMode(GL_MODELVIEW);
    call_glLoadMatrix(_internal_transform->get_mat());
  }

  if (has_fixed_function_pipeline() && _data_reader->is_vertex_transformed()) {
    // Restore the matrices that we pushed above.
    glMatrixMode(GL_PROJECTION);
    glPopMatrix();
    glMatrixMode(GL_MODELVIEW);
    glPopMatrix();
  }
#endif

  GraphicsStateGuardian::end_draw_primitives();
  maybe_gl_finish();
  report_my_gl_errors();
}

#ifndef OPENGLES_1
/**
 * Issues the given memory barriers, and clears the list of textures marked as
 * incoherent for the given bits.
 */
void CLP(GraphicsStateGuardian)::
issue_memory_barrier(GLbitfield barriers) {
  if (!gl_enable_memory_barriers || _glMemoryBarrier == nullptr) {
    return;
  }

  PStatGPUTimer timer(this, _memory_barrier_pcollector);

  if (GLCAT.is_spam()) {
    GLCAT.spam() << "Issuing memory barriers:";
  }

  _glMemoryBarrier(barriers);

  // Indicate that barriers no longer need to be issued for the relevant lists
  // of textures.
  if (barriers & GL_TEXTURE_FETCH_BARRIER_BIT) {
    _textures_needing_fetch_barrier.clear();
    GLCAT.spam(false) << " texture_fetch";
  }

  if (barriers & GL_SHADER_IMAGE_ACCESS_BARRIER_BIT) {
    _textures_needing_image_access_barrier.clear();
    GLCAT.spam(false) << " shader_image_access";
  }

  if (barriers & GL_TEXTURE_UPDATE_BARRIER_BIT) {
    _textures_needing_update_barrier.clear();
    GLCAT.spam(false) << " texture_update";
  }

  if (barriers & GL_FRAMEBUFFER_BARRIER_BIT) {
    _textures_needing_framebuffer_barrier.clear();
    GLCAT.spam(false) << " framebuffer";
  }

  GLCAT.spam(false) << "\n";

  report_my_gl_errors();
}
#endif  // OPENGLES_1

/**
 * Creates whatever structures the GSG requires to represent the texture
 * internally, and returns a newly-allocated TextureContext object with this
 * data.  It is the responsibility of the calling function to later call
 * release_texture() with this same pointer (which will also delete the
 * pointer).
 *
 * This function should not be called directly to prepare a texture.  Instead,
 * call Texture::prepare().
 */
TextureContext *CLP(GraphicsStateGuardian)::
prepare_texture(Texture *tex) {
  PStatGPUTimer timer(this, _prepare_texture_pcollector);

  report_my_gl_errors();
  // Make sure we'll support this texture when it's rendered.  Don't bother to
  // prepare it if we won't.
  Texture::TextureType texture_type = tex->get_texture_type();
  switch (texture_type) {
  case Texture::TT_3d_texture:
    if (!_supports_3d_texture) {
      GLCAT.warning()
        << "3-D textures are not supported by this OpenGL driver.\n";
      return nullptr;
    }
    break;

  case Texture::TT_2d_texture_array:
    if (!_supports_2d_texture_array) {
      GLCAT.warning()
        << "2-D texture arrays are not supported by this OpenGL driver.\n";
      return nullptr;
    }
    break;

  case Texture::TT_cube_map:
    if (!_supports_cube_map) {
      GLCAT.warning()
        << "Cube map textures are not supported by this OpenGL driver.\n";
      return nullptr;
    }
    break;

  case Texture::TT_buffer_texture:
    if (!_supports_buffer_texture) {
      GLCAT.warning()
        << "Buffer textures are not supported by this OpenGL driver.\n";
      return nullptr;
    }
    break;

  case Texture::TT_cube_map_array:
    if (!_supports_cube_map_array) {
      GLCAT.warning()
        << "Cube map arrays are not supported by this OpenGL driver.\n";
      return nullptr;
    }
    break;

  default:
    break;
  }

  CLP(TextureContext) *gtc = new CLP(TextureContext)(this, _prepared_objects, tex);

  // Make sure we have an index so that get_native_id() will already work.
  GLenum target = get_texture_target(texture_type);
  gtc->reset_data(target, 1);

  report_my_gl_errors();

  return gtc;
}

/**
 * Ensures that the current Texture data is refreshed onto the GSG.  This
 * means updating the texture properties and/or re-uploading the texture
 * image, if necessary.  This should only be called within the draw thread.
 *
 * If force is true, this function will not return until the texture has been
 * fully uploaded.  If force is false, the function may choose to upload a
 * simple version of the texture instead, if the texture is not fully resident
 * (and if get_incomplete_render() is true).
 */
bool CLP(GraphicsStateGuardian)::
update_texture(TextureContext *tc, bool force) {
  CLP(TextureContext) *gtc;
  DCAST_INTO_R(gtc, tc, false);

  Texture *tex = tc->get_texture();
  GLenum target = get_texture_target(tex->get_texture_type());
  if (gtc->_target != target) {
    // The target has changed.  That means we have to re-bind a new texture
    // object.
    gtc->reset_data(target, tex->get_num_views());
  }

  if (gtc->was_image_modified() || !gtc->_has_storage) {
    PStatGPUTimer timer(this, _texture_update_pcollector);

    // If the texture image was modified, reload the texture.
    bool okflag = upload_texture(gtc, force, tex->uses_mipmaps());
    if (!okflag) {
      GLCAT.error()
        << "Could not load " << *tex << "\n";
      return false;
    }

    if (gtc->was_properties_modified()) {
      for (int view = 0; view < gtc->_num_views; ++view) {
        apply_texture(gtc, view);
        specify_texture(gtc, tex->get_default_sampler());
      }
    }
  }
  else if (gtc->was_properties_modified()) {
    PStatGPUTimer timer(this, _texture_update_pcollector);

    // If only the properties have been modified, we don't necessarily need to
    // reload the texture.
    bool needs_reload = false;
    for (int view = 0; view < gtc->_num_views; ++view) {
      apply_texture(gtc, view);

      if (specify_texture(gtc, tex->get_default_sampler())) {
        // Actually, looks like the texture *does* need to be reloaded.
        needs_reload = true;
      }
    }

    if (needs_reload) {
      gtc->mark_needs_reload();
      bool okflag = upload_texture(gtc, force, tex->uses_mipmaps());
      if (!okflag) {
        GLCAT.error()
          << "Could not load " << *tex << "\n";
        return false;
      }
    }
    else {
      // The texture didn't need reloading, but mark it fully updated now.
      gtc->mark_loaded();
    }
  }

  gtc->enqueue_lru(&_prepared_objects->_graphics_memory_lru);

  report_my_gl_errors();
  return true;
}

/**
 * Frees the GL resources previously allocated for the texture.  This function
 * should never be called directly; instead, call Texture::release() (or
 * simply let the Texture destruct).
 */
void CLP(GraphicsStateGuardian)::
release_texture(TextureContext *tc) {
  CLP(TextureContext) *gtc = DCAST(CLP(TextureContext), tc);

#ifndef OPENGLES_1
  _textures_needing_fetch_barrier.erase(gtc);
  _textures_needing_image_access_barrier.erase(gtc);
  _textures_needing_update_barrier.erase(gtc);
  _textures_needing_framebuffer_barrier.erase(gtc);
#endif

  gtc->set_num_views(0);
  delete gtc;
}

/**
 * Frees the GL resources previously allocated for the textures.  This function
 * should never be called directly; instead, call Texture::release() (or
 * simply let the Texture destruct).
 */
void CLP(GraphicsStateGuardian)::
release_textures(const pvector<TextureContext *> &contexts) {
  if (contexts.empty()) {
    return;
  }

  size_t num_indices = 0;
  size_t num_buffers = 0;

  for (TextureContext *tc : contexts) {
    CLP(TextureContext) *gtc = DCAST(CLP(TextureContext), tc);

#ifndef OPENGLES_1
    _textures_needing_fetch_barrier.erase(gtc);
    _textures_needing_image_access_barrier.erase(gtc);
    _textures_needing_update_barrier.erase(gtc);
    _textures_needing_framebuffer_barrier.erase(gtc);
#endif

    num_indices += gtc->_num_views;
    if (gtc->_buffers != nullptr) {
      num_buffers += gtc->_num_views;
    }
  }

  GLuint *indices = (GLuint *)alloca(sizeof(GLuint) * num_indices);
  GLuint *buffers = (GLuint *)alloca(sizeof(GLuint) * num_buffers);
  num_indices = 0;
  num_buffers = 0;

  for (TextureContext *tc : contexts) {
    CLP(TextureContext) *gtc = (CLP(TextureContext) *)tc;

    for (int view = 0; view < gtc->_num_views; ++view) {
      indices[num_indices++] = gtc->_indices[view];

      if (gtc->_buffers != nullptr) {
        buffers[num_buffers++] = gtc->_buffers[view];
      }
    }
    gtc->_num_views = 0;

    delete gtc;
  }


  glDeleteTextures(num_indices, indices);

  if (num_buffers > 0) {
    _glDeleteBuffers(num_buffers, buffers);
  }
}

/**
 * This method should only be called by the GraphicsEngine.  Do not call it
 * directly; call GraphicsEngine::extract_texture_data() instead.
 *
 * This method will be called in the draw thread to download the texture
 * memory's image into its ram_image value.  It returns true on success, false
 * otherwise.
 */
bool CLP(GraphicsStateGuardian)::
extract_texture_data(Texture *tex) {
  bool success = true;
  // Make sure the error stack is cleared out before we begin.
  report_my_gl_errors();

  TextureContext *tc = tex->prepare_now(get_prepared_objects(), this);
  nassertr(tc != nullptr, false);
  CLP(TextureContext) *gtc = DCAST(CLP(TextureContext), tc);

  GLenum target = gtc->_target;

  int num_views = tex->get_num_views();
  for (int view = 0; view < num_views; ++view) {
    GLuint index = gtc->get_view_index(view);
    glBindTexture(target, index);
    if (GLCAT.is_spam()) {
      GLCAT.spam()
        << "glBindTexture(0x" << hex << target << dec << ", " << index << "): "
        << *tex << " view " << view << "\n";
    }

#ifndef OPENGLES_1
    if (target == GL_TEXTURE_BUFFER) {
      _glBindBuffer(GL_TEXTURE_BUFFER, gtc->get_view_buffer(view));
    }
#endif

    if (!do_extract_texture_data(gtc, view)) {
      success = false;
    }
  }

  glBindTexture(target, 0);
  if (GLCAT.is_spam()) {
    GLCAT.spam()
      << "glBindTexture(0x" << hex << target << dec << ", 0)\n";
  }

#ifndef OPENGLES_1
  if (target == GL_TEXTURE_BUFFER) {
    _glBindBuffer(GL_TEXTURE_BUFFER, 0);
  }
#endif

  return success;
}

#ifndef OPENGLES_1
/**
 * Creates whatever structures the GSG requires to represent the sampler state
 * internally, and returns a newly-allocated SamplerContext object with this
 * data.  It is the responsibility of the calling function to later call
 * release_sampler() with this same pointer (which will also delete the
 * pointer).
 *
 * This function should not be called directly to prepare a sampler object.
 * Instead, call SamplerState::prepare().
 */
SamplerContext *CLP(GraphicsStateGuardian)::
prepare_sampler(const SamplerState &sampler) {
  nassertr(_supports_sampler_objects, nullptr);
  PStatGPUTimer timer(this, _prepare_sampler_pcollector);

  CLP(SamplerContext) *gsc = new CLP(SamplerContext)(this, sampler);
  GLuint index = gsc->_index;

  // Sampler contexts are immutable in Panda, so might as well just initialize
  // all the settings here.
  _glSamplerParameteri(index, GL_TEXTURE_WRAP_S,
                       get_texture_wrap_mode(sampler.get_wrap_u()));
  _glSamplerParameteri(index, GL_TEXTURE_WRAP_T,
                       get_texture_wrap_mode(sampler.get_wrap_v()));
  _glSamplerParameteri(index, GL_TEXTURE_WRAP_R,
                       get_texture_wrap_mode(sampler.get_wrap_w()));

#ifndef OPENGLES
#ifdef STDFLOAT_DOUBLE
  LVecBase4f fvalue = LCAST(float, sampler.get_border_color());
  _glSamplerParameterfv(index, GL_TEXTURE_BORDER_COLOR, fvalue.get_data());
#else
  _glSamplerParameterfv(index, GL_TEXTURE_BORDER_COLOR,
                        sampler.get_border_color().get_data());
#endif
#endif  // OPENGLES

  SamplerState::FilterType minfilter = sampler.get_effective_minfilter();
  SamplerState::FilterType magfilter = sampler.get_effective_magfilter();
  bool uses_mipmaps = SamplerState::is_mipmap(minfilter) && !gl_ignore_mipmaps;

#ifndef NDEBUG
  if (gl_force_mipmaps) {
    minfilter = SamplerState::FT_linear_mipmap_linear;
    magfilter = SamplerState::FT_linear;
    uses_mipmaps = true;
  }
#endif

  _glSamplerParameteri(index, GL_TEXTURE_MIN_FILTER,
                              get_texture_filter_type(minfilter, !uses_mipmaps));
  _glSamplerParameteri(index, GL_TEXTURE_MAG_FILTER,
                              get_texture_filter_type(magfilter, true));

  // Set anisotropic filtering.
  if (_supports_anisotropy) {
    PN_stdfloat anisotropy = sampler.get_effective_anisotropic_degree();
    anisotropy = min(anisotropy, _max_anisotropy);
    anisotropy = max(anisotropy, (PN_stdfloat)1.0);
    _glSamplerParameterf(index, GL_TEXTURE_MAX_ANISOTROPY_EXT, anisotropy);
  }

  if (_supports_shadow_filter) {
    if ((sampler.get_magfilter() == SamplerState::FT_shadow) ||
        (sampler.get_minfilter() == SamplerState::FT_shadow)) {
      _glSamplerParameteri(index, GL_TEXTURE_COMPARE_MODE_ARB, GL_COMPARE_R_TO_TEXTURE_ARB);
      _glSamplerParameteri(index, GL_TEXTURE_COMPARE_FUNC_ARB, GL_LEQUAL);
    } else {
      _glSamplerParameteri(index, GL_TEXTURE_COMPARE_MODE_ARB, GL_NONE);
      _glSamplerParameteri(index, GL_TEXTURE_COMPARE_FUNC_ARB, GL_LEQUAL);
    }
  }

  if (_supports_texture_lod) {
    _glSamplerParameterf(index, GL_TEXTURE_MIN_LOD, sampler.get_min_lod());
    _glSamplerParameterf(index, GL_TEXTURE_MAX_LOD, sampler.get_max_lod());
  }

#ifndef OPENGLES
  if (_supports_texture_lod_bias) {
    _glSamplerParameterf(index, GL_TEXTURE_LOD_BIAS, sampler.get_lod_bias());
  }
#endif

  gsc->enqueue_lru(&_prepared_objects->_sampler_object_lru);

  report_my_gl_errors();
  return gsc;
}
#endif  // !OPENGLES_1

#ifndef OPENGLES_1
/**
 * Frees the GL resources previously allocated for the sampler.  This function
 * should never be called directly; instead, call SamplerState::release().
 */
void CLP(GraphicsStateGuardian)::
release_sampler(SamplerContext *sc) {
  CLP(SamplerContext) *gsc = DCAST(CLP(SamplerContext), sc);

  if (gsc->_index != 0) {
    _glDeleteSamplers(1, &gsc->_index);
  }

  delete gsc;
}
#endif  // !OPENGLES_1

/**
 * Creates a new retained-mode representation of the given geom, and returns a
 * newly-allocated GeomContext pointer to reference it.  It is the
 * responsibility of the calling function to later call release_geom() with
 * this same pointer (which will also delete the pointer).
 *
 * This function should not be called directly to prepare a geom.  Instead,
 * call Geom::prepare().
 */
GeomContext *CLP(GraphicsStateGuardian)::
prepare_geom(Geom *geom) {
  PStatGPUTimer timer(this, _prepare_geom_pcollector);
  return new CLP(GeomContext)(geom);
}

/**
 * Frees the GL resources previously allocated for the geom.  This function
 * should never be called directly; instead, call Geom::release() (or simply
 * let the Geom destruct).
 */
void CLP(GraphicsStateGuardian)::
release_geom(GeomContext *gc) {
  CLP(GeomContext) *ggc = DCAST(CLP(GeomContext), gc);
  if (has_fixed_function_pipeline()) {
    ggc->release_display_lists();
  }
  report_my_gl_errors();

  delete ggc;
}

/**
 *
 */
ShaderContext *CLP(GraphicsStateGuardian)::
prepare_shader(Shader *se) {
  PStatGPUTimer timer(this, se->get_prepare_shader_pcollector());

#ifndef OPENGLES_1
  ShaderContext *result = nullptr;

  switch (se->get_language()) {
  case Shader::SL_GLSL:
    if (_supports_glsl) {
      push_group_marker(std::string("Prepare Shader ") + se->get_debug_name());
      result = new CLP(ShaderContext)(this, se);
      pop_group_marker();
      break;
    } else {
      GLCAT.error()
        << "Tried to load GLSL shader, but GLSL shaders not supported.\n";
      return nullptr;
    }

  case Shader::SL_Cg:
#if defined(HAVE_CG) && !defined(OPENGLES)
    if (_supports_basic_shaders) {
      push_group_marker(std::string("Prepare Shader ") + se->get_debug_name());
      result = new CLP(CgShaderContext)(this, se);
      pop_group_marker();
      break;
    } else {
      GLCAT.error()
        << "Tried to load Cg shader, but basic shaders not supported.\n";
      return nullptr;
    }
#elif defined(OPENGLES)
    GLCAT.error()
      << "Tried to load Cg shader, but Cg support is not available for OpenGL ES.\n";
    return nullptr;
#else
    GLCAT.error()
      << "Tried to load Cg shader, but Cg support not compiled in.\n";
    return nullptr;
#endif

  default:
    GLCAT.error()
      << "Tried to load shader with unsupported shader language!\n";
    return nullptr;
  }

  if (result->valid()) {
    return result;
  }

  delete result;
#endif  // OPENGLES_1

  return nullptr;
}

/**
 *
 */
void CLP(GraphicsStateGuardian)::
release_shader(ShaderContext *sc) {
#ifndef OPENGLES_1
  if (sc->is_of_type(CLP(ShaderContext)::get_class_type())) {
    ((CLP(ShaderContext) *)sc)->release_resources();
  }
#if defined(HAVE_CG) && !defined(OPENGLES_2)
  else if (sc->is_of_type(CLP(CgShaderContext)::get_class_type())) {
    ((CLP(CgShaderContext) *)sc)->release_resources();
  }
#endif
#endif

  delete sc;
}

/**
 * This is intended to be called only from the GLGeomContext destructor.  It
 * saves the indicated display list index in the list to be deleted at the end
 * of the frame.
 */
void CLP(GraphicsStateGuardian)::
record_deleted_display_list(GLuint index) {
  LightMutexHolder holder(_lock);
  _deleted_display_lists.push_back(index);
}

#ifndef OPENGLES_1
/**
 * Creates a new buffer for client access.  It is bound when this returns.
 * If persistent mapping is possible, mapped_ptr will be filled in with a
 * pointer to the mapped data.
 */
void CLP(GraphicsStateGuardian)::
bind_new_client_buffer(GLuint &index, void *&mapped_ptr, GLenum target, size_t size) {
  _active_ppbuffer_memory_pcollector.add_level(size);

  {
    // Start at the end, because removing near the end is cheaper.
    LightMutexHolder holder(_lock);
    size_t i = _deleted_buffers.size();
    while (i > 1) {
      --i;
      DeletedBuffer &buffer = _deleted_buffers[i];
      if (buffer._size == size) {
        index = buffer._index;
        mapped_ptr = buffer._mapped_pointer;
        _glBindBuffer(target, buffer._index);
        if (!_supports_buffer_storage && mapped_ptr != nullptr) {
          // Need to unmap it before we can use it.
          _glUnmapBuffer(target);
          mapped_ptr = nullptr;
        }
        _deleted_buffers.erase(_deleted_buffers.begin() + i);
        _inactive_ppbuffer_memory_pcollector.sub_level(size);
        return;
      }
    }
  }

  _glGenBuffers(1, &index);
  _glBindBuffer(target, index);
#ifndef OPENGLES
  if (_supports_buffer_storage) {
    // Map persistently, we already use fences to synchronize access anyway.
    _glBufferStorage(target, size, nullptr, GL_MAP_READ_BIT |
                     GL_CLIENT_STORAGE_BIT | GL_MAP_PERSISTENT_BIT);
    mapped_ptr = _glMapBufferRange(target, 0, size,
                                   GL_MAP_READ_BIT | GL_MAP_PERSISTENT_BIT);
  } else
#endif
  {
    //XXX does it matter what usage hint we pass here?  None seem to fit well.
    _glBufferData(target, size, nullptr, GL_DYNAMIC_DRAW);
    mapped_ptr = nullptr;
  }
}

/**
 * Called when the given buffer, as returned by bind_new_client_buffer, is no
 * longer needed.
 */
void CLP(GraphicsStateGuardian)::
release_client_buffer(GLuint index, void *mapped_ptr, size_t size) {
  // This may be called from any thread, so we can't make OpenGL calls here
  // (like unmapping the buffer).
  LightMutexHolder holder(_lock);
  _deleted_buffers.push_back({index, 0, mapped_ptr, size});
  _active_ppbuffer_memory_pcollector.sub_level(size);
  _inactive_ppbuffer_memory_pcollector.add_level(size);
}
#endif  // !OPENGLES_1

/**
 * Creates a new retained-mode representation of the given data, and returns a
 * newly-allocated VertexBufferContext pointer to reference it.  It is the
 * responsibility of the calling function to later call
 * release_vertex_buffer() with this same pointer (which will also delete the
 * pointer).
 *
 * This function should not be called directly to prepare a buffer.  Instead,
 * call Geom::prepare().
 */
VertexBufferContext *CLP(GraphicsStateGuardian)::
prepare_vertex_buffer(GeomVertexArrayData *data) {
  if (_supports_buffers) {
    PStatGPUTimer timer(this, _prepare_vertex_buffer_pcollector);

    CLP(VertexBufferContext) *gvbc = new CLP(VertexBufferContext)(this, _prepared_objects, data);
    _glGenBuffers(1, &gvbc->_index);

    if (GLCAT.is_debug() && gl_debug_buffers) {
      GLCAT.debug()
        << "creating vertex buffer " << (int)gvbc->_index << ": "
        << data->get_num_rows() << " vertices "
        << *data->get_array_format() << "\n";
    }

    report_my_gl_errors();
    update_vertex_buffer(gvbc, data->get_handle(), false);
    return gvbc;
  }

  return nullptr;
}

/**
 * Makes sure that the data in the vertex buffer is up-to-date.  This may bind
 * it to the GL_ARRAY_BUFFER binding point if necessary.
 */
bool CLP(GraphicsStateGuardian)::
update_vertex_buffer(CLP(VertexBufferContext) *gvbc,
                     const GeomVertexArrayDataHandle *reader, bool force) {
  nassertr(_supports_buffers, false);
  if (reader->get_modified() == UpdateSeq::initial()) {
    // No need to re-apply.
    return true;
  }

  gvbc->set_active(true);

  if (gvbc->was_modified(reader)) {
    int num_bytes = reader->get_data_size_bytes();
    if (GLCAT.is_debug() && gl_debug_buffers) {
      GLCAT.debug()
        << "copying " << num_bytes
        << " bytes into vertex buffer " << (int)gvbc->_index << "\n";
    }
    if (num_bytes != 0) {
      const unsigned char *client_pointer = reader->get_read_pointer(force);
      if (client_pointer == nullptr) {
        return false;
      }

      PStatGPUTimer timer(this, _load_vertex_buffer_pcollector, reader->get_current_thread());
      if (_current_vbuffer_index != gvbc->_index) {
        if (GLCAT.is_spam() && gl_debug_buffers) {
          GLCAT.spam()
            << "binding vertex buffer " << (int)gvbc->_index << "\n";
        }
        _glBindBuffer(GL_ARRAY_BUFFER, gvbc->_index);
        _current_vbuffer_index = gvbc->_index;
      }

      if (gvbc->changed_size(reader) || gvbc->changed_usage_hint(reader)) {
        _glBufferData(GL_ARRAY_BUFFER, num_bytes, client_pointer,
                      get_usage(reader->get_usage_hint()));

      } else {
        _glBufferSubData(GL_ARRAY_BUFFER, 0, num_bytes, client_pointer);
      }
      _data_transferred_pcollector.add_level(num_bytes);
    }

    gvbc->mark_loaded(reader);
  }
  gvbc->enqueue_lru(&_prepared_objects->_graphics_memory_lru);

  maybe_gl_finish();
  report_my_gl_errors();
  return true;
}

/**
 * Frees the GL resources previously allocated for the data.  This function
 * should never be called directly; instead, call Data::release() (or simply
 * let the Data destruct).
 */
void CLP(GraphicsStateGuardian)::
release_vertex_buffer(VertexBufferContext *vbc) {
  nassertv(_supports_buffers);

  CLP(VertexBufferContext) *gvbc = DCAST(CLP(VertexBufferContext), vbc);

  if (GLCAT.is_debug() && gl_debug_buffers) {
    GLCAT.debug()
      << "deleting vertex buffer " << (int)gvbc->_index << "\n";
  }

  // Make sure the buffer is unbound before we delete it.  Not strictly
  // necessary according to the OpenGL spec, but it might help out a flaky
  // driver, and we need to keep our internal state consistent anyway.
  if (_current_vbuffer_index == gvbc->_index) {
    if (GLCAT.is_spam() && gl_debug_buffers) {
      GLCAT.spam()
        << "unbinding vertex buffer\n";
    }
    _glBindBuffer(GL_ARRAY_BUFFER, 0);
    _current_vbuffer_index = 0;
  }

  _glDeleteBuffers(1, &gvbc->_index);
  report_my_gl_errors();

  gvbc->_index = 0;

  delete gvbc;
}

/**
 * Frees the GL resources previously allocated for the data.  This function
 * should never be called directly; instead, call Data::release() (or simply
 * let the Data destruct).
 */
void CLP(GraphicsStateGuardian)::
release_vertex_buffers(const pvector<BufferContext *> &contexts) {
  if (contexts.empty()) {
    return;
  }
  nassertv(_supports_buffers);
  bool debug = GLCAT.is_debug() && gl_debug_buffers;

  GLuint *indices = (GLuint *)alloca(sizeof(GLuint) * contexts.size());
  size_t num_indices = 0;

  for (BufferContext *bc : contexts) {
    CLP(VertexBufferContext) *gvbc = DCAST(CLP(VertexBufferContext), bc);

    // Make sure the buffer is unbound before we delete it.  Not strictly
    // necessary according to the OpenGL spec, but it might help out a flaky
    // driver, and we need to keep our internal state consistent anyway.
    if (_current_vbuffer_index == gvbc->_index) {
      if (debug && GLCAT.is_spam()) {
        GLCAT.spam()
          << "unbinding vertex buffer " << gvbc->_index << "\n";
      }
      _glBindBuffer(GL_ARRAY_BUFFER, 0);
      _current_vbuffer_index = 0;
    }

    if (debug) {
      GLCAT.debug()
        << "deleting vertex buffer " << gvbc->_index << "\n";
    }

    indices[num_indices++] = gvbc->_index;
    gvbc->_index = 0;

    delete gvbc;
  }

  _glDeleteBuffers(num_indices, indices);
  report_my_gl_errors();
}

/**
 * Internal function to bind a buffer object for the indicated data array, if
 * appropriate, or to unbind a buffer object if it should be rendered from
 * client memory.
 *
 * If the buffer object is bound, this function sets client_pointer to NULL
 * (representing the start of the buffer object in server memory); if the
 * buffer object is not bound, this function sets client_pointer the pointer
 * to the data array in client memory, that is, the data array passed in.
 *
 * If force is not true, the function may return false indicating the data is
 * not currently available.
 */
bool CLP(GraphicsStateGuardian)::
setup_array_data(const unsigned char *&client_pointer,
                 const GeomVertexArrayDataHandle *array_reader,
                 bool force) {
  if (!_supports_buffers) {
    // No support for buffer objects; always render from client.
    client_pointer = array_reader->get_read_pointer(force);
    return (client_pointer != nullptr);
  }
  if (!vertex_buffers || _geom_display_list != 0 ||
      array_reader->get_usage_hint() < gl_min_buffer_usage_hint) {
    // The array specifies client rendering only, or buffer objects are
    // configured off.
    if (_current_vbuffer_index != 0) {
      if (GLCAT.is_spam() && gl_debug_buffers) {
        GLCAT.spam()
          << "unbinding vertex buffer\n";
      }
      _glBindBuffer(GL_ARRAY_BUFFER, 0);
      _current_vbuffer_index = 0;
    }
    client_pointer = array_reader->get_read_pointer(force);
    return (client_pointer != nullptr);
  }

#ifndef OPENGLES_1
  int stride = array_reader->get_array_format()->get_stride();
  if (stride > _max_vertex_attrib_stride) {
    GLCAT.error()
      << "Vertex array stride " << stride << " exceeds supported maximum "
      << _max_vertex_attrib_stride << "!\n";
    return false;
  }
#endif

  // Prepare the buffer object and bind it.
  CLP(VertexBufferContext) *gvbc = DCAST(CLP(VertexBufferContext),
    array_reader->prepare_now(get_prepared_objects(), this));

  nassertr(gvbc != (CLP(VertexBufferContext) *)nullptr, false);
  if (!update_vertex_buffer(gvbc, array_reader, force)) {
    return false;
  }

  if (_current_vbuffer_index != gvbc->_index) {
    if (GLCAT.is_spam() && gl_debug_buffers) {
      GLCAT.spam()
        << "binding vertex buffer " << (int)gvbc->_index << "\n";
    }
    _glBindBuffer(GL_ARRAY_BUFFER, gvbc->_index);
    _current_vbuffer_index = gvbc->_index;
  }

  // NULL is the OpenGL convention for the first byte of the buffer object.
  client_pointer = nullptr;
  return true;
}

/**
 * Creates a new retained-mode representation of the given data, and returns a
 * newly-allocated IndexBufferContext pointer to reference it.  It is the
 * responsibility of the calling function to later call release_index_buffer()
 * with this same pointer (which will also delete the pointer).
 *
 * This function should not be called directly to prepare a buffer.  Instead,
 * call Geom::prepare().
 */
IndexBufferContext *CLP(GraphicsStateGuardian)::
prepare_index_buffer(GeomPrimitive *data) {
  if (_supports_buffers) {
    PStatGPUTimer timer(this, _prepare_index_buffer_pcollector);

    CLP(IndexBufferContext) *gibc = new CLP(IndexBufferContext)(this, _prepared_objects, data);
    _glGenBuffers(1, &gibc->_index);

    if (GLCAT.is_debug() && gl_debug_buffers) {
      GLCAT.debug()
        << "creating index buffer " << (int)gibc->_index << ": "
        << data->get_num_vertices() << " indices ("
        << data->get_vertices()->get_array_format()->get_column(0)->get_numeric_type()
        << ")\n";
    }

    report_my_gl_errors();
    GeomPrimitivePipelineReader reader(data, Thread::get_current_thread());
    apply_index_buffer(gibc, &reader, false);
    return gibc;
  }

  return nullptr;
}

/**
 * Makes the data the currently available data for rendering.
 */
bool CLP(GraphicsStateGuardian)::
apply_index_buffer(IndexBufferContext *ibc,
                   const GeomPrimitivePipelineReader *reader,
                   bool force) {
  nassertr(_supports_buffers, false);
  if (reader->get_modified() == UpdateSeq::initial()) {
    // No need to re-apply.
    return true;
  }

  CLP(IndexBufferContext) *gibc = DCAST(CLP(IndexBufferContext), ibc);

  if (_current_ibuffer_index != gibc->_index) {
    if (GLCAT.is_spam() && gl_debug_buffers) {
      GLCAT.spam()
        << "binding index buffer " << (int)gibc->_index << "\n";
    }
    _glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gibc->_index);
    _current_ibuffer_index = gibc->_index;
    gibc->set_active(true);
  }

  if (gibc->was_modified(reader)) {
    int num_bytes = reader->get_data_size_bytes();
    if (GLCAT.is_debug() && gl_debug_buffers) {
      GLCAT.debug()
        << "copying " << num_bytes
        << " bytes into index buffer " << (int)gibc->_index << "\n";
    }
    if (num_bytes != 0) {
      const unsigned char *client_pointer = reader->get_read_pointer(force);
      if (client_pointer == nullptr) {
        return false;
      }

      PStatGPUTimer timer(this, _load_index_buffer_pcollector, reader->get_current_thread());
      if (gibc->changed_size(reader) || gibc->changed_usage_hint(reader)) {
        _glBufferData(GL_ELEMENT_ARRAY_BUFFER, num_bytes, client_pointer,
                      get_usage(reader->get_usage_hint()));

      } else {
        _glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, num_bytes,
                         client_pointer);
      }
      _data_transferred_pcollector.add_level(num_bytes);
    }
    gibc->mark_loaded(reader);
  }
  gibc->enqueue_lru(&_prepared_objects->_graphics_memory_lru);

  maybe_gl_finish();
  report_my_gl_errors();
  return true;
}

/**
 * Frees the GL resources previously allocated for the data.  This function
 * should never be called directly; instead, call Data::release() (or simply
 * let the Data destruct).
 */
void CLP(GraphicsStateGuardian)::
release_index_buffer(IndexBufferContext *ibc) {
  nassertv(_supports_buffers);

  CLP(IndexBufferContext) *gibc = DCAST(CLP(IndexBufferContext), ibc);

  if (GLCAT.is_debug() && gl_debug_buffers) {
    GLCAT.debug()
      << "deleting index buffer " << (int)gibc->_index << "\n";
  }

  // Make sure the buffer is unbound before we delete it.  Not strictly
  // necessary according to the OpenGL spec, but it might help out a flaky
  // driver, and we need to keep our internal state consistent anyway.
  if (_current_ibuffer_index == gibc->_index) {
    if (GLCAT.is_spam() && gl_debug_buffers) {
      GLCAT.spam()
        << "unbinding index buffer\n";
    }
    _glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    _current_ibuffer_index = 0;
  }

  _glDeleteBuffers(1, &gibc->_index);
  report_my_gl_errors();

  gibc->_index = 0;

  delete gibc;
}

/**
 * Frees the GL resources previously allocated for the data.  This function
 * should never be called directly; instead, call Data::release() (or simply
 * let the Data destruct).
 */
void CLP(GraphicsStateGuardian)::
release_index_buffers(const pvector<BufferContext *> &contexts) {
  if (contexts.empty()) {
    return;
  }
  nassertv(_supports_buffers);
  bool debug = GLCAT.is_debug() && gl_debug_buffers;

  GLuint *indices = (GLuint *)alloca(sizeof(GLuint) * contexts.size());
  size_t num_indices = 0;

  for (BufferContext *bc : contexts) {
    CLP(IndexBufferContext) *gibc = DCAST(CLP(IndexBufferContext), bc);

    // Make sure the buffer is unbound before we delete it.  Not strictly
    // necessary according to the OpenGL spec, but it might help out a flaky
    // driver, and we need to keep our internal state consistent anyway.
    if (_current_ibuffer_index == gibc->_index) {
      if (debug && GLCAT.is_spam()) {
        GLCAT.spam()
          << "unbinding index buffer " << gibc->_index << "\n";
      }
      _glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
      _current_ibuffer_index = 0;
    }

    if (debug) {
      GLCAT.debug()
        << "deleting index buffer " << gibc->_index << "\n";
    }

    indices[num_indices++] = gibc->_index;
    gibc->_index = 0;

    delete gibc;
  }

  _glDeleteBuffers(num_indices, indices);
  report_my_gl_errors();
}

/**
 * Internal function to bind a buffer object for the indicated primitive's
 * index list, if appropriate, or to unbind a buffer object if it should be
 * rendered from client memory.
 *
 * If the buffer object is bound, this function sets client_pointer to NULL
 * (representing the start of the buffer object in server memory); if the
 * buffer object is not bound, this function sets client_pointer to to the
 * data array in client memory, that is, the data array passed in.
 *
 * If force is not true, the function may return false indicating the data is
 * not currently available.
 */
bool CLP(GraphicsStateGuardian)::
setup_primitive(const unsigned char *&client_pointer,
                const GeomPrimitivePipelineReader *reader,
                bool force) {
  if (!_supports_buffers) {
    // No support for buffer objects; always render from client.
    client_pointer = reader->get_read_pointer(force);
    return (client_pointer != nullptr);
  }
  if (!vertex_buffers || _geom_display_list != 0 ||
      reader->get_usage_hint() == Geom::UH_client) {
    // The array specifies client rendering only, or buffer objects are
    // configured off.
    if (_current_ibuffer_index != 0) {
      if (GLCAT.is_spam() && gl_debug_buffers) {
        GLCAT.spam()
          << "unbinding index buffer\n";
      }
      _glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
      _current_ibuffer_index = 0;
    }
    client_pointer = reader->get_read_pointer(force);
    return (client_pointer != nullptr);
  }

  // Prepare the buffer object and bind it.
  IndexBufferContext *ibc = reader->prepare_now(get_prepared_objects(), this);
  nassertr(ibc != nullptr, false);
  if (!apply_index_buffer(ibc, reader, force)) {
    return false;
  }

  // NULL is the OpenGL convention for the first byte of the buffer object.
  client_pointer = nullptr;
  return true;
}

#ifndef OPENGLES
/**
 * Creates a new retained-mode representation of the given data, and returns a
 * newly-allocated BufferContext pointer to reference it.  It is the
 * responsibility of the calling function to later call release_shader_buffer()
 * with this same pointer (which will also delete the pointer).
 *
 * This function should not be called directly to prepare a buffer.  Instead,
 * call ShaderBuffer::prepare().
 */
BufferContext *CLP(GraphicsStateGuardian)::
prepare_shader_buffer(ShaderBuffer *data) {
  if (_supports_shader_buffers) {
    PStatGPUTimer timer(this, _prepare_shader_buffer_pcollector);

    CLP(BufferContext) *gbc = new CLP(BufferContext)(this, _prepared_objects, data);
    _glGenBuffers(1, &gbc->_index);

    if (GLCAT.is_debug() && gl_debug_buffers) {
      GLCAT.debug()
        << "creating shader buffer " << (int)gbc->_index << ": "<< *data << "\n";
    }
    _glBindBuffer(GL_SHADER_STORAGE_BUFFER, gbc->_index);
    _current_sbuffer_index = gbc->_index;

    if (_use_object_labels) {
      string name = data->get_name();
      _glObjectLabel(GL_BUFFER, gbc->_index, name.size(), name.data());
    }

    // Some drivers require the buffer to be padded to 16 byte boundary.
    uint64_t num_bytes = (data->get_data_size_bytes() + 15u) & ~15u;
    if (_supports_buffer_storage) {
      _glBufferStorage(GL_SHADER_STORAGE_BUFFER, num_bytes, data->get_initial_data(), 0);
    } else {
      _glBufferData(GL_SHADER_STORAGE_BUFFER, num_bytes, data->get_initial_data(), get_usage(data->get_usage_hint()));
    }

    gbc->enqueue_lru(&_prepared_objects->_graphics_memory_lru);

    report_my_gl_errors();
    return gbc;
  }

  return nullptr;
}

/**
 * Binds the given shader buffer to the given binding slot.
 */
void CLP(GraphicsStateGuardian)::
apply_shader_buffer(GLuint base, ShaderBuffer *buffer) {
  GLuint index = 0;
  if (buffer != nullptr) {
    BufferContext *bc = buffer->prepare_now(get_prepared_objects(), this);
    if (bc != nullptr) {
      CLP(BufferContext) *gbc = DCAST(CLP(BufferContext), bc);
      index = gbc->_index;
      gbc->set_active(true);
    }
  }

  if (base >= _current_sbuffer_base.size()) {
    _current_sbuffer_base.resize(base + 1, 0);
  }

  if (_current_sbuffer_base[base] != index) {
    if (GLCAT.is_spam() && gl_debug_buffers) {
      GLCAT.spam()
        << "binding shader buffer " << (int)index
        << " to index " << base << "\n";
    }
    _glBindBufferBase(GL_SHADER_STORAGE_BUFFER, base, index);
    _current_sbuffer_base[base] = index;
    _current_sbuffer_index = index;

    report_my_gl_errors();
  }
}

/**
 * Frees the GL resources previously allocated for the data.  This function
 * should never be called directly; instead, call Data::release() (or simply
 * let the Data destruct).
 */
void CLP(GraphicsStateGuardian)::
release_shader_buffer(BufferContext *bc) {
  nassertv(_supports_buffers);

  CLP(BufferContext) *gbc = DCAST(CLP(BufferContext), bc);

  if (GLCAT.is_debug() && gl_debug_buffers) {
    GLCAT.debug()
      << "deleting shader buffer " << (int)gbc->_index << "\n";
  }

  // Make sure the buffer is unbound before we delete it.  Not strictly
  // necessary according to the OpenGL spec, but it might help out a flaky
  // driver, and we need to keep our internal state consistent anyway.
  if (_current_sbuffer_index == gbc->_index) {
    if (GLCAT.is_spam() && gl_debug_buffers) {
      GLCAT.spam()
        << "unbinding shader buffer\n";
    }
    _glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
    _current_sbuffer_index = 0;
  }

  _glDeleteBuffers(1, &gbc->_index);
  report_my_gl_errors();

  gbc->_index = 0;

  delete gbc;
}

/**
 * Frees the GL resources previously allocated for the data.  This function
 * should never be called directly; instead, call Data::release() (or simply
 * let the Data destruct).
 */
void CLP(GraphicsStateGuardian)::
release_shader_buffers(const pvector<BufferContext *> &contexts) {
  if (contexts.empty()) {
    return;
  }
  nassertv(_supports_buffers);
  bool debug = GLCAT.is_debug() && gl_debug_buffers;

  GLuint *indices = (GLuint *)alloca(sizeof(GLuint) * contexts.size());
  size_t num_indices = 0;

  for (BufferContext *bc : contexts) {
    CLP(BufferContext) *gbc = DCAST(CLP(BufferContext), bc);

    // Make sure the buffer is unbound before we delete it.  Not strictly
    // necessary according to the OpenGL spec, but it might help out a flaky
    // driver, and we need to keep our internal state consistent anyway.
    if (_current_sbuffer_index == gbc->_index) {
      if (debug && GLCAT.is_spam()) {
        GLCAT.spam()
          << "unbinding shader buffer " << gbc->_index << "\n";
      }
      _glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
      _current_sbuffer_index = 0;
    }

    if (debug) {
      GLCAT.debug()
        << "deleting shader buffer " << gbc->_index << "\n";
    }

    indices[num_indices++] = gbc->_index;
    gbc->_index = 0;

    delete gbc;
  }

  _glDeleteBuffers(num_indices, indices);
  report_my_gl_errors();
}
#endif

#ifndef OPENGLES
/**
 * Begins a new occlusion query.  After this call, you may call
 * begin_draw_primitives() and draw_triangles()/draw_whatever() repeatedly.
 * Eventually, you should call end_occlusion_query() before the end of the
 * frame; that will return a new OcclusionQueryContext object that will tell
 * you how many pixels represented by the bracketed geometry passed the depth
 * test.
 *
 * It is not valid to call begin_occlusion_query() between another
 * begin_occlusion_query() .. end_occlusion_query() sequence.
 */
void CLP(GraphicsStateGuardian)::
begin_occlusion_query() {
  nassertv(_supports_occlusion_query);
  nassertv(_current_occlusion_query == nullptr);
  PT(CLP(OcclusionQueryContext)) query = new CLP(OcclusionQueryContext)(this);

  _glGenQueries(1, &query->_index);

  if (GLCAT.is_debug()) {
    GLCAT.debug()
      << "beginning occlusion query index " << (int)query->_index << "\n";
  }

  _glBeginQuery(GL_SAMPLES_PASSED, query->_index);
  _current_occlusion_query = query;

  report_my_gl_errors();
}
#endif  // !OPENGLES

#ifndef OPENGLES
/**
 * Ends a previous call to begin_occlusion_query(). This call returns the
 * OcclusionQueryContext object that will (eventually) report the number of
 * pixels that passed the depth test between the call to
 * begin_occlusion_query() and end_occlusion_query().
 */
PT(OcclusionQueryContext) CLP(GraphicsStateGuardian)::
end_occlusion_query() {
  nassertr(_current_occlusion_query != nullptr, nullptr);
  PT(OcclusionQueryContext) result = _current_occlusion_query;

  GLuint index = DCAST(CLP(OcclusionQueryContext), result)->_index;

  if (GLCAT.is_debug()) {
    GLCAT.debug()
      << "ending occlusion query index " << (int)index << "\n";
  }

  _current_occlusion_query = nullptr;
  _glEndQuery(GL_SAMPLES_PASSED);

  // Temporary hack to try working around an apparent driver bug on iMacs.
  // Occlusion queries sometimes incorrectly report 0 samples, unless we stall
  // the pipe to keep fewer than a certain maximum number of queries pending
  // at once.
  static ConfigVariableInt limit_occlusion_queries("limit-occlusion-queries", 0);
  if (limit_occlusion_queries > 0) {
    if (index > (unsigned int)limit_occlusion_queries) {
      PStatGPUTimer timer(this, _wait_occlusion_pcollector);
      GLuint result;
      _glGetQueryObjectuiv(index - (unsigned int)limit_occlusion_queries,
                           GL_QUERY_RESULT, &result);
    }
  }

  report_my_gl_errors();

  return result;
}
#endif  // !OPENGLES

/**
 * Adds a timer query to the command stream, associated with the given PStats
 * collector index.
 */
void CLP(GraphicsStateGuardian)::
issue_timer_query(int pstats_index) {
#if defined(DO_PSTATS) && !defined(OPENGLES)
  FrameTiming *frame = _current_frame_timing;
  if (frame == nullptr) {
    return;
  }

  nassertv(_supports_timer_query);

  if (_deleted_queries.empty()) {
    // Allocate some number at a time, since we'll need a lot of these.
    _deleted_queries.resize(_deleted_queries.size() + 16);
    _glGenQueries(16, _deleted_queries.data() + _deleted_queries.size() - 16);
  }

  GLuint index = _deleted_queries.back();
  _deleted_queries.pop_back();

  // Issue the timestamp query.
  _glQueryCounter(index, GL_TIMESTAMP);

  //if (_use_object_labels) {
  //  // Assign a label to it based on the PStatCollector name.
  //  const PStatClient *client = PStatClient::get_global_pstats();
  //  string name = client->get_collector_fullname(pstats_index & 0x7fff);
  //  _glObjectLabel(GL_QUERY, index, name.size(), name.data());
  //}

  frame->_queries.push_back(std::make_pair(index, pstats_index));
#endif
}

/**
 * A latency query is a special type of timer query that measures the
 * difference between CPU time and GPU time, ie. how far the GPU is behind in
 * processing the commands being generated by the CPU right now.
 */
void CLP(GraphicsStateGuardian)::
issue_latency_query(int pstats_index) {
#if defined(DO_PSTATS) && !defined(OPENGLES)
  FrameTiming *frame = _current_frame_timing;
  if (frame == nullptr) {
    return;
  }

  GLint64 time;
  _glGetInteger64v(GL_TIMESTAMP, &time);
  issue_timer_query(pstats_index | 0x10000);

  frame->_latency_refs.push_back(time);
#endif
}

#ifndef OPENGLES_1
/**
 * Dispatches a currently bound compute shader using the given work group
 * counts.
 */
void CLP(GraphicsStateGuardian)::
dispatch_compute(int num_groups_x, int num_groups_y, int num_groups_z) {
  maybe_gl_finish();

  PStatGPUTimer timer(this, _compute_dispatch_pcollector);
  nassertv(_supports_compute_shaders);
  nassertv(_current_shader_context != nullptr);
  _glDispatchCompute(num_groups_x, num_groups_y, num_groups_z);

  maybe_gl_finish();
}
#endif  // !OPENGLES_1

/**
 * Creates a new GeomMunger object to munge vertices appropriate to this GSG
 * for the indicated state.
 */
PT(GeomMunger) CLP(GraphicsStateGuardian)::
make_geom_munger(const RenderState *state, Thread *current_thread) {
  PT(CLP(GeomMunger)) munger = new CLP(GeomMunger)(this, state);
  return GeomMunger::register_munger(munger, current_thread);
}

/**
 * Copy the pixels within the indicated display region from the framebuffer
 * into texture memory.
 *
 * If z > -1, it is the cube map index or layer index into which to copy.
 */
bool CLP(GraphicsStateGuardian)::
framebuffer_copy_to_texture(Texture *tex, int view, int z,
                            const DisplayRegion *dr, const RenderBuffer &rb) {
  nassertr(tex != nullptr && dr != nullptr, false);
  set_read_buffer(rb._buffer_type);
  clear_color_write_mask();

  int xo, yo, w, h;
  dr->get_region_pixels(xo, yo, w, h);
  tex->set_size_padded(w, h, tex->get_z_size());

  if (tex->get_compression() == Texture::CM_default) {
    // Unless the user explicitly turned on texture compression, turn it off
    // for the copy-to-texture case.
    tex->set_compression(Texture::CM_off);
  }

  // Sanity check everything.
  if (z >= 0) {
    if (z >= tex->get_z_size()) {
      // This can happen, when textures with different layer counts are
      // attached to a buffer.  We simply ignore this if it happens.
      return false;
    }

    if ((w != tex->get_x_size()) ||
        (h != tex->get_y_size())) {
      return false;
    }

    if (tex->get_texture_type() == Texture::TT_cube_map) {
      if (!_supports_cube_map) {
        return false;
      }

      nassertr(z < 6, false);
      if (w != h) {
        return false;
      }

    } else if (tex->get_texture_type() == Texture::TT_3d_texture) {
      if (!_supports_3d_texture) {
        return false;
      }

    } else if (tex->get_texture_type() == Texture::TT_2d_texture_array) {
      if (!_supports_2d_texture_array) {
        return false;
      }

    } else {
      GLCAT.error()
        << "Don't know how to copy framebuffer to texture " << *tex << "\n";
    }
  } else {
    nassertr(tex->get_texture_type() == Texture::TT_2d_texture, false);
  }

  // Match framebuffer format if necessary.
  if (tex->get_match_framebuffer_format()) {

    switch (tex->get_format()) {
    case Texture::F_depth_component:
    case Texture::F_depth_component16:
    case Texture::F_depth_component24:
    case Texture::F_depth_component32:
    case Texture::F_depth_stencil:
      // Don't remap if we're one of these special format.
      break;

    default:
      // If the texture is a color format, we want to match the presence of
      // sRGB and alpha according to the framebuffer.
      if (_current_properties->get_srgb_color()) {
        if (_current_properties->get_alpha_bits()) {
          tex->set_format(Texture::F_srgb_alpha);
        } else {
          tex->set_format(Texture::F_srgb);
        }
      } else {
        if (_current_properties->get_alpha_bits()) {
          tex->set_format(Texture::F_rgba);
        } else {
          tex->set_format(Texture::F_rgb);
        }
      }
    }
  }

  TextureContext *tc = tex->prepare_now(get_prepared_objects(), this);
  nassertr(tc != nullptr, false);
  CLP(TextureContext) *gtc = DCAST(CLP(TextureContext), tc);

  GLenum target = get_texture_target(tex->get_texture_type());
  if (gtc->_target != target) {
    gtc->reset_data(target, view + 1);
  }
  else if (view >= gtc->_num_views) {
    gtc->set_num_views(view + 1);
  }

  apply_texture(gtc, view);
  bool needs_reload = specify_texture(gtc, tex->get_default_sampler());

  GLint internal_format = get_internal_image_format(tex);
  int width = tex->get_x_size();
  int height = tex->get_y_size();
  int depth = tex->get_z_size();

  bool uses_mipmaps = tex->uses_mipmaps() && !gl_ignore_mipmaps;
  int num_levels = 1;
  bool can_generate = _supports_generate_mipmap;
#if defined(OPENGLES) && !defined(OPENGLES_1)
  // OpenGL ES doesn't support generating mipmaps for sRGB textures, so we
  // have to disable mipmaps, unless we have a special extension.
  if (internal_format == GL_SRGB8 || internal_format == GL_SRGB8_ALPHA8) {
    can_generate = has_extension("GL_NV_generate_mipmap_sRGB");
  }
#endif
  if (uses_mipmaps) {
    if (can_generate) {
#ifndef OPENGLES_2
      if (_glGenerateMipmap == nullptr) {
        glTexParameteri(target, GL_GENERATE_MIPMAP, true);
      }
#endif
      num_levels = tex->get_expected_num_mipmap_levels();
    } else {
      // If we can't auto-generate mipmaps, do without mipmaps.
      glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
      uses_mipmaps = false;
    }
  }

  bool new_image = needs_reload || gtc->was_image_modified();

  if (z >= 0) {
    if (target == GL_TEXTURE_CUBE_MAP) {
      // Copy to a cube map face, which is treated as a 2D texture.
      target = GL_TEXTURE_CUBE_MAP_POSITIVE_X + z;
      depth = 1;
      z = -1;

      // Cube map faces seem to have trouble with CopyTexSubImage, so we
      // always reload the image.
      new_image = true;
    }
  }

  if (!gtc->_has_storage ||
      internal_format != gtc->_internal_format ||
      width != gtc->_width ||
      height != gtc->_height ||
      depth != gtc->_depth ||
      (gtc->_immutable && num_levels > gtc->_num_levels)) {
    // If the texture properties have changed, we need to reload the image.
    new_image = true;
  }

  if (new_image && gtc->_immutable) {
    gtc->reset_data(target, view + 1);
    GLuint index = gtc->get_view_index(view);
    glBindTexture(target, index);

    if (GLCAT.is_spam()) {
      GLCAT.spam()
        << "glBindTexture(0x" << hex << target << dec << ", " << index << "): "
        << *tex << " view " << view << "\n";
    }
  }

#ifndef OPENGLES_1
  if (gtc->needs_barrier(GL_TEXTURE_UPDATE_BARRIER_BIT)) {
    // Make sure that any incoherent writes to this texture have been synced.
    issue_memory_barrier(GL_TEXTURE_UPDATE_BARRIER_BIT);
  }
#endif

  if (z >= 0) {
#ifndef OPENGLES_1
    if (new_image) {
      // These won't be used because we pass a NULL image, but we still have
      // to specify them.  Might as well use the actual values.
      GLint external_format = get_external_image_format(tex);
      GLint component_type = get_component_type(tex->get_component_type());
      _glTexImage3D(target, 0, internal_format, width, height, depth, 0, external_format, component_type, nullptr);
    }

    _glCopyTexSubImage3D(target, 0, 0, 0, z, xo, yo, w, h);
#endif
  } else {
    if (new_image) {
      // We have to create a new image.  It seems that OpenGL accepts a size
      // higher than the framebuffer, but if we run into trouble we'll have to
      // replace this with something smarter.
      glCopyTexImage2D(target, 0, internal_format, xo, yo, width, height, 0);
    } else {
      // We can overlay the existing image.
      glCopyTexSubImage2D(target, 0, 0, 0, xo, yo, w, h);
    }
  }

  if (uses_mipmaps && _glGenerateMipmap != nullptr) {
    glEnable(target);
    _glGenerateMipmap(target);
    glDisable(target);
  }

  gtc->_has_storage = true;
  gtc->_internal_format = internal_format;
  gtc->_width = width;
  gtc->_height = height;
  gtc->_depth = depth;
  gtc->_num_levels = num_levels;
  gtc->_may_reload_with_mipmaps = !uses_mipmaps && can_generate;

  gtc->mark_loaded();
  gtc->enqueue_lru(&_prepared_objects->_graphics_memory_lru);

  report_my_gl_errors();

  // Force reload of texture state, since we've just monkeyed with it.
  _state_mask.clear_bit(TextureAttrib::get_class_slot());

  return true;
}

/**
 * Copy the pixels within the indicated display region from the framebuffer
 * into system memory, not texture memory.  Returns true on success, false on
 * failure.
 *
 * This completely redefines the ram image of the indicated texture.
 */
bool CLP(GraphicsStateGuardian)::
framebuffer_copy_to_ram(Texture *tex, int view, int z,
                        const DisplayRegion *dr, const RenderBuffer &rb,
                        ScreenshotRequest *request) {
  nassertr(tex != nullptr && dr != nullptr, false);
  set_read_buffer(rb._buffer_type);
  glPixelStorei(GL_PACK_ALIGNMENT, 1);
  clear_color_write_mask();

  // Bug fix for RE, RE2, and VTX - need to disable texturing in order for
  // glReadPixels() to work NOTE: reading the depth buffer is *much* slower
  // than reading the color buffer
  set_state_and_transform(RenderState::make_empty(), _internal_transform);

  int xo, yo, w, h;
  dr->get_region_pixels(xo, yo, w, h);

  Texture::ComponentType component_type = tex->get_component_type();

  Texture::Format format = tex->get_format();
  switch (format) {
  case Texture::F_depth_stencil:
    if (_current_properties->get_float_depth()) {
      //NB. In the future we may need a T_float_32_unsigned_int_24_8 format, but
      // for now we'll just try to grab the depth component.
      component_type = Texture::T_float;
      format = Texture::F_depth_component32;
    } else {
      component_type = Texture::T_unsigned_int_24_8;
    }
    break;

  case Texture::F_depth_component:
    if (_current_properties->get_float_depth()) {
      component_type = Texture::T_float;
    } else if (_current_properties->get_depth_bits() <= 8) {
      component_type = Texture::T_unsigned_byte;
    } else if (_current_properties->get_depth_bits() <= 16) {
      component_type = Texture::T_unsigned_short;
    } else {
      component_type = Texture::T_float;
    }
    break;

  case Texture::F_depth_component16:
    component_type = Texture::T_unsigned_short;
    break;

  case Texture::F_depth_component24:
  case Texture::F_depth_component32:
    component_type = Texture::T_float;
    break;

  default:
    if (_current_properties->get_srgb_color()) {
      if (_current_properties->get_alpha_bits()) {
        format = Texture::F_srgb_alpha;
      } else {
        format = Texture::F_srgb;
      }
    } else if (_current_properties->get_float_color()) {
      if (_current_properties->get_alpha_bits()) {
        if (_current_properties->get_red_bits() == 16 &&
            _current_properties->get_green_bits() == 16 &&
            _current_properties->get_blue_bits() == 16 &&
            _current_properties->get_alpha_bits() == 16) {
          format = Texture::F_rgba16;
        } else {
          format = Texture::F_rgba32;
        }
      } else if (_current_properties->get_blue_bits()) {
        if (_current_properties->get_red_bits() == 11 &&
            _current_properties->get_green_bits() == 11 &&
            _current_properties->get_blue_bits() == 10) {
          format = Texture::F_r11_g11_b10;
        } else if (_current_properties->get_red_bits() == 16 &&
                   _current_properties->get_green_bits() == 16 &&
                   _current_properties->get_blue_bits() == 16) {
          format = Texture::F_rgb16;
        } else {
          format = Texture::F_rgb32;
        }
      } else if (_current_properties->get_green_bits()) {
        if (_current_properties->get_red_bits() == 16 &&
            _current_properties->get_green_bits() == 16) {
          format = Texture::F_rg16;
        } else {
          format = Texture::F_rg32;
        }
      } else {
        if (_current_properties->get_red_bits() == 16) {
          format = Texture::F_r16;
        } else {
          format = Texture::F_r32;
        }
      }
    } else if (_current_properties->get_alpha_bits()) {
      if (_current_properties->get_red_bits() == 10 &&
          _current_properties->get_green_bits() == 10 &&
          _current_properties->get_blue_bits() == 10 &&
          _current_properties->get_alpha_bits() == 2) {
        format = Texture::F_rgb10_a2;
      } else {
        format = Texture::F_rgba;
      }
    } else if (_current_properties->get_blue_bits()) {
      format = Texture::F_rgb;
    } else if (_current_properties->get_green_bits()) {
      if (_current_properties->get_red_bits() == 16 &&
          _current_properties->get_green_bits() == 16) {
        format = Texture::F_rg16;
      } else {
        format = Texture::F_rg;
      }
    } else {
      if (_current_properties->get_color_bits() == 16) {
        format = Texture::F_r16;
      } else {
        format = Texture::F_red;
      }
    }

    if (_current_properties->get_float_color()) {
      component_type = Texture::T_float;
    } else if (_current_properties->get_color_bits() <= 24
            && _current_properties->get_red_bits() <= 8
            && _current_properties->get_green_bits() <= 8
            && _current_properties->get_blue_bits() <= 8
            && _current_properties->get_alpha_bits() <= 8) {
      component_type = Texture::T_unsigned_byte;
    } else {
      component_type = Texture::T_unsigned_short;
    }
  }

  Texture::TextureType texture_type;
  int z_size;
  // TODO: should be extended to support 3D textures, 2D arrays and cube map
  // arrays.
  if (z >= 0) {
    texture_type = Texture::TT_cube_map;
    z_size = 6;
  } else {
    texture_type = Texture::TT_2d_texture;
    z_size = 1;
  }

  int num_views = tex->get_num_views();
  if (tex->get_x_size() != w || tex->get_y_size() != h ||
      tex->get_z_size() != z_size ||
      tex->get_component_type() != component_type ||
      tex->get_format() != format ||
      tex->get_texture_type() != texture_type ||
      view >= num_views) {

    tex->setup_texture(texture_type, w, h, z_size, component_type, format);

    // The above resets the number of views to 1, so set this back.
    num_views = std::max(view + 1, num_views);
    if (num_views > 1) {
      tex->set_num_views(num_views);
    }
  }

  nassertr(z < tex->get_z_size(), false);

  GLenum external_format = get_external_image_format(tex);

  // OpenGL ES implementations may support BGRA, but that doesn't imply they
  // also support it for glReadPixels specifically.
  if (!_supports_bgra_read && external_format == GL_BGRA) {
    external_format = GL_RGBA;
  }

#ifndef NDEBUG
  if (GLCAT.is_spam()) {
    GLCAT.spam()
      << "glReadPixels(" << xo << ", " << yo << ", " << w << ", " << h << ", ";
    switch (external_format) {
    case GL_DEPTH_COMPONENT:
      GLCAT.spam(false) << "GL_DEPTH_COMPONENT, ";
      break;
    case GL_DEPTH_STENCIL:
      GLCAT.spam(false) << "GL_DEPTH_STENCIL, ";
      break;
#ifndef OPENGLES_1
    case GL_RG:
      GLCAT.spam(false) << "GL_RG, ";
      break;
#endif
    case GL_RGB:
      GLCAT.spam(false) << "GL_RGB, ";
      break;
    case GL_RGBA:
      GLCAT.spam(false) << "GL_RGBA, ";
      break;
#ifndef OPENGLES
    case GL_BGR:
      GLCAT.spam(false) << "GL_BGR, ";
      break;
#endif
    case GL_BGRA:
      GLCAT.spam(false) << "GL_BGRA, ";
      break;
    default:
      GLCAT.spam(false) << "unknown, ";
      break;
    }
    switch (get_component_type(component_type)) {
    case GL_UNSIGNED_BYTE:
      GLCAT.spam(false) << "GL_UNSIGNED_BYTE";
      break;
    case GL_UNSIGNED_SHORT:
      GLCAT.spam(false) << "GL_UNSIGNED_SHORT";
      break;
    case GL_FLOAT:
      GLCAT.spam(false) << "GL_FLOAT";
      break;
    case GL_UNSIGNED_SHORT_4_4_4_4:
      GLCAT.spam(false) << "GL_UNSIGNED_SHORT_4_4_4_4";
      break;
    case GL_UNSIGNED_SHORT_5_5_5_1:
      GLCAT.spam(false) << "GL_UNSIGNED_SHORT_5_5_5_1";
      break;
    case GL_UNSIGNED_SHORT_5_6_5:
      GLCAT.spam(false) << "GL_UNSIGNED_SHORT_5_6_5";
      break;
#ifndef OPENGLES_1
    case GL_INT:
      GLCAT.spam(false) << "GL_INT";
      break;
    case GL_BYTE:
      GLCAT.spam(false) << "GL_BYTE";
      break;
    case GL_SHORT:
      GLCAT.spam(false) << "GL_SHORT";
      break;
    case GL_UNSIGNED_INT:
      GLCAT.spam(false) << "GL_UNSIGNED_INT";
      break;
    case GL_HALF_FLOAT:
      GLCAT.spam(false) << "GL_HALF_FLOAT";
      break;
#endif
#ifndef OPENGLES
    case GL_UNSIGNED_BYTE_3_3_2:
      GLCAT.spam(false) << "GL_UNSIGNED_BYTE_3_3_2";
      break;
    case GL_UNSIGNED_BYTE_2_3_3_REV:
      GLCAT.spam(false) << "GL_UNSIGNED_BYTE_2_3_3_REV";
      break;
    case GL_UNSIGNED_SHORT_5_6_5_REV:
      GLCAT.spam(false) << "GL_UNSIGNED_SHORT_5_6_5_REV";
      break;
    case GL_UNSIGNED_SHORT_4_4_4_4_REV:
      GLCAT.spam(false) << "GL_UNSIGNED_SHORT_4_4_4_4_REV";
      break;
    case GL_UNSIGNED_SHORT_1_5_5_5_REV:
      GLCAT.spam(false) << "GL_UNSIGNED_SHORT_1_5_5_5_REV";
      break;
    case GL_UNSIGNED_INT_8_8_8_8:
      GLCAT.spam(false) << "GL_UNSIGNED_INT_8_8_8_8";
      break;
    case GL_UNSIGNED_INT_8_8_8_8_REV:
      GLCAT.spam(false) << "GL_UNSIGNED_INT_8_8_8_8_REV";
      break;
    case GL_UNSIGNED_INT_10_10_10_2:
      GLCAT.spam(false) << "GL_UNSIGNED_INT_10_10_10_2";
      break;
    case GL_UNSIGNED_INT_2_10_10_10_REV:
      GLCAT.spam(false) << "GL_UNSIGNED_INT_2_10_10_10_REV";
      break;
    case GL_UNSIGNED_INT_24_8:
      GLCAT.spam(false) << "GL_UNSIGNED_INT_24_8";
      break;
    case GL_UNSIGNED_INT_10F_11F_11F_REV:
      GLCAT.spam(false) << "GL_UNSIGNED_INT_10F_11F_11F_REV";
      break;
    case GL_UNSIGNED_INT_5_9_9_9_REV:
      GLCAT.spam(false) << "GL_UNSIGNED_INT_5_9_9_9_REV";
      break;
    case GL_FLOAT_32_UNSIGNED_INT_24_8_REV:
      GLCAT.spam(false) << "GL_FLOAT_32_UNSIGNED_INT_24_8_REV";
      break;
#endif
    default:
      GLCAT.spam(false) << "unknown";
      break;
    }
    GLCAT.spam(false)
      << ")" << endl;
  }
#endif  // NDEBUG

  size_t image_size = tex->get_expected_ram_page_size();
  unsigned char *image_ptr = nullptr;
#ifndef OPENGLES_1
  GLuint pbo = 0;
  void *mapped_ptr = nullptr;
  if (request != nullptr) {
    nassertr(z <= 0, false);
    image_size *= tex->get_z_size();
    bind_new_client_buffer(pbo, mapped_ptr, GL_PIXEL_PACK_BUFFER, image_size);
  } else
#endif
  {
    image_ptr = tex->modify_ram_image();
    if (z >= 0) {
      image_ptr += z * image_size;
    } else {
      image_size = tex->get_ram_image_size();
    }
    if (view > 0) {
      image_ptr += (view * tex->get_z_size()) * image_size;
      nassertr(view < tex->get_num_views(), false);
    }
  }

  glReadPixels(xo, yo, w, h, external_format,
               get_component_type(component_type), image_ptr);

#ifndef OPENGLES_1
  if (request != nullptr) {
    _glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
#ifndef OPENGLES
    if (_supports_buffer_storage) {
      _glMemoryBarrier(GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT);
    }
#endif
    GLsync fence = _glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
    _async_ram_copies.push_back({request, pbo, fence, external_format,
                                 view, mapped_ptr, image_size});
  } else
#endif
  if (external_format == GL_RGBA || external_format == GL_RGB) {
    // We may have to reverse the byte ordering of the image if GL didn't do it
    // for us.
    PTA_uchar new_image;
    const unsigned char *result =
      fix_component_ordering(new_image, image_ptr, image_size,
                             external_format, tex);
    if (result != image_ptr) {
      memcpy(image_ptr, result, image_size);
    }
  }

#ifdef OPENGLES_1
  if (request != nullptr) {
    request->finish();
  }
#endif

  report_my_gl_errors();
  return true;
}

/**
 * Finishes all asynchronous framebuffer-copy-to-ram operations.
 */
void CLP(GraphicsStateGuardian)::
finish_async_framebuffer_ram_copies(bool force) {
#ifndef OPENGLES_1
  if (_async_ram_copies.empty()) {
    return;
  }

  //XXX having a fixed number of threads is not a great idea.  We ought to have
  // a common thread pool that is sized based on the available number of CPUs.
#ifdef HAVE_THREADS
  AsyncTaskManager *task_mgr = AsyncTaskManager::get_global_ptr();
  static AsyncTaskChain *chain = task_mgr->make_task_chain("texture_download", 2, TP_low);
#endif

  PStatTimer timer(_copy_texture_finish_pcollector);

  if (force) {
    // Just wait for the last fence, the rest must be complete too then.
    PStatTimer timer(_wait_fence_pcollector);
    GLsync fence = _async_ram_copies.back()._fence;
    _glClientWaitSync(fence, 0, (GLuint64)-1);
  }

  while (!_async_ram_copies.empty()) {
    AsyncRamCopy &copy = _async_ram_copies.front();
    if (!force) {
      GLenum result = _glClientWaitSync(copy._fence, 0, 0);
      if (result != GL_ALREADY_SIGNALED && result != GL_CONDITION_SATISFIED) {
        // Not yet done.  The rest must not yet be done then, either.
        break;
      }
    }
    _glDeleteSync(copy._fence);

    GLuint pbo = copy._pbo;
    int view = copy._view;
    PT(ScreenshotRequest) request = std::move(copy._request);
    GLuint external_format = copy._external_format;
    void *mapped_ptr = copy._mapped_pointer;
    size_t size = copy._size;

    if (mapped_ptr == nullptr) {
      _glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo);
#ifdef OPENGLES
      // There is neither glMapBuffer nor persistent mapping in OpenGL ES
      mapped_ptr = _glMapBufferRange(GL_PIXEL_PACK_BUFFER, 0, size, GL_MAP_READ_BIT);
#else
      // If we get here in desktop GL, we must not have persistent mapping
      nassertv(!_supports_buffer_storage);
      mapped_ptr = _glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY);
#endif
    }

    // Do the memcpy in the background, since it can be slow.
    auto func = [=](AsyncTask *task) {
      const unsigned char *result = (unsigned char *)mapped_ptr;
      PTA_uchar new_image;
      if (external_format == GL_RGBA || external_format == GL_RGB) {
        // We may have to reverse the byte ordering of the image if GL didn't do
        // it for us.
        result = fix_component_ordering(new_image, result, size,
                                        external_format, request->get_result());
      }
      request->set_view_data(view, result);

      // Finishing can take a long time, release the client buffer first so it
      // can be reused for the next screenshot.
      this->release_client_buffer(pbo, mapped_ptr, size);
      request->finish();
      return AsyncTask::DS_done;
    };
#ifdef HAVE_THREADS
    // We assign a sort value based on the originating frame number, so that
    // earlier frames will be processed before subsequent frames, but we don't
    // make it unique for every frame, which would kill concurrency.
    int frame_number = request->get_frame_number();
    chain->add(std::move(func), "screenshot", frame_number >> 3, -(frame_number & ((1 << 3) - 1)));
#else
    func(nullptr);
#endif

    _async_ram_copies.pop_front();

    // If there is 1 remaining, save it for next frame.  This helps prevent an
    // inconsistent frame rate when the number of fetched frames alternates
    // between 0 and 2, which can settle into a stable feedback loop.
    if (!force && _async_ram_copies.size() == 1) {
      break;
    }
  }

  _glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
#endif
}

#ifdef SUPPORT_FIXED_FUNCTION
/**
 *
 */
void CLP(GraphicsStateGuardian)::
apply_fog(Fog *fog) {
  Fog::Mode fmode = fog->get_mode();
  glFogf(GL_FOG_MODE, get_fog_mode_type(fmode));

  if (fmode == Fog::M_linear) {
    PN_stdfloat onset, opaque;
    fog->get_linear_range(onset, opaque);
    glFogf(GL_FOG_START, onset);
    glFogf(GL_FOG_END, opaque);

  } else {
    // Exponential fog is always camera-relative.
    glFogf(GL_FOG_DENSITY, fog->get_exp_density());
  }

  call_glFogfv(GL_FOG_COLOR, fog->get_color());
  report_my_gl_errors();
}
#endif  // SUPPORT_FIXED_FUNCTION

/**
 * Sends the indicated transform matrix to the graphics API to be applied to
 * future vertices.
 *
 * This transform is the internal_transform, already converted into the GSG's
 * internal coordinate system.
 */
void CLP(GraphicsStateGuardian)::
do_issue_transform() {
#ifdef SUPPORT_FIXED_FUNCTION
  if (has_fixed_function_pipeline()) {
    // OpenGL ES 2 does not support glLoadMatrix.

    const TransformState *transform = _internal_transform;
    if (GLCAT.is_spam()) {
      GLCAT.spam()
        << "glLoadMatrix(GL_MODELVIEW): " << transform->get_mat() << endl;
    }

    DO_PSTATS_STUFF(_transform_state_pcollector.add_level(1));
    glMatrixMode(GL_MODELVIEW);
    call_glLoadMatrix(transform->get_mat());
  }
#endif
  _transform_stale = false;

  report_my_gl_errors();
}

#ifdef SUPPORT_FIXED_FUNCTION
/**
 *
 */
void CLP(GraphicsStateGuardian)::
do_issue_shade_model() {
  const ShadeModelAttrib *target_shade_model;
  _target_rs->get_attrib_def(target_shade_model);

  switch (target_shade_model->get_mode()) {
  case ShadeModelAttrib::M_smooth:
    glShadeModel(GL_SMOOTH);
    _flat_shade_model = false;
    break;

  case ShadeModelAttrib::M_flat:
    glShadeModel(GL_FLAT);
    _flat_shade_model = true;
    break;
  }
}
#endif  // SUPPORT_FIXED_FUNCTION

#ifndef OPENGLES_1
/**
 * Called when the current ShaderAttrib state has changed.
 */
void CLP(GraphicsStateGuardian)::
do_issue_shader() {
  PStatTimer timer(_draw_set_state_shader_pcollector);

  ShaderContext *context = 0;
  Shader *shader = (Shader *)_target_shader->get_shader();

  // If we don't have a shader, apply the default shader.
  if (!has_fixed_function_pipeline() && !shader) {
    shader = _default_shader;
    nassertv(shader != nullptr);
  }

  if (shader) {
    if (_current_shader != shader) {
      context = shader->prepare_now(get_prepared_objects(), this);
    } else {
      context = _current_shader_context;
    }
  }

  // If it failed, try applying the default shader.
  if (_default_shader != nullptr && shader != _default_shader &&
      (context == 0 || !context->valid())) {
    shader = _default_shader;
    nassertv(shader != nullptr);
    if (_current_shader != shader) {
      context = shader->prepare_now(get_prepared_objects(), this);
    } else {
      context = _current_shader_context;
    }
  }

  if (context == 0 || !context->valid()) {
    if (_current_shader_context != 0) {
      _current_shader_context->unbind();
      _current_shader = 0;
      _current_shader_context = 0;
    }
  } else {
    if (context != _current_shader_context) {
      // Use a completely different shader than before.  Unbind old shader,
      // bind the new one.
      if (_current_shader_context != nullptr &&
          _current_shader->get_language() != shader->get_language()) {
        // If it's a different type of shader, make sure to unbind the old.
        _current_shader_context->unbind();
      }
      context->bind();
      _current_shader = shader;
    }

    // Bind the shader storage buffers.
    context->update_shader_buffer_bindings(_current_shader_context);
    _current_shader_context = context;
  }

#ifndef OPENGLES
  // Is the point size provided by the shader or by OpenGL?
  bool shader_point_size = _target_shader->get_flag(ShaderAttrib::F_shader_point_size);
  if (shader_point_size != _shader_point_size) {
    if (shader_point_size) {
      glEnable(GL_PROGRAM_POINT_SIZE);
    } else {
      glDisable(GL_PROGRAM_POINT_SIZE);
    }
    _shader_point_size = shader_point_size;
  }
#endif

  report_my_gl_errors();
}
#endif  // !OPENGLES_1

/**
 *
 */
void CLP(GraphicsStateGuardian)::
do_issue_render_mode() {
  const RenderModeAttrib *target_render_mode;
  _target_rs->get_attrib_def(target_render_mode);

  _render_mode = target_render_mode->get_mode();
  PN_stdfloat thickness = target_render_mode->get_thickness();
  _point_perspective = target_render_mode->get_perspective();

#ifndef OPENGLES  // glPolygonMode not supported by OpenGL ES.
  switch (_render_mode) {
  case RenderModeAttrib::M_unchanged:
  case RenderModeAttrib::M_filled:
  case RenderModeAttrib::M_filled_flat:
    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
    break;

  case RenderModeAttrib::M_wireframe:
    glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
    break;

  case RenderModeAttrib::M_point:
    glPolygonMode(GL_FRONT_AND_BACK, GL_POINT);
    break;

  default:
    GLCAT.error()
      << "Unknown render mode " << (int)_render_mode << endl;
  }
#endif  // OPENGLES

  // The thickness affects both the line width and the point size.
  if (thickness != _point_size) {
    if (GLCAT.is_spam()) {
      GLCAT.spam() << "setting thickness to " << thickness << "\n";
    }

    glLineWidth(std::min((GLfloat)thickness, _max_line_width));
#ifndef OPENGLES_2
    glPointSize(thickness);
#endif
    _point_size = thickness;
  }
  report_my_gl_errors();

#ifdef SUPPORT_FIXED_FUNCTION
  if (has_fixed_function_pipeline()) {
    do_point_size();
  }
#endif
}

/**
 *
 */
void CLP(GraphicsStateGuardian)::
do_issue_antialias() {
  const AntialiasAttrib *target_antialias;
  _target_rs->get_attrib_def(target_antialias);

  if (target_antialias->get_mode_type() == AntialiasAttrib::M_auto) {
    // In this special mode, we must enable antialiasing on a case-by-case
    // basis, because we enable it differently for polygons and for points and
    // lines.
    _auto_antialias_mode = true;

  } else {
    // Otherwise, explicitly enable or disable according to the bits that are
    // set.  But if multisample is requested and supported, don't use the
    // other bits at all (they will be ignored by GL anyway).
    _auto_antialias_mode = false;
    unsigned short mode = target_antialias->get_mode();

    if (_supports_multisample &&
        (mode & AntialiasAttrib::M_multisample) != 0) {
      enable_multisample_antialias(true);

    } else {
      enable_multisample_antialias(false);
      enable_line_smooth((mode & AntialiasAttrib::M_line) != 0);
      enable_point_smooth((mode & AntialiasAttrib::M_point) != 0);
      enable_polygon_smooth((mode & AntialiasAttrib::M_polygon) != 0);
    }
  }

#ifndef OPENGLES_2
  GLenum quality;
  switch (target_antialias->get_mode_quality()) {
  case AntialiasAttrib::M_faster:
    quality = GL_FASTEST;
    break;

  case AntialiasAttrib::M_better:
    quality = GL_NICEST;
    break;

  default:
    quality = GL_DONT_CARE;
    break;
  }

  if (_line_smooth_enabled) {
    glHint(GL_LINE_SMOOTH_HINT, quality);
  }
  if (_point_smooth_enabled) {
    glHint(GL_POINT_SMOOTH_HINT, quality);
  }
#ifndef OPENGLES
  if (_polygon_smooth_enabled) {
    glHint(GL_POLYGON_SMOOTH_HINT, quality);
  }
#endif
#endif  // !OPENGLES_2

  report_my_gl_errors();
}

#ifdef SUPPORT_FIXED_FUNCTION // OpenGL ES 2.0 doesn't support rescaling normals.
/**
 *
 */
void CLP(GraphicsStateGuardian)::
do_issue_rescale_normal() {
  RescaleNormalAttrib::Mode mode = RescaleNormalAttrib::M_none;

  const RescaleNormalAttrib *target_rescale_normal;
  if (_target_rs->get_attrib(target_rescale_normal)) {
    mode = target_rescale_normal->get_mode();
  }

  switch (mode) {
  case RescaleNormalAttrib::M_none:
    glDisable(GL_NORMALIZE);
    if (_supports_rescale_normal && support_rescale_normal) {
      glDisable(GL_RESCALE_NORMAL);
    }
    break;

  case RescaleNormalAttrib::M_rescale:
    if (_supports_rescale_normal && support_rescale_normal) {
      glEnable(GL_RESCALE_NORMAL);
      glDisable(GL_NORMALIZE);
    } else {
      glEnable(GL_NORMALIZE);
    }
    break;

  case RescaleNormalAttrib::M_normalize:
    glEnable(GL_NORMALIZE);
    if (_supports_rescale_normal && support_rescale_normal) {
      glDisable(GL_RESCALE_NORMAL);
    }
    break;

  default:
    GLCAT.error()
      << "Unknown rescale_normal mode " << (int)mode << endl;
  }
  report_my_gl_errors();
}
#endif  // SUPPORT_FIXED_FUNCTION

// PandaCompareFunc - 1 + 0x200 === GL_NEVER, etc.  order is sequential
#define PANDA_TO_GL_COMPAREFUNC(PANDACMPFUNC) (PANDACMPFUNC-1 +0x200)

/**
 *
 */
void CLP(GraphicsStateGuardian)::
do_issue_depth_test() {
  const DepthTestAttrib *target_depth_test;
  _target_rs->get_attrib_def(target_depth_test);

  DepthTestAttrib::PandaCompareFunc mode = target_depth_test->get_mode();
  if (mode == DepthTestAttrib::M_none) {
    enable_depth_test(false);
  } else {
    enable_depth_test(true);
    glDepthFunc(PANDA_TO_GL_COMPAREFUNC(mode));
  }
  report_my_gl_errors();
}

#ifdef SUPPORT_FIXED_FUNCTION
/**
 *
 */
void CLP(GraphicsStateGuardian)::
do_issue_alpha_test() {
#ifndef OPENGLES_1
  if (_target_shader->get_flag(ShaderAttrib::F_subsume_alpha_test)) {
    enable_alpha_test(false);
  } else
#endif
  {
    const AlphaTestAttrib *target_alpha_test;
    _target_rs->get_attrib_def(target_alpha_test);

    AlphaTestAttrib::PandaCompareFunc mode = target_alpha_test->get_mode();
    if (mode == AlphaTestAttrib::M_none) {
      enable_alpha_test(false);
    } else {
      nassertv(GL_NEVER == (AlphaTestAttrib::M_never-1+0x200));
      glAlphaFunc(PANDA_TO_GL_COMPAREFUNC(mode), target_alpha_test->get_reference_alpha());
      enable_alpha_test(true);
    }
  }
}
#endif  // SUPPORT_FIXED_FUNCTION

/**
 *
 */
void CLP(GraphicsStateGuardian)::
do_issue_depth_write() {
  const DepthWriteAttrib *target_depth_write;
  _target_rs->get_attrib_def(target_depth_write);

  DepthWriteAttrib::Mode mode = target_depth_write->get_mode();
  if (mode == DepthWriteAttrib::M_off) {
#ifdef GSG_VERBOSE
    GLCAT.spam()
      << "glDepthMask(GL_FALSE)" << endl;
#endif
    glDepthMask(GL_FALSE);
  } else {
#ifdef GSG_VERBOSE
    GLCAT.spam()
      << "glDepthMask(GL_TRUE)" << endl;
#endif
    glDepthMask(GL_TRUE);
  }
  report_my_gl_errors();
}

/**
 *
 */
void CLP(GraphicsStateGuardian)::
do_issue_cull_face() {
  const CullFaceAttrib *target_cull_face;
  _target_rs->get_attrib_def(target_cull_face);

  CullFaceAttrib::Mode mode = target_cull_face->get_effective_mode();

  switch (mode) {
  case CullFaceAttrib::M_cull_none:
    glDisable(GL_CULL_FACE);
    break;
  case CullFaceAttrib::M_cull_clockwise:
    glEnable(GL_CULL_FACE);
    glCullFace(GL_BACK);
    break;
  case CullFaceAttrib::M_cull_counter_clockwise:
    glEnable(GL_CULL_FACE);
    glCullFace(GL_FRONT);
    break;
  default:
    GLCAT.error()
      << "invalid cull face mode " << (int)mode << endl;
    break;
  }
  report_my_gl_errors();
}

#ifdef SUPPORT_FIXED_FUNCTION
/**
 *
 */
void CLP(GraphicsStateGuardian)::
do_issue_fog() {
  const FogAttrib *target_fog;
  _target_rs->get_attrib_def(target_fog);

  if (!target_fog->is_off()) {
    enable_fog(true);
    Fog *fog = target_fog->get_fog();
    nassertv(fog != nullptr);
    apply_fog(fog);
  } else {
    enable_fog(false);
  }
  report_my_gl_errors();
}
#endif  // SUPPORT_FIXED_FUNCTION

/**
 *
 */
void CLP(GraphicsStateGuardian)::
do_issue_depth_bias() {
  const DepthOffsetAttrib *target_depth_offset;
  _target_rs->get_attrib_def(target_depth_offset);
  int offset = target_depth_offset->get_offset();

  const DepthBiasAttrib *target_depth_bias;
  if (_target_rs->get_attrib(target_depth_bias)) {
    GLfloat slope_factor = target_depth_bias->get_slope_factor();
    GLfloat constant_factor = target_depth_bias->get_constant_factor();

    slope_factor -= offset;
    constant_factor -= offset;

#ifndef OPENGLES_1
    GLfloat clamp = target_depth_bias->get_clamp();
    _glPolygonOffsetClamp(slope_factor, constant_factor, clamp);
#else
    glPolygonOffset(slope_factor, constant_factor);
#endif
    enable_polygon_offset(true);
  }
  else if (offset != 0) {
    // The relationship between these two parameters is a little unclear and
    // poorly explained in the GL man pages.
    glPolygonOffset((GLfloat) -offset, (GLfloat) -offset);
    enable_polygon_offset(true);
  }
  else {
    enable_polygon_offset(false);
  }

  PN_stdfloat min_value = target_depth_offset->get_min_value();
  PN_stdfloat max_value = target_depth_offset->get_max_value();
  if (min_value != (PN_stdfloat)0.0 ||
      max_value != (PN_stdfloat)1.0 ||
      _has_attrib_depth_range) {
    min_value = _depth_range_far * min_value + _depth_range_near * (1 - min_value);
    max_value = _depth_range_far * max_value + _depth_range_near * (1 - max_value);

#ifdef GSG_VERBOSE
    if (GLCAT.is_spam()) {
      GLCAT.spam()
        << "glDepthRange(" << min_value << ", " << max_value << ")" << endl;
    }
#endif
#ifdef OPENGLES
    // OpenGL ES uses a single-precision call.
    glDepthRangef((GLclampf)min_value, (GLclampf)max_value);
#else
    // Mainline OpenGL uses a double-precision call.
    if (!_use_remapped_depth_range) {
      glDepthRange((GLclampd)min_value, (GLclampd)max_value);
    } else {
      // If we have a remapped depth range, we should adjust the values to range
      // from -1 to 1.  We need to use an NV extension to pass unclamped values.
      _glDepthRangedNV(min_value * 2.0 - 1.0, max_value * 2.0 - 1.0);
    }
#endif  // OPENGLES

    _has_attrib_depth_range = true;
  }

  report_my_gl_errors();
}

#ifdef SUPPORT_FIXED_FUNCTION
/**
 *
 */
void CLP(GraphicsStateGuardian)::
do_issue_material() {
  static Material empty;
  const Material *material;

  const MaterialAttrib *target_material;
  _target_rs->get_attrib_def(target_material);

  if (target_material == nullptr ||
      target_material->is_off()) {
    material = &empty;
  } else {
    material = target_material->get_material();
  }

  bool has_material_force_color = _has_material_force_color;

#ifndef NDEBUG
  if (_show_texture_usage) {
    // In show_texture_usage mode, all colors are white, so as not to
    // contaminate the texture color.  This means we disable lighting
    // materials too.
    material = &empty;
    has_material_force_color = false;
  }
#endif  // NDEBUG

#ifdef OPENGLES
  const GLenum face = GL_FRONT_AND_BACK;
#else
  GLenum face = material->get_twoside() ? GL_FRONT_AND_BACK : GL_FRONT;
#endif

  call_glMaterialfv(face, GL_SPECULAR, material->get_specular());
  call_glMaterialfv(face, GL_EMISSION, material->get_emission());
  glMaterialf(face, GL_SHININESS, max(min(material->get_shininess(), (PN_stdfloat)128), (PN_stdfloat)0));

  if ((material->has_ambient() && material->has_diffuse()) || material->has_base_color()) {
    // The material has both an ambient and diffuse specified.  This means we
    // do not need glMaterialColor().
    glDisable(GL_COLOR_MATERIAL);
    call_glMaterialfv(face, GL_AMBIENT, material->get_ambient());
    call_glMaterialfv(face, GL_DIFFUSE, material->get_diffuse());

  } else if (material->has_ambient()) {
    // The material specifies an ambient, but not a diffuse component.  The
    // diffuse component comes from the object's color.
    if (has_material_force_color) {
      glDisable(GL_COLOR_MATERIAL);
      call_glMaterialfv(face, GL_DIFFUSE, _material_force_color);
    } else {
#ifndef OPENGLES
      glColorMaterial(face, GL_DIFFUSE);
#endif  // OPENGLES
      glEnable(GL_COLOR_MATERIAL);
    }
    call_glMaterialfv(face, GL_AMBIENT, material->get_ambient());

  } else if (material->has_diffuse()) {
    // The material specifies a diffuse, but not an ambient component.  The
    // ambient component comes from the object's color.
    if (has_material_force_color) {
      glDisable(GL_COLOR_MATERIAL);
      call_glMaterialfv(face, GL_AMBIENT, _material_force_color);
    } else {
#ifndef OPENGLES
      glColorMaterial(face, GL_AMBIENT);
#endif  // OPENGLES
      glEnable(GL_COLOR_MATERIAL);
    }
    call_glMaterialfv(face, GL_DIFFUSE, material->get_diffuse());

  } else {
    // The material specifies neither a diffuse nor an ambient component.
    // Both components come from the object's color.
    if (has_material_force_color) {
      glDisable(GL_COLOR_MATERIAL);
      call_glMaterialfv(face, GL_AMBIENT, _material_force_color);
      call_glMaterialfv(face, GL_DIFFUSE, _material_force_color);
    } else {
#ifndef OPENGLES
      glColorMaterial(face, GL_AMBIENT_AND_DIFFUSE);
#endif  // OPENGLES
      glEnable(GL_COLOR_MATERIAL);
    }
  }

#ifndef OPENGLES
  glLightModeli(GL_LIGHT_MODEL_LOCAL_VIEWER, material->get_local());
  glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, material->get_twoside());

  if (_use_separate_specular_color) {
    glLightModeli(GL_LIGHT_MODEL_COLOR_CONTROL, GL_SEPARATE_SPECULAR_COLOR);
  } else {
    glLightModeli(GL_LIGHT_MODEL_COLOR_CONTROL, GL_SINGLE_COLOR);
  }
#endif

  report_my_gl_errors();
}
#endif  // SUPPORT_FIXED_FUNCTION

/**
 * Issues the logic operation attribute to the GL.
 */
#if !defined(OPENGLES) || defined(OPENGLES_1)
void CLP(GraphicsStateGuardian)::
do_issue_logic_op() {
  const LogicOpAttrib *target_logic_op;
  _target_rs->get_attrib_def(target_logic_op);

  if (target_logic_op->get_operation() != LogicOpAttrib::O_none) {
    glEnable(GL_COLOR_LOGIC_OP);
    glLogicOp(GL_CLEAR - 1 + (int)target_logic_op->get_operation());

    if (GLCAT.is_spam()) {
      GLCAT.spam() << "glEnable(GL_COLOR_LOGIC_OP)\n";
      GLCAT.spam() << "glLogicOp(" << target_logic_op->get_operation() << ")\n";
    }
  } else {
    glDisable(GL_COLOR_LOGIC_OP);
    glLogicOp(GL_COPY);

    if (GLCAT.is_spam()) {
      GLCAT.spam() << "glDisable(GL_COLOR_LOGIC_OP)\n";
    }
  }
}
#endif

/**
 *
 */
void CLP(GraphicsStateGuardian)::
do_issue_blending() {
  // Handle the color_write attrib.  If color_write is off, then all the other
  // blending-related stuff doesn't matter.  If the device doesn't support
  // color-write, we use blending tricks to effectively disable color write.
  const ColorWriteAttrib *target_color_write;
  _target_rs->get_attrib_def(target_color_write);

  unsigned int color_channels =
    target_color_write->get_channels() & _color_write_mask;

#ifndef OPENGLES_1
  if (_target_shader->get_flag(ShaderAttrib::F_disable_alpha_write)) {
    color_channels &= ~(ColorWriteAttrib::C_alpha);
  }
#endif

  if (color_channels == ColorWriteAttrib::C_off) {
    enable_multisample_alpha_one(false);
    enable_multisample_alpha_mask(false);
    if (gl_color_mask) {
      enable_blend(false);
      set_color_write_mask(ColorWriteAttrib::C_off);
    } else {
      enable_blend(true);
      _glBlendEquation(GL_FUNC_ADD);
      glBlendFunc(GL_ZERO, GL_ONE);
    }

    if (GLCAT.is_spam()) {
      GLCAT.spam() << "glBlendEquation(GL_FUNC_ADD)\n";
      GLCAT.spam() << "glBlendFunc(GL_ZERO, GL_ONE)\n";
    }
    return;
  } else {
    set_color_write_mask(color_channels);
  }

  const ColorBlendAttrib *target_color_blend;
  _target_rs->get_attrib_def(target_color_blend);
  CPT(ColorBlendAttrib) color_blend = target_color_blend;
  ColorBlendAttrib::Mode color_blend_mode = target_color_blend->get_mode();
  ColorBlendAttrib::Mode alpha_blend_mode = target_color_blend->get_alpha_mode();

  const TransparencyAttrib *target_transparency;
  _target_rs->get_attrib_def(target_transparency);
  TransparencyAttrib::Mode transparency_mode = target_transparency->get_mode();

  _color_blend_involves_color_scale = color_blend->involves_color_scale();

  // Is there a color blend set?
  if (color_blend_mode != ColorBlendAttrib::M_none) {
    enable_multisample_alpha_one(false);
    enable_multisample_alpha_mask(false);
    enable_blend(true);

    if (_supports_blend_equation_separate) {
      _glBlendEquationSeparate(get_blend_equation_type(color_blend_mode),
                               get_blend_equation_type(alpha_blend_mode));
    } else {
      _glBlendEquation(get_blend_equation_type(color_blend_mode));
    }
    _glBlendFuncSeparate(get_blend_func(color_blend->get_operand_a()),
                         get_blend_func(color_blend->get_operand_b()),
                         get_blend_func(color_blend->get_alpha_operand_a()),
                         get_blend_func(color_blend->get_alpha_operand_b()));

#ifndef OPENGLES_1
    LColor c;
    if (_color_blend_involves_color_scale) {
      // Apply the current color scale to the blend mode.
      c = _current_color_scale;
    } else {
      c = color_blend->get_color();
    }

    _glBlendColor(c[0], c[1], c[2], c[3]);
#endif

    if (GLCAT.is_spam()) {
      if (_supports_blend_equation_separate) {
        GLCAT.spam() << "glBlendEquationSeparate(" << color_blend_mode << ", "
                                                   << alpha_blend_mode << ")\n";
      } else {
        GLCAT.spam() << "glBlendEquation(" << color_blend_mode << ")\n";
      }
      GLCAT.spam() << "glBlendFuncSeparate("
                   << color_blend->get_operand_a() << ", "
                   << color_blend->get_operand_b() << ", "
                   << color_blend->get_alpha_operand_a() << ", "
                   << color_blend->get_alpha_operand_b() << ")\n";
#ifndef OPENGLES_1
      GLCAT.spam() << "glBlendColor(" << c << ")\n";
#endif
    }
    return;
  }

  // No color blend; is there a transparency set?
  switch (transparency_mode) {
  case TransparencyAttrib::M_none:
  case TransparencyAttrib::M_binary:
    break;

  case TransparencyAttrib::M_alpha:
  case TransparencyAttrib::M_dual:
    enable_multisample_alpha_one(false);
    enable_multisample_alpha_mask(false);
    enable_blend(true);
    _glBlendEquation(GL_FUNC_ADD);

    if (old_alpha_blend) {
      glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    } else {
      _glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
    }

    if (GLCAT.is_spam()) {
      GLCAT.spam() << "glBlendEquation(GL_FUNC_ADD)\n";
      if (_supports_blend_equation_separate && !old_alpha_blend) {
        GLCAT.spam() << "glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA)\n";
      } else {
        GLCAT.spam() << "glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)\n";
      }
    }
    return;

  case TransparencyAttrib::M_premultiplied_alpha:
    enable_multisample_alpha_one(false);
    enable_multisample_alpha_mask(false);
    enable_blend(true);
    _glBlendEquation(GL_FUNC_ADD);
    glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);

    if (GLCAT.is_spam()) {
      GLCAT.spam() << "glBlendEquation(GL_FUNC_ADD)\n";
      GLCAT.spam() << "glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA)\n";
    }
    return;

  case TransparencyAttrib::M_multisample:
    // We need to enable *both* of these in M_multisample case.
    enable_multisample_alpha_one(true);
    enable_multisample_alpha_mask(true);
    enable_blend(false);
    return;

  case TransparencyAttrib::M_multisample_mask:
    enable_multisample_alpha_one(false);
    enable_multisample_alpha_mask(true);
    enable_blend(false);
    return;

  default:
    GLCAT.error()
      << "invalid transparency mode " << (int)transparency_mode << endl;
    break;
  }

  if (_line_smooth_enabled || _point_smooth_enabled) {
    // If we have either of these turned on, we also need to have blend mode
    // enabled in order to see it.
    enable_multisample_alpha_one(false);
    enable_multisample_alpha_mask(false);
    enable_blend(true);
    _glBlendEquation(GL_FUNC_ADD);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    if (GLCAT.is_spam()) {
      GLCAT.spam() << "glBlendEquation(GL_FUNC_ADD)\n";
      GLCAT.spam() << "glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)\n";
    }
    return;
  }

/*
 * For best polygon smoothing, we need: (1) a frame buffer that supports alpha
 * (2) sort polygons front-to-back (3) glBlendFunc(GL_SRC_ALPHA_SATURATE,
 * GL_ONE); Since these modes have other implications for the application, we
 * don't attempt to do this by default.  If you really want good polygon
 * smoothing (and you don't have multisample support), do all this yourself.
 */

  // Nothing's set, so disable blending.
  enable_multisample_alpha_one(false);
  enable_multisample_alpha_mask(false);
  enable_blend(false);
}

#ifdef SUPPORT_FIXED_FUNCTION
/**
 * Called the first time a particular light has been bound to a given id
 * within a frame, this should set up the associated hardware light with the
 * light's properties.
 */
void CLP(GraphicsStateGuardian)::
bind_light(PointLight *light_obj, const NodePath &light, int light_id) {
  nassertv(has_fixed_function_pipeline());

  // static PStatCollector
  // _draw_set_state_light_bind_point_pcollector("Draw:Set
  // State:Light:Bind:Point"); PStatGPUTimer timer(this,
  // _draw_set_state_light_bind_point_pcollector);

  GLenum id = get_light_id(light_id);
  static const LColor black(0.0f, 0.0f, 0.0f, 1.0f);
  call_glLightfv(id, GL_AMBIENT, black);
  call_glLightfv(id, GL_DIFFUSE, get_light_color(light_obj));
  call_glLightfv(id, GL_SPECULAR, light_obj->get_specular_color());

  // Position needs to specify x, y, z, and w w == 1 implies non-infinite
  // position
  CPT(TransformState) transform = light.get_transform(_scene_setup->get_scene_root().get_parent());
  LPoint3 pos = light_obj->get_point() * transform->get_mat();

  LPoint4 fpos(pos[0], pos[1], pos[2], 1.0f);
  call_glLightfv(id, GL_POSITION, fpos);

  // GL_SPOT_DIRECTION is not significant when cutoff == 180

  // Exponent == 0 implies uniform light distribution
  glLightf(id, GL_SPOT_EXPONENT, 0.0f);

  // Cutoff == 180 means uniform point light source
  glLightf(id, GL_SPOT_CUTOFF, 180.0f);

  const LVecBase3 &att = light_obj->get_attenuation();
  glLightf(id, GL_CONSTANT_ATTENUATION, att[0]);
  glLightf(id, GL_LINEAR_ATTENUATION, att[1]);
  glLightf(id, GL_QUADRATIC_ATTENUATION, att[2]);

  report_my_gl_errors();
}
#endif  // SUPPORT_FIXED_FUNCTION

#ifdef SUPPORT_FIXED_FUNCTION
/**
 * Called the first time a particular light has been bound to a given id
 * within a frame, this should set up the associated hardware light with the
 * light's properties.
 */
void CLP(GraphicsStateGuardian)::
bind_light(DirectionalLight *light_obj, const NodePath &light, int light_id) {
  nassertv(has_fixed_function_pipeline());

  // static PStatCollector
  // _draw_set_state_light_bind_directional_pcollector("Draw:Set
  // State:Light:Bind:Directional"); PStatGPUTimer timer(this,
  // _draw_set_state_light_bind_directional_pcollector);

  std::pair<DirectionalLights::iterator, bool> lookup = _dlights.insert(DirectionalLights::value_type(light, DirectionalLightFrameData()));
  DirectionalLightFrameData &fdata = (*lookup.first).second;
  if (lookup.second) {
    // The light was not computed yet this frame.  Compute it now.
    CPT(TransformState) transform = light.get_transform(_scene_setup->get_scene_root().get_parent());
    LVector3 dir = light_obj->get_direction() * transform->get_mat();
    fdata._neg_dir.set(-dir[0], -dir[1], -dir[2], 0);
  }

  GLenum id = get_light_id( light_id );
  static const LColor black(0.0f, 0.0f, 0.0f, 1.0f);
  call_glLightfv(id, GL_AMBIENT, black);
  call_glLightfv(id, GL_DIFFUSE, get_light_color(light_obj));
  call_glLightfv(id, GL_SPECULAR, light_obj->get_specular_color());

  // Position needs to specify x, y, z, and w.  w == 0 implies light is at
  // infinity
  call_glLightfv(id, GL_POSITION, fdata._neg_dir);

  // GL_SPOT_DIRECTION is not significant when cutoff == 180 In this case,
  // position x, y, z specifies direction

  // Exponent == 0 implies uniform light distribution
  glLightf(id, GL_SPOT_EXPONENT, 0.0f);

  // Cutoff == 180 means uniform point light source
  glLightf(id, GL_SPOT_CUTOFF, 180.0f);

  // Default attenuation values (only spotlight and point light can modify
  // these)
  glLightf(id, GL_CONSTANT_ATTENUATION, 1.0f);
  glLightf(id, GL_LINEAR_ATTENUATION, 0.0f);
  glLightf(id, GL_QUADRATIC_ATTENUATION, 0.0f);

  report_my_gl_errors();
}
#endif  // SUPPORT_FIXED_FUNCTION

#ifdef SUPPORT_FIXED_FUNCTION
/**
 * Called the first time a particular light has been bound to a given id
 * within a frame, this should set up the associated hardware light with the
 * light's properties.
 */
void CLP(GraphicsStateGuardian)::
bind_light(Spotlight *light_obj, const NodePath &light, int light_id) {
  nassertv(has_fixed_function_pipeline());

  // static PStatCollector
  // _draw_set_state_light_bind_spotlight_pcollector("Draw:Set
  // State:Light:Bind:Spotlight"); PStatGPUTimer timer(this,
  // _draw_set_state_light_bind_spotlight_pcollector);

  Lens *lens = light_obj->get_lens();
  nassertv(lens != nullptr);

  GLenum id = get_light_id(light_id);
  static const LColor black(0.0f, 0.0f, 0.0f, 1.0f);
  call_glLightfv(id, GL_AMBIENT, black);
  call_glLightfv(id, GL_DIFFUSE, get_light_color(light_obj));
  call_glLightfv(id, GL_SPECULAR, light_obj->get_specular_color());

  // Position needs to specify x, y, z, and w w == 1 implies non-infinite
  // position
  CPT(TransformState) transform = light.get_transform(_scene_setup->get_scene_root().get_parent());
  const LMatrix4 &light_mat = transform->get_mat();
  LPoint3 pos = lens->get_nodal_point() * light_mat;
  LVector3 dir = lens->get_view_vector() * light_mat;

  LPoint4 fpos(pos[0], pos[1], pos[2], 1.0f);
  call_glLightfv(id, GL_POSITION, fpos);
  call_glLightfv(id, GL_SPOT_DIRECTION, dir);

  glLightf(id, GL_SPOT_EXPONENT, max(min(light_obj->get_exponent(), (PN_stdfloat)128), (PN_stdfloat)0));
  glLightf(id, GL_SPOT_CUTOFF, lens->get_hfov() * 0.5f);

  const LVecBase3 &att = light_obj->get_attenuation();
  glLightf(id, GL_CONSTANT_ATTENUATION, att[0]);
  glLightf(id, GL_LINEAR_ATTENUATION, att[1]);
  glLightf(id, GL_QUADRATIC_ATTENUATION, att[2]);

  report_my_gl_errors();
}
#endif  // SUPPORT_FIXED_FUNCTION

/**
 * Creates a depth buffer for shadow mapping.  A derived GSG can override this
 * if it knows that a particular buffer type works best for shadow rendering.
 */
GraphicsOutput *CLP(GraphicsStateGuardian)::
make_shadow_buffer(LightLensNode *light, Texture *tex, GraphicsOutput *host) {
  // We override this to circumvent the fact that GraphicsEngine::make_output
  // can only be called from the app thread.
  if (!_supports_framebuffer_object) {
    return GraphicsStateGuardian::make_shadow_buffer(light, tex, host);
  }

  bool is_point = light->is_of_type(PointLight::get_class_type());

  // Determine the properties for creating the depth buffer.
  FrameBufferProperties fbp;
  fbp.set_depth_bits(shadow_depth_bits);

  WindowProperties props = WindowProperties::size(light->get_shadow_buffer_size());
  int flags = GraphicsPipe::BF_refuse_window;
  if (is_point) {
    flags |= GraphicsPipe::BF_size_square;
  }

  if (host != nullptr) {
    host = host->get_host();
  }

  CLP(GraphicsBuffer) *sbuffer = new CLP(GraphicsBuffer)(get_engine(), get_pipe(), light->get_name(), fbp, props, flags, this, host);
  sbuffer->add_render_texture(tex, GraphicsOutput::RTM_bind_or_copy, GraphicsOutput::RTP_depth);
  get_engine()->add_window(sbuffer, light->get_shadow_buffer_sort());
  return sbuffer;
}

#ifdef SUPPORT_IMMEDIATE_MODE
/**
 * Uses the ImmediateModeSender to draw a series of primitives of the
 * indicated type.
 */
void CLP(GraphicsStateGuardian)::
draw_immediate_simple_primitives(const GeomPrimitivePipelineReader *reader, GLenum mode) {
  int num_vertices = reader->get_num_vertices();
  _vertices_immediate_pcollector.add_level(num_vertices);
  glBegin(mode);

  if (reader->is_indexed()) {
    for (int v = 0; v < num_vertices; ++v) {
      _sender.set_vertex(reader->get_vertex(v));
      _sender.issue_vertex();
    }

  } else {
    _sender.set_vertex(reader->get_first_vertex());
    for (int v = 0; v < num_vertices; ++v) {
      _sender.issue_vertex();
    }
  }

  glEnd();
}
#endif  // SUPPORT_IMMEDIATE_MODE

#ifdef SUPPORT_IMMEDIATE_MODE
/**
 * Uses the ImmediateModeSender to draw a series of primitives of the
 * indicated type.  This form is for primitive types like tristrips which must
 * involve several begin/end groups.
 */
void CLP(GraphicsStateGuardian)::
draw_immediate_composite_primitives(const GeomPrimitivePipelineReader *reader, GLenum mode) {
  int num_vertices = reader->get_num_vertices();
  _vertices_immediate_pcollector.add_level(num_vertices);
  CPTA_int ends = reader->get_ends();
  int num_unused_vertices_per_primitive = reader->get_object()->get_num_unused_vertices_per_primitive();

  if (reader->is_indexed()) {
    int begin = 0;
    CPTA_int::const_iterator ei;
    for (ei = ends.begin(); ei != ends.end(); ++ei) {
      int end = (*ei);

      glBegin(mode);
      for (int v = begin; v < end; ++v) {
        _sender.set_vertex(reader->get_vertex(v));
        _sender.issue_vertex();
      }
      glEnd();

      begin = end + num_unused_vertices_per_primitive;
    }

  } else {
    _sender.set_vertex(reader->get_first_vertex());
    int begin = 0;
    CPTA_int::const_iterator ei;
    for (ei = ends.begin(); ei != ends.end(); ++ei) {
      int end = (*ei);

      glBegin(mode);
      for (int v = begin; v < end; ++v) {
        _sender.issue_vertex();
      }
      glEnd();

      begin = end + num_unused_vertices_per_primitive;
    }
  }
}
#endif  // SUPPORT_IMMEDIATE_MODE

/**
 * Calls glFlush().
 */
void CLP(GraphicsStateGuardian)::
gl_flush() const {
  PStatTimer timer(_flush_pcollector);
  glFlush();
}

/**
 * Returns the result of glGetError().
 */
GLenum CLP(GraphicsStateGuardian)::
gl_get_error() const {
  if (_check_errors) {
    PStatTimer timer(_check_error_pcollector);
    return glGetError();
  } else {
    return GL_NO_ERROR;
  }
}

/**
 * The internal implementation of report_errors(). Don't call this function;
 * use report_errors() instead.  The return value is true if everything is ok,
 * or false if we should shut down.
 */
bool CLP(GraphicsStateGuardian)::
report_errors_loop(int line, const char *source_file, GLenum error_code,
                   int &error_count) {
  while ((gl_max_errors < 0 || error_count < gl_max_errors) &&
         (error_code != GL_NO_ERROR)) {
    GLCAT.error()
      << "at " << line << " of " << source_file << " : "
      << get_error_string(error_code) << "\n";

    error_code = glGetError();
    error_count++;
  }

  return (error_code == GL_NO_ERROR);
}

/**
 * Returns an error string for an OpenGL error code.
 */
string CLP(GraphicsStateGuardian)::
get_error_string(GLenum error_code) {
  // We used to use gluErrorString here, but I (rdb) took it out because that
  // was really the only function we used from GLU. The idea with the error
  // table was taken from SGI's sample implementation.
  static const char *error_strings[] = {
    "invalid enumerant",
    "invalid value",
    "invalid operation",
    "stack overflow",
    "stack underflow",
    "out of memory",
    "invalid framebuffer operation",
    "context lost",
  };

  if (error_code == GL_NO_ERROR) {
    return "no error";
#ifndef OPENGLES
  } else if (error_code == GL_TABLE_TOO_LARGE) {
    return "table too large";
#endif
  } else if (error_code >= 0x0500 && error_code <= 0x0507) {
    return error_strings[error_code - 0x0500];
  }

  // Other error, somehow?  Just display the error code then.
  std::ostringstream strm;
  strm << "GL error " << (int)error_code;

  return strm.str();
}

/**
 * Outputs the result of glGetString() on the indicated tag.  The output
 * string is returned.
 */
string CLP(GraphicsStateGuardian)::
show_gl_string(const string &name, GLenum id) {
  string result;

  const GLubyte *text = glGetString(id);

  if (text == nullptr) {
    GLCAT.warning()
      << "Unable to query " << name << "\n";

  } else {
    result = (const char *)text;
    if (GLCAT.is_debug()) {
      GLCAT.debug()
        << name << " = " << result << "\n";
    }
  }

  return result;
}

/**
 * Queries the runtime version of OpenGL in use.
 */
void CLP(GraphicsStateGuardian)::
query_gl_version() {
  _gl_vendor = show_gl_string("GL_VENDOR", GL_VENDOR);
  _gl_renderer = show_gl_string("GL_RENDERER", GL_RENDERER);
  _gl_version = show_gl_string("GL_VERSION", GL_VERSION);

  _gl_version_major = 0;
  _gl_version_minor = 0;

    // This is the most preposterous driver bug: NVIDIA drivers will claim
    // that the version is 1.2 as long as the process is named pview.exe!
#ifndef OPENGLES
  if (_gl_version.substr(0, 10) == "1.2 NVIDIA") {
    Filename exec_name = ExecutionEnvironment::get_binary_name();
    if (cmp_nocase(exec_name.get_basename(), "pview.exe") == 0) {
      glGetIntegerv(GL_MAJOR_VERSION, &_gl_version_major);
      glGetIntegerv(GL_MINOR_VERSION, &_gl_version_minor);

      if (glGetError() == GL_INVALID_ENUM) {
        _gl_version_major = 1;
        _gl_version_minor = 2;
        GLCAT.warning()
          << "Driver possibly misreported GL_VERSION!  Unable to detect "
             "correct OpenGL version.\n";

      } else if (_gl_version_major != 1 || _gl_version_minor != 2) {
        GLCAT.debug()
          << "Driver misreported GL_VERSION!  Correct version detected as "
          << _gl_version_major << "." << _gl_version_minor << "\n";
      }
      return;
    }
  }

  // If we asked for a GL 3 context, let's first try and see if we can use the
  // OpenGL 3 way to query version.
  if (gl_version.get_num_words() > 0 && gl_version[0] >= 3) {
    glGetIntegerv(GL_MAJOR_VERSION, &_gl_version_major);
    glGetIntegerv(GL_MINOR_VERSION, &_gl_version_minor);

    if (_gl_version_major >= 1) {
      // Fair enough, seems to check out.
      if (GLCAT.is_debug()) {
        GLCAT.debug()
          << "Detected OpenGL version: "
          << _gl_version_major << "." << _gl_version_minor << "\n";
      }
      return;
    }
  }
#endif  // !OPENGLES

  // Otherwise, parse the GL_VERSION string.
  if (_gl_version.empty()) {
    GLCAT.error() << "Unable to detect OpenGL version\n";

  } else {
    string input = _gl_version;

    // Skip any initial words that don't begin with a digit.
    while (!input.empty() && !isdigit(input[0])) {
      size_t space = input.find(' ');
      if (space == string::npos) {
        break;
      }
      size_t next = space + 1;
      while (next < input.length() && isspace(input[next])) {
        ++next;
      }
      input = input.substr(next);
    }

    // Truncate after the first space.
    size_t space = input.find(' ');
    if (space != string::npos) {
      input = input.substr(0, space);
    }

    vector_string components;
    tokenize(input, components, ".");
    if (components.size() >= 1) {
      string_to_int(components[0], _gl_version_major);
    }
    if (components.size() >= 2) {
      string_to_int(components[1], _gl_version_minor);
    }

    if (GLCAT.is_debug()) {
      GLCAT.debug()
        << "GL_VERSION decoded to: "
        << _gl_version_major << "." << _gl_version_minor
        << "\n";
    }
  }
}

/**
 * Queries the supported GLSL version.
 */
void CLP(GraphicsStateGuardian)::
query_glsl_version() {
  _gl_shadlang_ver_major = 0;
  _gl_shadlang_ver_minor = 0;
#ifndef OPENGLES_1

#ifndef OPENGLES
  // OpenGL 2.0 introduces GLSL in the core.  In 1.x, it is an extension.
  if (_gl_version_major >= 2 || has_extension("GL_ARB_shading_language_100")) {
    string ver = show_gl_string("GL_SHADING_LANGUAGE_VERSION", GL_SHADING_LANGUAGE_VERSION);
    _gl_shadlang_ver_major = 1;
    _gl_shadlang_ver_minor = (_gl_version_major >= 2) ? 1 : 0;
    if (ver.empty() ||
        sscanf(ver.c_str(), "%d.%d", &_gl_shadlang_ver_major,
                                     &_gl_shadlang_ver_minor) != 2) {
      GLCAT.warning() << "Invalid GL_SHADING_LANGUAGE_VERSION format: " << ver << "\n";
    }
  }
#else
  // OpenGL ES 2.0 and above has shader support built-in.
  string ver = show_gl_string("GL_SHADING_LANGUAGE_VERSION", GL_SHADING_LANGUAGE_VERSION);
  _gl_shadlang_ver_major = 1;
  _gl_shadlang_ver_minor = 0;
  if (ver.empty() ||
      sscanf(ver.c_str(), "OpenGL ES GLSL ES %d.%d", &_gl_shadlang_ver_major,
                                                     &_gl_shadlang_ver_minor) != 2) {
#ifdef __EMSCRIPTEN__  // See emscripten bug 4070
    if (sscanf(ver.c_str(), "OpenGL ES GLSL %d.%d", &_gl_shadlang_ver_major,
                                                    &_gl_shadlang_ver_minor) != 2)
#endif
    GLCAT.warning() << "Invalid GL_SHADING_LANGUAGE_VERSION format: " << ver << "\n";
  }
#endif

  if (GLCAT.is_debug()) {
    GLCAT.debug()
      << "Detected GLSL "
#ifdef OPENGLES
         "ES "
#endif
         "version: "
      << _gl_shadlang_ver_major << "." << _gl_shadlang_ver_minor << "\n";
  }
#endif  // !OPENGLES_1
}

/**
 * Separates the string returned by GL_EXTENSIONS (or glx or wgl extensions)
 * into its individual tokens and saves them in the _extensions member.
 */
void CLP(GraphicsStateGuardian)::
save_extensions(const char *extensions) {
  if (extensions != nullptr) {
    vector_string tokens;
    extract_words(extensions, tokens);

    vector_string::iterator ti;
    for (ti = tokens.begin(); ti != tokens.end(); ++ti) {
      _extensions.insert(*ti);
    }
  }
}

/**
 * This may be redefined by a derived class (e.g.  glx or wgl) to get whatever
 * further extensions strings may be appropriate to that interface, in
 * addition to the GL extension strings return by glGetString().
 */
void CLP(GraphicsStateGuardian)::
get_extra_extensions() {
}

/**
 * Outputs the list of GL extensions to notify, if debug mode is enabled.
 */
void CLP(GraphicsStateGuardian)::
report_extensions() const {
  if (GLCAT.is_debug()) {
    std::ostream &out = GLCAT.debug();
    out << "GL Extensions:\n";

    pset<string>::const_iterator ei;
    for (ei = _extensions.begin(); ei != _extensions.end(); ++ei) {
      size_t len = (*ei).size();
      out << "  " << (*ei);

      // Display a second column.
      if (len <= 38) {
        if (++ei != _extensions.end()) {
          for (int i = len; i < 38; ++i) {
            out.put(' ');
          }
          out << ' ' << (*ei);
        } else {
          out.put('\n');
          break;
        }
      }
      out.put('\n');
    }
  }
}

/**
 * Returns the pointer to the GL extension function with the indicated name,
 * or NULL if the function is not available.
 */
void *CLP(GraphicsStateGuardian)::
get_extension_func(const char *name) {
  // First, look in the static-compiled namespace.  If we were compiled to
  // expect at least a certain minimum runtime version of OpenGL, then we can
  // expect those extension functions to be available at compile time.
  // Somewhat more reliable than poking around in the runtime pointers.
  static struct {
    const char *name;
    void *fptr;
  } compiled_function_table[] = {
#ifdef EXPECT_GL_VERSION_1_2
    { "glBlendColor", (void *)&glBlendColor },
    { "glBlendEquation", (void *)&glBlendEquation },
    { "glDrawRangeElements", (void *)&glDrawRangeElements },
    { "glTexImage3D", (void *)&glTexImage3D },
    { "glTexSubImage3D", (void *)&glTexSubImage3D },
    { "glCopyTexSubImage3D", (void *)&glCopyTexSubImage3D },
#endif
#ifdef EXPECT_GL_VERSION_1_3
    { "glActiveTexture", (void *)&glActiveTexture },
    { "glClientActiveTexture", (void *)&glClientActiveTexture },
    { "glCompressedTexImage1D", (void *)&glCompressedTexImage1D },
    { "glCompressedTexImage2D", (void *)&glCompressedTexImage2D },
    { "glCompressedTexImage3D", (void *)&glCompressedTexImage3D },
    { "glCompressedTexSubImage1D", (void *)&glCompressedTexSubImage1D },
    { "glCompressedTexSubImage2D", (void *)&glCompressedTexSubImage2D },
    { "glCompressedTexSubImage3D", (void *)&glCompressedTexSubImage3D },
    { "glGetCompressedTexImage", (void *)&glGetCompressedTexImage },
    { "glMultiTexCoord1f", (void *)&glMultiTexCoord1f },
    { "glMultiTexCoord2", (void *)&glMultiTexCoord2 },
    { "glMultiTexCoord3", (void *)&glMultiTexCoord3 },
    { "glMultiTexCoord4", (void *)&glMultiTexCoord4 },
#endif
#ifdef EXPECT_GL_VERSION_1_4
    { "glPointParameterfv", (void *)&glPointParameterfv },
    { "glSecondaryColorPointer", (void *)&glSecondaryColorPointer },
#endif
#ifdef EXPECT_GL_VERSION_1_5
    { "glBeginQuery", (void *)&glBeginQuery },
    { "glBindBuffer", (void *)&glBindBuffer },
    { "glBufferData", (void *)&glBufferData },
    { "glBufferSubData", (void *)&glBufferSubData },
    { "glDeleteBuffers", (void *)&glDeleteBuffers },
    { "glDeleteQueries", (void *)&glDeleteQueries },
    { "glEndQuery", (void *)&glEndQuery },
    { "glGenBuffers", (void *)&glGenBuffers },
    { "glGenQueries", (void *)&glGenQueries },
    { "glGetQueryObjectuiv", (void *)&glGetQueryObjectuiv },
    { "glGetQueryiv", (void *)&glGetQueryiv },
#endif
#ifdef OPENGLES
    { "glActiveTexture", (void *)&glActiveTexture },
#ifndef OPENGLES_2
    { "glClientActiveTexture", (void *)&glClientActiveTexture },
#endif
    { "glBindBuffer", (void *)&glBindBuffer },
    { "glBufferData", (void *)&glBufferData },
    { "glBufferSubData", (void *)&glBufferSubData },
    { "glDeleteBuffers", (void *)&glDeleteBuffers },
    { "glGenBuffers", (void *)&glGenBuffers },
#endif
    { nullptr, nullptr }
  };

  int i = 0;
  while (compiled_function_table[i].name != nullptr) {
    if (strcmp(compiled_function_table[i].name, name) == 0) {
      return compiled_function_table[i].fptr;
    }
    ++i;
  }

  // If the extension function wasn't compiled in, then go get it from the
  // runtime.  There's a different interface for each API.
  return do_get_extension_func(name);
}

/**
 * This is the virtual implementation of get_extension_func().  Each API-
 * specific GL implementation will map this method to the appropriate API call
 * to retrieve the extension function pointer.  Returns NULL if the function
 * is not available.
 */
void *CLP(GraphicsStateGuardian)::
do_get_extension_func(const char *) {
  return nullptr;
}

/**
 * Sets up the glDrawBuffer to render into the buffer indicated by the
 * RenderBuffer object.  This only sets up the color and aux bits; it does not
 * affect the depth, stencil, accum layers.
 */
void CLP(GraphicsStateGuardian)::
set_draw_buffer(int rbtype) {
#ifndef OPENGLES_1  // Draw buffers not supported by OpenGL ES 1.
  if (_current_fbo) {
    GLuint buffers[16];
    int nbuffers = 0;
    int index = 0;
    if (_current_properties->get_color_bits() > 0) {
      if (rbtype & RenderBuffer::T_left) {
        buffers[nbuffers++] = GL_COLOR_ATTACHMENT0_EXT + index;
      }
      ++index;
      if (_current_properties->is_stereo()) {
        if (rbtype & RenderBuffer::T_right) {
          buffers[nbuffers++] = GL_COLOR_ATTACHMENT0_EXT + index;
        }
        ++index;
      }
    }
    for (int i = 0; i < _current_properties->get_aux_rgba(); ++i) {
      if (rbtype & (RenderBuffer::T_aux_rgba_0 << i)) {
        buffers[nbuffers++] = GL_COLOR_ATTACHMENT0_EXT + index;
      }
      ++index;
    }
    for (int i = 0; i < _current_properties->get_aux_hrgba(); ++i) {
      if (rbtype & (RenderBuffer::T_aux_hrgba_0 << i)) {
        buffers[nbuffers++] = GL_COLOR_ATTACHMENT0_EXT + index;
      }
      ++index;
    }
    for (int i = 0; i < _current_properties->get_aux_float(); ++i) {
      if (rbtype & (RenderBuffer::T_aux_float_0 << i)) {
        buffers[nbuffers++] = GL_COLOR_ATTACHMENT0_EXT + index;
      }
      ++index;
    }
    if (_glDrawBuffers != nullptr) {
      _glDrawBuffers(nbuffers, buffers);
    } else {
      nassertv(nbuffers == 1 && buffers[0] == GL_COLOR_ATTACHMENT0_EXT);
    }

  } else {
#ifndef OPENGLES
    switch (rbtype & RenderBuffer::T_color) {
    case RenderBuffer::T_front:
      glDrawBuffer(GL_FRONT);
      break;

    case RenderBuffer::T_back:
      glDrawBuffer(GL_BACK);
      break;

    case RenderBuffer::T_right:
      glDrawBuffer(GL_RIGHT);
      break;

    case RenderBuffer::T_left:
      glDrawBuffer(GL_LEFT);
      break;

    case RenderBuffer::T_front_right:
      nassertv(_current_properties->is_stereo());
      glDrawBuffer(GL_FRONT_RIGHT);
      break;

    case RenderBuffer::T_front_left:
      nassertv(_current_properties->is_stereo());
      glDrawBuffer(GL_FRONT_LEFT);
      break;

    case RenderBuffer::T_back_right:
      nassertv(_current_properties->is_stereo());
      glDrawBuffer(GL_BACK_RIGHT);
      break;

    case RenderBuffer::T_back_left:
      nassertv(_current_properties->is_stereo());
      glDrawBuffer(GL_BACK_LEFT);
      break;

    default:
      break;
    }
#endif  // OPENGLES
  }
#endif  // OPENGLES_1

  // Also ensure that any global color channels are masked out.
  set_color_write_mask(_color_write_mask);

  report_my_gl_errors();
}

/**
 * Sets up the glReadBuffer to render into the buffer indicated by the
 * RenderBuffer object.  This only sets up the color bits; it does not affect
 * the depth, stencil, accum layers.
 */
void CLP(GraphicsStateGuardian)::
set_read_buffer(int rbtype) {
#ifndef OPENGLES_1  // Draw buffers not supported by OpenGL ES 1.
  if (rbtype & (RenderBuffer::T_depth | RenderBuffer::T_stencil)) {
    // Special case: don't have to call ReadBuffer for these.
    return;
  }

  if (_current_fbo) {
    GLuint buffer = GL_COLOR_ATTACHMENT0_EXT;
    int index = 1;
    if (_current_properties->is_stereo()) {
      if (rbtype & RenderBuffer::T_right) {
        buffer = GL_COLOR_ATTACHMENT1_EXT;
      }
      ++index;
    }
    for (int i = 0; i < _current_properties->get_aux_rgba(); ++i) {
      if (rbtype & (RenderBuffer::T_aux_rgba_0 << i)) {
        buffer = GL_COLOR_ATTACHMENT0_EXT + index;
      }
      ++index;
    }
    for (int i = 0; i < _current_properties->get_aux_hrgba(); ++i) {
      if (rbtype & (RenderBuffer::T_aux_hrgba_0 << i)) {
        buffer = GL_COLOR_ATTACHMENT0_EXT + index;
      }
      ++index;
    }
    for (int i = 0; i < _current_properties->get_aux_float(); ++i) {
      if (rbtype & (RenderBuffer::T_aux_float_0 << i)) {
        buffer = GL_COLOR_ATTACHMENT0_EXT + index;
      }
      ++index;
    }
#ifdef OPENGLES
    _glReadBuffer(buffer);
#else
    glReadBuffer(buffer);
#endif

  } else {
#ifndef OPENGLES
    switch (rbtype & RenderBuffer::T_color) {
    case RenderBuffer::T_front:
      glReadBuffer(GL_FRONT);
      break;

    case RenderBuffer::T_back:
      glReadBuffer(GL_BACK);
      break;

    case RenderBuffer::T_right:
      glReadBuffer(GL_RIGHT);
      break;

    case RenderBuffer::T_left:
      glReadBuffer(GL_LEFT);
      break;

    case RenderBuffer::T_front_right:
      glReadBuffer(GL_FRONT_RIGHT);
      break;

    case RenderBuffer::T_front_left:
      glReadBuffer(GL_FRONT_LEFT);
      break;

    case RenderBuffer::T_back_right:
      glReadBuffer(GL_BACK_RIGHT);
      break;

    case RenderBuffer::T_back_left:
      glReadBuffer(GL_BACK_LEFT);
      break;

    default:
      break;
    }
#endif  // OPENGLES
  }

  report_my_gl_errors();
#endif  // OPENGLES_1
}

/**
 * Maps from the Geom's internal numeric type symbols to GL's.
 */
GLenum CLP(GraphicsStateGuardian)::
get_numeric_type(Geom::NumericType numeric_type) {
  switch (numeric_type) {
  case Geom::NT_uint16:
    return GL_UNSIGNED_SHORT;

  case Geom::NT_uint32:
#ifndef OPENGLES_1
    return GL_UNSIGNED_INT;
#else
    break;
#endif

  case Geom::NT_uint8:
  case Geom::NT_packed_dcba:
  case Geom::NT_packed_dabc:
    return GL_UNSIGNED_BYTE;

  case Geom::NT_float32:
    return GL_FLOAT;

  case Geom::NT_float64:
#ifndef OPENGLES
    return GL_DOUBLE;
#else
    break;
#endif

  case Geom::NT_stdfloat:
    // Shouldn't happen, display error.
    break;

  case Geom::NT_int8:
    return GL_BYTE;

  case Geom::NT_int16:
    return GL_SHORT;

  case Geom::NT_int32:
#ifndef OPENGLES_1
    return GL_INT;
#else
    break;
#endif

  case Geom::NT_packed_ufloat:
#ifndef OPENGLES_1
    return GL_UNSIGNED_INT_10F_11F_11F_REV;
#else
    break;
#endif
  }

  GLCAT.error()
    << "Invalid NumericType value (" << (int)numeric_type << ")\n";
  return GL_UNSIGNED_BYTE;
}

/**
 * Maps from the Texture's texture type symbols to GL's.
 */
GLenum CLP(GraphicsStateGuardian)::
get_texture_target(Texture::TextureType texture_type) const {
  switch (texture_type) {
  case Texture::TT_1d_texture:
    // There are no 1D textures in OpenGL ES.  Fall back to 2D textures.
#ifndef OPENGLES
    return GL_TEXTURE_1D;
#endif

  case Texture::TT_1d_texture_array:
    // There are no 1D array textures in OpenGL ES.  Fall back to 2D textures.
#ifndef OPENGLES
    return GL_TEXTURE_1D_ARRAY;
#endif

  case Texture::TT_2d_texture:
    return GL_TEXTURE_2D;

  case Texture::TT_3d_texture:
#ifndef OPENGLES_1
    if (_supports_3d_texture) {
      return GL_TEXTURE_3D;
    }
#endif
    return GL_NONE;

  case Texture::TT_2d_texture_array:
#ifndef OPENGLES_1
    if (_supports_2d_texture_array) {
      return GL_TEXTURE_2D_ARRAY;
    }
#endif
    return GL_NONE;

  case Texture::TT_cube_map:
    if (_supports_cube_map) {
      return GL_TEXTURE_CUBE_MAP;
    } else {
      return GL_NONE;
    }

  case Texture::TT_cube_map_array:
#ifndef OPENGLES
    if (_supports_cube_map_array) {
      return GL_TEXTURE_CUBE_MAP_ARRAY;
    }
#endif
    return GL_NONE;

  case Texture::TT_buffer_texture:
#ifndef OPENGLES_1
    if (_supports_buffer_texture) {
      return GL_TEXTURE_BUFFER;
    }
#endif
    return GL_NONE;
  }

  GLCAT.error() << "Invalid Texture::TextureType value!\n";
  return GL_TEXTURE_2D;
}

/**
 * Maps from the Texture's internal wrap mode symbols to GL's.
 */
GLenum CLP(GraphicsStateGuardian)::
get_texture_wrap_mode(SamplerState::WrapMode wm) const {
  if (gl_ignore_clamp) {
    return GL_REPEAT;
  }
  switch (wm) {
  case SamplerState::WM_clamp:
    return _edge_clamp;

  case SamplerState::WM_repeat:
    return GL_REPEAT;

  case SamplerState::WM_mirror:
    return _mirror_repeat;

  case SamplerState::WM_mirror_once:
    return _mirror_border_clamp;

  case SamplerState::WM_border_color:
    return _border_clamp;

  case SamplerState::WM_invalid:
    break;
  }
  GLCAT.error() << "Invalid SamplerState::WrapMode value!\n";
  return _edge_clamp;
}

/**
 * Maps from the GL's internal wrap mode symbols to Panda's.
 */
SamplerState::WrapMode CLP(GraphicsStateGuardian)::
get_panda_wrap_mode(GLenum wm) {
  switch (wm) {
#ifndef OPENGLES
  case GL_CLAMP:
#endif
  case GL_CLAMP_TO_EDGE:
    return SamplerState::WM_clamp;

#ifndef OPENGLES
  case GL_CLAMP_TO_BORDER:
    return SamplerState::WM_border_color;
#endif

  case GL_REPEAT:
    return SamplerState::WM_repeat;

  case GL_MIRRORED_REPEAT:
    return SamplerState::WM_mirror;

#ifndef OPENGLES
  case GL_MIRROR_CLAMP_EXT:
  case GL_MIRROR_CLAMP_TO_EDGE_EXT:
    return SamplerState::WM_mirror;

  case GL_MIRROR_CLAMP_TO_BORDER_EXT:
    return SamplerState::WM_mirror_once;
#endif
  }
  GLCAT.error() << "Unexpected GL wrap mode " << (int)wm << "\n";
  return SamplerState::WM_clamp;
}

/**
 * Maps from the Texture's internal filter type symbols to GL's.
 */
GLenum CLP(GraphicsStateGuardian)::
get_texture_filter_type(SamplerState::FilterType ft, bool ignore_mipmaps) {
  if (gl_ignore_filters) {
    return GL_NEAREST;

  } else if (ignore_mipmaps) {
    switch (ft) {
    case SamplerState::FT_nearest_mipmap_nearest:
    case SamplerState::FT_nearest:
      return GL_NEAREST;
    case SamplerState::FT_linear:
    case SamplerState::FT_linear_mipmap_nearest:
    case SamplerState::FT_nearest_mipmap_linear:
    case SamplerState::FT_linear_mipmap_linear:
      return GL_LINEAR;
    case SamplerState::FT_shadow:
      return GL_LINEAR;
    case SamplerState::FT_default:
    case SamplerState::FT_invalid:
      break;
    }

  } else {
    switch (ft) {
    case SamplerState::FT_nearest:
      return GL_NEAREST;
    case SamplerState::FT_linear:
      return GL_LINEAR;
    case SamplerState::FT_nearest_mipmap_nearest:
      return GL_NEAREST_MIPMAP_NEAREST;
    case SamplerState::FT_linear_mipmap_nearest:
      return GL_LINEAR_MIPMAP_NEAREST;
    case SamplerState::FT_nearest_mipmap_linear:
      return GL_NEAREST_MIPMAP_LINEAR;
    case SamplerState::FT_linear_mipmap_linear:
      return GL_LINEAR_MIPMAP_LINEAR;
    case SamplerState::FT_shadow:
      return GL_LINEAR;
    case SamplerState::FT_default:
    case SamplerState::FT_invalid:
      break;
    }
  }
  GLCAT.error() << "Invalid SamplerState::FilterType value!\n";
  return GL_NEAREST;
}

/**
 * Maps from the GL's internal filter type symbols to Panda's.
 */
SamplerState::FilterType CLP(GraphicsStateGuardian)::
get_panda_filter_type(GLenum ft) {
  switch (ft) {
  case GL_NEAREST:
    return SamplerState::FT_nearest;
  case GL_LINEAR:
    return SamplerState::FT_linear;
  case GL_NEAREST_MIPMAP_NEAREST:
    return SamplerState::FT_nearest_mipmap_nearest;
  case GL_LINEAR_MIPMAP_NEAREST:
    return SamplerState::FT_linear_mipmap_nearest;
  case GL_NEAREST_MIPMAP_LINEAR:
    return SamplerState::FT_nearest_mipmap_linear;
  case GL_LINEAR_MIPMAP_LINEAR:
    return SamplerState::FT_linear_mipmap_linear;
  }
  GLCAT.error() << "Unexpected GL filter type " << (int)ft << "\n";
  return SamplerState::FT_linear;
}

/**
 * Maps from the Texture's internal ComponentType symbols to GL's.
 */
GLenum CLP(GraphicsStateGuardian)::
get_component_type(Texture::ComponentType component_type) {
  switch (component_type) {
  case Texture::T_unsigned_byte:
    return GL_UNSIGNED_BYTE;
  case Texture::T_unsigned_short:
    return GL_UNSIGNED_SHORT;
  case Texture::T_float:
    return GL_FLOAT;
  case Texture::T_unsigned_int_24_8:
    if (_supports_depth_stencil) {
      return GL_UNSIGNED_INT_24_8_EXT;
    } else {
      return GL_UNSIGNED_BYTE;
    }
  case Texture::T_int:
#ifndef OPENGLES_1
    return GL_INT;
#endif
  case Texture::T_byte:
    return GL_BYTE;
  case Texture::T_short:
    return GL_SHORT;

#ifndef OPENGLES_1
  case Texture::T_half_float:
    return GL_HALF_FLOAT;
#endif

#ifndef OPENGLES_1
  case Texture::T_unsigned_int:
    return GL_UNSIGNED_INT;
#endif

  default:
    GLCAT.error() << "Invalid Texture::Type value!\n";
    return GL_UNSIGNED_BYTE;
  }
}

/**
 * Maps from the Texture's Format symbols to GL's.
 */
GLint CLP(GraphicsStateGuardian)::
get_external_image_format(Texture *tex) const {
  Texture::CompressionMode compression = tex->get_ram_image_compression();
  Texture::Format format = tex->get_format();
  if (compression != Texture::CM_off &&
      get_supports_compressed_texture_format(compression)) {
    switch (compression) {
    case Texture::CM_on:
#ifndef OPENGLES
      switch (format) {
      case Texture::F_color_index:
      case Texture::F_depth_component:
      case Texture::F_depth_component16:
      case Texture::F_depth_component24:
      case Texture::F_depth_component32:
      case Texture::F_depth_stencil:
      case Texture::F_r11_g11_b10:
      case Texture::F_rgb9_e5:
        // This shouldn't be possible.
        nassertr(false, GL_RGB);
        break;

      case Texture::F_rgba:
      case Texture::F_rgbm:
      case Texture::F_rgba4:
      case Texture::F_rgba8:
      case Texture::F_rgba12:
      case Texture::F_rgba16:
      case Texture::F_rgba32:
      case Texture::F_rgba8i:
      case Texture::F_rgba16i:
      case Texture::F_rgba32i:
      case Texture::F_rgb10_a2:
        return GL_COMPRESSED_RGBA;

      case Texture::F_rgb:
      case Texture::F_rgb5:
      case Texture::F_rgba5:
      case Texture::F_rgb8:
      case Texture::F_rgb8i:
      case Texture::F_rgb12:
      case Texture::F_rgb332:
      case Texture::F_rgb16:
      case Texture::F_rgb16i:
      case Texture::F_rgb32:
      case Texture::F_rgb32i:
        return GL_COMPRESSED_RGB;

      case Texture::F_alpha:
        return GL_COMPRESSED_ALPHA;

      case Texture::F_red:
      case Texture::F_green:
      case Texture::F_blue:
      case Texture::F_r8i:
      case Texture::F_r16:
      case Texture::F_r16i:
      case Texture::F_r32:
      case Texture::F_r32i:
        return GL_COMPRESSED_RED;

      case Texture::F_rg:
      case Texture::F_rg8i:
      case Texture::F_rg16:
      case Texture::F_rg16i:
      case Texture::F_rg32:
      case Texture::F_rg32i:
        return GL_COMPRESSED_RG;

      case Texture::F_luminance:
        return GL_COMPRESSED_LUMINANCE;

      case Texture::F_luminance_alpha:
      case Texture::F_luminance_alphamask:
        return GL_COMPRESSED_LUMINANCE_ALPHA;

      case Texture::F_srgb:
        return GL_COMPRESSED_SRGB;

      case Texture::F_srgb_alpha:
        return GL_COMPRESSED_SRGB_ALPHA;

      case Texture::F_sluminance:
        return GL_COMPRESSED_SLUMINANCE;

      case Texture::F_sluminance_alpha:
        return GL_COMPRESSED_SLUMINANCE_ALPHA;
      }
#endif
      break;

    case Texture::CM_dxt1:
#ifndef OPENGLES
      if (format == Texture::F_srgb_alpha) {
        return GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT;
      } else if (format == Texture::F_srgb) {
        return GL_COMPRESSED_SRGB_S3TC_DXT1_EXT;
      } else
#endif
      if (Texture::has_alpha(format)) {
        return GL_COMPRESSED_RGBA_S3TC_DXT1_EXT;
      } else {
        return GL_COMPRESSED_RGB_S3TC_DXT1_EXT;
      }

    case Texture::CM_dxt3:
#ifndef OPENGLES
      if (format == Texture::F_srgb || format == Texture::F_srgb_alpha) {
        return GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT;
      }
#endif
#ifndef OPENGLES_1
      return GL_COMPRESSED_RGBA_S3TC_DXT3_EXT;
#endif
      break;

    case Texture::CM_dxt5:
#ifndef OPENGLES
      if (format == Texture::F_srgb || format == Texture::F_srgb_alpha) {
        return GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT;
      }
#endif
#ifndef OPENGLES_1
      return GL_COMPRESSED_RGBA_S3TC_DXT5_EXT;
#endif
      break;

    case Texture::CM_fxt1:
#ifndef OPENGLES
      if (Texture::has_alpha(format)) {
        return GL_COMPRESSED_RGBA_FXT1_3DFX;
      } else {
        return GL_COMPRESSED_RGB_FXT1_3DFX;
      }
#endif
      break;

#ifdef OPENGLES
    case Texture::CM_pvr1_2bpp:
      if (Texture::has_alpha(format)) {
        return GL_COMPRESSED_RGBA_PVRTC_2BPPV1_IMG;
      } else {
        return GL_COMPRESSED_RGB_PVRTC_2BPPV1_IMG;
      }

    case Texture::CM_pvr1_4bpp:
      if (Texture::has_alpha(format)) {
        return GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG;
      } else {
        return GL_COMPRESSED_RGB_PVRTC_4BPPV1_IMG;
      }
#else
    case Texture::CM_pvr1_2bpp:
    case Texture::CM_pvr1_4bpp:
      break;
#endif  // OPENGLES

    case Texture::CM_rgtc:
#ifndef OPENGLES
      if (format == Texture::F_luminance) {
        return GL_COMPRESSED_LUMINANCE_LATC1_EXT;
      } else if (format == Texture::F_luminance_alpha) {
        return GL_COMPRESSED_LUMINANCE_ALPHA_LATC2_EXT;
      } else if (tex->get_num_components() == 1) {
        return GL_COMPRESSED_RED_RGTC1;
      } else {
        return GL_COMPRESSED_RG_RGTC2;
      }
#endif
      break;

    case Texture::CM_etc1:
#ifdef OPENGLES
      return GL_ETC1_RGB8_OES;
#endif
      // Fall through - ETC2 is backward compatible
    case Texture::CM_etc2:
      if (format == Texture::F_rgbm) {
        return GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2;
      } else if (format == Texture::F_srgb_alpha) {
        return GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC;
      } else if (format == Texture::F_srgb) {
        return GL_COMPRESSED_SRGB8_ETC2;
      } else if (Texture::has_alpha(format)) {
        return GL_COMPRESSED_RGBA8_ETC2_EAC;
      } else {
        return GL_COMPRESSED_RGB8_ETC2;
      }
      break;

    case Texture::CM_eac:
      if (Texture::is_unsigned(tex->get_component_type())) {
        if (tex->get_num_components() == 1) {
          return GL_COMPRESSED_R11_EAC;
        } else {
          return GL_COMPRESSED_RG11_EAC;
        }
      } else {
        if (tex->get_num_components() == 1) {
          return GL_COMPRESSED_SIGNED_R11_EAC;
        } else {
          return GL_COMPRESSED_SIGNED_RG11_EAC;
        }
      }
      break;

    case Texture::CM_default:
    case Texture::CM_off:
    case Texture::CM_dxt2:
    case Texture::CM_dxt4:
      // This shouldn't happen.
      nassertr(false, GL_RGB);
      break;
    }
  }

  switch (format) {
#ifndef OPENGLES
  case Texture::F_color_index:
    return GL_COLOR_INDEX;
#endif
  case Texture::F_depth_component:
  case Texture::F_depth_component16:
  case Texture::F_depth_component24:
  case Texture::F_depth_component32:
    return GL_DEPTH_COMPONENT;
  case Texture::F_depth_stencil:
    return _supports_depth_stencil ? GL_DEPTH_STENCIL : GL_DEPTH_COMPONENT;
#ifndef OPENGLES_1
  case Texture::F_red:
  case Texture::F_r16:
  case Texture::F_r32:
    return GL_RED;
  case Texture::F_green:
    return GL_GREEN;
  case Texture::F_blue:
    return GL_BLUE;
#endif

  case Texture::F_alpha:
#ifdef OPENGLES
    return GL_ALPHA;
#else
    return _supports_luminance_texture ? GL_ALPHA : GL_RED;
#endif

#ifndef OPENGLES_1
  case Texture::F_rg:
  case Texture::F_rg16:
  case Texture::F_rg32:
    return GL_RG;
#endif
  case Texture::F_rgb:
  case Texture::F_rgb5:
  case Texture::F_rgb8:
  case Texture::F_rgb12:
  case Texture::F_rgb332:
  case Texture::F_rgb16:
  case Texture::F_rgb32:
  case Texture::F_srgb:
  case Texture::F_r11_g11_b10:
  case Texture::F_rgb9_e5:
#ifdef OPENGLES
    // OpenGL ES never supports BGR, even if _supports_bgr is true.
    return GL_RGB;
#else
    return _supports_bgr ? GL_BGR : GL_RGB;
#endif

  case Texture::F_rgba:
  case Texture::F_rgbm:
  case Texture::F_rgba8:
  case Texture::F_rgba12:
    return _supports_bgr ? GL_BGRA : GL_RGBA;

  case Texture::F_rgba4:
  case Texture::F_rgba5:
  case Texture::F_rgba16:
  case Texture::F_rgba32:
  case Texture::F_srgb_alpha:
  case Texture::F_rgb10_a2:
#ifdef OPENGLES
    // OpenGL ES doesn't have sized BGRA formats.
    return GL_RGBA;
#else
    return _supports_bgr ? GL_BGRA : GL_RGBA;
#endif

  case Texture::F_luminance:
#ifdef OPENGLES
    return GL_LUMINANCE;
#else
    return _supports_luminance_texture ? GL_LUMINANCE : GL_RED;
#endif
  case Texture::F_luminance_alphamask:
  case Texture::F_luminance_alpha:
#ifdef OPENGLES
    return GL_LUMINANCE_ALPHA;
#else
    return _supports_luminance_texture ? GL_LUMINANCE_ALPHA : GL_RG;
#endif

  case Texture::F_sluminance:
#ifdef OPENGLES
    return GL_LUMINANCE;
#else
    return _supports_luminance_texture ? GL_LUMINANCE : GL_RGB;
#endif
  case Texture::F_sluminance_alpha:
#ifdef OPENGLES
    return GL_LUMINANCE_ALPHA;
#else
    return _supports_luminance_texture ? GL_LUMINANCE_ALPHA : GL_RGBA;
#endif

#ifndef OPENGLES_1
  case Texture::F_r8i:
  case Texture::F_r16i:
  case Texture::F_r32i:
    return GL_RED_INTEGER;
  case Texture::F_rg8i:
  case Texture::F_rg16i:
  case Texture::F_rg32i:
    return GL_RG_INTEGER;
  case Texture::F_rgb8i:
  case Texture::F_rgb16i:
  case Texture::F_rgb32i:
    return GL_RGB_INTEGER;
  case Texture::F_rgba8i:
  case Texture::F_rgba16i:
  case Texture::F_rgba32i:
    return GL_RGBA_INTEGER;
#endif

  default:
    break;
  }
  GLCAT.error()
    << "Invalid Texture::Format value in get_external_image_format(): "
    << format << "\n";
  return GL_RGB;
}

/**
 * Maps from the Texture's Format symbols to a suitable internal format for GL
 * textures.
 */
GLint CLP(GraphicsStateGuardian)::
get_internal_image_format(Texture *tex, bool force_sized) const {
  Texture::CompressionMode compression = tex->get_compression();
  if (compression == Texture::CM_default) {
    compression = (compressed_textures) ? Texture::CM_on : Texture::CM_off;
  }
  Texture::Format format = tex->get_format();
  if (tex->get_render_to_texture()) {
    // no compression for render targets
    compression = Texture::CM_off;
  }
  Texture::ComponentType component_type = tex->get_component_type();
  Texture::TextureType texture_type = tex->get_texture_type();
  bool is_3d = (texture_type == Texture::TT_3d_texture ||
                texture_type == Texture::TT_2d_texture_array);

  if (get_supports_compressed_texture_format(compression) &&
      texture_type != Texture::TT_buffer_texture) {
    switch (compression) {
    case Texture::CM_on:
      // The user asked for just generic compression.  OpenGL supports
      // requesting just generic compression, but we'd like to go ahead and
      // request a specific type (if we can figure out an appropriate choice),
      // since that makes saving the result as a pre-compressed texture more
      // dependable--this way, we will know which compression algorithm was
      // applied.
      switch (format) {
      case Texture::F_color_index:
      case Texture::F_depth_component:
      case Texture::F_depth_component16:
      case Texture::F_depth_component24:
      case Texture::F_depth_component32:
      case Texture::F_depth_stencil:
      case Texture::F_r8i:
      case Texture::F_rg8i:
      case Texture::F_rgb8i:
      case Texture::F_rgba8i:
      case Texture::F_r16i:
      case Texture::F_rg16i:
      case Texture::F_rgb16i:
      case Texture::F_rgba16i:
      case Texture::F_r32i:
      case Texture::F_rg32i:
      case Texture::F_rgb32i:
      case Texture::F_rgba32i:
      case Texture::F_r11_g11_b10:
      case Texture::F_rgb9_e5:
        // Unsupported; fall through to below.
        break;

      case Texture::F_rgbm:
      case Texture::F_rgba5:
      case Texture::F_rgb10_a2:
        if (get_supports_compressed_texture_format(Texture::CM_dxt1) && !is_3d) {
          return GL_COMPRESSED_RGBA_S3TC_DXT1_EXT;
        }
        if (get_supports_compressed_texture_format(Texture::CM_etc2) && !is_3d) {
          return GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2;
        }
#ifndef OPENGLES
        if (get_supports_compressed_texture_format(Texture::CM_fxt1) && !is_3d) {
          return GL_COMPRESSED_RGBA_FXT1_3DFX;
        }
        return GL_COMPRESSED_RGBA;
#endif
        break;

      case Texture::F_rgba4:
#ifndef OPENGLES_1
        if (get_supports_compressed_texture_format(Texture::CM_dxt3) && !is_3d) {
          return GL_COMPRESSED_RGBA_S3TC_DXT3_EXT;
        }
#endif
        if (get_supports_compressed_texture_format(Texture::CM_etc2) && !is_3d) {
          return GL_COMPRESSED_RGBA8_ETC2_EAC;
        }
#ifndef OPENGLES
        if (get_supports_compressed_texture_format(Texture::CM_fxt1) && !is_3d) {
          return GL_COMPRESSED_RGBA_FXT1_3DFX;
        }
        return GL_COMPRESSED_RGBA;
#endif
        break;

      case Texture::F_rgba:
      case Texture::F_rgba8:
      case Texture::F_rgba12:
      case Texture::F_rgba16:
      case Texture::F_rgba32:
#ifndef OPENGLES_1
        if (get_supports_compressed_texture_format(Texture::CM_dxt5) && !is_3d) {
          return GL_COMPRESSED_RGBA_S3TC_DXT5_EXT;
        }
#endif
        if (get_supports_compressed_texture_format(Texture::CM_etc2) && !is_3d) {
          return GL_COMPRESSED_RGBA8_ETC2_EAC;
        }
#ifndef OPENGLES
        if (get_supports_compressed_texture_format(Texture::CM_fxt1) && !is_3d) {
          return GL_COMPRESSED_RGBA_FXT1_3DFX;
        }
        return GL_COMPRESSED_RGBA;
#endif
        break;

      case Texture::F_rgb:
      case Texture::F_rgb5:
      case Texture::F_rgb8:
      case Texture::F_rgb12:
      case Texture::F_rgb332:
      case Texture::F_rgb16:
      case Texture::F_rgb32:
        if (get_supports_compressed_texture_format(Texture::CM_dxt1) && !is_3d) {
          return GL_COMPRESSED_RGB_S3TC_DXT1_EXT;
        }
        if (get_supports_compressed_texture_format(Texture::CM_etc2) && !is_3d) {
          return GL_COMPRESSED_RGB8_ETC2;
        }
#ifdef OPENGLES
        if (get_supports_compressed_texture_format(Texture::CM_etc1) && !is_3d) {
          return GL_ETC1_RGB8_OES;
        }
#else
        if (get_supports_compressed_texture_format(Texture::CM_fxt1) && !is_3d) {
          return GL_COMPRESSED_RGB_FXT1_3DFX;
        }
        return GL_COMPRESSED_RGB;
#endif
        break;

      case Texture::F_alpha:
#ifndef OPENGLES_1
        if (get_supports_compressed_texture_format(Texture::CM_dxt5) && !is_3d) {
          return GL_COMPRESSED_RGBA_S3TC_DXT5_EXT;
        }
#endif
#ifndef OPENGLES
        if (get_supports_compressed_texture_format(Texture::CM_fxt1) && !is_3d) {
          return GL_COMPRESSED_RGBA_FXT1_3DFX;
        }
        return GL_COMPRESSED_ALPHA;
#endif
        break;

      case Texture::F_red:
      case Texture::F_green:
      case Texture::F_blue:
      case Texture::F_r16:
      case Texture::F_r32:
#ifndef OPENGLES
        if (get_supports_compressed_texture_format(Texture::CM_rgtc) && !is_3d) {
          return GL_COMPRESSED_RED_RGTC1;
        }
#endif
        if (get_supports_compressed_texture_format(Texture::CM_eac) && !is_3d) {
          if (Texture::is_unsigned(component_type)) {
            return GL_COMPRESSED_R11_EAC;
          } else {
            return GL_COMPRESSED_SIGNED_R11_EAC;
          }
        }
        if (get_supports_compressed_texture_format(Texture::CM_dxt1) && !is_3d) {
          return GL_COMPRESSED_RGB_S3TC_DXT1_EXT;
        }
#ifndef OPENGLES
        if (get_supports_compressed_texture_format(Texture::CM_fxt1) && !is_3d) {
          return GL_COMPRESSED_RGB_FXT1_3DFX;
        }
        return GL_COMPRESSED_RED;
#endif
        break;

      case Texture::F_rg:
      case Texture::F_rg16:
      case Texture::F_rg32:
#ifndef OPENGLES
        if (get_supports_compressed_texture_format(Texture::CM_rgtc) && !is_3d) {
          return GL_COMPRESSED_RG_RGTC2;
        }
#endif
        if (get_supports_compressed_texture_format(Texture::CM_eac) && !is_3d) {
          if (Texture::is_unsigned(component_type)) {
            return GL_COMPRESSED_RG11_EAC;
          } else {
            return GL_COMPRESSED_SIGNED_RG11_EAC;
          }
        }
        if (get_supports_compressed_texture_format(Texture::CM_dxt1) && !is_3d) {
          return GL_COMPRESSED_RGB_S3TC_DXT1_EXT;
        }
#ifndef OPENGLES
        if (get_supports_compressed_texture_format(Texture::CM_fxt1) && !is_3d) {
          return GL_COMPRESSED_RGB_FXT1_3DFX;
        }
        return GL_COMPRESSED_RG;
#endif
        break;

      case Texture::F_luminance:
        if (get_supports_compressed_texture_format(Texture::CM_dxt1) && !is_3d) {
          return GL_COMPRESSED_RGB_S3TC_DXT1_EXT;
        }
#ifndef OPENGLES
        if (get_supports_compressed_texture_format(Texture::CM_fxt1) && !is_3d) {
          return GL_COMPRESSED_RGB_FXT1_3DFX;
        }
        return GL_COMPRESSED_LUMINANCE;
#endif
        break;

      case Texture::F_luminance_alpha:
      case Texture::F_luminance_alphamask:
#ifndef OPENGLES_1
        if (get_supports_compressed_texture_format(Texture::CM_dxt5) && !is_3d) {
          return GL_COMPRESSED_RGBA_S3TC_DXT5_EXT;
        }
#endif
#ifndef OPENGLES
        if (get_supports_compressed_texture_format(Texture::CM_fxt1) && !is_3d) {
          return GL_COMPRESSED_RGBA_FXT1_3DFX;
        }
        return GL_COMPRESSED_LUMINANCE_ALPHA;
#endif
        break;

#ifndef OPENGLES
      case Texture::F_srgb:
        if (get_supports_compressed_texture_format(Texture::CM_dxt1) && !is_3d) {
          return GL_COMPRESSED_SRGB_S3TC_DXT1_EXT;
        }
        if (get_supports_compressed_texture_format(Texture::CM_etc2) && !is_3d) {
          return GL_COMPRESSED_SRGB8_ETC2;
        }
        return GL_COMPRESSED_SRGB;

      case Texture::F_srgb_alpha:
        if (get_supports_compressed_texture_format(Texture::CM_dxt5) && !is_3d) {
          return GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT;
        }
        if (get_supports_compressed_texture_format(Texture::CM_etc2) && !is_3d) {
          return GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC;
        }
        return GL_COMPRESSED_SRGB_ALPHA;

      case Texture::F_sluminance:
        return GL_COMPRESSED_SLUMINANCE;

      case Texture::F_sluminance_alpha:
        return GL_COMPRESSED_SLUMINANCE_ALPHA;
#else
      // For now, we don't support compressed sRGB textures in OpenGL ES.
      case Texture::F_srgb:
      case Texture::F_srgb_alpha:
      case Texture::F_sluminance:
      case Texture::F_sluminance_alpha:
        break;
#endif
      }
      break;

    case Texture::CM_dxt1:
#ifndef OPENGLES
      if (format == Texture::F_srgb_alpha) {
        return GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT;
      } else if (format == Texture::F_srgb) {
        return GL_COMPRESSED_SRGB_S3TC_DXT1_EXT;
      } else
#endif
      if (Texture::has_alpha(format)) {
        return GL_COMPRESSED_RGBA_S3TC_DXT1_EXT;
      } else {
        return GL_COMPRESSED_RGB_S3TC_DXT1_EXT;
      }

    case Texture::CM_dxt3:
#ifndef OPENGLES
      if (format == Texture::F_srgb || format == Texture::F_srgb_alpha) {
        return GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT;
      }
#endif
#ifndef OPENGLES_1
      return GL_COMPRESSED_RGBA_S3TC_DXT3_EXT;
#endif
      break;

    case Texture::CM_dxt5:
#ifndef OPENGLES
      if (format == Texture::F_srgb || format == Texture::F_srgb_alpha) {
        return GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT;
      }
#endif
#ifndef OPENGLES_1
      return GL_COMPRESSED_RGBA_S3TC_DXT5_EXT;
#endif

    case Texture::CM_fxt1:
#ifndef OPENGLES
      if (Texture::has_alpha(format)) {
        return GL_COMPRESSED_RGBA_FXT1_3DFX;
      } else {
        return GL_COMPRESSED_RGB_FXT1_3DFX;
      }
#endif
      break;

#ifdef OPENGLES
    case Texture::CM_pvr1_2bpp:
      if (Texture::has_alpha(format)) {
        return GL_COMPRESSED_RGBA_PVRTC_2BPPV1_IMG;
      } else {
        return GL_COMPRESSED_RGB_PVRTC_2BPPV1_IMG;
      }

    case Texture::CM_pvr1_4bpp:
      if (Texture::has_alpha(format)) {
        return GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG;
      } else {
        return GL_COMPRESSED_RGB_PVRTC_4BPPV1_IMG;
      }
#else
    case Texture::CM_pvr1_2bpp:
    case Texture::CM_pvr1_4bpp:
      break;
#endif

    case Texture::CM_rgtc:
#ifndef OPENGLES
      if (format == Texture::F_luminance) {
        return GL_COMPRESSED_LUMINANCE_LATC1_EXT;
      } else if (format == Texture::F_luminance_alpha) {
        return GL_COMPRESSED_LUMINANCE_ALPHA_LATC2_EXT;
      } else if (tex->get_num_components() == 1) {
        return GL_COMPRESSED_RED_RGTC1;
      } else if (tex->get_num_components() == 2) {
        return GL_COMPRESSED_RG_RGTC2;
      }
#endif
      break;

    case Texture::CM_etc1:
#ifdef OPENGLES
      return GL_ETC1_RGB8_OES;
#endif
      // Fall through - ETC2 is backward compatible
    case Texture::CM_etc2:
      if (format == Texture::F_rgbm) {
        return GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2;
      } else if (format == Texture::F_srgb_alpha) {
        return GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC;
      } else if (format == Texture::F_srgb) {
        return GL_COMPRESSED_SRGB8_ETC2;
      } else if (Texture::has_alpha(format)) {
        return GL_COMPRESSED_RGBA8_ETC2_EAC;
      } else {
        return GL_COMPRESSED_RGB8_ETC2;
      }
      break;

    case Texture::CM_eac:
      if (Texture::is_unsigned(component_type)) {
        if (tex->get_num_components() == 1) {
          return GL_COMPRESSED_R11_EAC;
        } else {
          return GL_COMPRESSED_RG11_EAC;
        }
      } else {
        if (tex->get_num_components() == 1) {
          return GL_COMPRESSED_SIGNED_R11_EAC;
        } else {
          return GL_COMPRESSED_SIGNED_RG11_EAC;
        }
      }
      break;

    case Texture::CM_default:
    case Texture::CM_off:
    case Texture::CM_dxt2:
    case Texture::CM_dxt4:
      // No compression: fall through to below.
      break;
    }
  }

#if defined(__EMSCRIPTEN__) && defined(OPENGLES)
  // WebGL 1 has no sized formats, it would seem.
  if (!is_at_least_gles_version(3, 0)) {
    return get_external_image_format(tex);
  }
#endif

  switch (format) {
#ifndef OPENGLES
  case Texture::F_color_index:
    return GL_COLOR_INDEX;
#endif

  case Texture::F_depth_stencil:
    if (_supports_depth_stencil) {
#ifndef OPENGLES
      if (component_type == Texture::T_float) {
        return GL_DEPTH32F_STENCIL8;
      } else
#endif
      {
        return force_sized ? GL_DEPTH24_STENCIL8 : GL_DEPTH_STENCIL;
      }
    }
    // Fall through.

  case Texture::F_depth_component:
#ifndef OPENGLES
    if (component_type == Texture::T_float) {
      return GL_DEPTH_COMPONENT32F;
    } else
#endif
    {
      return force_sized ? GL_DEPTH_COMPONENT16 : GL_DEPTH_COMPONENT;
    }
  case Texture::F_depth_component16:
#ifdef OPENGLES
    return GL_DEPTH_COMPONENT16_OES;
#else
    return GL_DEPTH_COMPONENT16;
#endif

  case Texture::F_depth_component24:
#ifdef OPENGLES
    if (_supports_depth24) {
      return GL_DEPTH_COMPONENT24_OES;
    } else {
      return GL_DEPTH_COMPONENT16_OES;
    }
#else
    return GL_DEPTH_COMPONENT24;
#endif

  case Texture::F_depth_component32:
#ifdef OPENGLES
    if (_supports_depth32) {
      return GL_DEPTH_COMPONENT32_OES;
    } else if (_supports_depth24) {
      return GL_DEPTH_COMPONENT24_OES;
    } else {
      return GL_DEPTH_COMPONENT16_OES;
    }
#else
    if (component_type == Texture::T_float) {
      return GL_DEPTH_COMPONENT32F;
    } else {
      return GL_DEPTH_COMPONENT32;
    }
#endif

  case Texture::F_rgba:
  case Texture::F_rgbm:
#ifndef OPENGLES_1
    if (component_type == Texture::T_float ||
        component_type == Texture::T_half_float) {
      return GL_RGBA16F;
    } else
#endif
#ifdef OPENGLES
    {
      // In OpenGL ES, the internal format must match the external format.
      return _supports_bgr ? GL_BGRA : GL_RGBA;
    }
#else
    if (component_type == Texture::T_unsigned_short) {
      return GL_RGBA16;
    } else if (component_type == Texture::T_short) {
      return GL_RGBA16_SNORM;
    } else if (component_type == Texture::T_byte) {
      return GL_RGBA8_SNORM;
    } else {
      return force_sized ? GL_RGBA8 : GL_RGBA;
    }
#endif

  case Texture::F_rgba4:
    return GL_RGBA4;

#ifdef OPENGLES
  case Texture::F_rgba8:
    return _supports_bgr ? GL_BGRA : GL_RGBA8_OES;
  case Texture::F_rgba12:
    return _supports_bgr ? GL_BGRA : (force_sized ? GL_RGBA8 : GL_RGBA);
#else
  case Texture::F_rgba8:
    if (Texture::is_unsigned(component_type)) {
      return GL_RGBA8;
    } else {
      return GL_RGBA8_SNORM;
    }

  case Texture::F_r8i:
    if (Texture::is_unsigned(component_type)) {
      return GL_R8UI;
    } else {
      return GL_R8I;
    }
  case Texture::F_rg8i:
    if (Texture::is_unsigned(component_type)) {
      return GL_RG8UI;
    } else {
      return GL_RG8I;
    }
  case Texture::F_rgb8i:
    if (Texture::is_unsigned(component_type)) {
      return GL_RGB8UI;
    } else {
      return GL_RGB8I;
    }
  case Texture::F_rgba8i:
    if (Texture::is_unsigned(component_type)) {
      return GL_RGBA8UI;
    } else {
      return GL_RGBA8I;
    }
  case Texture::F_rgba12:
    return GL_RGBA12;
#endif  // OPENGLES
#ifndef OPENGLES
  case Texture::F_rgba16:
    if (component_type == Texture::T_float ||
        component_type == Texture::T_half_float) {
      return GL_RGBA16F;
    } else if (Texture::is_unsigned(component_type)) {
      return GL_RGBA16;
    } else {
      return GL_RGBA16_SNORM;
    }
#elif !defined(OPENGLES_1)
  case Texture::F_rgba16:
    return GL_RGBA16F;
#endif  // OPENGLES

#ifndef OPENGLES_1
  case Texture::F_rgba32:
    return GL_RGBA32F;
#endif

  case Texture::F_rgb:
    switch (component_type) {
    case Texture::T_float: return GL_RGB16F;
    case Texture::T_half_float: return GL_RGB16F;
#ifndef OPENGLES
    case Texture::T_unsigned_short: return GL_RGB16;
    case Texture::T_short: return GL_RGB16_SNORM;
    case Texture::T_byte: return GL_RGB8_SNORM;
#endif
    default:
      return force_sized ? GL_RGB8 : GL_RGB;
    }

  case Texture::F_rgb5:
#ifdef OPENGLES
    // Close enough.
    return GL_RGB565_OES;
#else
    return GL_RGB5;
#endif
  case Texture::F_rgba5:
    return GL_RGB5_A1;

#ifdef OPENGLES
  case Texture::F_rgb8:
    return GL_RGB8_OES;
  case Texture::F_rgb12:
    return force_sized ? GL_RGB8 : GL_RGB;
  case Texture::F_rgb16:
    return GL_RGB16F;
#else
  case Texture::F_rgb8:
    if (Texture::is_unsigned(component_type)) {
      return GL_RGB8;
    } else {
      return GL_RGB8_SNORM;
    }
  case Texture::F_rgb12:
    return GL_RGB12;
  case Texture::F_rgb16:
    if (component_type == Texture::T_float ||
        component_type == Texture::T_half_float) {
      return GL_RGB16F;
    } else if (Texture::is_unsigned(component_type)) {
      return GL_RGB16;
    } else {
      return GL_RGB16_SNORM;
    }
#endif  // OPENGLES
  case Texture::F_rgb32:
    return GL_RGB32F;

#ifndef OPENGLES
  case Texture::F_rgb332:
    return GL_R3_G3_B2;
#endif

#if defined(OPENGLES_2)
  case Texture::F_r16:
    return GL_R16F_EXT;
  case Texture::F_rg16:
    return GL_RG16F_EXT;
#elif !defined(OPENGLES_1)
  case Texture::F_r16:
    if (component_type == Texture::T_float ||
        component_type == Texture::T_half_float) {
      return GL_R16F;
    } else if (Texture::is_unsigned(component_type)) {
      return GL_R16;
    } else {
      return GL_R16_SNORM;
    }
  case Texture::F_rg16:
    if (component_type == Texture::T_float ||
        component_type == Texture::T_half_float) {
      return GL_RG16F;
    } else if (Texture::is_unsigned(component_type)) {
      return GL_RG16;
    } else {
      return GL_RG16_SNORM;
    }
  case Texture::F_r16i:
    if (Texture::is_unsigned(component_type)) {
      return GL_R16UI;
    } else {
      return GL_R16I;
    }
  case Texture::F_rg16i:
    if (Texture::is_unsigned(component_type)) {
      return GL_RG16UI;
    } else {
      return GL_RG16I;
    }
  case Texture::F_rgb16i:
    if (Texture::is_unsigned(component_type)) {
      return GL_RGB16UI;
    } else {
      return GL_RGB16I;
    }
  case Texture::F_rgba16i:
    if (Texture::is_unsigned(component_type)) {
      return GL_RGBA16UI;
    } else {
      return GL_RGBA16I;
    }
#endif

#ifndef OPENGLES_1
  case Texture::F_r32:
    return GL_R32F;
  case Texture::F_rg32:
    return GL_RG32F;

  case Texture::F_red:
  case Texture::F_green:
  case Texture::F_blue:
#ifndef OPENGLES
    if (!Texture::is_unsigned(component_type)) {
      return GL_R8_SNORM;
    } else
#endif
    {
      return force_sized ? GL_R8 : GL_RED;
    }
#endif

  case Texture::F_alpha:
#ifdef OPENGLES
    return force_sized ? GL_ALPHA8 : GL_ALPHA;
#else
    if (_supports_luminance_texture) {
      return force_sized ? GL_ALPHA8 : GL_ALPHA;
    } else {
      return force_sized ? GL_R8 : GL_RED;
    }
#endif

  case Texture::F_luminance:
#ifdef OPENGLES
    return force_sized ? GL_LUMINANCE8 : GL_LUMINANCE;
#else
    if (_supports_luminance_texture) {
      switch (component_type) {
      case Texture::T_float:
      case Texture::T_half_float:
        return GL_LUMINANCE16F_ARB;
      case Texture::T_short:
        return GL_LUMINANCE16_SNORM;
      case Texture::T_unsigned_short:
        return GL_LUMINANCE16;
      default:
        return force_sized ? GL_LUMINANCE8 : GL_LUMINANCE;
      }
    } else {
      switch (component_type) {
      case Texture::T_float:
      case Texture::T_half_float:
        return GL_R16F;
      case Texture::T_short:
        return GL_R16_SNORM;
      case Texture::T_unsigned_short:
        return GL_R16;
      default:
        return force_sized ? GL_R8 : GL_RED;
      }
    }
#endif
  case Texture::F_luminance_alpha:
  case Texture::F_luminance_alphamask:
#ifdef OPENGLES
    return force_sized ? GL_LUMINANCE8_ALPHA8 : GL_LUMINANCE_ALPHA;
#else
    if (_supports_luminance_texture) {
      switch (component_type) {
      case Texture::T_float:
      case Texture::T_half_float:
        return GL_LUMINANCE_ALPHA16F_ARB;
      case Texture::T_short:
        return GL_LUMINANCE16_SNORM;
      case Texture::T_unsigned_short:
        return GL_LUMINANCE16_ALPHA16;
      default:
        return force_sized ? GL_LUMINANCE8_ALPHA8 : GL_LUMINANCE_ALPHA;
      }
    } else {
      switch (component_type) {
      case Texture::T_float:
      case Texture::T_half_float:
        return GL_RG16F;
      case Texture::T_short:
        return GL_RG16_SNORM;
      case Texture::T_unsigned_short:
        return GL_RG16;
      default:
        return force_sized ? GL_RG8 : GL_RG;
      }
    }
#endif

#ifndef OPENGLES_1
  case Texture::F_rg:
    return force_sized ? GL_RG8 : GL_RG;
#endif

#ifndef OPENGLES_1
  case Texture::F_srgb:
    return _supports_texture_srgb ? GL_SRGB8 : GL_RGB8;
  case Texture::F_srgb_alpha:
    return _supports_texture_srgb ? GL_SRGB8_ALPHA8 : GL_RGBA8;
#endif
#ifndef OPENGLES
  case Texture::F_sluminance:
    return _core_profile ? GL_SRGB8 : GL_SLUMINANCE8;
  case Texture::F_sluminance_alpha:
    return _core_profile ? GL_SRGB8_ALPHA8 : GL_SLUMINANCE8_ALPHA8;
#endif

#ifndef OPENGLES_1
  case Texture::F_r32i:
    if (Texture::is_unsigned(component_type)) {
      return GL_R32UI;
    } else {
      return GL_R32I;
    }
  case Texture::F_rg32i:
    if (Texture::is_unsigned(component_type)) {
      return GL_RG32UI;
    } else {
      return GL_RG32I;
    }
  case Texture::F_rgb32i:
    if (Texture::is_unsigned(component_type)) {
      return GL_RGB32UI;
    } else {
      return GL_RGB32I;
    }
  case Texture::F_rgba32i:
    if (Texture::is_unsigned(component_type)) {
      return GL_RGBA32UI;
    } else {
      return GL_RGBA32I;
    }
#endif

#ifndef OPENGLES_1
  case Texture::F_r11_g11_b10:
    return GL_R11F_G11F_B10F;

  case Texture::F_rgb9_e5:
    return GL_RGB9_E5;

  case Texture::F_rgb10_a2:
    return GL_RGB10_A2;
#endif

  default:
    GLCAT.error()
      << "Invalid image format in get_internal_image_format(): "
      << (int)format << "\n";
    return force_sized ? GL_RGB8 : GL_RGB;
  }
}

/**
 * Returns true if the indicated GL minfilter type represents a mipmap format,
 * false otherwise.
 */
bool CLP(GraphicsStateGuardian)::
is_mipmap_filter(GLenum min_filter) {
  switch (min_filter) {
  case GL_NEAREST_MIPMAP_NEAREST:
  case GL_LINEAR_MIPMAP_NEAREST:
  case GL_NEAREST_MIPMAP_LINEAR:
  case GL_LINEAR_MIPMAP_LINEAR:
    return true;

  default:
    return false;
  }
}

/**
 * Returns true if the indicated GL internal format represents a compressed
 * texture format, false otherwise.
 */
bool CLP(GraphicsStateGuardian)::
is_compressed_format(GLenum format) {
  switch (format) {
  case GL_COMPRESSED_RGB_S3TC_DXT1_EXT:
  case GL_COMPRESSED_RGBA_S3TC_DXT1_EXT:
#ifdef OPENGLES
  case GL_COMPRESSED_RGB_PVRTC_4BPPV1_IMG:
  case GL_COMPRESSED_RGB_PVRTC_2BPPV1_IMG:
  case GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG:
  case GL_COMPRESSED_RGBA_PVRTC_2BPPV1_IMG:
#else
  case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT:
  case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT:
  case GL_COMPRESSED_RGB_FXT1_3DFX:
  case GL_COMPRESSED_RGBA_FXT1_3DFX:

  case GL_COMPRESSED_RED_RGTC1:
  case GL_COMPRESSED_SIGNED_RED_RGTC1:
  case GL_COMPRESSED_RG_RGTC2:
  case GL_COMPRESSED_SIGNED_RG_RGTC2:
  case GL_COMPRESSED_LUMINANCE_LATC1_EXT:
  case GL_COMPRESSED_SIGNED_LUMINANCE_LATC1_EXT:
  case GL_COMPRESSED_LUMINANCE_ALPHA_LATC2_EXT:
  case GL_COMPRESSED_SIGNED_LUMINANCE_ALPHA_LATC2_EXT:

  case GL_COMPRESSED_RGB:
  case GL_COMPRESSED_SRGB_EXT:
  case GL_COMPRESSED_RGBA:
  case GL_COMPRESSED_SRGB_ALPHA_EXT:
  case GL_COMPRESSED_ALPHA:
  case GL_COMPRESSED_LUMINANCE:
  case GL_COMPRESSED_LUMINANCE_ALPHA:
#endif
    return true;

  default:
    return false;
  }
}

/**
 * Maps from the texture stage's mode types to the corresponding OpenGL ids
 */
GLint CLP(GraphicsStateGuardian)::
get_texture_apply_mode_type(TextureStage::Mode am) {
#ifdef SUPPORT_FIXED_FUNCTION
  switch (am) {
  case TextureStage::M_modulate: return GL_MODULATE;
  case TextureStage::M_decal: return GL_DECAL;
  case TextureStage::M_blend: return GL_BLEND;
  case TextureStage::M_replace: return GL_REPLACE;
  case TextureStage::M_add: return GL_ADD;
  case TextureStage::M_combine: return GL_COMBINE;
  case TextureStage::M_blend_color_scale: return GL_BLEND;
  case TextureStage::M_modulate_glow: return GL_MODULATE;
  case TextureStage::M_modulate_gloss: return GL_MODULATE;
  default:
    // Other modes shouldn't get here.  Fall through and error.
    break;
  }

  GLCAT.error()
    << "Invalid TextureStage::Mode value" << endl;
  return GL_MODULATE;
#else
  return 0;
#endif
}

/**
 * Maps from the texture stage's CombineMode types to the corresponding OpenGL
 * ids
 */
GLint CLP(GraphicsStateGuardian)::
get_texture_combine_type(TextureStage::CombineMode cm) {
#ifdef SUPPORT_FIXED_FUNCTION
  switch (cm) {
  case TextureStage::CM_undefined: // fall through
  case TextureStage::CM_replace: return GL_REPLACE;
  case TextureStage::CM_modulate: return GL_MODULATE;
  case TextureStage::CM_add: return GL_ADD;
  case TextureStage::CM_add_signed: return GL_ADD_SIGNED;
  case TextureStage::CM_interpolate: return GL_INTERPOLATE;
  case TextureStage::CM_subtract: return GL_SUBTRACT;
  case TextureStage::CM_dot3_rgb: return GL_DOT3_RGB;
  case TextureStage::CM_dot3_rgba: return GL_DOT3_RGBA;
  }
  GLCAT.error()
    << "Invalid TextureStage::CombineMode value" << endl;
#endif
  return GL_REPLACE;
}

/**
 * Maps from the texture stage's CombineSource types to the corresponding
 * OpenGL ids
 */
GLint CLP(GraphicsStateGuardian)::
get_texture_src_type(TextureStage::CombineSource cs,
                     int last_stage, int last_saved_result,
                     int this_stage) const {
#ifdef SUPPORT_FIXED_FUNCTION
  switch (cs) {
  case TextureStage::CS_undefined: // fall through
  case TextureStage::CS_texture: return GL_TEXTURE;
  case TextureStage::CS_constant: return GL_CONSTANT;
  case TextureStage::CS_primary_color: return GL_PRIMARY_COLOR;
  case TextureStage::CS_constant_color_scale: return GL_CONSTANT;

  case TextureStage::CS_previous:
    if (last_stage == this_stage - 1) {
      return GL_PREVIOUS;
    } else if (last_stage == -1) {
      return GL_PRIMARY_COLOR;
    } else if (_supports_texture_saved_result) {
      return GL_TEXTURE0 + last_stage;
    } else {
      GLCAT.warning()
        << "Current OpenGL driver does not support texture crossbar blending.\n";
      return GL_PRIMARY_COLOR;
    }

  case TextureStage::CS_last_saved_result:
    if (last_saved_result == this_stage - 1) {
      return GL_PREVIOUS;
    } else if (last_saved_result == -1) {
      return GL_PRIMARY_COLOR;
    } else if (_supports_texture_saved_result) {
      return GL_TEXTURE0 + last_saved_result;
    } else {
      GLCAT.warning()
        << "Current OpenGL driver does not support texture crossbar blending.\n";
      return GL_PRIMARY_COLOR;
    }
  }

  GLCAT.error()
    << "Invalid TextureStage::CombineSource value" << endl;
#endif
  return GL_TEXTURE;
}

/**
 * Maps from the texture stage's CombineOperand types to the corresponding
 * OpenGL ids
 */
GLint CLP(GraphicsStateGuardian)::
get_texture_operand_type(TextureStage::CombineOperand co) {
  switch (co) {
  case TextureStage::CO_undefined: // fall through
  case TextureStage::CO_src_alpha: return GL_SRC_ALPHA;
  case TextureStage::CO_one_minus_src_alpha: return GL_ONE_MINUS_SRC_ALPHA;
  case TextureStage::CO_src_color: return GL_SRC_COLOR;
  case TextureStage::CO_one_minus_src_color: return GL_ONE_MINUS_SRC_COLOR;
  }

  GLCAT.error()
    << "Invalid TextureStage::CombineOperand value" << endl;
  return GL_SRC_COLOR;
}

#ifdef SUPPORT_FIXED_FUNCTION
/**
 * Maps from the fog types to gl version
 */
GLenum CLP(GraphicsStateGuardian)::
get_fog_mode_type(Fog::Mode m) {
  switch(m) {
  case Fog::M_linear: return GL_LINEAR;
  case Fog::M_exponential: return GL_EXP;
  case Fog::M_exponential_squared: return GL_EXP2;
    /*
      case Fog::M_spline: return GL_FOG_FUNC_SGIS;
    */

  default:
    GLCAT.error() << "Invalid Fog::Mode value" << endl;
    return GL_EXP;
  }
}
#endif

/**
 * Maps from ColorBlendAttrib::Mode to glBlendEquation value.
 */
GLenum CLP(GraphicsStateGuardian)::
get_blend_equation_type(ColorBlendAttrib::Mode mode) {
  switch (mode) {
  case ColorBlendAttrib::M_none:
  case ColorBlendAttrib::M_add:
    return GL_FUNC_ADD;

  case ColorBlendAttrib::M_subtract:
    return GL_FUNC_SUBTRACT;

  case ColorBlendAttrib::M_inv_subtract:
    return GL_FUNC_REVERSE_SUBTRACT;

#ifdef OPENGLES
  case ColorBlendAttrib::M_min:
    return GL_MIN_EXT;

  case ColorBlendAttrib::M_max:
    return GL_MAX_EXT;
#else
  case ColorBlendAttrib::M_min:
    return GL_MIN;

  case ColorBlendAttrib::M_max:
    return GL_MAX;
#endif
  }

  GLCAT.error()
    << "Unknown color blend mode " << (int)mode << endl;
  return GL_FUNC_ADD;
}

/**
 * Maps from ColorBlendAttrib::Operand to glBlendFunc value.
 */
GLenum CLP(GraphicsStateGuardian)::
get_blend_func(ColorBlendAttrib::Operand operand) {
  switch (operand) {
  case ColorBlendAttrib::O_zero:
    return GL_ZERO;

  case ColorBlendAttrib::O_one:
    return GL_ONE;

  case ColorBlendAttrib::O_incoming_color:
    return GL_SRC_COLOR;

  case ColorBlendAttrib::O_one_minus_incoming_color:
    return GL_ONE_MINUS_SRC_COLOR;

  case ColorBlendAttrib::O_fbuffer_color:
    return GL_DST_COLOR;

  case ColorBlendAttrib::O_one_minus_fbuffer_color:
    return GL_ONE_MINUS_DST_COLOR;

  case ColorBlendAttrib::O_incoming_alpha:
    return GL_SRC_ALPHA;

  case ColorBlendAttrib::O_one_minus_incoming_alpha:
    return GL_ONE_MINUS_SRC_ALPHA;

  case ColorBlendAttrib::O_fbuffer_alpha:
    return GL_DST_ALPHA;

  case ColorBlendAttrib::O_one_minus_fbuffer_alpha:
    return GL_ONE_MINUS_DST_ALPHA;

#ifdef OPENGLES_1
  // OpenGL ES 1 has no constant blend factor.
  case ColorBlendAttrib::O_constant_color:
  case ColorBlendAttrib::O_color_scale:
  case ColorBlendAttrib::O_one_minus_constant_color:
  case ColorBlendAttrib::O_one_minus_color_scale:
  case ColorBlendAttrib::O_constant_alpha:
  case ColorBlendAttrib::O_alpha_scale:
  case ColorBlendAttrib::O_one_minus_constant_alpha:
  case ColorBlendAttrib::O_one_minus_alpha_scale:
    break;

  // No dual-source blending, either.
  case ColorBlendAttrib::O_incoming1_color:
  case ColorBlendAttrib::O_one_minus_incoming1_color:
  case ColorBlendAttrib::O_incoming1_alpha:
  case ColorBlendAttrib::O_one_minus_incoming1_alpha:
    break;
#else
  case ColorBlendAttrib::O_constant_color:
  case ColorBlendAttrib::O_color_scale:
    return GL_CONSTANT_COLOR;

  case ColorBlendAttrib::O_one_minus_constant_color:
  case ColorBlendAttrib::O_one_minus_color_scale:
    return GL_ONE_MINUS_CONSTANT_COLOR;

  case ColorBlendAttrib::O_constant_alpha:
  case ColorBlendAttrib::O_alpha_scale:
    return GL_CONSTANT_ALPHA;

  case ColorBlendAttrib::O_one_minus_constant_alpha:
  case ColorBlendAttrib::O_one_minus_alpha_scale:
    return GL_ONE_MINUS_CONSTANT_ALPHA;

  case ColorBlendAttrib::O_incoming1_color:
    return GL_SRC1_COLOR;

  case ColorBlendAttrib::O_one_minus_incoming1_color:
    return GL_ONE_MINUS_SRC1_COLOR;

  case ColorBlendAttrib::O_incoming1_alpha:
    return GL_SRC1_ALPHA;

  case ColorBlendAttrib::O_one_minus_incoming1_alpha:
    return GL_ONE_MINUS_SRC1_ALPHA;
#endif

  case ColorBlendAttrib::O_incoming_color_saturate:
    return GL_SRC_ALPHA_SATURATE;
  }

  GLCAT.error()
    << "Unknown color blend operand " << (int)operand << endl;
  return GL_ZERO;
}

/**
 * Maps from UsageHint to the GL symbol.
 */
GLenum CLP(GraphicsStateGuardian)::
get_usage(Geom::UsageHint usage_hint) {
  switch (usage_hint) {
  case Geom::UH_stream:
#ifdef OPENGLES_1
    return GL_DYNAMIC_DRAW;
#else
    return GL_STREAM_DRAW;
#endif  // OPENGLES

  case Geom::UH_static:
  case Geom::UH_unspecified:
    return GL_STATIC_DRAW;

  case Geom::UH_dynamic:
    return GL_DYNAMIC_DRAW;

  case Geom::UH_client:
    break;
  }

  GLCAT.error()
    << "Unexpected usage_hint " << (int)usage_hint << endl;
  return GL_STATIC_DRAW;
}

#ifndef NDEBUG
/**
 * Returns a string describing an compression format.
 */
const char *CLP(GraphicsStateGuardian)::
get_compressed_format_string(GLenum format) {
  switch (format) {
  case 0x83F0: return "GL_COMPRESSED_RGB_S3TC_DXT1_EXT";
  case 0x83F1: return "GL_COMPRESSED_RGBA_S3TC_DXT1_EXT";
  case 0x83F2: return "GL_COMPRESSED_RGBA_S3TC_DXT3_EXT";
  case 0x83F3: return "GL_COMPRESSED_RGBA_S3TC_DXT5_EXT";
  case 0x86B0: return "GL_COMPRESSED_RGB_FXT1_3DFX";
  case 0x86B1: return "GL_COMPRESSED_RGBA_FXT1_3DFX";
  case 0x88EE: return "GL_ETC1_SRGB8_NV";
  case 0x8A54: return "GL_COMPRESSED_SRGB_PVRTC_2BPPV1_EXT";
  case 0x8A55: return "GL_COMPRESSED_SRGB_PVRTC_4BPPV1_EXT";
  case 0x8A56: return "GL_COMPRESSED_SRGB_ALPHA_PVRTC_2BPPV1_EXT";
  case 0x8A57: return "GL_COMPRESSED_SRGB_ALPHA_PVRTC_4BPPV1_EXT";
  case 0x8B90: return "GL_PALETTE4_RGB8_OES";
  case 0x8B91: return "GL_PALETTE4_RGBA8_OES";
  case 0x8B92: return "GL_PALETTE4_R5_G6_B5_OES";
  case 0x8B93: return "GL_PALETTE4_RGBA4_OES";
  case 0x8B94: return "GL_PALETTE4_RGB5_A1_OES";
  case 0x8B95: return "GL_PALETTE8_RGB8_OES";
  case 0x8B96: return "GL_PALETTE8_RGBA8_OES";
  case 0x8B97: return "GL_PALETTE8_R5_G6_B5_OES";
  case 0x8B98: return "GL_PALETTE8_RGBA4_OES";
  case 0x8B99: return "GL_PALETTE8_RGB5_A1_OES";
  case 0x8C00: return "GL_COMPRESSED_RGB_PVRTC_4BPPV1_IMG";
  case 0x8C01: return "GL_COMPRESSED_RGB_PVRTC_2BPPV1_IMG";
  case 0x8C02: return "GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG";
  case 0x8C03: return "GL_COMPRESSED_RGBA_PVRTC_2BPPV1_IMG";
  case 0x8C48: return "GL_COMPRESSED_SRGB_EXT";
  case 0x8C49: return "GL_COMPRESSED_SRGB_ALPHA_EXT";
  case 0x8C4A: return "GL_COMPRESSED_SLUMINANCE_EXT";
  case 0x8C4B: return "GL_COMPRESSED_SLUMINANCE_ALPHA_EXT";
  case 0x8C4C: return "GL_COMPRESSED_SRGB_S3TC_DXT1_EXT";
  case 0x8C4D: return "GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT";
  case 0x8C4E: return "GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT";
  case 0x8C4F: return "GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT";
  case 0x8C70: return "GL_COMPRESSED_LUMINANCE_LATC1_EXT";
  case 0x8C71: return "GL_COMPRESSED_SIGNED_LUMINANCE_LATC1_EXT";
  case 0x8C72: return "GL_COMPRESSED_LUMINANCE_ALPHA_LATC2_EXT";
  case 0x8C73: return "GL_COMPRESSED_SIGNED_LUMINANCE_ALPHA_LATC2_EXT";
  case 0x8D64: return "GL_ETC1_RGB8_OES";
  case 0x8DBB: return "GL_COMPRESSED_RED_RGTC1";
  case 0x8DBC: return "GL_COMPRESSED_SIGNED_RED_RGTC1";
  case 0x8DBD: return "GL_COMPRESSED_RG_RGTC2";
  case 0x8DBE: return "GL_COMPRESSED_SIGNED_RG_RGTC2";
  case 0x8E8C: return "GL_COMPRESSED_RGBA_BPTC_UNORM";
  case 0x8E8D: return "GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM";
  case 0x8E8E: return "GL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT";
  case 0x8E8F: return "GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT";
  case 0x9137: return "GL_COMPRESSED_RGBA_PVRTC_2BPPV2_IMG";
  case 0x9138: return "GL_COMPRESSED_RGBA_PVRTC_4BPPV2_IMG";
  case 0x9270: return "GL_COMPRESSED_R11_EAC";
  case 0x9271: return "GL_COMPRESSED_SIGNED_R11_EAC";
  case 0x9272: return "GL_COMPRESSED_RG11_EAC";
  case 0x9273: return "GL_COMPRESSED_SIGNED_RG11_EAC";
  case 0x9274: return "GL_COMPRESSED_RGB8_ETC2";
  case 0x9275: return "GL_COMPRESSED_SRGB8_ETC2";
  case 0x9276: return "GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2";
  case 0x9277: return "GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2";
  case 0x9278: return "GL_COMPRESSED_RGBA8_ETC2_EAC";
  case 0x9279: return "GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC";
  case 0x93B0: return "GL_COMPRESSED_RGBA_ASTC_4x4_KHR";
  case 0x93B1: return "GL_COMPRESSED_RGBA_ASTC_5x4_KHR";
  case 0x93B2: return "GL_COMPRESSED_RGBA_ASTC_5x5_KHR";
  case 0x93B3: return "GL_COMPRESSED_RGBA_ASTC_6x5_KHR";
  case 0x93B4: return "GL_COMPRESSED_RGBA_ASTC_6x6_KHR";
  case 0x93B5: return "GL_COMPRESSED_RGBA_ASTC_8x5_KHR";
  case 0x93B6: return "GL_COMPRESSED_RGBA_ASTC_8x6_KHR";
  case 0x93B7: return "GL_COMPRESSED_RGBA_ASTC_8x8_KHR";
  case 0x93B8: return "GL_COMPRESSED_RGBA_ASTC_10x5_KHR";
  case 0x93B9: return "GL_COMPRESSED_RGBA_ASTC_10x6_KHR";
  case 0x93BA: return "GL_COMPRESSED_RGBA_ASTC_10x8_KHR";
  case 0x93BB: return "GL_COMPRESSED_RGBA_ASTC_10x10_KHR";
  case 0x93BC: return "GL_COMPRESSED_RGBA_ASTC_12x10_KHR";
  case 0x93BD: return "GL_COMPRESSED_RGBA_ASTC_12x12_KHR";
  case 0x93C0: return "GL_COMPRESSED_RGBA_ASTC_3x3x3_OES";
  case 0x93C1: return "GL_COMPRESSED_RGBA_ASTC_4x3x3_OES";
  case 0x93C2: return "GL_COMPRESSED_RGBA_ASTC_4x4x3_OES";
  case 0x93C3: return "GL_COMPRESSED_RGBA_ASTC_4x4x4_OES";
  case 0x93C4: return "GL_COMPRESSED_RGBA_ASTC_5x4x4_OES";
  case 0x93C5: return "GL_COMPRESSED_RGBA_ASTC_5x5x4_OES";
  case 0x93C6: return "GL_COMPRESSED_RGBA_ASTC_5x5x5_OES";
  case 0x93C7: return "GL_COMPRESSED_RGBA_ASTC_6x5x5_OES";
  case 0x93C8: return "GL_COMPRESSED_RGBA_ASTC_6x6x5_OES";
  case 0x93C9: return "GL_COMPRESSED_RGBA_ASTC_6x6x6_OES";
  case 0x93D0: return "GL_COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR";
  case 0x93D1: return "GL_COMPRESSED_SRGB8_ALPHA8_ASTC_5x4_KHR";
  case 0x93D2: return "GL_COMPRESSED_SRGB8_ALPHA8_ASTC_5x5_KHR";
  case 0x93D3: return "GL_COMPRESSED_SRGB8_ALPHA8_ASTC_6x5_KHR";
  case 0x93D4: return "GL_COMPRESSED_SRGB8_ALPHA8_ASTC_6x6_KHR";
  case 0x93D5: return "GL_COMPRESSED_SRGB8_ALPHA8_ASTC_8x5_KHR";
  case 0x93D6: return "GL_COMPRESSED_SRGB8_ALPHA8_ASTC_8x6_KHR";
  case 0x93D7: return "GL_COMPRESSED_SRGB8_ALPHA8_ASTC_8x8_KHR";
  case 0x93D8: return "GL_COMPRESSED_SRGB8_ALPHA8_ASTC_10x5_KHR";
  case 0x93D9: return "GL_COMPRESSED_SRGB8_ALPHA8_ASTC_10x6_KHR";
  case 0x93DA: return "GL_COMPRESSED_SRGB8_ALPHA8_ASTC_10x8_KHR";
  case 0x93DB: return "GL_COMPRESSED_SRGB8_ALPHA8_ASTC_10x10_KHR";
  case 0x93DC: return "GL_COMPRESSED_SRGB8_ALPHA8_ASTC_12x10_KHR";
  case 0x93DD: return "GL_COMPRESSED_SRGB8_ALPHA8_ASTC_12x12_KHR";
  case 0x93E0: return "GL_COMPRESSED_SRGB8_ALPHA8_ASTC_3x3x3_OES";
  case 0x93E1: return "GL_COMPRESSED_SRGB8_ALPHA8_ASTC_4x3x3_OES";
  case 0x93E2: return "GL_COMPRESSED_SRGB8_ALPHA8_ASTC_4x4x3_OES";
  case 0x93E3: return "GL_COMPRESSED_SRGB8_ALPHA8_ASTC_4x4x4_OES";
  case 0x93E4: return "GL_COMPRESSED_SRGB8_ALPHA8_ASTC_5x4x4_OES";
  case 0x93E5: return "GL_COMPRESSED_SRGB8_ALPHA8_ASTC_5x5x4_OES";
  case 0x93E6: return "GL_COMPRESSED_SRGB8_ALPHA8_ASTC_5x5x5_OES";
  case 0x93E7: return "GL_COMPRESSED_SRGB8_ALPHA8_ASTC_6x5x5_OES";
  case 0x93E8: return "GL_COMPRESSED_SRGB8_ALPHA8_ASTC_6x6x5_OES";
  case 0x93E9: return "GL_COMPRESSED_SRGB8_ALPHA8_ASTC_6x6x6_OES";
  case 0x93F0: return "GL_COMPRESSED_SRGB_ALPHA_PVRTC_2BPPV2_IMG";
  case 0x93F1: return "GL_COMPRESSED_SRGB_ALPHA_PVRTC_4BPPV2_IMG";
  default:
    return nullptr;
  }
}
#endif

/**
 * Returns the value that that should be issued as the light's color, as
 * scaled by the current value of _light_color_scale, in the case of
 * color_scale_via_lighting.
 */
LVecBase4 CLP(GraphicsStateGuardian)::
get_light_color(Light *light) const {
#ifndef NDEBUG
  if (_show_texture_usage) {
    // In show_texture_usage mode, all lights are white, so as not to
    // contaminate the texture color.
    return LVecBase4(1.0, 1.0, 1.0, 1.0);
  }
#endif  // NDEBUG

  const LColor &c = light->get_color();

  LVecBase4 light_color(c[0] * _light_color_scale[0],
                        c[1] * _light_color_scale[1],
                        c[2] * _light_color_scale[2],
                        c[3] * _light_color_scale[3]);
  return light_color;
}


/**
 * Called by clear_state_and_transform() to ensure that the current modelview
 * and projection matrices are properly loaded in the graphics state, after a
 * callback might have mucked them up.
 */
void CLP(GraphicsStateGuardian)::
reissue_transforms() {
  prepare_lens();
  do_issue_transform();

  _active_texture_stage = -1;

#ifndef OPENGLES_1
  // Might also want to reissue the vertex format, for good measure.
  _current_vertex_format.clear();
  memset(_vertex_attrib_columns, 0, sizeof(const GeomVertexColumn *) * 32);
#endif

  // Some libraries (Kivy) leave their buffers bound.  How clumsy of them.
  if (_supports_buffers) {
    _glBindBuffer(GL_ARRAY_BUFFER, 0);
    _glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    _current_vbuffer_index = 0;
    _current_ibuffer_index = 0;
  }
#ifndef OPENGLES
  if (_supports_glsl) {
    _glDisableVertexAttribArray(0);
    _glDisableVertexAttribArray(1);
  }
#endif

  // Since this is called by clear_state_and_transform(), we also should reset
  // the states that won't automatically be respecified when clearing the
  // state mask.
  _active_color_write_mask = ColorWriteAttrib::C_all;
  glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);

  if (_dithering_enabled) {
    glEnable(GL_DITHER);
  } else {
    glDisable(GL_DITHER);
  }
  if (_depth_test_enabled) {
    glEnable(GL_DEPTH_TEST);
  } else {
    glDisable(GL_DEPTH_TEST);
  }
  if (_stencil_test_enabled) {
    glEnable(GL_STENCIL_TEST);
  } else {
    glDisable(GL_STENCIL_TEST);
  }
  if (_blend_enabled) {
    glEnable(GL_BLEND);
  } else {
    glDisable(GL_BLEND);
  }

#ifndef OPENGLES_2
  if (_multisample_mode != 0) {
    glEnable(GL_MULTISAMPLE);
  } else {
    glDisable(GL_MULTISAMPLE);
    glDisable(GL_SAMPLE_ALPHA_TO_ONE);
    glDisable(GL_SAMPLE_ALPHA_TO_COVERAGE);
  }
  if (_line_smooth_enabled) {
    glEnable(GL_LINE_SMOOTH);
  } else {
    glDisable(GL_LINE_SMOOTH);
  }
#endif

#ifndef OPENGLES
  if (_polygon_smooth_enabled) {
    glEnable(GL_POLYGON_SMOOTH);
  } else {
    glDisable(GL_POLYGON_SMOOTH);
  }
#endif

#ifdef SUPPORT_FIXED_FUNCTION
  if (has_fixed_function_pipeline()) {
    if (_alpha_test_enabled) {
      glEnable(GL_ALPHA_TEST);
    } else {
      glDisable(GL_ALPHA_TEST);
    }
    if (_point_smooth_enabled) {
      glEnable(GL_POINT_SMOOTH);
    } else {
      glDisable(GL_POINT_SMOOTH);
    }
  }
#endif
}

#ifdef SUPPORT_FIXED_FUNCTION
/**
 * Intended to be overridden by a derived class to enable or disable the use
 * of lighting overall.  This is called by do_issue_light() according to
 * whether any lights are in use or not.
 */
void CLP(GraphicsStateGuardian)::
enable_lighting(bool enable) {
  nassertv(has_fixed_function_pipeline());

  // static PStatCollector
  // _draw_set_state_light_enable_lighting_pcollector("Draw:Set
  // State:Light:Enable lighting"); PStatGPUTimer timer(this,
  // _draw_set_state_light_enable_lighting_pcollector);

  if (enable) {
    glEnable(GL_LIGHTING);
  } else {
    glDisable(GL_LIGHTING);
  }
}
#endif  // SUPPORT_FIXED_FUNCTION

#ifdef SUPPORT_FIXED_FUNCTION
/**
 * Intended to be overridden by a derived class to indicate the color of the
 * ambient light that should be in effect.  This is called by do_issue_light()
 * after all other lights have been enabled or disabled.
 */
void CLP(GraphicsStateGuardian)::
set_ambient_light(const LColor &color) {
  nassertv(has_fixed_function_pipeline());

  // static PStatCollector _draw_set_state_light_ambient_pcollector("Draw:Set
  // State:Light:Ambient"); PStatGPUTimer timer(this,
  // _draw_set_state_light_ambient_pcollector);

  LColor c = color;
  c.set(c[0] * _light_color_scale[0],
        c[1] * _light_color_scale[1],
        c[2] * _light_color_scale[2],
        c[3] * _light_color_scale[3]);
  call_glLightModelfv(GL_LIGHT_MODEL_AMBIENT, c);
}
#endif  // SUPPORT_FIXED_FUNCTION

#ifdef SUPPORT_FIXED_FUNCTION
/**
 * Intended to be overridden by a derived class to enable the indicated light
 * id.  A specific Light will already have been bound to this id via
 * bind_light().
 */
void CLP(GraphicsStateGuardian)::
enable_light(int light_id, bool enable) {
  nassertv(has_fixed_function_pipeline());

  // static PStatCollector
  // _draw_set_state_light_enable_light_pcollector("Draw:Set
  // State:Light:Enable light"); PStatGPUTimer timer(this,
  // _draw_set_state_light_enable_light_pcollector);

  if (enable) {
    glEnable(get_light_id(light_id));
  } else {
    glDisable(get_light_id(light_id));
  }
}
#endif  // SUPPORT_FIXED_FUNCTION

#ifdef SUPPORT_FIXED_FUNCTION
/**
 * Called immediately before bind_light() is called, this is intended to
 * provide the derived class a hook in which to set up some state (like
 * transform) that might apply to several lights.
 *
 * The sequence is: begin_bind_lights() will be called, then one or more
 * bind_light() calls, then end_bind_lights().
 */
void CLP(GraphicsStateGuardian)::
begin_bind_lights() {
  nassertv(has_fixed_function_pipeline());

  // static PStatCollector
  // _draw_set_state_light_begin_bind_pcollector("Draw:Set State:Light:Begin
  // bind"); PStatGPUTimer timer(this,
  // _draw_set_state_light_begin_bind_pcollector);

  // We need to temporarily load a new matrix so we can define the light in a
  // known coordinate system.  We pick the transform of the root.
  // (Alternatively, we could leave the current transform where it is and
  // compute the light position relative to that transform instead of relative
  // to the root, by composing with the matrix computed by
  // _internal_transform->invert_compose(render_transform). But I think
  // loading a completely new matrix is simpler.)
  CPT(TransformState) render_transform =
    _cs_transform->compose(_scene_setup->get_world_transform());

  glMatrixMode(GL_MODELVIEW);
  glPushMatrix();
  call_glLoadMatrix(render_transform->get_mat());
}
#endif  // SUPPORT_FIXED_FUNCTION

#ifdef SUPPORT_FIXED_FUNCTION
/**
 * Called after before bind_light() has been called one or more times (but
 * before any geometry is issued or additional state is changed), this is
 * intended to clean up any temporary changes to the state that may have been
 * made by begin_bind_lights().
 */
void CLP(GraphicsStateGuardian)::
end_bind_lights() {
  nassertv(has_fixed_function_pipeline());

  // static PStatCollector _draw_set_state_light_end_bind_pcollector("Draw:Set
  // State:Light:End bind"); PStatGPUTimer timer(this,
  // _draw_set_state_light_end_bind_pcollector);

  glMatrixMode(GL_MODELVIEW);
  glPopMatrix();
}
#endif  // SUPPORT_FIXED_FUNCTION

#ifdef SUPPORT_FIXED_FUNCTION
/**
 * Intended to be overridden by a derived class to enable the indicated
 * clip_plane id.  A specific PlaneNode will already have been bound to this
 * id via bind_clip_plane().
 */
void CLP(GraphicsStateGuardian)::
enable_clip_plane(int plane_id, bool enable) {
  nassertv(has_fixed_function_pipeline());

  if (enable) {
    glEnable(get_clip_plane_id(plane_id));
  } else {
    glDisable(get_clip_plane_id(plane_id));
  }
}
#endif  // SUPPORT_FIXED_FUNCTION

#ifdef SUPPORT_FIXED_FUNCTION
/**
 * Called immediately before bind_clip_plane() is called, this is intended to
 * provide the derived class a hook in which to set up some state (like
 * transform) that might apply to several clip_planes.
 *
 * The sequence is: begin_bind_clip_planes() will be called, then one or more
 * bind_clip_plane() calls, then end_bind_clip_planes().
 */
void CLP(GraphicsStateGuardian)::
begin_bind_clip_planes() {
  nassertv(has_fixed_function_pipeline());

  // We need to temporarily load a new matrix so we can define the clip_plane
  // in a known coordinate system.  We pick the transform of the root.
  // (Alternatively, we could leave the current transform where it is and
  // compute the clip_plane position relative to that transform instead of
  // relative to the root, by composing with the matrix computed by
  // _internal_transform->invert_compose(render_transform). But I think
  // loading a completely new matrix is simpler.)
  CPT(TransformState) render_transform =
    _cs_transform->compose(_scene_setup->get_world_transform());

  glMatrixMode(GL_MODELVIEW);
  glPushMatrix();
  call_glLoadMatrix(render_transform->get_mat());
}
#endif  // SUPPORT_FIXED_FUNCTION

#ifdef SUPPORT_FIXED_FUNCTION
/**
 * Called the first time a particular clip_plane has been bound to a given id
 * within a frame, this should set up the associated hardware clip_plane with
 * the clip_plane's properties.
 */
void CLP(GraphicsStateGuardian)::
bind_clip_plane(const NodePath &plane, int plane_id) {
  nassertv(has_fixed_function_pipeline());

  GLenum id = get_clip_plane_id(plane_id);

  CPT(TransformState) transform = plane.get_transform(_scene_setup->get_scene_root().get_parent());
  const PlaneNode *plane_node;
  DCAST_INTO_V(plane_node, plane.node());
  LPlane xformed_plane = plane_node->get_plane() * transform->get_mat();

#ifdef OPENGLES
  // OpenGL ES uses a single-precision call.
  LPlanef single_plane(LCAST(float, xformed_plane));
  glClipPlanef(id, single_plane.get_data());
#else
  // Mainline OpenGL uses a double-precision call.
  LPlaned double_plane(LCAST(double, xformed_plane));
  glClipPlane(id, double_plane.get_data());
#endif  // OPENGLES

  report_my_gl_errors();
}
#endif  // SUPPORT_FIXED_FUNCTION

#ifdef SUPPORT_FIXED_FUNCTION
/**
 * Called after before bind_clip_plane() has been called one or more times
 * (but before any geometry is issued or additional state is changed), this is
 * intended to clean up any temporary changes to the state that may have been
 * made by begin_bind_clip_planes().
 */
void CLP(GraphicsStateGuardian)::
end_bind_clip_planes() {
  nassertv(has_fixed_function_pipeline());

  glMatrixMode(GL_MODELVIEW);
  glPopMatrix();
}
#endif  // SUPPORT_FIXED_FUNCTION

/**
 * Simultaneously resets the render state and the transform state.
 *
 * This transform specified is the "internal" net transform, already converted
 * into the GSG's internal coordinate space by composing it to
 * get_cs_transform().  (Previously, this used to be the "external" net
 * transform, with the assumption that that GSG would convert it internally,
 * but that is no longer the case.)
 *
 * Special case: if (state==NULL), then the target state is already stored in
 * _target.
 */
void CLP(GraphicsStateGuardian)::
set_state_and_transform(const RenderState *target,
                        const TransformState *transform) {
  report_my_gl_errors();
#ifndef NDEBUG
  if (gsg_cat.is_spam()) {
    gsg_cat.spam() << "Setting GSG state to " << (void *)target << ":\n";
    target->write(gsg_cat.spam(false), 2);
  }
#endif

  _state_pcollector.add_level(1);
  PStatTimer timer1(_draw_set_state_pcollector);

  bool transform_changed = transform != _internal_transform;
  if (transform_changed) {
    // PStatGPUTimer timer(this, _draw_set_state_transform_pcollector);
    _transform_state_pcollector.add_level(1);
    _internal_transform = transform;
    do_issue_transform();
  }

  if (target == _state_rs && (_state_mask | _inv_state_mask).is_all_on()) {
#ifndef OPENGLES_1
    if (transform_changed) {
      // The state has not changed, but the transform has. Set the new
      // transform on the shader, if we have one.
      if (_current_shader_context != nullptr) {
        _current_shader_context->set_state_and_transform(_state_rs, transform,
          _scene_setup->get_camera_transform(), _projection_mat);
      }
    }
#endif
    return;
  }
  _target_rs = target;

#ifndef OPENGLES_1
  determine_target_shader();
  _sattr_instance_count = _target_shader->get_instance_count();

  if (_target_shader != _state_shader) {
    do_issue_shader();
    _state_shader = _target_shader;
    _state_mask.clear_bit(TextureAttrib::get_class_slot());
    _state_mask.set_bit(ShaderAttrib::get_class_slot());
  }
  else if (!has_fixed_function_pipeline() && _current_shader == nullptr) { // In the case of OpenGL ES 2.x, we need to glUseShader before we draw anything.
    do_issue_shader();
    _state_mask.clear_bit(TextureAttrib::get_class_slot());
    _state_mask.set_bit(ShaderAttrib::get_class_slot());
  }

  // Update all of the state that is bound to the shader program.
  if (_current_shader_context != nullptr) {
    _current_shader_context->set_state_and_transform(target, transform, _scene_setup->get_camera_transform(), _projection_mat);
  }
#endif

#ifdef SUPPORT_FIXED_FUNCTION
  if (has_fixed_function_pipeline()) {
    int alpha_test_slot = AlphaTestAttrib::get_class_slot();
    if (_target_rs->get_attrib(alpha_test_slot) != _state_rs->get_attrib(alpha_test_slot) ||
        !_state_mask.get_bit(alpha_test_slot)
#ifndef OPENGLES_1
        || (_target_shader->get_flag(ShaderAttrib::F_subsume_alpha_test) !=
            _state_shader->get_flag(ShaderAttrib::F_subsume_alpha_test))
#endif
        ) {
      // PStatGPUTimer timer(this, _draw_set_state_alpha_test_pcollector);
      do_issue_alpha_test();
      _state_mask.set_bit(alpha_test_slot);
    }
  }
#endif

  int antialias_slot = AntialiasAttrib::get_class_slot();
  if (_target_rs->get_attrib(antialias_slot) != _state_rs->get_attrib(antialias_slot) ||
      !_state_mask.get_bit(antialias_slot)) {
    // PStatGPUTimer timer(this, _draw_set_state_antialias_pcollector);
    do_issue_antialias();
    _state_mask.set_bit(antialias_slot);
  }

  int clip_plane_slot = ClipPlaneAttrib::get_class_slot();
  if (_target_rs->get_attrib(clip_plane_slot) != _state_rs->get_attrib(clip_plane_slot) ||
      !_state_mask.get_bit(clip_plane_slot)) {
    // PStatGPUTimer timer(this, _draw_set_state_clip_plane_pcollector);
    do_issue_clip_plane();
    _state_mask.set_bit(clip_plane_slot);
  }

  int color_slot = ColorAttrib::get_class_slot();
  int color_scale_slot = ColorScaleAttrib::get_class_slot();
  if (_target_rs->get_attrib(color_slot) != _state_rs->get_attrib(color_slot) ||
      _target_rs->get_attrib(color_scale_slot) != _state_rs->get_attrib(color_scale_slot) ||
      !_state_mask.get_bit(color_slot) ||
      !_state_mask.get_bit(color_scale_slot)) {
    // PStatGPUTimer timer(this, _draw_set_state_color_pcollector);
    do_issue_color();
    do_issue_color_scale();
    _state_mask.set_bit(color_slot);
    _state_mask.set_bit(color_scale_slot);
  }

  int cull_face_slot = CullFaceAttrib::get_class_slot();
  if (_target_rs->get_attrib(cull_face_slot) != _state_rs->get_attrib(cull_face_slot) ||
      !_state_mask.get_bit(cull_face_slot)) {
    // PStatGPUTimer timer(this, _draw_set_state_cull_face_pcollector);
    do_issue_cull_face();
    _state_mask.set_bit(cull_face_slot);
  }

  int depth_bias_slot = DepthBiasAttrib::get_class_slot();
  int depth_offset_slot = DepthOffsetAttrib::get_class_slot();
  if (_target_rs->get_attrib(depth_bias_slot) != _state_rs->get_attrib(depth_bias_slot) ||
      _target_rs->get_attrib(depth_offset_slot) != _state_rs->get_attrib(depth_offset_slot) ||
      !_state_mask.get_bit(depth_bias_slot) ||
      !_state_mask.get_bit(depth_offset_slot)) {
    // PStatGPUTimer timer(this, _draw_set_state_depth_offset_pcollector);
    do_issue_depth_bias();
    _state_mask.set_bit(depth_bias_slot);
    _state_mask.set_bit(depth_offset_slot);
  }

  int depth_test_slot = DepthTestAttrib::get_class_slot();
  if (_target_rs->get_attrib(depth_test_slot) != _state_rs->get_attrib(depth_test_slot) ||
      !_state_mask.get_bit(depth_test_slot)) {
    // PStatGPUTimer timer(this, _draw_set_state_depth_test_pcollector);
    do_issue_depth_test();
    _state_mask.set_bit(depth_test_slot);
  }

  int depth_write_slot = DepthWriteAttrib::get_class_slot();
  if (_target_rs->get_attrib(depth_write_slot) != _state_rs->get_attrib(depth_write_slot) ||
      !_state_mask.get_bit(depth_write_slot)) {
    // PStatGPUTimer timer(this, _draw_set_state_depth_write_pcollector);
    do_issue_depth_write();
    _state_mask.set_bit(depth_write_slot);
  }

  int render_mode_slot = RenderModeAttrib::get_class_slot();
  if (_target_rs->get_attrib(render_mode_slot) != _state_rs->get_attrib(render_mode_slot) ||
      !_state_mask.get_bit(render_mode_slot)) {
    // PStatGPUTimer timer(this, _draw_set_state_render_mode_pcollector);
    do_issue_render_mode();
    _state_mask.set_bit(render_mode_slot);
  }

#ifdef SUPPORT_FIXED_FUNCTION
  if (has_fixed_function_pipeline()) {
    int rescale_normal_slot = RescaleNormalAttrib::get_class_slot();
    if (_target_rs->get_attrib(rescale_normal_slot) != _state_rs->get_attrib(rescale_normal_slot) ||
        !_state_mask.get_bit(rescale_normal_slot)) {
      // PStatGPUTimer timer(this, _draw_set_state_rescale_normal_pcollector);
      do_issue_rescale_normal();
      _state_mask.set_bit(rescale_normal_slot);
    }

    int shade_model_slot = ShadeModelAttrib::get_class_slot();
    if (_target_rs->get_attrib(shade_model_slot) != _state_rs->get_attrib(shade_model_slot) ||
        !_state_mask.get_bit(shade_model_slot)) {
      // PStatGPUTimer timer(this, _draw_set_state_shade_model_pcollector);
      do_issue_shade_model();
      _state_mask.set_bit(shade_model_slot);
    }
  }
#endif

#if !defined(OPENGLES) || defined(OPENGLES_1)
  int logic_op_slot = LogicOpAttrib::get_class_slot();
  if (_target_rs->get_attrib(logic_op_slot) != _state_rs->get_attrib(logic_op_slot) ||
      !_state_mask.get_bit(logic_op_slot)) {
    // PStatGPUTimer timer(this, _draw_set_state_logic_op_pcollector);
    do_issue_logic_op();
    _state_mask.set_bit(logic_op_slot);
  }
#endif

  int transparency_slot = TransparencyAttrib::get_class_slot();
  int color_write_slot = ColorWriteAttrib::get_class_slot();
  int color_blend_slot = ColorBlendAttrib::get_class_slot();
  if (_target_rs->get_attrib(transparency_slot) != _state_rs->get_attrib(transparency_slot) ||
      _target_rs->get_attrib(color_write_slot) != _state_rs->get_attrib(color_write_slot) ||
      _target_rs->get_attrib(color_blend_slot) != _state_rs->get_attrib(color_blend_slot) ||
      !_state_mask.get_bit(transparency_slot) ||
      !_state_mask.get_bit(color_write_slot) ||
      !_state_mask.get_bit(color_blend_slot)
#ifndef OPENGLES_1
      || (_target_shader->get_flag(ShaderAttrib::F_disable_alpha_write) !=
          _state_shader->get_flag(ShaderAttrib::F_disable_alpha_write))
#endif
      ) {
    // PStatGPUTimer timer(this, _draw_set_state_blending_pcollector);
    do_issue_blending();
    _state_mask.set_bit(transparency_slot);
    _state_mask.set_bit(color_write_slot);
    _state_mask.set_bit(color_blend_slot);
  }

  int texture_slot = TextureAttrib::get_class_slot();
  int tex_gen_slot = TexGenAttrib::get_class_slot();
  int tex_matrix_slot = TexMatrixAttrib::get_class_slot();
  if (_target_rs->get_attrib(texture_slot) != _state_rs->get_attrib(texture_slot) ||
      !_state_mask.get_bit(texture_slot)) {
    //PStatGPUTimer timer(this, _draw_set_state_texture_pcollector);
#ifdef OPENGLES_1
    determine_target_texture();
#else
    if (has_fixed_function_pipeline() ||
        _current_shader == nullptr ||
        _current_shader == _default_shader) {
      determine_target_texture();
    } else {
      // If we have a custom shader, don't filter down the list of textures.
      _target_texture = (const TextureAttrib *)
        _target_rs->get_attrib_def(TextureAttrib::get_class_slot());
      _target_tex_gen = (const TexGenAttrib *)
        _target_rs->get_attrib_def(tex_gen_slot);
    }
#endif
    do_issue_texture();

    // Since the TexGen and TexMatrix states depend partly on the particular
    // set of textures in use, we should force both of those to be reissued
    // every time we change the texture state.
    _state_mask.clear_bit(tex_gen_slot);
    _state_mask.clear_bit(tex_matrix_slot);

    _state_texture = _target_texture;
    _state_mask.set_bit(texture_slot);
  }
  else if (_target_rs->get_attrib(tex_gen_slot) != _state_rs->get_attrib(tex_gen_slot) ||
           !_state_mask.get_bit(tex_gen_slot)) {
    _target_tex_gen = (const TexGenAttrib *)_target_rs->get_attrib_def(tex_gen_slot);

#ifdef SUPPORT_FIXED_FUNCTION
#ifdef OPENGLES_1
    if (_has_texture_alpha_scale) {
#else
    if (_has_texture_alpha_scale && _current_shader == nullptr) {
#endif
      PT(TextureStage) stage = get_alpha_scale_texture_stage();
      _target_tex_gen = DCAST(TexGenAttrib, _target_tex_gen->add_stage
                              (stage, TexGenAttrib::M_constant, LTexCoord3(_current_color_scale[3], 0.0f, 0.0f)));
    }
#endif  // SUPPORT_FIXED_FUNCTION

    // If one of the previously-loaded TexGen modes modified the texture matrix,
    // then if either state changed, we have to change both of them now.
    if (_tex_gen_modifies_mat) {
      _state_mask.clear_bit(tex_gen_slot);
      _state_mask.clear_bit(tex_matrix_slot);
    }
  }

  if (_target_rs->get_attrib(tex_matrix_slot) != _state_rs->get_attrib(tex_matrix_slot) ||
      !_state_mask.get_bit(tex_matrix_slot)) {
    // PStatGPUTimer timer(this, _draw_set_state_tex_matrix_pcollector);
#ifdef SUPPORT_FIXED_FUNCTION
    if (has_fixed_function_pipeline()) {
      do_issue_tex_matrix();
    }
#endif
    _state_mask.set_bit(tex_matrix_slot);
#ifndef OPENGLES_1
    if (_current_shader_context) {
      _current_shader_context->issue_parameters(Shader::SSD_tex_matrix);
    }
#endif

    // See previous occurrence of this check.
    if (_tex_gen_modifies_mat) {
      _state_mask.clear_bit(tex_gen_slot);
    }
  }

#ifdef SUPPORT_FIXED_FUNCTION
  if (has_fixed_function_pipeline()) {
    if (_target_tex_gen != _state_tex_gen ||
        !_state_mask.get_bit(tex_gen_slot)) {
      // PStatGPUTimer timer(this, _draw_set_state_tex_gen_pcollector);
      do_issue_tex_gen();
      _state_tex_gen = _target_tex_gen;
      _state_mask.set_bit(tex_gen_slot);
    }

    int material_slot = MaterialAttrib::get_class_slot();
    if (_target_rs->get_attrib(material_slot) != _state_rs->get_attrib(material_slot) ||
        !_state_mask.get_bit(material_slot)) {
      // PStatGPUTimer timer(this, _draw_set_state_material_pcollector);
      do_issue_material();
      _state_mask.set_bit(material_slot);
    }

    int light_slot = LightAttrib::get_class_slot();
    if (_target_rs->get_attrib(light_slot) != _state_rs->get_attrib(light_slot) ||
        !_state_mask.get_bit(light_slot)) {
      // PStatGPUTimer timer(this, _draw_set_state_light_pcollector);
      do_issue_light();
      _state_mask.set_bit(light_slot);
    }

    int fog_slot = FogAttrib::get_class_slot();
    if (_target_rs->get_attrib(fog_slot) != _state_rs->get_attrib(fog_slot) ||
        !_state_mask.get_bit(fog_slot)) {
      // PStatGPUTimer timer(this, _draw_set_state_fog_pcollector);
      do_issue_fog();
      _state_mask.set_bit(fog_slot);
    }
  }
#endif

  int stencil_slot = StencilAttrib::get_class_slot();
  if (_target_rs->get_attrib(stencil_slot) != _state_rs->get_attrib(stencil_slot) ||
      !_state_mask.get_bit(stencil_slot)) {
    // PStatGPUTimer timer(this, _draw_set_state_stencil_pcollector);
    do_issue_stencil();
    _state_mask.set_bit(stencil_slot);
  }

  int scissor_slot = ScissorAttrib::get_class_slot();
  if (_target_rs->get_attrib(scissor_slot) != _state_rs->get_attrib(scissor_slot) ||
      !_state_mask.get_bit(scissor_slot)) {
    // PStatGPUTimer timer(this, _draw_set_state_scissor_pcollector);
    do_issue_scissor();
    _state_mask.set_bit(scissor_slot);
  }

  _state_rs = _target_rs;
  maybe_gl_finish();
  report_my_gl_errors();
}

/**
 * Frees some memory that was explicitly allocated within the glgsg.
 */
void CLP(GraphicsStateGuardian)::
free_pointers() {
#if defined(HAVE_CG) && !defined(OPENGLES)
  if (_cg_context) {
    _destroyed_cg_contexts.push_back(_cg_context);
    _cg_context = nullptr;

    // Don't destroy the Cg context until the last GSG that uses Cg has been
    // destroyed.  This works around a Cg bug, see #1117.
    if (!AtomicAdjust::dec(_num_gsgs_with_cg_contexts)) {
      for (CGcontext context : _destroyed_cg_contexts) {
        cgDestroyContext(context);
      }
      _destroyed_cg_contexts.clear();
    }
  }
#endif
}

/**
 * Returns a Cg context for this GSG.
 */
#if defined(HAVE_CG) && !defined(OPENGLES)
CGcontext CLP(GraphicsStateGuardian)::
get_cg_context() {
  CGcontext context = _cg_context;
  if (context == nullptr) {
    context = cgCreateContext();

#if CG_VERSION_NUM >= 3100
    // This just sounds like a good thing to do.
    cgGLSetContextGLSLVersion(context, cgGLDetectGLSLVersion());
    if (_shader_caps._active_vprofile == CG_PROFILE_GLSLV) {
      cgGLSetContextOptimalOptions(context, CG_PROFILE_GLSLC);
    }
#endif

    AtomicAdjust::inc(_num_gsgs_with_cg_contexts);
    _cg_context = context;
  }

  return context;
}
#endif

/**
 * This is called by set_state_and_transform() when the texture state has
 * changed.
 */
void CLP(GraphicsStateGuardian)::
do_issue_texture() {
  DO_PSTATS_STUFF(_texture_state_pcollector.add_level(1));

#ifdef OPENGLES_1
  update_standard_texture_bindings();
#else
  if (_current_shader_context == 0) {
    // No shader, or a non-Cg shader.
    if (_texture_binding_shader_context != 0) {
      _texture_binding_shader_context->disable_shader_texture_bindings();
    }
#ifdef SUPPORT_FIXED_FUNCTION
    if (has_fixed_function_pipeline()) {
      update_standard_texture_bindings();
    }
#endif
  } else {
    if (_texture_binding_shader_context == 0) {
#ifdef SUPPORT_FIXED_FUNCTION
      if (has_fixed_function_pipeline()) {
        disable_standard_texture_bindings();
      }
#endif
      _current_shader_context->update_shader_texture_bindings(nullptr);
    } else {
      _current_shader_context->
        update_shader_texture_bindings(_texture_binding_shader_context);
    }
  }

  _texture_binding_shader = _current_shader;
  _texture_binding_shader_context = _current_shader_context;
#endif
}

#ifdef SUPPORT_FIXED_FUNCTION
/**
 * Applies the appropriate set of textures for the current state, using the
 * standard fixed-function pipeline.
 */
void CLP(GraphicsStateGuardian)::
update_standard_texture_bindings() {
#ifndef NDEBUG
  if (_show_texture_usage) {
    update_show_usage_texture_bindings(-1);
    return;
  }
#endif // NDEBUG

  int num_stages = _target_texture->get_num_on_ff_stages();

#ifndef NDEBUG
  // Also check the _flash_texture.  If it is non-NULL, we need to check to
  // see if our flash_texture is in the texture stack here.  If so, then we
  // need to call the special show_texture method instead of the normal
  // texture stack.
  if (_flash_texture != nullptr) {
    double now = ClockObject::get_global_clock()->get_frame_time();
    int this_second = (int)floor(now);
    if (this_second & 1) {
      int show_stage_index = -1;
      for (int i = 0; i < num_stages && show_stage_index < 0; ++i) {
        TextureStage *stage = _target_texture->get_on_ff_stage(i);
        Texture *texture = _target_texture->get_on_texture(stage);
        if (texture == _flash_texture) {
          show_stage_index = i;
        }
      }

      if (show_stage_index >= 0) {
        update_show_usage_texture_bindings(show_stage_index);
        return;
      }
    }
  }
#endif  // NDEBUG

  nassertv(num_stages <= _max_texture_stages &&
           _num_active_texture_stages <= _max_texture_stages);

  _texture_involves_color_scale = false;

  int last_saved_result = -1;
  int last_stage = -1;
  int i;
  for (i = 0; i < num_stages; i++) {
    TextureStage *stage = _target_texture->get_on_ff_stage(i);
    Texture *texture = _target_texture->get_on_texture(stage);
    nassertv(texture != nullptr);

    // Issue the texture on stage i.
    set_active_texture_stage(i);

    // First, turn off the previous texture mode.
    glDisable(GL_TEXTURE_2D);
    if (_supports_cube_map) {
      glDisable(GL_TEXTURE_CUBE_MAP);
    }

#ifndef OPENGLES
    glDisable(GL_TEXTURE_1D);
    if (_supports_3d_texture) {
      glDisable(GL_TEXTURE_3D);
    }
#endif  // OPENGLES

    int view = get_current_tex_view_offset() + stage->get_tex_view_offset();
    TextureContext *tc = texture->prepare_now(_prepared_objects, this);
    if (tc == nullptr) {
      // Something wrong with this texture; skip it.
      continue;
    }

    // Then, turn on the current texture mode.
    GLenum target = get_texture_target(texture->get_texture_type());
    if (target == GL_NONE) {
      // Unsupported texture mode.
      continue;
    }
#ifndef OPENGLES_1
    if (target == GL_TEXTURE_2D_ARRAY || target == GL_TEXTURE_CUBE_MAP_ARRAY) {
      // Cannot be applied via the FFP.
      continue;
    }
#endif  // OPENGLES
    glEnable(target);

    if (!update_texture(tc, false)) {
      glDisable(target);
      continue;
    }
    // Don't DCAST(); we already did the verification in update_texture.
    CLP(TextureContext) *gtc = (CLP(TextureContext) *)tc;
    apply_texture(gtc, view);
    apply_sampler(i, _target_texture->get_on_sampler(stage), gtc, view);

    if (stage->involves_color_scale() && _color_scale_enabled) {
      LColor color = stage->get_color();
      color.set(color[0] * _current_color_scale[0],
                color[1] * _current_color_scale[1],
                color[2] * _current_color_scale[2],
                color[3] * _current_color_scale[3]);
      _texture_involves_color_scale = true;
      call_glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, color);
    } else {
      call_glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, stage->get_color());
    }

    if (stage->get_mode() == TextureStage::M_decal) {
      if (texture->get_num_components() < 3 && _supports_texture_combine) {
        // Make a special case for 1- and 2-channel decal textures.  OpenGL
        // does not define their use with GL_DECAL for some reason, so
        // implement them using the combiner instead.
        glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE);
        glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_INTERPOLATE);
        glTexEnvi(GL_TEXTURE_ENV, GL_RGB_SCALE, 1);
        glTexEnvi(GL_TEXTURE_ENV, GL_ALPHA_SCALE, 1);
        glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_RGB, GL_TEXTURE);
        glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_COLOR);
        glTexEnvi(GL_TEXTURE_ENV, GL_SRC1_RGB, GL_PREVIOUS);
        glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_COLOR);
        glTexEnvi(GL_TEXTURE_ENV, GL_SRC2_RGB, GL_TEXTURE);
        glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND2_RGB, GL_SRC_ALPHA);

      } else {
        // Normal 3- and 4-channel decal textures.
        glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);
      }

    } else if (stage->get_mode() == TextureStage::M_combine) {
      if (!_supports_texture_combine) {
        GLCAT.warning()
          << "TextureStage::M_combine mode is not supported.\n";
        glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
      } else {
        glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE);
        glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_INTERPOLATE);
        glTexEnvi(GL_TEXTURE_ENV, GL_RGB_SCALE, stage->get_rgb_scale());
        glTexEnvi(GL_TEXTURE_ENV, GL_ALPHA_SCALE, stage->get_alpha_scale());
        glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB,
                     get_texture_combine_type(stage->get_combine_rgb_mode()));

        switch (stage->get_num_combine_rgb_operands()) {
        case 3:
          glTexEnvi(GL_TEXTURE_ENV, GL_SRC2_RGB,
                       get_texture_src_type(stage->get_combine_rgb_source2(),
                                            last_stage, last_saved_result, i));
          glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND2_RGB,
                       get_texture_operand_type(stage->get_combine_rgb_operand2()));
          // fall through

        case 2:
          glTexEnvi(GL_TEXTURE_ENV, GL_SRC1_RGB,
                       get_texture_src_type(stage->get_combine_rgb_source1(),
                                            last_stage, last_saved_result, i));
          glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB,
                       get_texture_operand_type(stage->get_combine_rgb_operand1()));
          // fall through

        case 1:
          glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_RGB,
                       get_texture_src_type(stage->get_combine_rgb_source0(),
                                            last_stage, last_saved_result, i));
          glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB,
                       get_texture_operand_type(stage->get_combine_rgb_operand0()));
          // fall through

        default:
          break;
        }
        glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA,
                     get_texture_combine_type(stage->get_combine_alpha_mode()));

        switch (stage->get_num_combine_alpha_operands()) {
        case 3:
          glTexEnvi(GL_TEXTURE_ENV, GL_SRC2_ALPHA,
                       get_texture_src_type(stage->get_combine_alpha_source2(),
                                            last_stage, last_saved_result, i));
          glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND2_ALPHA,
                       get_texture_operand_type(stage->get_combine_alpha_operand2()));
          // fall through

        case 2:
          glTexEnvi(GL_TEXTURE_ENV, GL_SRC1_ALPHA,
                       get_texture_src_type(stage->get_combine_alpha_source1(),
                                            last_stage, last_saved_result, i));
          glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_ALPHA,
                       get_texture_operand_type(stage->get_combine_alpha_operand1()));
          // fall through

        case 1:
          glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_ALPHA,
                       get_texture_src_type(stage->get_combine_alpha_source0(),
                                            last_stage, last_saved_result, i));
          glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_ALPHA,
                       get_texture_operand_type(stage->get_combine_alpha_operand0()));
          // fall through

        default:
          break;
        }
      }
    } else {
      GLint glmode = get_texture_apply_mode_type(stage->get_mode());
      glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, glmode);
    }

    if (stage->get_saved_result()) {
      // This texture's result will be "saved" for a future stage's input.
      last_saved_result = i;
    } else {
      // This is a regular texture stage; it will be the "previous" input for
      // the next stage.
      last_stage = i;
    }
  }

  // Disable the texture stages that are no longer used.
  for (i = num_stages; i < _num_active_texture_stages; i++) {
    set_active_texture_stage(i);
    glDisable(GL_TEXTURE_2D);
    if (_supports_cube_map) {
      glDisable(GL_TEXTURE_CUBE_MAP);
    }
#ifndef OPENGLES
    glDisable(GL_TEXTURE_1D);
    if (_supports_3d_texture) {
      glDisable(GL_TEXTURE_3D);
    }
#endif  // OPENGLES
  }

  // Save the count of texture stages for next time.
  _num_active_texture_stages = num_stages;

  report_my_gl_errors();
}
#endif  // SUPPORT_FIXED_FUNCTION

/**
 * Applies a white dummy texture.  This is useful to bind to a texture slot
 * when a texture is missing.  Also binds the default sampler to the unit.
 */
void CLP(GraphicsStateGuardian)::
apply_white_texture(GLuint unit) {
#ifndef OPENGLES
  if (_supports_dsa) {
    _glBindTextureUnit(unit, get_white_texture());
  } else
#endif
  {
    set_active_texture_stage(unit);

    GLuint index = get_white_texture();
    glBindTexture(GL_TEXTURE_2D, index);

    if (GLCAT.is_spam()) {
      GLCAT.spam()
        << "glBindTexture(GL_TEXTURE_2D, " << index << "): all-white texture\n";
    }
  }

  // Also apply the default sampler, if there's a chance we'd applied anything
  // else.
#ifndef OPENGLES_1
  if (_supports_sampler_objects) {
    _glBindSampler(unit, 0);
  }
#endif
}

/**
 * Returns a white dummy texture.  This is useful to bind to a texture slot
 * when a texture is missing.
 */
GLuint CLP(GraphicsStateGuardian)::
get_white_texture() {
  if (_white_texture != 0) {
    return _white_texture;
  }

  const unsigned char data[] = {0xff, 0xff, 0xff, 0xff};

#ifndef OPENGLES
  if (_supports_dsa && _supports_tex_storage) {
    _glCreateTextures(GL_TEXTURE_2D, 1, &_white_texture);
    _glTextureParameteri(_white_texture, GL_TEXTURE_WRAP_S, GL_REPEAT);
    _glTextureParameteri(_white_texture, GL_TEXTURE_WRAP_T, GL_REPEAT);
    _glTextureParameteri(_white_texture, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    _glTextureParameteri(_white_texture, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    _glTextureStorage2D(_white_texture, 1, GL_RGBA8, 1, 1);
    _glTextureSubImage2D(_white_texture, 0, 0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, data);
  } else
#endif
  {
    glGenTextures(1, &_white_texture);
    glBindTexture(GL_TEXTURE_2D, _white_texture);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0,
                 GL_RGBA, GL_UNSIGNED_BYTE, data);
  }
  return _white_texture;
}

#ifndef NDEBUG
/**
 * This is a special function that loads the usage textures in gl-show-
 * texture-usage mode, instead of loading the actual used textures.
 *
 * If the indicated stage_index is >= 0, then it is the particular texture
 * that is shown.  Otherwise, the textures are rotated through based on
 * show_texture_usage_index.
 */
void CLP(GraphicsStateGuardian)::
update_show_usage_texture_bindings(int show_stage_index) {
  int num_stages = _target_texture->get_num_on_ff_stages();

  nassertv(num_stages <= _max_texture_stages &&
           _num_active_texture_stages <= _max_texture_stages);

  _texture_involves_color_scale = false;

  // First, we walk through the list of textures and pretend to render them
  // all, even though we don't actually render them, just so Panda will keep
  // track of the list of "active" textures correctly during the flash.
  int i;
  for (i = 0; i < num_stages; i++) {
    TextureStage *stage = _target_texture->get_on_ff_stage(i);
    Texture *texture = _target_texture->get_on_texture(stage);
    nassertv(texture != nullptr);

    TextureContext *tc = texture->prepare_now(_prepared_objects, this);
    if (tc == nullptr) {
      // Something wrong with this texture; skip it.
      break;
    }
    tc->enqueue_lru(&_prepared_objects->_graphics_memory_lru);
  }

#ifdef SUPPORT_FIXED_FUNCTION
  if (has_fixed_function_pipeline()) {
    // Disable all texture stages.
    for (i = 0; i < _num_active_texture_stages; i++) {
      set_active_texture_stage(i);
#ifndef OPENGLES
      glDisable(GL_TEXTURE_1D);
#endif  // OPENGLES
      glDisable(GL_TEXTURE_2D);
      if (_supports_3d_texture) {
#ifndef OPENGLES_1
        glDisable(GL_TEXTURE_3D);
#endif  // OPENGLES_1
      }
      if (_supports_cube_map) {
        glDisable(GL_TEXTURE_CUBE_MAP);
      }
    }
  }
#endif

  // Save the count of texture stages for next time.
  _num_active_texture_stages = num_stages;

  if (num_stages > 0) {
    // Now, pick just one texture stage to apply.
    if (show_stage_index >= 0 && show_stage_index < num_stages) {
      i = show_stage_index;
    } else {
      i = _show_texture_usage_index % num_stages;
    }

    TextureStage *stage = _target_texture->get_on_ff_stage(i);
    Texture *texture = _target_texture->get_on_texture(stage);
    nassertv(texture != nullptr);

    // Choose the corresponding usage texture and apply it.
    set_active_texture_stage(i);
#ifdef SUPPORT_FIXED_FUNCTION
    if (has_fixed_function_pipeline()) {
      glEnable(GL_TEXTURE_2D);
    }
#endif

    UsageTextureKey key(texture->get_x_size(), texture->get_y_size());
    UsageTextures::iterator ui = _usage_textures.find(key);
    GLuint index;
    if (ui == _usage_textures.end()) {
      // Need to create a new texture for this size.
      glGenTextures(1, &index);
      glBindTexture(GL_TEXTURE_2D, index);
      // TODO: this could be a lot simpler with glTexStorage2D followed by a
      // call to glClearTexImage.
      upload_usage_texture(texture->get_x_size(), texture->get_y_size());
      _usage_textures[key] = index;

    } else {
      // Just bind the previously-created texture.
      index = (*ui).second;
      glBindTexture(GL_TEXTURE_2D, index);
    }

    if (GLCAT.is_spam()) {
      GLCAT.spam()
        << "glBindTexture(GL_TEXTURE_2D, " << index << ")\n";
    }

    // TODO: glBindSampler(0) ?
  }

  report_my_gl_errors();
}
#endif  // NDEBUG

#ifndef NDEBUG
/**
 * Uploads a special "usage" texture intended to be applied only in gl-show-
 * texture-usage mode, to reveal where texture memory is being spent.
 */
void CLP(GraphicsStateGuardian)::
upload_usage_texture(int width, int height) {
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

  if (GLCAT.is_debug()) {
    GLCAT.debug()
      << "upload_usage_texture(" << width << ", " << height << ")\n";
  }

  static LColor colors[3] = {
    LColor(0.4, 0.5f, 0.8f, 1.0f),   // mipmap 0: blue
    LColor(1.0f, 1.0f, 0.0f, 1.0f),   // mipmap 1: yellow
    LColor(0.8f, 0.3, 0.3, 1.0f),   // mipmap 2 and higher: red
  };


  // Allocate a temporary array large enough to contain the toplevel mipmap.
  uint32_t *buffer = (uint32_t *)PANDA_MALLOC_ARRAY(width * height * 4);

  int n = 0;
  while (true) {
    // Choose the color for the nth mipmap.
    LColor c = colors[min(n, 2)];

    // A simple union to store the colors values bytewise, and get the answer
    // wordwise, independently of machine byte-ordernig.
    union {
      struct {
        unsigned char r, g, b, a;
      } b;
      uint32_t w;
    } store;

    store.b.r = (unsigned char)(c[0] * 255.0f);
    store.b.g = (unsigned char)(c[1] * 255.0f);
    store.b.b = (unsigned char)(c[2] * 255.0f);
    store.b.a = 0xff;

    // Fill in the array.
    int num_pixels = width * height;
    for (int p = 0; p < num_pixels; ++p) {
      buffer[p] = store.w;
    }

    glTexImage2D(GL_TEXTURE_2D, n, GL_RGBA, width, height, 0,
                 GL_RGBA, GL_UNSIGNED_BYTE, buffer);
    if (width == 1 && height == 1) {
      // That was the last mipmap level.
      break;
    }

    width = max(width >> 1, 1);
    height = max(height >> 1, 1);
    ++n;
  }

  PANDA_FREE_ARRAY(buffer);
}
#endif  // NDEBUG

#ifdef SUPPORT_FIXED_FUNCTION
/**
 *
 */
void CLP(GraphicsStateGuardian)::
disable_standard_texture_bindings() {
  // Disable the texture stages that are no longer used.
  for (int i = 0; i < _num_active_texture_stages; i++) {
    set_active_texture_stage(i);
#ifndef OPENGLES
    glDisable(GL_TEXTURE_1D);
#endif  // OPENGLES
    glDisable(GL_TEXTURE_2D);
    if (_supports_3d_texture) {
#ifndef OPENGLES_1
      glDisable(GL_TEXTURE_3D);
#endif  // OPENGLES_1
    }
    if (_supports_cube_map) {
      glDisable(GL_TEXTURE_CUBE_MAP);
    }
  }

  _num_active_texture_stages = 0;

  report_my_gl_errors();
}
#endif  // SUPPORT_FIXED_FUNCTION

#ifdef SUPPORT_FIXED_FUNCTION
/**
 *
 */
void CLP(GraphicsStateGuardian)::
do_issue_tex_matrix() {
  nassertv(_num_active_texture_stages <= _max_texture_stages);

  for (int i = 0; i < _num_active_texture_stages; i++) {
    TextureStage *stage = _target_texture->get_on_ff_stage(i);
    set_active_texture_stage(i);

    glMatrixMode(GL_TEXTURE);

    const TexMatrixAttrib *target_tex_matrix;
    _target_rs->get_attrib_def(target_tex_matrix);

    if (target_tex_matrix->has_stage(stage)) {
      call_glLoadMatrix(target_tex_matrix->get_mat(stage));
    } else {
      glLoadIdentity();

      // For some reason, the glLoadIdentity() call doesn't work on my Dell
      // laptop's IBM OpenGL driver, when used in conjunction with glTexGen(),
      // below.  But explicitly loading an identity matrix does work.  But
      // this buggy-driver workaround might have other performance
      // implications, so I leave it out.
      // call_glLoadMatrix(LMatrix4::ident_mat());
    }
  }
  report_my_gl_errors();
}
#endif  // SUPPORT_FIXED_FUNCTION

#ifdef SUPPORT_FIXED_FUNCTION
/**
 *
 */
void CLP(GraphicsStateGuardian)::
do_issue_tex_gen() {
  nassertv(_num_active_texture_stages <= _max_texture_stages);

  // These are passed in for the four OBJECT_PLANE or EYE_PLANE values; they
  // effectively define an identity matrix that maps the spatial coordinates
  // one-for-one to UV's.  If you want a mapping other than identity, use a
  // TexMatrixAttrib (or a TexProjectorEffect).
#ifndef OPENGLES
  static const PN_stdfloat s_data[4] = { 1, 0, 0, 0 };
  static const PN_stdfloat t_data[4] = { 0, 1, 0, 0 };
  static const PN_stdfloat r_data[4] = { 0, 0, 1, 0 };
  static const PN_stdfloat q_data[4] = { 0, 0, 0, 1 };
#endif

  _tex_gen_modifies_mat = false;

  for (int i = 0; i < _num_active_texture_stages; i++) {
    set_active_texture_stage(i);
    if (_supports_point_sprite) {
#ifdef OPENGLES
      glTexEnvi(GL_POINT_SPRITE_OES, GL_COORD_REPLACE_OES, GL_FALSE);
#else
      glTexEnvi(GL_POINT_SPRITE_ARB, GL_COORD_REPLACE_ARB, GL_FALSE);
#endif  // OPENGLES
    }

#ifndef OPENGLES  // TexGen not supported by OpenGL ES.
    glDisable(GL_TEXTURE_GEN_S);
    glDisable(GL_TEXTURE_GEN_T);
    glDisable(GL_TEXTURE_GEN_R);
    glDisable(GL_TEXTURE_GEN_Q);

    TextureStage *stage = _target_texture->get_on_ff_stage(i);
    TexGenAttrib::Mode mode = _target_tex_gen->get_mode(stage);
    switch (mode) {
    case TexGenAttrib::M_off:
    case TexGenAttrib::M_unused2:
      break;

    case TexGenAttrib::M_eye_sphere_map:
      glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);
      glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);
      glEnable(GL_TEXTURE_GEN_S);
      glEnable(GL_TEXTURE_GEN_T);
      break;

    case TexGenAttrib::M_eye_cube_map:
      if (_supports_cube_map) {
        // We need to rotate the normals out of GL's coordinate system and
        // into the user's coordinate system.  We do this by composing a
        // transform onto the texture matrix.
        LMatrix4 mat = _inv_cs_transform->get_mat();
        mat.set_row(3, LVecBase3(0.0f, 0.0f, 0.0f));
        glMatrixMode(GL_TEXTURE);
        GLPf(MultMatrix)(mat.get_data());

        // Now we need to reset the texture matrix next time around to undo
        // this.
        _tex_gen_modifies_mat = true;

        glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_REFLECTION_MAP);
        glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_REFLECTION_MAP);
        glTexGeni(GL_R, GL_TEXTURE_GEN_MODE, GL_REFLECTION_MAP);
        glEnable(GL_TEXTURE_GEN_S);
        glEnable(GL_TEXTURE_GEN_T);
        glEnable(GL_TEXTURE_GEN_R);
      }
      break;

    case TexGenAttrib::M_world_cube_map:
      if (_supports_cube_map) {
        // We dynamically transform normals from eye space to world space by
        // applying the appropriate rotation transform to the current texture
        // matrix.  Unlike M_world_position, we can't achieve this effect by
        // monkeying with the modelview transform, since the current modelview
        // doesn't affect GL_REFLECTION_MAP.
        CPT(TransformState) camera_transform = _scene_setup->get_camera_transform()->compose(_inv_cs_transform);

        LMatrix4 mat = camera_transform->get_mat();
        mat.set_row(3, LVecBase3(0.0f, 0.0f, 0.0f));
        glMatrixMode(GL_TEXTURE);
        GLPf(MultMatrix)(mat.get_data());

        // Now we need to reset the texture matrix next time around to undo
        // this.
        _tex_gen_modifies_mat = true;

        glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_REFLECTION_MAP);
        glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_REFLECTION_MAP);
        glTexGeni(GL_R, GL_TEXTURE_GEN_MODE, GL_REFLECTION_MAP);
        glEnable(GL_TEXTURE_GEN_S);
        glEnable(GL_TEXTURE_GEN_T);
        glEnable(GL_TEXTURE_GEN_R);
      }
      break;

    case TexGenAttrib::M_eye_normal:
      if (_supports_cube_map) {
        // We need to rotate the normals out of GL's coordinate system and
        // into the user's coordinate system.  We do this by composing a
        // transform onto the texture matrix.
        LMatrix4 mat = _inv_cs_transform->get_mat();
        mat.set_row(3, LVecBase3(0.0f, 0.0f, 0.0f));
        glMatrixMode(GL_TEXTURE);
        GLPf(MultMatrix)(mat.get_data());

        // Now we need to reset the texture matrix next time around to undo
        // this.
        _tex_gen_modifies_mat = true;

        glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_NORMAL_MAP);
        glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_NORMAL_MAP);
        glTexGeni(GL_R, GL_TEXTURE_GEN_MODE, GL_NORMAL_MAP);
        glEnable(GL_TEXTURE_GEN_S);
        glEnable(GL_TEXTURE_GEN_T);
        glEnable(GL_TEXTURE_GEN_R);
      }
      break;

    case TexGenAttrib::M_world_normal:
      if (_supports_cube_map) {
        // We dynamically transform normals from eye space to world space by
        // applying the appropriate rotation transform to the current texture
        // matrix.  Unlike M_world_position, we can't achieve this effect by
        // monkeying with the modelview transform, since the current modelview
        // doesn't affect GL_NORMAL_MAP.
        CPT(TransformState) camera_transform = _scene_setup->get_camera_transform()->compose(_inv_cs_transform);

        LMatrix4 mat = camera_transform->get_mat();
        mat.set_row(3, LVecBase3(0.0f, 0.0f, 0.0f));
        glMatrixMode(GL_TEXTURE);
        GLPf(MultMatrix)(mat.get_data());

        // Now we need to reset the texture matrix next time around to undo
        // this.
        _tex_gen_modifies_mat = true;

        glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_NORMAL_MAP);
        glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_NORMAL_MAP);
        glTexGeni(GL_R, GL_TEXTURE_GEN_MODE, GL_NORMAL_MAP);
        glEnable(GL_TEXTURE_GEN_S);
        glEnable(GL_TEXTURE_GEN_T);
        glEnable(GL_TEXTURE_GEN_R);
      }
      break;

    case TexGenAttrib::M_eye_position:
      // To represent eye position correctly, we need to temporarily load the
      // coordinate-system transform.
      glMatrixMode(GL_MODELVIEW);
      glPushMatrix();
      call_glLoadMatrix(_cs_transform->get_mat());

      glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
      glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
      glTexGeni(GL_R, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
      glTexGeni(GL_Q, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);

      GLPfv(TexGen)(GL_S, GL_EYE_PLANE, s_data);
      GLPfv(TexGen)(GL_T, GL_EYE_PLANE, t_data);
      GLPfv(TexGen)(GL_R, GL_EYE_PLANE, r_data);
      GLPfv(TexGen)(GL_Q, GL_EYE_PLANE, q_data);

      glEnable(GL_TEXTURE_GEN_S);
      glEnable(GL_TEXTURE_GEN_T);
      glEnable(GL_TEXTURE_GEN_R);
      glEnable(GL_TEXTURE_GEN_Q);

      glMatrixMode(GL_MODELVIEW);
      glPopMatrix();
      break;

    case TexGenAttrib::M_world_position:
      // We achieve world position coordinates by using the eye position mode,
      // and loading the transform of the root node--thus putting the "eye" at
      // the root.
      {
        glMatrixMode(GL_MODELVIEW);
        glPushMatrix();
        CPT(TransformState) root_transform = _cs_transform->compose(_scene_setup->get_world_transform());
        call_glLoadMatrix(root_transform->get_mat());
        glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
        glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
        glTexGeni(GL_R, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
        glTexGeni(GL_Q, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);

        GLPfv(TexGen)(GL_S, GL_EYE_PLANE, s_data);
        GLPfv(TexGen)(GL_T, GL_EYE_PLANE, t_data);
        GLPfv(TexGen)(GL_R, GL_EYE_PLANE, r_data);
        GLPfv(TexGen)(GL_Q, GL_EYE_PLANE, q_data);

        glEnable(GL_TEXTURE_GEN_S);
        glEnable(GL_TEXTURE_GEN_T);
        glEnable(GL_TEXTURE_GEN_R);
        glEnable(GL_TEXTURE_GEN_Q);

        glMatrixMode(GL_MODELVIEW);
        glPopMatrix();
      }
      break;

    case TexGenAttrib::M_point_sprite:
      if (_supports_point_sprite) {
#ifdef OPENGLES
        glTexEnvi(GL_POINT_SPRITE_OES, GL_COORD_REPLACE_OES, GL_TRUE);
#else
        glTexEnvi(GL_POINT_SPRITE_ARB, GL_COORD_REPLACE_ARB, GL_TRUE);
#endif
      }
      break;

    case TexGenAttrib::M_constant:
      // To generate a constant UV(w) coordinate everywhere, we use EYE_LINEAR
      // mode, but we construct a special matrix that flattens the vertex
      // position to zero and then adds our desired value.
      {
        const LTexCoord3 &v = _target_tex_gen->get_constant_value(stage);

        glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);
        glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);
        glTexGeni(GL_R, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);
        glTexGeni(GL_Q, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);

        LVecBase4 s(0.0f, 0.0f, 0.0f, v[0]);
        LVecBase4 t(0.0f, 0.0f, 0.0f, v[1]);
        LVecBase4 r(0.0f, 0.0f, 0.0f, v[2]);

        GLPfv(TexGen)(GL_S, GL_OBJECT_PLANE, s.get_data());
        GLPfv(TexGen)(GL_T, GL_OBJECT_PLANE, t.get_data());
        GLPfv(TexGen)(GL_R, GL_OBJECT_PLANE, r.get_data());
        GLPfv(TexGen)(GL_Q, GL_OBJECT_PLANE, q_data);

        glEnable(GL_TEXTURE_GEN_S);
        glEnable(GL_TEXTURE_GEN_T);
        glEnable(GL_TEXTURE_GEN_R);
        glEnable(GL_TEXTURE_GEN_Q);
      }
      break;

    case TexGenAttrib::M_unused:
      break;
    }
#endif  // OPENGLES
  }

  bool got_point_sprites = _supports_point_sprite &&
    (_target_tex_gen->get_geom_rendering(Geom::GR_point) & GeomEnums::GR_point_sprite) != 0;

  if (got_point_sprites != _tex_gen_point_sprite) {
    _tex_gen_point_sprite = got_point_sprites;
#ifdef OPENGLES
    if (_tex_gen_point_sprite) {
      glEnable(GL_POINT_SPRITE_OES);
    } else {
      glDisable(GL_POINT_SPRITE_OES);
    }
#else
    if (_tex_gen_point_sprite) {
      glEnable(GL_POINT_SPRITE_ARB);
    } else {
      glDisable(GL_POINT_SPRITE_ARB);
    }
#endif  // OPENGLES
  }

  report_my_gl_errors();
}
#endif  // SUPPORT_FIXED_FUNCTION

/**
 * Specifies the texture parameters.  Returns true if the texture may need to
 * be reloaded.  Pass non-NULL sampler argument to use different sampler
 * settings.
 */
bool CLP(GraphicsStateGuardian)::
specify_texture(CLP(TextureContext) *gtc, const SamplerState &sampler) {
  Texture *tex = gtc->get_texture();
  GLenum target = gtc->_target;
  if (target == GL_NONE) {
    // Unsupported target (e.g.  3-d texturing on GL 1.1).
    return false;
  }
#ifndef OPENGLES_1
  if (target == GL_TEXTURE_BUFFER) {
    // Buffer textures may not receive texture parameters.
    return false;
  }
#endif  // OPENGLES

  // Record the active sampler settings.
  gtc->_active_sampler = sampler;

  glTexParameteri(target, GL_TEXTURE_WRAP_S,
                  get_texture_wrap_mode(sampler.get_wrap_u()));
#ifndef OPENGLES
  if (target != GL_TEXTURE_1D)
#endif
  {
    glTexParameteri(target, GL_TEXTURE_WRAP_T,
                    get_texture_wrap_mode(sampler.get_wrap_v()));
  }
#ifndef OPENGLES_1
  if (target == GL_TEXTURE_3D) {
    glTexParameteri(target, GL_TEXTURE_WRAP_R,
                    get_texture_wrap_mode(sampler.get_wrap_w()));
  }
#endif

#ifndef OPENGLES
  LColor border_color = sampler.get_border_color();
  call_glTexParameterfv(target, GL_TEXTURE_BORDER_COLOR, border_color);
#endif  // OPENGLES

  SamplerState::FilterType minfilter = sampler.get_effective_minfilter();
  SamplerState::FilterType magfilter = sampler.get_effective_magfilter();
  bool uses_mipmaps = SamplerState::is_mipmap(minfilter) && !gl_ignore_mipmaps;

#ifndef NDEBUG
  if (gl_force_mipmaps) {
    minfilter = SamplerState::FT_linear_mipmap_linear;
    magfilter = SamplerState::FT_linear;
    uses_mipmaps = true;
  }
#endif

  if (!tex->might_have_ram_image()) {
    // If it's a dynamically generated texture (that is, the RAM image isn't
    // available so it didn't pass through the CPU), we should enable GL-
    // generated mipmaps if we can.
    if (!_supports_generate_mipmap) {
      // However, if the GPU doesn't support mipmap generation, we have to
      // turn it off.
      uses_mipmaps = false;
    }
  }

  if (Texture::is_integer(tex->get_format())) {
    // Integer format textures can't have filtering enabled, and in fact, some
    // drivers (looking at you, Intel) will always sample (0, 0, 0, 1) if we
    // don't set this correctly!
    glTexParameteri(target, GL_TEXTURE_MIN_FILTER,
                    uses_mipmaps ? GL_NEAREST_MIPMAP_NEAREST
                                 : GL_NEAREST);
    glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
  } else {
    glTexParameteri(target, GL_TEXTURE_MIN_FILTER,
                    get_texture_filter_type(minfilter, !uses_mipmaps));
    glTexParameteri(target, GL_TEXTURE_MAG_FILTER,
                    get_texture_filter_type(magfilter, true));
  }

  // Set anisotropic filtering.
  if (_supports_anisotropy) {
    PN_stdfloat anisotropy = sampler.get_effective_anisotropic_degree();
    anisotropy = min(anisotropy, _max_anisotropy);
    anisotropy = max(anisotropy, (PN_stdfloat)1.0);
    glTexParameterf(target, GL_TEXTURE_MAX_ANISOTROPY_EXT, anisotropy);
  }

#ifndef OPENGLES_1
  if (tex->get_format() == Texture::F_depth_stencil ||
      tex->get_format() == Texture::F_depth_component ||
      tex->get_format() == Texture::F_depth_component16 ||
      tex->get_format() == Texture::F_depth_component24 ||
      tex->get_format() == Texture::F_depth_component32) {
#ifdef SUPPORT_FIXED_FUNCTION
    if (has_fixed_function_pipeline()) {
      glTexParameteri(target, GL_DEPTH_TEXTURE_MODE_ARB, GL_INTENSITY);
    }
#endif
    if (_supports_shadow_filter) {
      if ((sampler.get_magfilter() == SamplerState::FT_shadow) ||
          (sampler.get_minfilter() == SamplerState::FT_shadow)) {
        glTexParameteri(target, GL_TEXTURE_COMPARE_MODE_ARB, GL_COMPARE_R_TO_TEXTURE_ARB);
        glTexParameteri(target, GL_TEXTURE_COMPARE_FUNC_ARB, GL_LEQUAL);
      } else {
        glTexParameteri(target, GL_TEXTURE_COMPARE_MODE_ARB, GL_NONE);
        glTexParameteri(target, GL_TEXTURE_COMPARE_FUNC_ARB, GL_LEQUAL);
      }
    }
  }
#endif

#ifndef OPENGLES_1
  if (_supports_texture_lod) {
    glTexParameterf(target, GL_TEXTURE_MIN_LOD, sampler.get_min_lod());
    glTexParameterf(target, GL_TEXTURE_MAX_LOD, sampler.get_max_lod());
  }
#endif

#ifndef OPENGLES
  if (_supports_texture_lod_bias) {
    glTexParameterf(target, GL_TEXTURE_LOD_BIAS, sampler.get_lod_bias());
  }
#endif

  report_my_gl_errors();

  if (uses_mipmaps && gtc->_may_reload_with_mipmaps) {
    // Suddenly we require mipmaps, which means the texture may need reloading.
    return true;
  }

  return false;
}

/**
 * Updates OpenGL with the current information for this texture, and makes it
 * the current texture available for rendering.
 *
 * The texture needs to have valid storage, call update_texture() first.
 */
bool CLP(GraphicsStateGuardian)::
apply_texture(CLP(TextureContext) *gtc, int view) {
  gtc->set_active(true);

  GLenum target = gtc->_target;
  assert(target != GL_NONE); // REMOVE ME
  if (target == GL_NONE) {
    return false;
  }

  GLuint index = gtc->get_view_index(view);
  glBindTexture(target, index);
  if (GLCAT.is_spam()) {
    Texture *tex = gtc->get_texture();
    GLCAT.spam()
      << "glBindTexture(GL_TEXTURE_2D, " << index << "): " << *tex
      << " view " << view << "\n";
  }

  report_my_gl_errors();
  return true;
}

/**
 * Updates OpenGL with the current information for this sampler, and makes it
 * the current sampler available for rendering.  Use NULL to unbind the
 * sampler.
 *
 * If the GSG doesn't support sampler objects, the sampler settings are
 * applied to the given texture context instead.
 */
bool CLP(GraphicsStateGuardian)::
apply_sampler(GLuint unit, const SamplerState &sampler, CLP(TextureContext) *gtc, int view) {
#ifndef OPENGLES_1
  if (_supports_sampler_objects) {
    // We support sampler objects.  Prepare the sampler object and bind it to
    // the indicated texture unit.
    SamplerContext *sc = sampler.prepare_now(get_prepared_objects(), this);
    nassertr(sc != nullptr, false);
    CLP(SamplerContext) *gsc = DCAST(CLP(SamplerContext), sc);

    gsc->enqueue_lru(&_prepared_objects->_sampler_object_lru);

    _glBindSampler(unit, gsc->_index);

    if (GLCAT.is_spam()) {
      GLCAT.spam() << "glBindSampler(" << unit << ", "
                   << gsc->_index << "): " << sampler << "\n";
    }

  } else
#endif  // !OPENGLES_1
  {
    // We don't support sampler objects.  We'll have to bind the texture and
    // change the texture parameters if they don't match.
    if (gtc->_active_sampler != sampler) {
      set_active_texture_stage(unit);
      apply_texture(gtc, view);
      specify_texture(gtc, sampler);
    }
  }

  if (sampler.uses_mipmaps() && gtc->_may_reload_with_mipmaps) {
    // The texture wasn't created with mipmaps, but we are trying to sample it
    // with mipmaps.  We will need to reload it.
    GLCAT.info()
      << "reloading texture " << gtc->get_texture()->get_name()
      << " with mipmaps\n";

    gtc->mark_needs_reload();
    bool okflag = upload_texture(gtc, false, true);
    if (!okflag) {
      GLCAT.error()
        << "Could not load " << *gtc->get_texture() << "\n";
      return false;
    }
    // Make sure that the correct view is bound again after upload_texture.
    apply_texture(gtc, view);
  }

  report_my_gl_errors();
  return true;
}

/**
 * Uploads the entire texture image to OpenGL, including all pages.
 *
 * It does not need to be currently bound.
 *
 * The return value is true if successful, or false if the texture has no
 * image.
 */
bool CLP(GraphicsStateGuardian)::
upload_texture(CLP(TextureContext) *gtc, bool force, bool uses_mipmaps) {
  PStatGPUTimer timer(this, _load_texture_pcollector);

  Texture *tex = gtc->get_texture();

  if (_effective_incomplete_render && !force) {
    bool has_image = _supports_compressed_texture ? tex->has_ram_image() : tex->has_uncompressed_ram_image();
    if (!has_image && tex->might_have_ram_image() &&
        tex->has_simple_ram_image() &&
        !_loader.is_null()) {
      // If we don't have the texture data right now, go get it, but in the
      // meantime load a temporary simple image in its place.
      async_reload_texture(gtc);
      has_image = _supports_compressed_texture ? tex->has_ram_image() : tex->has_uncompressed_ram_image();
      if (!has_image) {
        return upload_simple_texture(gtc);
      }
    }
  }

  CPTA_uchar image;
  if (_supports_compressed_texture) {
    image = tex->get_ram_image();
  } else {
    image = tex->get_uncompressed_ram_image();
  }

  Texture::TextureType texture_type = tex->get_texture_type();
  Texture::CompressionMode image_compression;
  if (image.is_null()) {
    image_compression = Texture::CM_off;
  } else {
    image_compression = tex->get_ram_image_compression();

    if (texture_type == Texture::TT_buffer_texture ||
        !get_supports_compressed_texture_format(image_compression)) {
      image = tex->get_uncompressed_ram_image();
      image_compression = Texture::CM_off;

      // If this triggers, Panda cannot decompress the texture.  Compile with
      // libsquish support or precompress the texture.
      nassertr(!image.is_null(), false);
    }
  }

  // If we'll use immutable texture storage, we have to pick a sized image
  // format.
  bool force_sized = (gl_immutable_texture_storage && _supports_tex_storage) ||
                     (texture_type == Texture::TT_buffer_texture);

  GLint internal_format = get_internal_image_format(tex, force_sized);
  GLint external_format = get_external_image_format(tex);
  GLenum component_type = get_component_type(tex->get_component_type());

  if (GLCAT.is_debug()) {
    if (image.is_null()) {
      GLCAT.debug()
        << "loading texture with NULL image";
    } else if (image_compression != Texture::CM_off) {
      GLCAT.debug()
        << "loading pre-compressed texture";
    } else if (is_compressed_format(internal_format)) {
      GLCAT.debug()
        << "loading compressed texture";
    } else {
      GLCAT.debug()
        << "loading uncompressed texture";
    }
    GLCAT.debug(false) << " " << tex->get_name() << "\n";
  }

  // Ensure that the texture fits within the GL's specified limits.  Need to
  // split dimensions because of texture arrays
  int max_dimension_x;
  int max_dimension_y;
  int max_dimension_z;

  switch (texture_type) {
  case Texture::TT_3d_texture:
    max_dimension_x = _max_3d_texture_dimension;
    max_dimension_y = _max_3d_texture_dimension;
    max_dimension_z = _max_3d_texture_dimension;
    break;

  case Texture::TT_cube_map:
    max_dimension_x = _max_cube_map_dimension;
    max_dimension_y = _max_cube_map_dimension;
    max_dimension_z = 6;
    break;

  case Texture::TT_2d_texture_array:
    max_dimension_x = _max_texture_dimension;
    max_dimension_y = _max_texture_dimension;
    max_dimension_z = _max_2d_texture_array_layers;
    break;

  case Texture::TT_cube_map_array:
    max_dimension_x = _max_texture_dimension;
    max_dimension_y = _max_texture_dimension;
    max_dimension_z = int(_max_2d_texture_array_layers / 6) * 6;
    break;

  case Texture::TT_buffer_texture:
    max_dimension_x = _max_buffer_texture_size;
    max_dimension_y = 1;
    max_dimension_z = 1;
    break;

  default:
    max_dimension_x = _max_texture_dimension;
    max_dimension_y = _max_texture_dimension;
    max_dimension_z = 1;
  }

  if (max_dimension_x == 0 || max_dimension_y == 0 || max_dimension_z == 0) {
    // Guess this GL doesn't support cube mapping3d textures2d texture arrays.
    report_my_gl_errors();
    return false;
  }

  // If it doesn't fit, we have to reduce it on-the-fly.  We do this by
  // incrementing the mipmap_bias, so we're effectively loading a lower mipmap
  // level.  This requires generating the mipmaps on the CPU if they haven't
  // already been generated.  It would have been better if the user had
  // specified max-texture-dimension to reduce the texture at load time
  // instead; of course, the user doesn't always know ahead of time what the
  // hardware limits are.

  int mipmap_bias = 0;
  if ((max_dimension_x > 0 && max_dimension_y > 0 && max_dimension_z > 0) &&
      image_compression == Texture::CM_off) {
    while (tex->get_expected_mipmap_x_size(mipmap_bias) > max_dimension_x ||
           tex->get_expected_mipmap_y_size(mipmap_bias) > max_dimension_y ||
           tex->get_expected_mipmap_z_size(mipmap_bias) > max_dimension_z) {
      ++mipmap_bias;
    }

    if (mipmap_bias >= tex->get_num_ram_mipmap_images()) {
      // We need to generate some more mipmap images.
      if (tex->has_ram_image()) {
        tex->generate_ram_mipmap_images();
        if (mipmap_bias >= tex->get_num_ram_mipmap_images()) {
          // It didn't work.  Send the smallest we've got, and hope for the
          // best.
          mipmap_bias = tex->get_num_ram_mipmap_images() - 1;
        }
      }
    }
  }

  int width = tex->get_expected_mipmap_x_size(mipmap_bias);
  int height = tex->get_expected_mipmap_y_size(mipmap_bias);
  int depth = tex->get_expected_mipmap_z_size(mipmap_bias);

  if (mipmap_bias != 0) {
    GLCAT.info()
      << "Reducing image " << tex->get_name()
      << " from " << tex->get_x_size() << " x " << tex->get_y_size()
      << " x " << tex->get_z_size() << " to "
      << width << " x " << height << " x " << depth << "\n";
  }

  if (image_compression != Texture::CM_off) {
#ifndef OPENGLES
    switch (tex->get_effective_quality_level()) {
    case Texture::QL_fastest:
      glHint(GL_TEXTURE_COMPRESSION_HINT, GL_FASTEST);
      break;

    case Texture::QL_default:
    case Texture::QL_normal:
      glHint(GL_TEXTURE_COMPRESSION_HINT, GL_DONT_CARE);
      break;

    case Texture::QL_best:
      glHint(GL_TEXTURE_COMPRESSION_HINT, GL_NICEST);
      break;
    }
#endif
  }

  glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

  bool needs_reload = false;
  if (!gtc->_has_storage ||
      gtc->_internal_format != internal_format ||
      gtc->_width != width ||
      gtc->_height != height ||
      gtc->_depth != depth) {
    // We need to reload a new GL Texture object.
    needs_reload = true;
  }

  // Figure out whether mipmaps will be generated by the GPU or by Panda (or
  // not at all), and how many mipmap levels should be created.
  uses_mipmaps = (uses_mipmaps && !gl_ignore_mipmaps) || gl_force_mipmaps;
  int num_levels = 1;
  if (texture_type == Texture::TT_buffer_texture ||
      (width == 1 && height == 1 && depth == 1)) {
    // Buffer textures, or 1x1x1 textures, never have mipmaps.
    gtc->_num_levels = 1;
    gtc->_generate_mipmaps = false;
    gtc->_may_reload_with_mipmaps = false;
  }
  else if (needs_reload || (uses_mipmaps && gtc->_num_levels <= 1)) {
    gtc->_generate_mipmaps = false;
    gtc->_may_reload_with_mipmaps = false;

    bool can_generate = _supports_generate_mipmap;
#if defined(OPENGLES) && !defined(OPENGLES_1)
    // OpenGL ES doesn't support generating mipmaps for sRGB textures, so we
    // have to do this on the CPU, unless we have a special extension.
    if (internal_format == GL_SRGB8 || internal_format == GL_SRGB8_ALPHA8) {
      can_generate = has_extension("GL_NV_generate_mipmap_sRGB");
    }
#endif

    if (image.is_null()) {
      // We don't even have a RAM image, so we have no choice but to let
      // mipmaps be generated on the GPU.
      if (can_generate) {
        if (uses_mipmaps) {
          num_levels = tex->get_expected_num_mipmap_levels() - mipmap_bias;
          gtc->_generate_mipmaps = true;
        }
        else if (!gl_ignore_mipmaps) {
          // If someone asks for mipmaps, we can generate them.
          gtc->_may_reload_with_mipmaps = true;
        }
      }
    } else {
      if (uses_mipmaps) {
        num_levels = tex->get_num_ram_mipmap_images() - mipmap_bias;

        if (num_levels <= 1) {
          // No RAM mipmap levels available.  Should we generate some?
          if (!can_generate || !driver_generate_mipmaps ||
              image_compression != Texture::CM_off) {
            // Yes, the GL can't or won't generate them, so we need to.  Note
            // that some drivers (nVidia) will *corrupt memory* if you ask
            // them to generate mipmaps for a pre-compressed texture.
            tex->generate_ram_mipmap_images();
            num_levels = tex->get_num_ram_mipmap_images() - mipmap_bias;
          }
        }

        if (num_levels <= 1) {
          // We don't have mipmap levels in RAM.  Ask the GL to generate them
          // if it can.
          if (can_generate) {
            num_levels = tex->get_expected_num_mipmap_levels() - mipmap_bias;
            gtc->_generate_mipmaps = true;
          } else {
            // If it can't, do without mipmaps.
            uses_mipmaps = false;
            num_levels = 1;
          }
        }
      }
      else if (!gl_ignore_mipmaps) {
        // We don't yet know whether this might succeed, so we will try if
        // someone asks for it.
        gtc->_may_reload_with_mipmaps = true;
      }
    }
    if (num_levels > gtc->_num_levels) {
      // Always need to reallocate storage when we're adding new mipmap levels.
      needs_reload = true;
    }
  }
  else {
    // Maybe we need to generate mipmaps on the CPU.
    num_levels = gtc->_num_levels;
    if (!image.is_null() && uses_mipmaps) {
      if (tex->get_num_ram_mipmap_images() - mipmap_bias <= 1) {
        // No RAM mipmap levels available.  Should we generate some?
        if (!_supports_generate_mipmap || !driver_generate_mipmaps ||
            image_compression != Texture::CM_off) {
          // Yes, the GL can't or won't generate them, so we need to.  Note
          // that some drivers (nVidia) will *corrupt memory* if you ask them
          // to generate mipmaps for a pre-compressed texture.
          tex->generate_ram_mipmap_images();
        }
      }
    }
  }

  int num_views = tex->get_num_views();
  if (needs_reload) {
    if (gtc->_immutable) {
      GLCAT.info()
        << "Attempt to modify texture with immutable storage, recreating texture.\n";
      gtc->reset_data(gtc->_target, num_views);
    }
    else if (_supports_tex_storage && gl_immutable_texture_storage &&
             texture_type != Texture::TT_buffer_texture) {
      gtc->_immutable = true;
    }
  }

#ifndef OPENGLES_1
  if (needs_reload || !image.is_null()) {
    // Make sure that any incoherent writes to this texture have been synced.
    if (gtc->needs_barrier(GL_TEXTURE_UPDATE_BARRIER_BIT)) {
      issue_memory_barrier(GL_TEXTURE_UPDATE_BARRIER_BIT);
    }
  }
#endif

  // Make sure we have a GL texture name for every view of the texture.
  int old_num_views = gtc->_num_views;
  if (old_num_views != num_views) {
    gtc->set_num_views(num_views);
  }

  // For a buffer texture, make sure we've created the buffer indices.
  if (texture_type == Texture::TT_buffer_texture) {
    nassertr(gtc->_buffers != nullptr, false);
  }

  bool extract_success = false;
  if (tex->get_post_load_store_cache()) {
    extract_success = true;
  }

  bool success = true;
  for (int view = 0; view < num_views; ++view) {
    if (upload_texture_image(gtc, view, needs_reload || view >= old_num_views,
                             mipmap_bias, num_levels,
                             internal_format, external_format,
                             component_type, image_compression)) {
      gtc->_has_storage = true;
      gtc->_internal_format = internal_format;
      gtc->_width = width;
      gtc->_height = height;
      gtc->_depth = depth;
      gtc->_num_levels = num_levels;

      if (extract_success) {
        // The next call assumes the texture is still bound.
        if (!do_extract_texture_data(gtc, view)) {
          extract_success = false;
        }
      }
    }
    else {
      success = false;
    }
  }

  report_my_gl_errors();

  if (success) {
    if (needs_reload) {
      gtc->update_data_size_bytes(get_texture_memory_size(gtc));
    }

    nassertr(gtc->_has_storage, false);

    if (extract_success) {
      tex->set_post_load_store_cache(false);
      // OK, get the RAM image, and save it in a BamCache record.
      if (tex->has_ram_image()) {
        BamCache *cache = BamCache::get_global_ptr();
        PT(BamCacheRecord) record = cache->lookup(tex->get_fullpath(), "txo");
        if (record != nullptr) {
          record->set_data(tex, tex);
          cache->store(record);
        }
      }
    }

    GraphicsEngine *engine = get_engine();
    nassertr(engine != nullptr, false);
    engine->texture_uploaded(tex);
    gtc->mark_loaded();

    return true;
  }

  return false;
}

/**
 * Loads a texture image, or one page of a cube map image, from system RAM to
 * texture memory.
 */
bool CLP(GraphicsStateGuardian)::
upload_texture_image(CLP(TextureContext) *gtc, int view, bool needs_reload,
                     int mipmap_bias, int num_levels, GLint internal_format,
                     GLint external_format, GLenum component_type,
                     Texture::CompressionMode image_compression) {
  // Make sure the error stack is cleared out before we begin.
  clear_my_gl_errors();

  if (image_compression != Texture::CM_off && !_supports_compressed_texture) {
    return false;
  }

  GLenum target = gtc->_target;
  if (target == GL_NONE) {
    // Unsupported target (e.g.  3-d texturing on GL 1.1).
    return false;
  }

  Texture *tex = gtc->get_texture();
  nassertr(tex != nullptr, false);
  Texture::TextureType texture_type = tex->get_texture_type();

  GLuint index = gtc->get_view_index(view);
  glBindTexture(target, index);

  if (GLCAT.is_spam()) {
    GLCAT.spam()
      << "glBindTexture(0x" << hex << target << dec << ", " << index << "): "
      << *tex << " view " << view << "\n";
  }

  if (_use_object_labels && needs_reload) {
    // This seems like a good time to assign a label for the debug messages.
    const string &name = tex->get_name();
    if (gtc->_num_views == 1) {
      _glObjectLabel(GL_TEXTURE, index, name.size(), name.data());
    } else {
      // Add a suffix for a multiview texture.
      char *buffer = (char *)alloca(name.size() + 32);
      int size = sprintf(buffer, "%s#%d", name.c_str(), view);
      _glObjectLabel(GL_TEXTURE, index, size, buffer);
    }
  }

  CPTA_uchar image = tex->get_ram_mipmap_image(mipmap_bias);
  int width = tex->get_expected_mipmap_x_size(mipmap_bias);
  int height = tex->get_expected_mipmap_y_size(mipmap_bias);
  int depth = tex->get_expected_mipmap_z_size(mipmap_bias);

#ifndef OPENGLES_1
  if (target == GL_TEXTURE_BUFFER) {
    GLuint buffer = gtc->get_view_buffer(view);
    nassertr(buffer != 0, false);
    _glBindBuffer(GL_TEXTURE_BUFFER, buffer);
    if (needs_reload) {
      _glTexBuffer(GL_TEXTURE_BUFFER, internal_format, buffer);
    }
  } else
#endif  // !OPENGLES
  if (needs_reload) {
    if (num_levels <= 1) {
      // If we have no mipmaps, tell OpenGL this before we upload the texture
      // images, to give it a hint not to expect them.
      glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    }

    if (_supports_texture_max_level) {
      // By the time we get here, we have a pretty good prediction for the
      // number of mipmaps we're going to have, so tell the GL that's all it's
      // going to get.
      glTexParameteri(target, GL_TEXTURE_MAX_LEVEL, num_levels - 1);
    }

#ifndef OPENGLES_2
    if (gtc->_generate_mipmaps && _glGenerateMipmap == nullptr) {
      // The old, deprecated way to generate mipmaps.
      glTexParameteri(target, GL_GENERATE_MIPMAP, GL_TRUE);
    }
#endif

#ifndef OPENGLES
    if (!_supports_luminance_texture) {
      // Do we need to apply a swizzle mask to emulate these deprecated texture
      // formats?
      switch (tex->get_format()) {
      case Texture::F_alpha:
        glTexParameteri(target, GL_TEXTURE_SWIZZLE_R, GL_ZERO);
        glTexParameteri(target, GL_TEXTURE_SWIZZLE_G, GL_ZERO);
        glTexParameteri(target, GL_TEXTURE_SWIZZLE_B, GL_ZERO);
        glTexParameteri(target, GL_TEXTURE_SWIZZLE_A, GL_RED);
        break;

      case Texture::F_luminance:
        glTexParameteri(target, GL_TEXTURE_SWIZZLE_R, GL_RED);
        glTexParameteri(target, GL_TEXTURE_SWIZZLE_G, GL_RED);
        glTexParameteri(target, GL_TEXTURE_SWIZZLE_B, GL_RED);
        glTexParameteri(target, GL_TEXTURE_SWIZZLE_A, GL_ONE);
        break;

      case Texture::F_luminance_alpha:
        glTexParameteri(target, GL_TEXTURE_SWIZZLE_R, GL_RED);
        glTexParameteri(target, GL_TEXTURE_SWIZZLE_G, GL_RED);
        glTexParameteri(target, GL_TEXTURE_SWIZZLE_B, GL_RED);
        glTexParameteri(target, GL_TEXTURE_SWIZZLE_A, GL_GREEN);
        break;

      default:
        break;
      }
    }
#endif

    // Allocate immutable storage for the texture, after which we can subload
    // it.  Pre-allocating storage using glTexStorage is more efficient than
    // using glTexImage to load all of the individual images one by one later,
    // but we are not allowed to change the texture size or number of mipmap
    // levels after this point.
    if (gtc->_immutable) {
      if (GLCAT.is_debug()) {
        GLCAT.debug()
          << "allocating storage for texture " << tex->get_name() << ", "
          << width << " x " << height << " x " << depth << ", mipmaps "
          << num_levels << ", internal_format = 0x" << std::hex
          << internal_format << std::dec << "\n";
      }

      switch (texture_type) {
      case Texture::TT_buffer_texture:
        // Won't get here, but squelch compiler warning
      case Texture::TT_1d_texture:
#ifndef OPENGLES
        _glTexStorage1D(target, num_levels, internal_format, width);
        break;
#endif
      case Texture::TT_2d_texture:
      case Texture::TT_cube_map:
      case Texture::TT_1d_texture_array:
        _glTexStorage2D(target, num_levels, internal_format, width, height);
        break;
      case Texture::TT_3d_texture:
      case Texture::TT_2d_texture_array:
      case Texture::TT_cube_map_array:
        _glTexStorage3D(target, num_levels, internal_format, width, height, depth);
        break;
      }

      needs_reload = false;
    }
  }

  // How many mipmap levels do we have available to upload?
  int num_ram_mipmap_levels = 0;
  if (!image.is_null()) {
    num_ram_mipmap_levels = std::min(num_levels, tex->get_num_ram_mipmap_images() - mipmap_bias);
  }

  if (!needs_reload) {
    // Try to subload the image over the existing GL Texture object, possibly
    // saving on texture memory fragmentation.

    if (GLCAT.is_debug()) {
      SparseArray pages = gtc->get_view_modified_pages(view, 0);
      if (num_ram_mipmap_levels == 0) {
        if (tex->has_clear_color()) {
          GLCAT.debug()
            << "clearing texture " << tex->get_name() << " view " << view << ", "
            << width << " x " << height << " x " << depth << ", pages " << pages
            << ", mipmaps " << num_levels << ", clear_color = "
            << tex->get_clear_color() << "\n";
        } else {
          GLCAT.debug()
            << "not loading NULL image for texture " << tex->get_name()
            << " view " << view << ", " << width << " x " << height << " x " << depth
            << ", pages " << pages << ", mipmaps = " << num_levels << "\n";
        }
      } else {
        GLCAT.debug()
          << "updating image data of texture " << tex->get_name() << " view "
          << view << ", " << width << " x " << height << " x " << depth
          << ", pages " << pages << ", mipmaps " << num_ram_mipmap_levels
          << " / " << num_levels << "\n";
      }
    }

    for (int n = mipmap_bias; n < num_levels + mipmap_bias; ++n) {
      SparseArray pages = gtc->get_view_modified_pages(view, n);

      // we grab the mipmap pointer first, if it is NULL we grab the normal
      // mipmap image pointer which is a PTA_uchar
      const unsigned char *image_ptr = (unsigned char*)tex->get_ram_mipmap_pointer(n);
      CPTA_uchar ptimage;
      if (image_ptr == nullptr) {
        ptimage = tex->get_ram_mipmap_image(n);
        if (ptimage.is_null()) {
          if (n - mipmap_bias < num_ram_mipmap_levels) {
            // We were told we'd have this many RAM mipmap images, but we
            // don't.  Raise a warning.
            GLCAT.warning()
              << "No mipmap level " << n << " defined for " << tex->get_name()
              << "\n";
            break;
          }

          if (tex->has_clear_color()) {
            // The texture has a clear color, so we should fill this mipmap
            // level to a solid color.
#ifndef OPENGLES
            if (target != GL_TEXTURE_BUFFER) {
              if (_supports_clear_texture) {
                // We can do that with the convenient glClearTexImage
                // function.
                vector_uchar clear_data = tex->get_clear_data();

                if (pages.has_all_of(0, depth)) {
                  _glClearTexImage(index, n - mipmap_bias, external_format,
                                   component_type, (void *)&clear_data[0]);
                }
                else for (size_t sri = 0; sri < pages.get_num_subranges(); ++sri) {
                  int begin = pages.get_subrange_begin(sri);
                  int num_pages = pages.get_subrange_end(sri) - begin;
                  _glClearTexSubImage(index, n - mipmap_bias, 0, 0, begin,
                                      width, height, num_pages, external_format,
                                      component_type, (void *)&clear_data[0]);
                }
                continue;
              }
            } else {
              if (_supports_clear_buffer) {
                // For buffer textures we need to clear the underlying
                // storage.
                vector_uchar clear_data = tex->get_clear_data();

                _glClearBufferData(GL_TEXTURE_BUFFER, internal_format, external_format,
                                   component_type, (const void *)&clear_data[0]);
                continue;
              }
            }
#endif  // OPENGLES
            // Ask the Texture class to create the mipmap level in RAM. It'll
            // fill it in with the correct clear color, which we can then
            // upload.
            ptimage = tex->make_ram_mipmap_image(n);

          } else {
            // No clear color and no more images.
            break;
          }
        }
        image_ptr = ptimage;
      }

      PTA_uchar bgr_image;
      size_t page_size = tex->get_ram_mipmap_page_size(n);
      if (image_ptr != nullptr) {
        const unsigned char *orig_image_ptr = image_ptr;
        size_t view_size = tex->get_ram_mipmap_view_size(n);
        image_ptr += view_size * view;
        nassertr(image_ptr >= orig_image_ptr && image_ptr + view_size <= orig_image_ptr + tex->get_ram_mipmap_image_size(n), false);

        if (image_compression == Texture::CM_off) {
          // If the GL doesn't claim to support BGR, we may have to reverse
          // the component ordering of the image.
          image_ptr = fix_component_ordering(bgr_image, image_ptr, view_size,
                                             external_format, tex);
        }
      }

      int width = tex->get_expected_mipmap_x_size(n);
      int height = tex->get_expected_mipmap_y_size(n);

#ifdef DO_PSTATS
      _data_transferred_pcollector.add_level(page_size * pages.get_num_on_bits());
#endif
      switch (target) {
#ifndef OPENGLES_1
      case GL_TEXTURE_3D:
        if (_supports_3d_texture) {
          for (size_t sri = 0; sri < pages.get_num_subranges(); ++sri) {
            int begin = pages.get_subrange_begin(sri);
            int num_pages = pages.get_subrange_end(sri) - begin;
            const unsigned char *page_ptr = image_ptr + page_size * begin;

            if (image_compression == Texture::CM_off) {
              _glTexSubImage3D(target, n - mipmap_bias,
                               0, 0, begin, width, height, num_pages,
                               external_format, component_type, page_ptr);
            } else {
              _glCompressedTexSubImage3D(target, n - mipmap_bias,
                                         0, 0, begin, width, height, num_pages,
                                         external_format,
                                         page_size * num_pages, page_ptr);
            }
          }
        } else {
          report_my_gl_errors();
          return false;
        }
        break;
#endif  // OPENGLES_1

#ifndef OPENGLES
      case GL_TEXTURE_1D:
        if (image_compression == Texture::CM_off) {
          glTexSubImage1D(target, n - mipmap_bias, 0, width,
                          external_format, component_type, image_ptr);
        } else {
          _glCompressedTexSubImage1D(target, n - mipmap_bias, 0, width,
                                     external_format, page_size, image_ptr);
        }
        break;
#endif  // OPENGLES

#ifndef OPENGLES_1
      case GL_TEXTURE_2D_ARRAY:
      case GL_TEXTURE_CUBE_MAP_ARRAY:
        if (_supports_2d_texture_array) {
          for (size_t sri = 0; sri < pages.get_num_subranges(); ++sri) {
            int begin = pages.get_subrange_begin(sri);
            int num_pages = pages.get_subrange_end(sri) - begin;
            const unsigned char *page_ptr = image_ptr + page_size * begin;

            if (image_compression == Texture::CM_off) {
              _glTexSubImage3D(target, n - mipmap_bias,
                               0, 0, begin, width, height, num_pages,
                               external_format, component_type, page_ptr);
            } else {
              _glCompressedTexSubImage3D(target, n - mipmap_bias,
                                         0, 0, begin, width, height, num_pages,
                                         external_format,
                                         page_size * num_pages, page_ptr);
            }
          }
        } else {
          report_my_gl_errors();
          return false;
        }
        break;
#endif  // OPENGLES_1

#ifndef OPENGLES_1
      case GL_TEXTURE_BUFFER:
        if (_supports_buffer_texture) {
          _glBufferSubData(GL_TEXTURE_BUFFER, 0, page_size, image_ptr);
        } else {
          report_my_gl_errors();
          return false;
        }
        break;
#endif  // OPENGLES

      case GL_TEXTURE_CUBE_MAP:
        if (_supports_cube_map) {
          // This is the only texture type that must be specified using separate
          // per-page calls.
          if (n == 0) {
            height = tex->get_y_size() - tex->get_pad_y_size();
          }
          for (int z = 0; z < 6; ++z) {
            if (pages.get_bit(z)) {
              GLenum page_target = GL_TEXTURE_CUBE_MAP_POSITIVE_X + z;
              const unsigned char *page_ptr = image_ptr + page_size * z;

              if (image_compression == Texture::CM_off) {
                glTexSubImage2D(page_target, n - mipmap_bias, 0, 0, width, height,
                                external_format, component_type, page_ptr);
              } else {
                _glCompressedTexSubImage2D(page_target, n - mipmap_bias,
                                           0, 0, width, height,
                                           external_format, page_size, page_ptr);
              }
            }
          }
        } else {
          report_my_gl_errors();
          return false;
        }
        break;

      default:
        if (image_compression == Texture::CM_off) {
          if (n == 0) {
            // It's unfortunate that we can't adjust the width, too, but
            // TexSubImage2D doesn't accept a row-stride parameter.
            height = tex->get_y_size() - tex->get_pad_y_size();
          }
          glTexSubImage2D(target, n - mipmap_bias, 0, 0, width, height,
                          external_format, component_type, image_ptr);
        } else {
          _glCompressedTexSubImage2D(target, n - mipmap_bias, 0, 0, width, height,
                                     external_format, page_size, image_ptr);
        }
        break;
      }
    }

    // Did that fail?  If it did, we'll immediately try again, this time
    // loading the texture from scratch.
    GLenum error_code = gl_get_error();
    if (error_code != GL_NO_ERROR) {
      if (GLCAT.is_debug()) {
        GLCAT.debug()
          << "GL texture subload failed for " << tex->get_name()
          << " : " << get_error_string(error_code) << "\n";
      }
      needs_reload = true;
    }
  }

  if (needs_reload) {
    // Load the image up from scratch, creating a new GL Texture object.
    if (GLCAT.is_debug()) {
      GLCAT.debug()
        << "loading new texture object for " << tex->get_name() << " view "
        << view << ", " << width << " x " << height << " x " << depth
        << ", mipmaps " << num_ram_mipmap_levels << " / " << num_levels << "\n";
    }

    // If there is immutable storage, this is impossible to do, and we should
    // not have gotten here at all.
    nassertr(!gtc->_immutable, false);

    if (num_ram_mipmap_levels == 0) {
      if (GLCAT.is_debug()) {
        GLCAT.debug()
          << "  (initializing NULL image)\n";
      }

      if ((external_format == GL_DEPTH_STENCIL) && get_supports_depth_stencil()) {
#ifdef OPENGLES
        component_type = GL_UNSIGNED_INT_24_8_OES;
#else
        component_type = GL_UNSIGNED_INT_24_8_EXT;
#endif
      }
    }

    for (int n = mipmap_bias; n < num_levels + mipmap_bias; ++n) {
      const unsigned char *image_ptr = (unsigned char*)tex->get_ram_mipmap_pointer(n);
      CPTA_uchar ptimage;
      if (image_ptr == nullptr) {
        ptimage = tex->get_ram_mipmap_image(n);
        if (ptimage.is_null()) {
          if (n - mipmap_bias < num_ram_mipmap_levels) {
            // We were told we'd have this many RAM mipmap images, but we
            // don't.  Raise a warning.
            GLCAT.warning()
              << "No mipmap level " << n << " defined for " << tex->get_name()
              << "\n";
            if (_supports_texture_max_level) {
              // Tell the GL we have no more mipmaps for it to use.
              glTexParameteri(target, GL_TEXTURE_MAX_LEVEL, n - mipmap_bias);
            }
            break;
          }

          if (tex->has_clear_color()) {
            // Ask the Texture class to create the mipmap level in RAM. It'll
            // fill it in with the correct clear color, which we can then
            // upload.
            ptimage = tex->make_ram_mipmap_image(n);
          }
          else if (image_compression != Texture::CM_off) {
            // We can't upload a NULL compressed texture.
            if (_supports_texture_max_level) {
              // Tell the GL we have no more mipmaps for it to use.
              glTexParameteri(target, GL_TEXTURE_MAX_LEVEL, n - mipmap_bias);
            }
            break;
          }
        }
        image_ptr = ptimage;
      }

      PTA_uchar bgr_image;
      size_t view_size = tex->get_ram_mipmap_view_size(n);
      if (image_ptr != nullptr) {
        const unsigned char *orig_image_ptr = image_ptr;
        image_ptr += view_size * view;
        nassertr(image_ptr >= orig_image_ptr && image_ptr + view_size <= orig_image_ptr + tex->get_ram_mipmap_image_size(n), false);

        if (image_compression == Texture::CM_off) {
          // If the GL doesn't claim to support BGR, we may have to reverse
          // the component ordering of the image.
          image_ptr = fix_component_ordering(bgr_image, image_ptr, view_size,
                                             external_format, tex);
        }
      }

      int width = tex->get_expected_mipmap_x_size(n);
      int height = tex->get_expected_mipmap_y_size(n);
#ifndef OPENGLES_1
      int depth = tex->get_expected_mipmap_z_size(n);
#endif

#ifdef DO_PSTATS
      _data_transferred_pcollector.add_level(view_size);
#endif
      switch (target) {
#ifndef OPENGLES  // 1-d textures not supported by OpenGL ES.  Fall through.
      case GL_TEXTURE_1D:
        if (image_compression == Texture::CM_off) {
          glTexImage1D(target, n - mipmap_bias, internal_format,
                       width, 0, external_format, component_type, image_ptr);
        } else {
          _glCompressedTexImage1D(target, n - mipmap_bias, external_format,
                                  width, 0, view_size, image_ptr);
        }
        break;
#endif  // OPENGLES  // OpenGL ES will fall through.

#ifndef OPENGLES_1
      case GL_TEXTURE_3D:
        if (_supports_3d_texture) {
          if (image_compression == Texture::CM_off) {
            _glTexImage3D(target, n - mipmap_bias, internal_format,
                          width, height, depth, 0,
                          external_format, component_type, image_ptr);
          } else {
            _glCompressedTexImage3D(target, n - mipmap_bias, external_format,
                                    width, height, depth, 0, view_size, image_ptr);
          }
        } else {
          report_my_gl_errors();
          return false;
        }
        break;
#endif  // OPENGLES_1

#ifndef OPENGLES_1
      case GL_TEXTURE_2D_ARRAY:
      case GL_TEXTURE_CUBE_MAP_ARRAY:
        if (_supports_2d_texture_array) {
          if (image_compression == Texture::CM_off) {
            _glTexImage3D(target, n - mipmap_bias, internal_format,
                          width, height, depth, 0,
                          external_format, component_type, image_ptr);
          } else {
            _glCompressedTexImage3D(target, n - mipmap_bias, external_format,
                                    width, height, depth, 0, view_size, image_ptr);
          }
        } else {
          report_my_gl_errors();
          return false;
        }
        break;

      case GL_TEXTURE_BUFFER:
        if (_supports_buffer_texture) {
          _glBufferData(GL_TEXTURE_BUFFER, view_size, image_ptr,
                        get_usage(tex->get_usage_hint()));
        } else {
          report_my_gl_errors();
          return false;
        }
        break;
#endif  // OPENGLES_1

      case GL_TEXTURE_CUBE_MAP:
        if (_supports_cube_map) {
          // This is the only texture type that must be specified using separate
          // per-page calls.
          size_t page_size = tex->get_ram_mipmap_page_size(n);
          for (int z = 0; z < 6; ++z) {
            GLenum page_target = GL_TEXTURE_CUBE_MAP_POSITIVE_X + z;
            const unsigned char *page_ptr =
              (image_ptr != nullptr) ? image_ptr + page_size * z : nullptr;

            if (image_compression == Texture::CM_off) {
              glTexImage2D(page_target, n - mipmap_bias, internal_format,
                           width, height, 0,
                           external_format, component_type, page_ptr);
            } else {
              _glCompressedTexImage2D(page_target, n - mipmap_bias, external_format,
                                      width, height, 0, page_size, page_ptr);
            }
          }
        } else {
          report_my_gl_errors();
          return false;
        }
        break;

      default:
        if (image_compression == Texture::CM_off) {
          glTexImage2D(target, n - mipmap_bias, internal_format,
                       width, height, 0,
                       external_format, component_type, image_ptr);
        } else {
          _glCompressedTexImage2D(target, n - mipmap_bias, external_format,
                                  width, height, 0, view_size, image_ptr);
        }
      }
    }

    // Report the error message explicitly if the GL texture creation failed.
    GLenum error_code = gl_get_error();
    if (error_code != GL_NO_ERROR) {
      GLCAT.error()
        << "GL texture creation failed for " << tex->get_name()
        << " : " << get_error_string(error_code) << "\n";

      gtc->_has_storage = false;
      return false;
    }
  }

  if (gtc->_generate_mipmaps && _glGenerateMipmap != nullptr && !image.is_null()) {
    // We uploaded an image; we may need to generate mipmaps.
    if (GLCAT.is_debug()) {
      GLCAT.debug()
        << "generating mipmaps for texture " << tex->get_name() << " view "
        << view << ", " << width << " x " << height << " x " << depth
        << ", mipmaps = " << num_levels << "\n";
    }
    _glGenerateMipmap(target);
  }

  report_my_gl_errors();

  return true;
}

/**
 * Causes mipmaps to be generated for an uploaded texture.
 */
void CLP(GraphicsStateGuardian)::
generate_mipmaps(CLP(TextureContext) *gtc) {
  gtc->set_active(true);

  if (gtc->_num_levels <= 1) {
    return;
  }

#ifndef OPENGLES
  if (_supports_dsa) {
    // OpenGL 4.5 offers an easy way to do this without binding.
    for (int view = 0; view < gtc->_num_views; ++view) {
      _glGenerateTextureMipmap(gtc->_indices[view]);
    }
    return;
  }
#endif

  if (_glGenerateMipmap != nullptr) {
    _state_texture = 0;
    update_texture(gtc, true);

    GLenum target = gtc->_target;
    for (int view = 0; view < gtc->_num_views; ++view) {
      glBindTexture(target, gtc->_indices[view]);
      _glGenerateMipmap(target);
    }
    glBindTexture(target, 0);

    if (GLCAT.is_spam()) {
      GLCAT.spam()
        << "glBindTexture(0x" << hex << target << dec << ", 0)\n";
    }

    report_my_gl_errors();
  }
}

/**
 * This is used as a standin for upload_texture when the texture in question
 * is unavailable (e.g.  it hasn't yet been loaded from disk).  Until the
 * texture image itself becomes available, we will render the texture's
 * "simple" image--a sharply reduced version of the same texture.
 */
bool CLP(GraphicsStateGuardian)::
upload_simple_texture(CLP(TextureContext) *gtc) {
  report_my_gl_errors();

  PStatGPUTimer timer(this, _load_texture_pcollector);
  Texture *tex = gtc->get_texture();
  nassertr(tex != nullptr, false);

  gtc->set_num_views(1);
  GLuint index = gtc->get_view_index(0);
  glBindTexture(GL_TEXTURE_2D, index);

  if (GLCAT.is_spam()) {
    GLCAT.spam()
      << "glBindTexture(GL_TEXTURE_2D, " << index << "): " << *tex << " simple\n";
  }

#ifdef OPENGLES
  GLenum internal_format = GL_BGRA;
#else
  GLenum internal_format = GL_RGBA;
#endif
  GLenum external_format = GL_BGRA;

  CPTA_uchar image = tex->get_simple_ram_image();
  const unsigned char *image_ptr = image.p();
  if (image_ptr == nullptr) {
    return false;
  }

  size_t image_size = tex->get_simple_ram_image_size();
  PTA_uchar bgr_image;
  if (!_supports_bgr) {
    // If the GL doesn't claim to support BGR, we may have to reverse the
    // component ordering of the image.
    external_format = GL_RGBA;
#ifdef OPENGLES
    internal_format = GL_RGBA;
#endif
    image_ptr = fix_component_ordering(bgr_image, image_ptr, image_size,
                                       external_format, tex);
  }

  int width = tex->get_simple_x_size();
  int height = tex->get_simple_y_size();
  GLenum component_type = GL_UNSIGNED_BYTE;

  if (GLCAT.is_debug()) {
    GLCAT.debug()
      << "loading simple image for " << tex->get_name() << "\n";
  }

  // Turn off mipmaps for the simple texture.
  if (_supports_texture_max_level) {
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
  }

#ifdef DO_PSTATS
  _data_transferred_pcollector.add_level(image_size);
#endif

#ifdef OPENGLES
  internal_format = external_format;
#endif
  glTexImage2D(GL_TEXTURE_2D, 0, internal_format,
               width, height, 0,
               external_format, component_type, image_ptr);

  gtc->_has_storage = true;
  gtc->_immutable = false;
  gtc->_internal_format = internal_format;
  gtc->_width = width;
  gtc->_height = height;
  gtc->_depth = 1;
  gtc->_num_levels = 1;
  gtc->_may_reload_with_mipmaps = false;
  gtc->update_data_size_bytes(width * height * 4);

  gtc->mark_loaded();

  report_my_gl_errors();
  return true;
}

/**
 * Asks OpenGL how much texture memory is consumed by the indicated texture
 * (which is also the currently-selected texture).
 */
size_t CLP(GraphicsStateGuardian)::
get_texture_memory_size(CLP(TextureContext) *gtc) {
  Texture *tex = gtc->get_texture();
#ifdef OPENGLES  // Texture querying not supported on OpenGL ES.
  int width = tex->get_x_size();
  int height = tex->get_y_size();
  int depth = 1;
  int scale = 1;

  size_t num_bytes = 2;  // Temporary assumption?

#else
  GLenum target = get_texture_target(tex->get_texture_type());

  GLenum page_target = target;
  GLint scale = 1;
  if (target == GL_TEXTURE_CUBE_MAP) {
    // We need a particular page to get the level parameter from.
    page_target = GL_TEXTURE_CUBE_MAP_POSITIVE_X;
    scale = 6;

  } else if (target == GL_TEXTURE_BUFFER) {
    // In the case of buffer textures, we provided the size to begin with, so
    // no point in querying anything.  Plus, glGetTexParameter is not even
    // supported for buffer textures.
    return tex->get_expected_ram_image_size();
  }

  clear_my_gl_errors();

  GLint internal_format;
  glGetTexLevelParameteriv(page_target, 0, GL_TEXTURE_INTERNAL_FORMAT, &internal_format);

  if (is_compressed_format(internal_format)) {
    // Try to get the compressed size.
    GLint image_size;
    glGetTexLevelParameteriv(page_target, 0,
                             GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &image_size);

    GLenum error_code = gl_get_error();
    if (error_code != GL_NO_ERROR) {
      if (GLCAT.is_debug()) {
        GLCAT.debug()
          << "Couldn't get compressed size for " << tex->get_name()
          << " : " << get_error_string(error_code) << "\n";
      }
      // Fall through to the noncompressed case.
    } else {
      return image_size * scale;
    }
  }

  // OK, get the noncompressed size.
  GLint red_size, green_size, blue_size, alpha_size;
  GLint depth_size = 0, luminance_size = 0, intensity_size = 0;
  glGetTexLevelParameteriv(page_target, 0,
                           GL_TEXTURE_RED_SIZE, &red_size);
  glGetTexLevelParameteriv(page_target, 0,
                           GL_TEXTURE_GREEN_SIZE, &green_size);
  glGetTexLevelParameteriv(page_target, 0,
                           GL_TEXTURE_BLUE_SIZE, &blue_size);
  glGetTexLevelParameteriv(page_target, 0,
                           GL_TEXTURE_ALPHA_SIZE, &alpha_size);
  if (get_supports_luminance_texture()) {
    glGetTexLevelParameteriv(page_target, 0,
                             GL_TEXTURE_LUMINANCE_SIZE, &luminance_size);
    glGetTexLevelParameteriv(page_target, 0,
                             GL_TEXTURE_INTENSITY_SIZE, &intensity_size);
  }
  if (get_supports_depth_texture()) {
    glGetTexLevelParameteriv(page_target, 0,
                             GL_TEXTURE_DEPTH_SIZE, &depth_size);
  }

  GLint width = 1, height = 1, depth = 1;
  glGetTexLevelParameteriv(page_target, 0, GL_TEXTURE_WIDTH, &width);
  glGetTexLevelParameteriv(page_target, 0, GL_TEXTURE_HEIGHT, &height);
  if (_supports_3d_texture || _supports_2d_texture_array) {
    glGetTexLevelParameteriv(page_target, 0, GL_TEXTURE_DEPTH, &depth);
  }

  report_my_gl_errors();

  size_t num_bits = (red_size + green_size + blue_size + alpha_size + luminance_size + intensity_size + depth_size);
  size_t num_bytes = (num_bits + 7) / 8;
#endif  // OPENGLES

  size_t result = num_bytes * width * height * depth * scale;
  for (int n = 1; n < gtc->_num_levels; ++n) {
    width = max(1, width << 1);
    height = max(1, height << 1);
    depth = max(1, depth << 1);
    result += num_bytes * width * height * depth * scale;
  }

  return result * gtc->_num_views;
}

/**
 * Checks the list of resident texture objects to see if any have recently
 * been evicted.
 */
void CLP(GraphicsStateGuardian)::
check_nonresident_texture(BufferContextChain &chain) {
#if defined(SUPPORT_FIXED_FUNCTION) && !defined(OPENGLES)  // Residency queries not supported by OpenGL ES.
  size_t num_textures = chain.get_count();
  if (num_textures == 0) {
    return;
  }

  CLP(TextureContext) **gtc_list = (CLP(TextureContext) **)alloca(num_textures * sizeof(CLP(TextureContext) *));
  GLuint *texture_list = (GLuint *)alloca(num_textures * sizeof(GLuint));
  size_t ti = 0;
  BufferContext *node = chain.get_first();
  while (node != nullptr) {
    // For now, we just check whether the first view is resident.
    CLP(TextureContext) *gtc = DCAST(CLP(TextureContext), node);
    gtc_list[ti] = gtc;
    texture_list[ti] = gtc->get_view_index(0);
    node = node->get_next();
    ++ti;
  }
  nassertv(ti == num_textures);
  GLboolean *results = (GLboolean *)alloca(num_textures * sizeof(GLboolean));
  bool all_resident = (glAreTexturesResident(num_textures, texture_list, results) != 0);

  report_my_gl_errors();

  if (!all_resident) {
    // Some are now nonresident.
    for (ti = 0; ti < num_textures; ++ti) {
      if (!results[ti]) {
        gtc_list[ti]->set_resident(false);
      }
    }
  }
#endif  // OPENGLES
}

/**
 * The internal implementation of extract_texture_data(), given an already-
 * created TextureContext.
 *
 * Assumes that the texture is already bound.
 */
bool CLP(GraphicsStateGuardian)::
do_extract_texture_data(CLP(TextureContext) *gtc, int view) {
  report_my_gl_errors();

  GLenum target = gtc->_target;
  if (target == GL_NONE) {
    return false;
  }

#ifndef OPENGLES_1
  // Make sure any incoherent writes to the texture have been synced.
  if (gtc->needs_barrier(GL_TEXTURE_UPDATE_BARRIER_BIT)) {
    issue_memory_barrier(GL_TEXTURE_UPDATE_BARRIER_BIT);
  }
#endif

  Texture *tex = gtc->get_texture();

  GLint wrap_u, wrap_v, wrap_w;
  GLint minfilter, magfilter;

#ifndef OPENGLES
  GLfloat border_color[4];
#endif

#ifdef OPENGLES_1
  if (true) {
#else
  if (target != GL_TEXTURE_BUFFER) {
#endif
    glGetTexParameteriv(target, GL_TEXTURE_WRAP_S, &wrap_u);
    glGetTexParameteriv(target, GL_TEXTURE_WRAP_T, &wrap_v);
    wrap_w = GL_REPEAT;
#ifndef OPENGLES_1
    if (_supports_3d_texture) {
      glGetTexParameteriv(target, GL_TEXTURE_WRAP_R, &wrap_w);
    }
#endif
    glGetTexParameteriv(target, GL_TEXTURE_MIN_FILTER, &minfilter);
    glGetTexParameteriv(target, GL_TEXTURE_MAG_FILTER, &magfilter);

#ifndef OPENGLES
    glGetTexParameterfv(target, GL_TEXTURE_BORDER_COLOR, border_color);
#endif
  }

  GLenum page_target = target;
  if (target == GL_TEXTURE_CUBE_MAP) {
    // We need a particular page to get the level parameter from.
    page_target = GL_TEXTURE_CUBE_MAP_POSITIVE_X;
  }

  GLint width = gtc->_width, height = gtc->_height, depth = gtc->_depth;
#ifndef OPENGLES
  glGetTexLevelParameteriv(page_target, 0, GL_TEXTURE_WIDTH, &width);
  if (target != GL_TEXTURE_1D) {
    glGetTexLevelParameteriv(page_target, 0, GL_TEXTURE_HEIGHT, &height);
  }

  if (_supports_3d_texture && target == GL_TEXTURE_3D) {
    glGetTexLevelParameteriv(page_target, 0, GL_TEXTURE_DEPTH, &depth);
  } else if (target == GL_TEXTURE_2D_ARRAY || target == GL_TEXTURE_CUBE_MAP_ARRAY) {
    glGetTexLevelParameteriv(page_target, 0, GL_TEXTURE_DEPTH, &depth);
  } else if (target == GL_TEXTURE_CUBE_MAP) {
    depth = 6;
  }
#endif
  clear_my_gl_errors();
  if (width <= 0 || height <= 0 || depth <= 0) {
    GLCAT.error()
      << "No texture data for " << tex->get_name() << "\n";
    return false;
  }

  GLint internal_format = GL_RGBA;
#ifndef OPENGLES
  if (target != GL_TEXTURE_BUFFER) {
    glGetTexLevelParameteriv(page_target, 0, GL_TEXTURE_INTERNAL_FORMAT, &internal_format);
  } else {
    // Some drivers give the wrong result for the above call.  No problem; we
    // already know the internal format of a buffer texture since glTexBuffer
    // required passing the exact sized format.
    internal_format = gtc->_internal_format;
  }
#endif  // OPENGLES

  // Make sure we were able to query those parameters properly.
  GLenum error_code = gl_get_error();
  if (error_code != GL_NO_ERROR) {
    GLCAT.error()
      << "Unable to query texture parameters for " << tex->get_name()
      << " : " << get_error_string(error_code) << "\n";

    return false;
  }

  Texture::ComponentType type = Texture::T_unsigned_byte;
  Texture::Format format = Texture::F_rgb;
  Texture::CompressionMode compression = Texture::CM_off;

  switch (internal_format) {
#ifndef OPENGLES
  case GL_COLOR_INDEX:
    format = Texture::F_color_index;
    break;
#endif
#if GL_DEPTH_COMPONENT != GL_DEPTH_COMPONENT24
  case GL_DEPTH_COMPONENT:
#endif
  case GL_DEPTH_COMPONENT16:
  case GL_DEPTH_COMPONENT24:
  case GL_DEPTH_COMPONENT32:
    type = Texture::T_unsigned_short;
    format = Texture::F_depth_component;
    break;
#ifndef OPENGLES
  case GL_DEPTH_COMPONENT32F:
    type = Texture::T_float;
    format = Texture::F_depth_component;
    break;
#endif
  case GL_DEPTH_STENCIL:
  case GL_DEPTH24_STENCIL8:
    type = Texture::T_unsigned_int_24_8;
    format = Texture::F_depth_stencil;
    break;
#ifndef OPENGLES
  case GL_DEPTH32F_STENCIL8:
    type = Texture::T_float;
    format = Texture::F_depth_stencil;
    break;
#endif
  case GL_RGBA:
  case 4:
    format = Texture::F_rgba;
    break;
  case GL_RGBA4:
    format = Texture::F_rgba4;
    break;
#ifdef OPENGLES
  case GL_RGBA8_OES:
    format = Texture::F_rgba8;
    break;
#else
  case GL_RGBA8:
    format = Texture::F_rgba8;
    break;
#endif
#ifndef OPENGLES
  case GL_RGBA12:
    type = Texture::T_unsigned_short;
    format = Texture::F_rgba12;
    break;
#endif

  case GL_RGB:
  case 3:
    format = Texture::F_rgb;
    break;
#ifndef OPENGLES
  case GL_RGB5:
    format = Texture::F_rgb5;
    break;
#endif
  case GL_RGB5_A1:
    format = Texture::F_rgba5;
    break;
#ifndef OPENGLES
  case GL_RGB8:
    format = Texture::F_rgb8;
    break;
  case GL_RGB12:
    format = Texture::F_rgb12;
    break;
  case GL_RGBA16:
    format = Texture::F_rgba16;
    break;
  case GL_R3_G3_B2:
    format = Texture::F_rgb332;
    break;
#endif

#ifndef OPENGLES_1
  case GL_R8I:
    type = Texture::T_byte;
    format = Texture::F_r8i;
    break;
  case GL_RG8I:
    type = Texture::T_byte;
    format = Texture::F_rg8i;
    break;
  case GL_RGB8I:
    type = Texture::T_byte;
    format = Texture::F_rgb8i;
    break;
  case GL_RGBA8I:
    type = Texture::T_byte;
    format = Texture::F_rgba8i;
    break;

  case GL_R8UI:
    type = Texture::T_unsigned_byte;
    format = Texture::F_r8i;
    break;
  case GL_RG8UI:
    type = Texture::T_unsigned_byte;
    format = Texture::F_rg8i;
    break;
  case GL_RGB8UI:
    type = Texture::T_unsigned_byte;
    format = Texture::F_rgb8i;
    break;
  case GL_RGBA8UI:
    type = Texture::T_unsigned_byte;
    format = Texture::F_rgba8i;
    break;

  case GL_R16I:
    type = Texture::T_short;
    format = Texture::F_r16i;
    break;
  case GL_RG16I:
    type = Texture::T_short;
    format = Texture::F_rg16i;
    break;
  case GL_RGB16I:
    type = Texture::T_short;
    format = Texture::F_rgb16i;
    break;
  case GL_RGBA16I:
    type = Texture::T_short;
    format = Texture::F_rgba16i;
    break;

  case GL_R16UI:
    type = Texture::T_unsigned_short;
    format = Texture::F_r16i;
    break;
  case GL_RG16UI:
    type = Texture::T_unsigned_short;
    format = Texture::F_rg16i;
    break;
  case GL_RGB16UI:
    type = Texture::T_unsigned_short;
    format = Texture::F_rgb16i;
    break;
  case GL_RGBA16UI:
    type = Texture::T_unsigned_short;
    format = Texture::F_rgba16i;
    break;

  case GL_R32I:
    type = Texture::T_int;
    format = Texture::F_r32i;
    break;
  case GL_RG32I:
    type = Texture::T_int;
    format = Texture::F_rg32i;
    break;
  case GL_RGB32I:
    type = Texture::T_int;
    format = Texture::F_rgb32i;
    break;
  case GL_RGBA32I:
    type = Texture::T_int;
    format = Texture::F_rgba32i;
    break;

  case GL_R32UI:
    type = Texture::T_unsigned_int;
    format = Texture::F_r32i;
    break;
  case GL_RG32UI:
    type = Texture::T_unsigned_int;
    format = Texture::F_rg32i;
    break;
  case GL_RGB32UI:
    type = Texture::T_unsigned_int;
    format = Texture::F_rgb32i;
    break;
  case GL_RGBA32UI:
    type = Texture::T_unsigned_int;
    format = Texture::F_rgba32i;
    break;
#endif

#ifndef OPENGLES_1
  case GL_RGBA16F:
    type = Texture::T_float;
    format = Texture::F_rgba16;
    break;
  case GL_RGB16F:
    type = Texture::T_float;
    format = Texture::F_rgb16;
    break;
  case GL_RG16F:
    type = Texture::T_float;
    format = Texture::F_rg16;
    break;
  case GL_R16F:
    type = Texture::T_float;
    format = Texture::F_r16;
    break;
  case GL_RGBA32F:
    type = Texture::T_float;
    format = Texture::F_rgba32;
    break;
  case GL_RGB32F:
    type = Texture::T_float;
    format = Texture::F_rgb32;
    break;
  case GL_RG32F:
    type = Texture::T_float;
    format = Texture::F_rg32;
    break;
  case GL_R32F:
    type = Texture::T_float;
    format = Texture::F_r32;
    break;
#endif

#ifndef OPENGLES
  case GL_RGB16:
    type = Texture::T_unsigned_short;
    format = Texture::F_rgb16;
    break;
  case GL_RG16:
    type = Texture::T_unsigned_short;
    format = Texture::F_rg16;
    break;
  case GL_R16:
    type = Texture::T_unsigned_short;
    format = Texture::F_r16;
    break;

  case GL_RGB16_SNORM:
    type = Texture::T_short;
    format = Texture::F_rgb16;
    break;
  case GL_RG16_SNORM:
    type = Texture::T_short;
    format = Texture::F_rg16;
    break;
  case GL_R16_SNORM:
    type = Texture::T_short;
    format = Texture::F_r16;
    break;
#endif

#ifndef OPENGLES_1
  case GL_R11F_G11F_B10F:
    type = Texture::T_float;
    format = Texture::F_r11_g11_b10;
    break;

  case GL_RGB9_E5:
    type = Texture::T_float;
    format = Texture::F_rgb9_e5;
    break;

  case GL_RGB10_A2:
    type = Texture::T_unsigned_short;
    format = Texture::F_rgb10_a2;
    break;
#endif

#ifdef OPENGLES_2
  case GL_RED_EXT:
  case GL_R8_EXT:
    format = Texture::F_red;
    break;
#endif

#ifndef OPENGLES
  case GL_RED:
    format = Texture::F_red;
    break;
  case GL_GREEN:
    format = Texture::F_green;
    break;
  case GL_BLUE:
    format = Texture::F_blue;
    break;
#endif  // OPENGLES
  case GL_ALPHA:
    format = Texture::F_alpha;
    break;
  case GL_LUMINANCE:
#ifndef OPENGLES
  case GL_LUMINANCE8_EXT:
  case GL_LUMINANCE16:
  case GL_LUMINANCE16F_ARB:
#endif
  case 1:
    format = Texture::F_luminance;
    break;
  case GL_LUMINANCE_ALPHA:
#ifndef OPENGLES
  case GL_LUMINANCE_ALPHA16F_ARB:
#endif
  case 2:
    format = Texture::F_luminance_alpha;
    break;

#ifndef OPENGLES_1
  case GL_SRGB:
  case GL_SRGB8:
    format = Texture::F_srgb;
    break;
  case GL_SRGB_ALPHA:
  case GL_SRGB8_ALPHA8:
    format = Texture::F_srgb_alpha;
    break;
#endif  // OPENGLES_1
#ifndef OPENGLES
  case GL_SLUMINANCE:
  case GL_SLUMINANCE8:
    format = Texture::F_sluminance;
    break;
  case GL_SLUMINANCE_ALPHA:
  case GL_SLUMINANCE8_ALPHA8:
    format = Texture::F_sluminance_alpha;
    break;
#endif  // OPENGLES

#ifndef OPENGLES
  case GL_COMPRESSED_RGB:
    format = Texture::F_rgb;
    compression = Texture::CM_on;
    break;
  case GL_COMPRESSED_RGBA:
    format = Texture::F_rgba;
    compression = Texture::CM_on;
    break;
  case GL_COMPRESSED_ALPHA:
    format = Texture::F_alpha;
    compression = Texture::CM_on;
    break;
  case GL_COMPRESSED_LUMINANCE:
    format = Texture::F_luminance;
    compression = Texture::CM_on;
    break;
  case GL_COMPRESSED_LUMINANCE_ALPHA:
    format = Texture::F_luminance_alpha;
    compression = Texture::CM_on;
    break;

  case GL_COMPRESSED_SRGB:
    format = Texture::F_srgb;
    compression = Texture::CM_on;
    break;
  case GL_COMPRESSED_SRGB_ALPHA:
    format = Texture::F_srgb_alpha;
    compression = Texture::CM_on;
    break;
  case GL_COMPRESSED_SLUMINANCE:
    format = Texture::F_sluminance;
    compression = Texture::CM_on;
    break;
  case GL_COMPRESSED_SLUMINANCE_ALPHA:
    format = Texture::F_sluminance_alpha;
    compression = Texture::CM_on;
    break;
#endif

  case GL_COMPRESSED_RGB_S3TC_DXT1_EXT:
    format = Texture::F_rgb;
    compression = Texture::CM_dxt1;
    break;
  case GL_COMPRESSED_RGBA_S3TC_DXT1_EXT:
    format = Texture::F_rgbm;
    compression = Texture::CM_dxt1;
    break;
#ifndef OPENGLES
  case GL_COMPRESSED_SRGB_S3TC_DXT1_EXT:
    format = Texture::F_srgb;
    compression = Texture::CM_dxt1;
    break;
  case GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT:
    format = Texture::F_srgb_alpha;
    compression = Texture::CM_dxt1;
    break;
#endif

#ifdef OPENGLES
  case GL_COMPRESSED_RGB_PVRTC_2BPPV1_IMG:
    format = Texture::F_rgb;
    compression = Texture::CM_pvr1_2bpp;
    break;
  case GL_COMPRESSED_RGBA_PVRTC_2BPPV1_IMG:
    format = Texture::F_rgba;
    compression = Texture::CM_pvr1_2bpp;
    break;
  case GL_COMPRESSED_RGB_PVRTC_4BPPV1_IMG:
    format = Texture::F_rgb;
    compression = Texture::CM_pvr1_4bpp;
    break;
  case GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG:
    format = Texture::F_rgba;
    compression = Texture::CM_pvr1_4bpp;
    break;

#else
  case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT:
    format = Texture::F_rgba;
    compression = Texture::CM_dxt3;
    break;
  case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT:
    format = Texture::F_rgba;
    compression = Texture::CM_dxt5;
    break;
  case GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT:
    format = Texture::F_srgb_alpha;
    compression = Texture::CM_dxt3;
    break;
  case GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT:
    format = Texture::F_srgb_alpha;
    compression = Texture::CM_dxt5;
    break;
  case GL_COMPRESSED_RGB_FXT1_3DFX:
    format = Texture::F_rgb;
    compression = Texture::CM_fxt1;
    break;
  case GL_COMPRESSED_RGBA_FXT1_3DFX:
    format = Texture::F_rgba;
    compression = Texture::CM_fxt1;
    break;
  case GL_COMPRESSED_LUMINANCE_LATC1_EXT:
    format = Texture::F_luminance;
    compression = Texture::CM_rgtc;
    break;
  case GL_COMPRESSED_LUMINANCE_ALPHA_LATC2_EXT:
    format = Texture::F_luminance_alpha;
    compression = Texture::CM_rgtc;
    break;
  case GL_COMPRESSED_RED_RGTC1:
    format = Texture::F_red;
    compression = Texture::CM_rgtc;
    break;
  case GL_COMPRESSED_SIGNED_RED_RGTC1:
    type = Texture::T_byte;
    format = Texture::F_red;
    compression = Texture::CM_rgtc;
    break;
  case GL_COMPRESSED_RG_RGTC2:
    format = Texture::F_rg;
    compression = Texture::CM_rgtc;
    break;
  case GL_COMPRESSED_SIGNED_RG_RGTC2:
    type = Texture::T_byte;
    format = Texture::F_rg;
    compression = Texture::CM_rgtc;
    break;
#endif
  default:
    GLCAT.warning()
      << "Unhandled internal format for " << tex->get_name()
      << " : " << hex << "0x" << internal_format << dec << "\n";
    return false;
  }

  // We don't want to call setup_texture() again; that resets too much.
  // Instead, we'll just set the individual components.
  tex->set_x_size(width);
  tex->set_y_size(height);
  tex->set_z_size(depth);
  tex->set_component_type(type);
  tex->set_format(format);

#ifdef OPENGLES_1
  if (true) {
#else
  if (target != GL_TEXTURE_BUFFER) {
#endif
    tex->set_wrap_u(get_panda_wrap_mode(wrap_u));
    tex->set_wrap_v(get_panda_wrap_mode(wrap_v));
    tex->set_wrap_w(get_panda_wrap_mode(wrap_w));
    tex->set_minfilter(get_panda_filter_type(minfilter));
    //tex->set_magfilter(get_panda_filter_type(magfilter));

#ifndef OPENGLES
    tex->set_border_color(LColor(border_color[0], border_color[1],
                                 border_color[2], border_color[3]));
#endif
  }

  PTA_uchar image;
  size_t page_size = 0;

  if (!extract_texture_image(image, page_size, tex, target, page_target,
                             type, compression, 0)) {
    return false;
  }

  int num_views = tex->get_num_views();
  if (num_views == 1) {
    // Replace the entire image, since we are modifying the only view.
    tex->set_ram_image(image, compression, page_size);
  } else {
    // We're only modifying a single view, so we can't stomp all over the
    // existing content.
    PTA_uchar ram_image = tex->modify_ram_image();
    nassertr(ram_image.size() == image.size() * num_views, false);
    memcpy(ram_image.p() + image.size() * view, image.p(), image.size());
  }

  // Also get the mipmap levels.
  for (int n = 1; n < gtc->_num_levels; ++n) {
    if (!extract_texture_image(image, page_size, tex, target, page_target,
                               type, compression, n)) {
      return false;
    }
    if (num_views == 1) {
      tex->set_ram_mipmap_image(n, image, page_size);
    } else {
      PTA_uchar ram_mipmap_image = tex->modify_ram_mipmap_image(n);
      memcpy(ram_mipmap_image.p() + image.size() * view, image.p(), image.size());
    }
  }

  return true;
}

/**
 * Called from extract_texture_data(), this gets just the image array for a
 * particular mipmap level (or for the base image).
 */
bool CLP(GraphicsStateGuardian)::
extract_texture_image(PTA_uchar &image, size_t &page_size,
                      Texture *tex, GLenum target, GLenum page_target,
                      Texture::ComponentType type,
                      Texture::CompressionMode compression, int n) {
#ifdef OPENGLES  // Extracting texture data unsupported in OpenGL ES.
  nassert_raise("OpenGL ES does not support extracting texture data");
  return false;
#else

  // Make sure the GL driver does not align textures, otherwise we get corrupt
  // memory, since we don't take alignment into account.
  glPixelStorei(GL_PACK_ALIGNMENT, 1);

  if (target == GL_TEXTURE_CUBE_MAP) {
    // A cube map, compressed or uncompressed.  This we must extract one page
    // at a time.

    // If the cube map is compressed, we assume that all the compressed pages
    // are exactly the same size.  OpenGL doesn't make this assumption, but it
    // happens to be true for all currently extant compression schemes, and it
    // makes things simpler for us.  (It also makes things much simpler for
    // the graphics hardware, so it's likely to continue to be true for a
    // while at least.)

    GLenum external_format = get_external_image_format(tex);
    GLenum pixel_type = get_component_type(type);
    page_size = tex->get_expected_ram_mipmap_page_size(n);

    if (compression != Texture::CM_off) {
      GLint image_size;
      glGetTexLevelParameteriv(page_target, n,
                                  GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &image_size);
      nassertr(image_size <= (int)page_size, false);
      page_size = image_size;
    }

    image = PTA_uchar::empty_array(page_size * 6);

    for (int z = 0; z < 6; ++z) {
      page_target = GL_TEXTURE_CUBE_MAP_POSITIVE_X + z;

      if (compression == Texture::CM_off) {
        glGetTexImage(page_target, n, external_format, pixel_type,
                         image.p() + z * page_size);
      } else {
        _glGetCompressedTexImage(page_target, 0, image.p() + z * page_size);
      }
    }

#ifndef OPENGLES_1
  } else if (target == GL_TEXTURE_BUFFER) {
    // In the case of a buffer texture, we need to get it from the buffer.
    image = PTA_uchar::empty_array(tex->get_expected_ram_mipmap_view_size(n));
    _glGetBufferSubData(target, 0, image.size(), image.p());
#endif

  } else if (compression == Texture::CM_off) {
    // An uncompressed 1-d, 2-d, or 3-d texture.
    image = PTA_uchar::empty_array(tex->get_expected_ram_mipmap_view_size(n));
    GLenum external_format = get_external_image_format(tex);
    GLenum pixel_type = get_component_type(type);
    glGetTexImage(target, n, external_format, pixel_type, image.p());

  } else {
    // A compressed 1-d, 2-d, or 3-d texture.
    GLint image_size;
    glGetTexLevelParameteriv(target, n, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &image_size);
    page_size = image_size / tex->get_z_size();
    image = PTA_uchar::empty_array(image_size);

    // Some drivers (ATI!) seem to try to overstuff more bytes in the array
    // than they asked us to allocate (that is, more bytes than
    // GL_TEXTURE_COMPRESSED_IMAGE_SIZE), requiring us to overallocate and
    // then copy the result into our final buffer.  Sheesh.

    // We'll only do this for small textures (the ATI bug doesn't *seem* to
    // affect large textures), to save on the overhead of the double-copy, and
    // reduce risk from an overly-large alloca().
#ifndef NDEBUG
    static const int max_trouble_buffer = 102400;
#else
    static const int max_trouble_buffer = 1024;
#endif
    if (image_size < max_trouble_buffer) {
      static const int extra_space = 32;
      unsigned char *buffer = (unsigned char *)alloca(image_size + extra_space);
#ifndef NDEBUG
      // Tag the buffer with a specific byte so we can report on whether that
      // driver bug is still active.
      static unsigned char keep_token = 0x00;
      unsigned char token = ++keep_token;
      memset(buffer + image_size, token, extra_space);
#endif
      _glGetCompressedTexImage(target, n, buffer);
      memcpy(image.p(), buffer, image_size);
#ifndef NDEBUG
      int count = extra_space;
      while (count > 0 && buffer[image_size + count - 1] == token) {
        --count;
      }
      if (count != 0) {
        GLCAT.warning()
          << "GL graphics driver overfilled " << count
          << " bytes into a " << image_size
          << "-byte buffer provided to glGetCompressedTexImage()\n";
      }

      // This had better not equal the amount of buffer space we set aside.
      // If it does, we assume the driver might have overfilled even our
      // provided extra buffer.
      nassertr(count != extra_space, true)
#endif  // NDEBUG
    } else {
      _glGetCompressedTexImage(target, n, image.p());
    }
  }

  // Now see if we were successful.
  GLenum error_code = gl_get_error();
  if (error_code != GL_NO_ERROR) {
    GLCAT.error()
      << "Unable to extract texture for " << *tex
      << ", mipmap level " << n
      << " : " << get_error_string(error_code) << "\n";
    nassertr(false, false);
    return false;
  }
  return true;
#endif  // OPENGLES
}

/**
 * Internally sets the point size parameters after any of the properties have
 * changed that might affect this.
 */
#ifdef SUPPORT_FIXED_FUNCTION
void CLP(GraphicsStateGuardian)::
do_point_size() {
  if (!_point_perspective) {
    // Normal, constant-sized points.  Here _point_size is a width in pixels.
    static LVecBase3f constant(1.0f, 0.0f, 0.0f);
    _glPointParameterfv(GL_POINT_DISTANCE_ATTENUATION, constant.get_data());

  } else {
    // Perspective-sized points.  Here _point_size is a width in 3-d units.
    // To arrange that, we need to figure out the appropriate scaling factor
    // based on the current viewport and projection matrix.
    LVector3 height(0.0f, _point_size, 1.0f);
    height = height * _projection_mat->get_mat();
    height = height * _internal_transform->get_scale()[1];
    PN_stdfloat s = height[1] * _viewport_height / _point_size;

    if (_current_lens->is_orthographic()) {
      // If we have an orthographic lens in effect, we don't actually apply a
      // perspective transform: we just scale the points once, regardless of
      // the distance from the camera.
      LVecBase3f constant(1.0f / (s * s), 0.0f, 0.0f);
      _glPointParameterfv(GL_POINT_DISTANCE_ATTENUATION, constant.get_data());

    } else {
      // Otherwise, we give it a true perspective adjustment.
      LVecBase3f square(0.0f, 0.0f, 1.0f / (s * s));
      _glPointParameterfv(GL_POINT_DISTANCE_ATTENUATION, square.get_data());
    }
  }

  report_my_gl_errors();
}
#endif

/**
 * Returns true if this particular GSG supports the specified Cg Shader
 * Profile.
 */
bool CLP(GraphicsStateGuardian)::
get_supports_cg_profile(const string &name) const {
#if !defined(HAVE_CG) || defined(OPENGLES)
  return false;
#else
  CGprofile profile = cgGetProfile(name.c_str());

  if (profile == CG_PROFILE_UNKNOWN) {
    GLCAT.error() << name << ", unknown Cg-profile\n";
    return false;
  }
  return (cgGLIsProfileSupported(profile) != 0);
#endif
}

/**
 * Binds a framebuffer object.
 */
void CLP(GraphicsStateGuardian)::
bind_fbo(GLuint fbo) {
  if (_current_fbo == fbo) {
    return;
  }

  PStatGPUTimer timer(this, _fbo_bind_pcollector);

  nassertv(_glBindFramebuffer != 0);
#if defined(OPENGLES_2)
  _glBindFramebuffer(GL_FRAMEBUFFER, fbo);
#elif defined(OPENGLES_1)
  _glBindFramebuffer(GL_FRAMEBUFFER_OES, fbo);
#else
  _glBindFramebuffer(GL_FRAMEBUFFER_EXT, fbo);
#endif

  _current_fbo = fbo;
}

// GL stencil code section

static int gl_stencil_operations_array[] = {
  GL_KEEP,
  GL_ZERO,
  GL_REPLACE,
#ifdef OPENGLES_1
  GL_INCR_WRAP_OES,
  GL_DECR_WRAP_OES,
#else
  GL_INCR_WRAP,
  GL_DECR_WRAP,
#endif
  GL_INVERT,

  GL_INCR,
  GL_DECR,
};

/**
 * Set stencil render states.
 */
void CLP(GraphicsStateGuardian)::
do_issue_stencil() {
  if (!_supports_stencil) {
    return;
  }

  const StencilAttrib *stencil;
  if (_target_rs->get_attrib(stencil)) {
    // DEBUG
    if (false) {
      GLCAT.debug() << "STENCIL STATE CHANGE\n";
      GLCAT.debug() << "\n"
                    << "SRS_front_comparison_function " << (int)stencil->get_render_state(StencilAttrib::SRS_front_comparison_function) << "\n"
                    << "SRS_front_stencil_fail_operation " << (int)stencil->get_render_state(StencilAttrib::SRS_front_stencil_fail_operation) << "\n"
                    << "SRS_front_stencil_pass_z_fail_operation " << (int)stencil->get_render_state(StencilAttrib::SRS_front_stencil_pass_z_fail_operation) << "\n"
                    << "SRS_front_stencil_pass_z_pass_operation " << (int)stencil->get_render_state(StencilAttrib::SRS_front_stencil_pass_z_pass_operation) << "\n"
                    << "SRS_reference " << (int)stencil->get_render_state(StencilAttrib::SRS_reference) << "\n"
                    << "SRS_read_mask " << (int)stencil->get_render_state(StencilAttrib::SRS_read_mask) << "\n"
                    << "SRS_write_mask " << (int)stencil->get_render_state(StencilAttrib::SRS_write_mask) << "\n"
                    << "SRS_back_comparison_function " << (int)stencil->get_render_state(StencilAttrib::SRS_back_comparison_function) << "\n"
                    << "SRS_back_stencil_fail_operation " << (int)stencil->get_render_state(StencilAttrib::SRS_back_stencil_fail_operation) << "\n"
                    << "SRS_back_stencil_pass_z_fail_operation " << (int)stencil->get_render_state(StencilAttrib::SRS_back_stencil_pass_z_fail_operation) << "\n"
                    << "SRS_back_stencil_pass_z_pass_operation " << (int)stencil->get_render_state(StencilAttrib::SRS_back_stencil_pass_z_pass_operation) << "\n";
    }

#ifndef OPENGLES
    if (_supports_two_sided_stencil) {
      // TODO: add support for OpenGL 2.0-style glStencilFuncSeparate.
      unsigned int back_compare;
      back_compare = stencil->get_render_state(StencilAttrib::SRS_back_comparison_function);

      if (back_compare != RenderAttrib::M_none) {
        glEnable(GL_STENCIL_TEST_TWO_SIDE_EXT);
        _glActiveStencilFaceEXT(GL_BACK);

        glStencilFunc(
          PANDA_TO_GL_COMPAREFUNC(back_compare),
          stencil->get_render_state(StencilAttrib::SRS_reference),
          stencil->get_render_state(StencilAttrib::SRS_read_mask));

        glStencilOp(
           gl_stencil_operations_array[stencil->get_render_state(StencilAttrib::SRS_back_stencil_fail_operation)],
           gl_stencil_operations_array[stencil->get_render_state(StencilAttrib::SRS_back_stencil_pass_z_fail_operation)],
           gl_stencil_operations_array[stencil->get_render_state(StencilAttrib::SRS_back_stencil_pass_z_pass_operation)]
        );
        glStencilMask(stencil->get_render_state(StencilAttrib::SRS_write_mask));
      } else {
        glDisable(GL_STENCIL_TEST_TWO_SIDE_EXT);
      }

      _glActiveStencilFaceEXT(GL_FRONT);
    }
#endif  // OPENGLES

    unsigned int front_compare;
    front_compare = stencil->get_render_state(StencilAttrib::SRS_front_comparison_function);

    if (front_compare != RenderAttrib::M_none) {
      glEnable(GL_STENCIL_TEST);

      glStencilFunc(
        PANDA_TO_GL_COMPAREFUNC(front_compare),
        stencil->get_render_state(StencilAttrib::SRS_reference),
        stencil->get_render_state(StencilAttrib::SRS_read_mask));

      glStencilOp(
         gl_stencil_operations_array[stencil->get_render_state(StencilAttrib::SRS_front_stencil_fail_operation)],
         gl_stencil_operations_array[stencil->get_render_state(StencilAttrib::SRS_front_stencil_pass_z_fail_operation)],
         gl_stencil_operations_array[stencil->get_render_state(StencilAttrib::SRS_front_stencil_pass_z_pass_operation)]
      );
      glStencilMask(stencil->get_render_state(StencilAttrib::SRS_write_mask));
    } else {
      glDisable(GL_STENCIL_TEST);
    }

    if (stencil->get_render_state(StencilAttrib::SRS_clear)) {
      // clear stencil buffer
      glClearStencil(stencil->get_render_state(StencilAttrib::SRS_clear_value));
      glClear(GL_STENCIL_BUFFER_BIT);
    }
  } else {
    glDisable(GL_STENCIL_TEST);
#ifndef OPENGLES
    if (_supports_two_sided_stencil) {
      glDisable(GL_STENCIL_TEST_TWO_SIDE_EXT);
    }
#endif  // OPENGLES
  }
}

/**
 *
 */
void CLP(GraphicsStateGuardian)::
do_issue_scissor() {
  const ScissorAttrib *target_scissor;
  _target_rs->get_attrib_def(target_scissor);

  if (!target_scissor->is_off()) {
    // A non-off ScissorAttrib means to override the scissor setting that was
    // specified by the DisplayRegion.
    if (!_scissor_enabled) {
      if (GLCAT.is_spam()) {
        GLCAT.spam()
          << "glEnable(GL_SCISSOR_TEST)\n";
      }
      glEnable(GL_SCISSOR_TEST);
      _scissor_enabled = true;
    }

    const LVecBase4 &frame = target_scissor->get_frame();

    int x = (int)(_viewport_x + _viewport_width * frame[0] + 0.5f);
    int y = (int)(_viewport_y + _viewport_height * frame[2] + 0.5f);
    int width = (int)(_viewport_width * (frame[1] - frame[0]) + 0.5f);
    int height = (int)(_viewport_height * (frame[3] - frame[2]) + 0.5f);

    if (GLCAT.is_spam()) {
      GLCAT.spam()
        << "glScissor(" << x << ", " << y << ", " << width << ", " << height << ")\n";
    }
    glScissor(x, y, width, height);

    _scissor_attrib_active = true;

  } else if (_scissor_attrib_active) {
    _scissor_attrib_active = false;

    if (_scissor_array.size() > 0) {
      // Scissoring is enabled on the display region.  Revert to the scissor
      // state specified in the DisplayRegion.
#ifndef OPENGLES
      if (_supports_viewport_arrays) {
        _glScissorArrayv(0, _scissor_array.size(), _scissor_array[0].get_data());
      } else
#endif  // OPENGLES
      {
        const LVecBase4i sr = _scissor_array[0];
        glScissor(sr[0], sr[1], sr[2], sr[3]);
      }

    } else if (_scissor_enabled) {
      // The display region had no scissor enabled.  Disable scissoring.
      if (GLCAT.is_spam()) {
        GLCAT.spam()
          << "glDisable(GL_SCISSOR_TEST)\n";
      }
      glDisable(GL_SCISSOR_TEST);
      _scissor_enabled = false;
    }
  }
}
