/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */

/*
  Part of the Processing project - http://processing.org

  Copyright (c) 2012-21 The Processing Foundation
  Copyright (c) 2004-12 Ben Fry and Casey Reas
  Copyright (c) 2001-04 Massachusetts Institute of Technology

  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation, version 2.1.

  This library is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  Lesser General Public License for more details.

  You should have received a copy of the GNU Lesser General
  Public License along with this library; if not, write to the
  Free Software Foundation, Inc., 59 Temple Place, Suite 330,
  Boston, MA  02111-1307  USA
*/

package processing.opengl;

import java.io.IOException;
import java.net.URL;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;
import java.util.Arrays;
import java.util.regex.Pattern;

import processing.core.PApplet;
import processing.core.PConstants;
import processing.core.PGraphics;


/**
 * Processing-OpenGL abstraction layer. Needs to be implemented by subclasses
 * using specific OpenGL-Java bindings.
 *
 * It includes a full GLES 2.0 interface.
 *
 */
public abstract class PGL {
  // ........................................................

  // Basic fields

  /** The PGraphics and PApplet objects using this interface */
  protected PGraphicsOpenGL graphics;
  protected PApplet sketch;

  /** OpenGL thread */
  protected Thread glThread;

  /** ID of the GL context associated to the surface **/
  protected int glContext;

  /** true if this is the GL interface for a primary surface PGraphics */
  public boolean primaryPGL;

  // ........................................................

  // Parameters

  public static int REQUESTED_DEPTH_BITS   = 24;
  public static int REQUESTED_STENCIL_BITS = 8;
  public static int REQUESTED_ALPHA_BITS   = 8;

  /** Switches between the use of regular and direct buffers. */
  protected static boolean USE_DIRECT_BUFFERS = true;
  protected static int MIN_DIRECT_BUFFER_SIZE = 1;

  /** Enables/disables mipmap use. */
  protected static boolean MIPMAPS_ENABLED = true;

  /** Initial sizes for arrays of input and tessellated data. */
  protected static int DEFAULT_IN_VERTICES   = 64;
  protected static int DEFAULT_IN_EDGES      = 128;
  protected static int DEFAULT_IN_TEXTURES   = 64;
  protected static int DEFAULT_TESS_VERTICES = 64;
  protected static int DEFAULT_TESS_INDICES  = 128;

  /** Maximum lights by default is 8, the minimum defined by OpenGL. */
  protected static int MAX_LIGHTS = 8;

  /** Maximum index value of a tessellated vertex. GLES restricts the vertex
   * indices to be of type unsigned short. Since Java only supports signed
   * shorts as primitive type we have 2^15 = 32768 as the maximum number of
   * vertices that can be referred to within a single VBO.
   */
  protected static int MAX_VERTEX_INDEX  = 32767;
  protected static int MAX_VERTEX_INDEX1 = MAX_VERTEX_INDEX + 1;

  /** Count of tessellated fill, line or point vertices that will
   * trigger a flush in the immediate mode. It doesn't necessarily
   * be equal to MAX_VERTEX_INDEX1, since the number of vertices can
   * be effectively much large since the renderer uses offsets to
   * refer to vertices beyond the MAX_VERTEX_INDEX limit.
   */
  protected static int FLUSH_VERTEX_COUNT = MAX_VERTEX_INDEX1;

  /** Minimum/maximum dimensions of a texture used to hold font data. */
  protected static int MIN_FONT_TEX_SIZE = 256;
  protected static int MAX_FONT_TEX_SIZE = 1024;

  /** Minimum stroke weight needed to apply the full path stroking
   * algorithm that properly generates caps and joins.
   */
  protected static float MIN_CAPS_JOINS_WEIGHT = 2f;

  /** Maximum length of linear paths to be stroked with the
   * full algorithm that generates accurate caps and joins.
   */
  protected static int MAX_CAPS_JOINS_LENGTH = 5000;

  /** Minimum array size to use arrayCopy method(). */
  protected static int MIN_ARRAYCOPY_SIZE = 2;

  /** Factor used to displace the stroke vertices towards the camera in
   * order to make sure the lines are always on top of the fill geometry */
  protected static float STROKE_DISPLACEMENT = 0.999f;

  // ........................................................

  // These parameters are left public so advanced users can experiment with different
  // configurations of buffer object streaming, buffer usage modes and access policies.

  /** Controls the use of buffer object streaming:
   * https://www.khronos.org/opengl/wiki/Buffer_Object_Streaming
   * In combination with use direct buffers,
   * the only advantage of enabling it in immediate mode would be to reduce memory footprint
   * since the direct vertex buffers would not be allocated, simply mapped from the OpenGL
   * objects and thus only the vertex arrays would be created.
   * In the case of the retained mode (PShape), memory footprint would be reduced (for the same
   * reason) but it may enable some speed-ups when editing a geometry in within a being/end
   * tessellation update block. */
  static public boolean bufferStreamingImmediate = false;
  static public boolean bufferStreamingRetained = true;

  /** Controls the usage of the buffer data store:
   * https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glBufferData.xhtml
   * Supported options include STATIC_DRAW, DYNAMIC_DRAW, STREAM_DRAW, and STREAM_READ.
   */
  static public int bufferUsageRetained;
  static public int bufferUsageImmediate;

  /** Controls the access to the mapped buffer object's data store (when using buffer streaming).
   * https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glMapBuffer.xhtml
   * Supported options are READ_ONLY, WRITE_ONLY, and READ_WRITE.
   */
  static public int bufferMapAccess;

  // ........................................................

  // Variables to handle single-buffered situations (i.e.: Android)

  protected IntBuffer firstFrame;
  protected static boolean SINGLE_BUFFERED = false;

  // ........................................................

  // FBO layer

  protected boolean fboLayerEnabled = false;
  protected boolean fboLayerCreated = false;
  protected boolean fboLayerEnabledReq = false;
  protected boolean fboLayerDisableReq = false;
  protected boolean fbolayerResetReq = false;
  public int reqNumSamples;
  protected int numSamples;

  protected IntBuffer glColorFbo;
  protected IntBuffer glColorTex;
  protected IntBuffer glDepthStencil;
  protected IntBuffer glDepth;
  protected IntBuffer glStencil;

  protected IntBuffer glMultiFbo;
  protected IntBuffer glMultiColor;
  protected IntBuffer glMultiDepthStencil;
  protected IntBuffer glMultiDepth;
  protected IntBuffer glMultiStencil;

  protected int fboWidth, fboHeight;
  protected int backTex, frontTex;

  /** Flags used to handle the creation of a separate front texture */
  protected boolean usingFrontTex = false;
  protected boolean needSepFrontTex = false;

  /**
   * Defines if FBO Layer is allowed in the given environment.
   * Using FBO can cause a fatal error during runtime for
   * Intel HD Graphics 3000 chipsets (commonly used on older MacBooks)
   * <a href="https://github.com/processing/processing/issues/4104">#4104</a>
   * Changed to private because needs to be accessed via isFboAllowed().
   * <a href="https://github.com/processing/processing4/pull/76">#76</a> and
   * <a href="https://github.com/processing/processing4/issues/50">#50</a>
   */
  private Boolean fboAllowed = true;

  // ........................................................

  // Texture rendering

  protected boolean loadedTex2DShader = false;
  protected int tex2DShaderProgram;
  protected int tex2DVertShader;
  protected int tex2DFragShader;
  protected int tex2DShaderContext;
  protected int tex2DVertLoc;
  protected int tex2DTCoordLoc;
  protected int tex2DSamplerLoc;
  protected int tex2DGeoVBO;

  protected boolean loadedTexRectShader = false;
  protected int texRectShaderProgram;
  protected int texRectVertShader;
  protected int texRectFragShader;
  protected int texRectShaderContext;
  protected int texRectVertLoc;
  protected int texRectTCoordLoc;
  protected int texRectSamplerLoc;
  protected int texRectGeoVBO;

  protected float[] texCoords = {
    //  X,     Y,    U,    V
    -1.0f, -1.0f, 0.0f, 0.0f,
    +1.0f, -1.0f, 1.0f, 0.0f,
    -1.0f, +1.0f, 0.0f, 1.0f,
    +1.0f, +1.0f, 1.0f, 1.0f
  };
  protected FloatBuffer texData;

  protected static final String SHADER_PREPROCESSOR_DIRECTIVE =
    "#ifdef GL_ES\n" +
    "precision mediump float;\n" +
    "precision mediump int;\n" +
    "#endif\n";

  protected static String[] texVertShaderSource = {
    "attribute vec2 position;",
    "attribute vec2 texCoord;",
    "varying vec2 vertTexCoord;",
    "void main() {",
    "  gl_Position = vec4(position, 0, 1);",
    "  vertTexCoord = texCoord;",
    "}"
  };

  protected static String[] tex2DFragShaderSource = {
    SHADER_PREPROCESSOR_DIRECTIVE,
    "uniform sampler2D texMap;",
    "varying vec2 vertTexCoord;",
    "void main() {",
   "  gl_FragColor = texture2D(texMap, vertTexCoord.st);",
    "}"
  };

  protected static String[] texRectFragShaderSource = {
    SHADER_PREPROCESSOR_DIRECTIVE,
    "uniform sampler2DRect texMap;",
    "varying vec2 vertTexCoord;",
    "void main() {",
    "  gl_FragColor = texture2DRect(texMap, vertTexCoord.st);",
    "}"
  };

  /** Which texturing targets are enabled */
  protected boolean[] texturingTargets = { false, false };

  /** Used to keep track of which textures are bound to each target */
  protected int maxTexUnits;
  protected int activeTexUnit = 0;
  protected int[][] boundTextures;

  // ........................................................

  // Framerate handling

  protected float targetFps = 60;
  protected float currentFps = 60;
  protected boolean setFps = false;

  // ........................................................

  // Utility buffers

  protected ByteBuffer byteBuffer;
  protected IntBuffer intBuffer;
  protected IntBuffer viewBuffer;

  protected IntBuffer colorBuffer;
  protected FloatBuffer depthBuffer;
  protected ByteBuffer stencilBuffer;

  //........................................................

  // Rendering information

  /** Used to register amount of geometry rendered in each frame. */
  protected int geomCount = 0;
  protected int pgeomCount;

  /** Used to register calls to background. */
  protected boolean clearColor = false;
  protected boolean pclearColor;

  protected boolean clearDepth = false;
  protected boolean pclearDepth;

  protected boolean clearStencil = false;
  protected boolean pclearStencil;


  // ........................................................

  // Error messages

  public static final String WIKI =
    " Read http://wiki.processing.org/w/OpenGL_Issues for help.";

  public static final String FRAMEBUFFER_ERROR =
    "Framebuffer error (%1$s), rendering will probably not work as expected" + WIKI;

  public static final String MISSING_FBO_ERROR =
    "Framebuffer objects are not supported by this hardware (or driver)" + WIKI;

  public static final String MISSING_GLSL_ERROR =
    "GLSL shaders are not supported by this hardware (or driver)" + WIKI;

  public static final String MISSING_GLFUNC_ERROR =
    "GL function %1$s is not available on this hardware (or driver)" + WIKI;

  public static final String UNSUPPORTED_GLPROF_ERROR =
    "Unsupported OpenGL profile.";

  public static final String TEXUNIT_ERROR =
    "Number of texture units not supported by this hardware (or driver)" + WIKI;

  public static final String NONPRIMARY_ERROR =
    "The renderer is trying to call a PGL function that can only be called on a primary PGL. " +
    "This is most likely due to a bug in the renderer's code, please report it with an " +
    "issue on Processing's github page https://github.com/processing/processing/issues?state=open " +
    "if using any of the built-in OpenGL renderers. If you are using a contributed " +
    "library, contact the library's developers.";

  protected static final String DEPTH_READING_NOT_ENABLED_ERROR =
    "Reading depth and stencil values from this multisampled buffer is not enabled. " +
    "You can enable it by calling hint(ENABLE_DEPTH_READING) once. " +
    "If your sketch becomes too slow, disable multisampling with noSmooth() instead.";

  // ........................................................

  // Constants

  /** Size of different types in bytes */
  protected static int SIZEOF_SHORT = Short.SIZE / 8;
  protected static int SIZEOF_INT   = Integer.SIZE / 8;
  protected static int SIZEOF_FLOAT = Float.SIZE / 8;
  protected static int SIZEOF_BYTE  = Byte.SIZE / 8;
  protected static int SIZEOF_INDEX = SIZEOF_SHORT;
  protected static int INDEX_TYPE   = 0x1403; // GL_UNSIGNED_SHORT

  /** Machine Epsilon for float precision. */
  protected static float FLOAT_EPS;
  // Calculation of the Machine Epsilon for float precision. From:
  // http://en.wikipedia.org/wiki/Machine_epsilon#Approximation_using_Java
  static {
    float eps = 1.0f;

    do {
      eps /= 2.0f;
    } while ((float)(1.0 + (eps / 2.0)) != 1.0);

    FLOAT_EPS = eps;
  }

  /**
   * Set to true if the host system is big endian (PowerPC, MIPS, SPARC), false
   * if little endian (x86 Intel for Mac or PC).
   */
  protected static boolean BIG_ENDIAN =
    ByteOrder.nativeOrder() == ByteOrder.BIG_ENDIAN;

  // ........................................................

  // Present mode

  // ........................................................

  // Present mode

  protected boolean presentMode = false;
  protected boolean showStopButton = true;
  public float presentX;
  public float presentY;
  protected IntBuffer closeButtonTex;
  protected int stopButtonColor;
  protected int stopButtonWidth = 28;
  protected int stopButtonHeight = 12;
  protected int stopButtonX = 21; // The position of the close button is relative to the
  protected int closeButtonY = 21; // lower left corner
  protected static int[] closeButtonPix = {
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, 0, 0, 0, -1, -1, -1, -1, -1, 0, 0, 0, -1,
    -1, -1, 0, 0, 0, -1, -1, -1, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, 0, -1, 0,
    0, 0, 0, -1, -1, 0, -1, -1, 0, 0, -1, -1, 0, -1, -1, 0, 0, -1, 0, 0, 0, 0, 0,
    0, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1,
    -1, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, -1, 0, 0, 0, -1,
    0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, -1,
    0, 0, 0, -1, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, -1, 0, -1,
    -1, 0, 0, -1, -1, 0, -1, -1, 0, 0, 0, -1, -1, -1, 0, 0, 0, 0, 0, -1, -1, -1,
    0, 0, 0, -1, -1, -1, 0, 0, 0, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0};


  ///////////////////////////////////////////////////////////////

  // Initialization, finalization


  public PGL() { }


  public PGL(PGraphicsOpenGL pg) {
    this.graphics = pg;
    if (glColorTex == null) {
      glColorFbo = allocateIntBuffer(1);
      glColorTex = allocateIntBuffer(2);
      glDepthStencil = allocateIntBuffer(1);
      glDepth = allocateIntBuffer(1);
      glStencil = allocateIntBuffer(1);

      glMultiFbo = allocateIntBuffer(1);
      glMultiColor = allocateIntBuffer(1);
      glMultiDepthStencil = allocateIntBuffer(1);
      glMultiDepth = allocateIntBuffer(1);
      glMultiStencil = allocateIntBuffer(1);
    }

    byteBuffer = allocateByteBuffer(1);
    intBuffer = allocateIntBuffer(1);
    viewBuffer = allocateIntBuffer(4);
  }


  public void dispose() {
    destroyFBOLayer();
  }


  public void setPrimary(boolean primary) {
    primaryPGL = primary;
  }


  static public int smoothToSamples(int smooth) {
    if (smooth == 0) {
      // smooth(0) is noSmooth(), which is 1x sampling
      return 1;
    } else if (smooth == 1) {
      // smooth(1) means "default smoothing", which is 2x for OpenGL
      return 2;
    } else {
      // smooth(N) can be used for 4x, 8x, etc
      return smooth;
    }
  }


  abstract public Object getNative();


  abstract protected void setFrameRate(float fps);


  abstract protected void initSurface(int antialias);


  abstract protected void reinitSurface();


  abstract protected void registerListeners();


  protected int getReadFramebuffer()  {
    return fboLayerEnabled ? glColorFbo.get(0) : 0;
  }


  protected int getDrawFramebuffer()  {
    if (fboLayerEnabled) return 1 < numSamples ? glMultiFbo.get(0) :
                                                 glColorFbo.get(0);
    else return 0;
  }


  protected int getDefaultDrawBuffer()  {
    return fboLayerEnabled ? COLOR_ATTACHMENT0 : BACK;
  }


  protected int getDefaultReadBuffer()  {
    return fboLayerEnabled ? COLOR_ATTACHMENT0 : FRONT;
  }


  protected boolean isFBOBacked() {
    return fboLayerEnabled;
  }


  @Deprecated
  public void requestFBOLayer() {
    enableFBOLayer();
  }


  public void enableFBOLayer() {
    fboLayerEnabledReq = true;
  }


  public void disableFBOLayer() {
    fboLayerDisableReq = true;
  }


  public void resetFBOLayer() {
    fbolayerResetReq = true;
  }


  protected boolean isMultisampled() {
    return 1 < numSamples;
  }


  abstract protected int getDepthBits();


  abstract protected int getStencilBits();


  protected boolean getDepthTest() {
    intBuffer.rewind();
    getBooleanv(DEPTH_TEST, intBuffer);
    return intBuffer.get(0) != 0;
  }


  protected boolean getDepthWriteMask() {
    intBuffer.rewind();
    getBooleanv(DEPTH_WRITEMASK, intBuffer);
    return intBuffer.get(0) != 0;
  }


  protected Texture wrapBackTexture(Texture texture) {
    if (texture == null) {
      texture = new Texture(graphics);
      texture.init(graphics.width, graphics.height,
                   glColorTex.get(backTex), TEXTURE_2D, RGBA,
                   fboWidth, fboHeight, NEAREST, NEAREST,
                   CLAMP_TO_EDGE, CLAMP_TO_EDGE);
      texture.invertedY(true);
      texture.colorBuffer(true);
      graphics.setCache(graphics, texture);
    } else {
      texture.glName = glColorTex.get(backTex);
    }
    return texture;
  }


  protected Texture wrapFrontTexture(Texture texture)  {
    if (texture == null) {
      texture = new Texture(graphics);
      texture.init(graphics.width, graphics.height,
                   glColorTex.get(frontTex), TEXTURE_2D, RGBA,
                   fboWidth, fboHeight, NEAREST, NEAREST,
                   CLAMP_TO_EDGE, CLAMP_TO_EDGE);
      texture.invertedY(true);
      texture.colorBuffer(true);
    } else {
      texture.glName = glColorTex.get(frontTex);
    }
    return texture;
  }


  protected void bindFrontTexture() {
    usingFrontTex = true;
    if (!texturingIsEnabled(TEXTURE_2D)) {
      enableTexturing(TEXTURE_2D);
    }
    bindTexture(TEXTURE_2D, glColorTex.get(frontTex));
  }


  protected void unbindFrontTexture() {
    if (textureIsBound(TEXTURE_2D, glColorTex.get(frontTex))) {
      // We don't want to unbind another texture
      // that might be bound instead of this one.
      if (!texturingIsEnabled(TEXTURE_2D)) {
        enableTexturing(TEXTURE_2D);
        bindTexture(TEXTURE_2D, 0);
        disableTexturing(TEXTURE_2D);
      } else {
        bindTexture(TEXTURE_2D, 0);
      }
    }
  }


  protected void syncBackTexture() {
    if (usingFrontTex) needSepFrontTex = true;
    if (1 < numSamples) {
      bindFramebufferImpl(READ_FRAMEBUFFER, glMultiFbo.get(0));
      bindFramebufferImpl(DRAW_FRAMEBUFFER, glColorFbo.get(0));
      int mask = COLOR_BUFFER_BIT;
      if (graphics.getHint(PConstants.ENABLE_BUFFER_READING)) {
        mask |= DEPTH_BUFFER_BIT | STENCIL_BUFFER_BIT;
      }
      blitFramebuffer(0, 0, fboWidth, fboHeight,
                      0, 0, fboWidth, fboHeight,
                      mask, NEAREST);
    }
  }


  abstract protected float getPixelScale();

  ///////////////////////////////////////////////////////////

  // Present mode


  public void initPresentMode(float x, float y, int stopColor) {
    presentMode = true;
    showStopButton = stopColor != 0;
    stopButtonColor = stopColor;
    presentX = x;
    presentY = y;
    enableFBOLayer();
  }


  public boolean presentMode() {
    return presentMode;
  }


  public float presentX() {
    return presentX;
  }


  public float presentY() {
    return presentY;
  }


  public boolean insideStopButton(float x, float y) {
    if (!showStopButton) return false;
    return stopButtonX < x && x < stopButtonX + stopButtonWidth &&
           -(closeButtonY + stopButtonHeight) < y && y < -closeButtonY;
  }


  ///////////////////////////////////////////////////////////

  // Frame rendering


  protected void clearDepthStencil() {
    if (!pclearDepth && !pclearStencil) {
      depthMask(true);
      clearDepth(1);
      clearStencil(0);
      clear(DEPTH_BUFFER_BIT | STENCIL_BUFFER_BIT);
    } else if (!pclearDepth) {
      depthMask(true);
      clearDepth(1);
      clear(DEPTH_BUFFER_BIT);
    } else if (!pclearStencil) {
      clearStencil(0);
      clear(STENCIL_BUFFER_BIT);
    }
  }


  protected void clearBackground(float r, float g, float b, float a,
                                 boolean depth, boolean stencil) {
    clearColor(r, g, b, a);
    if (depth && stencil) {
      clearDepth(1);
      clearStencil(0);
      clear(DEPTH_BUFFER_BIT | STENCIL_BUFFER_BIT | COLOR_BUFFER_BIT);
      if (0 < sketch.frameCount) {
        clearDepth = true;
        clearStencil = true;
      }
    } else if (depth) {
      clearDepth(1);
      clear(DEPTH_BUFFER_BIT | COLOR_BUFFER_BIT);
      if (0 < sketch.frameCount) {
        clearDepth = true;
      }
    } else if (stencil) {
      clearStencil(0);
      clear(STENCIL_BUFFER_BIT | COLOR_BUFFER_BIT);
      if (0 < sketch.frameCount) {
        clearStencil = true;
      }
    } else {
      clear(PGL.COLOR_BUFFER_BIT);
    }
    if (0 < sketch.frameCount) {
      clearColor = true;
    }
  }


  protected void beginRender() {
    if (sketch == null) {
      sketch = graphics.parent;
    }

    pgeomCount = geomCount;
    geomCount = 0;

    pclearColor = clearColor;
    clearColor = false;

    pclearDepth = clearDepth;
    clearDepth = false;

    pclearStencil = clearStencil;
    clearStencil = false;

    if (SINGLE_BUFFERED && sketch.frameCount == 1) {
      restoreFirstFrame();
    }

    if (fboLayerEnabledReq) {
      fboLayerEnabled = true;
      fboLayerEnabledReq = false;
    }

    if (fboLayerEnabled) {
      if (fbolayerResetReq) {
        destroyFBOLayer();
        fbolayerResetReq = false;
      }
      if (!fboLayerCreated) {
        createFBOLayer();
      }

      // Draw to the back texture
      bindFramebufferImpl(FRAMEBUFFER, glColorFbo.get(0));
      framebufferTexture2D(FRAMEBUFFER, COLOR_ATTACHMENT0,
                           TEXTURE_2D, glColorTex.get(backTex), 0);

      if (1 < numSamples) {
        bindFramebufferImpl(FRAMEBUFFER, glMultiFbo.get(0));
      }

      if (sketch.frameCount == 0) {
        // No need to draw back color buffer because we are in the first frame.
        int argb = graphics.backgroundColor;
        float ba = ((argb >> 24) & 0xff) / 255.0f;
        float br = ((argb >> 16) & 0xff) / 255.0f;
        float bg = ((argb >> 8) & 0xff) / 255.0f;
        float bb = ((argb) & 0xff) / 255.0f;
        clearColor(br, bg, bb, ba);
        clear(COLOR_BUFFER_BIT);
      } else if (!pclearColor || !sketch.isLooping()) {
        // Render previous back texture (now is the front) as background,
        // because no background() is being used ("incremental drawing")
        int x = 0;
        int y = 0;
        if (presentMode) {
          x = (int)presentX;
          y = (int)presentY;
        }
        float scale = getPixelScale();
        drawTexture(TEXTURE_2D, glColorTex.get(frontTex), fboWidth, fboHeight,
                    x, y, graphics.width, graphics.height,
                    0, 0, (int)(scale * graphics.width), (int)(scale * graphics.height),
                    0, 0, graphics.width, graphics.height);
      }
    }
  }


  protected void endRender(int windowColor) {
    if (fboLayerEnabled) {
      syncBackTexture();

      // Draw the contents of the back texture to the screen framebuffer.
      bindFramebufferImpl(FRAMEBUFFER, 0);

      if (presentMode) {
        float wa = ((windowColor >> 24) & 0xff)  / 255.0f;
        float wr = ((windowColor >> 16) & 0xff) / 255.0f;
        float wg = ((windowColor >> 8) & 0xff) / 255.0f;
        float wb = (windowColor & 0xff) / 255.0f;
        clearDepth(1);
        clearColor(wr, wg, wb, wa);
        clear(COLOR_BUFFER_BIT | DEPTH_BUFFER_BIT);

        if (showStopButton) {
          if (closeButtonTex == null) {
            closeButtonTex = allocateIntBuffer(1);
            genTextures(1, closeButtonTex);
            bindTexture(TEXTURE_2D, closeButtonTex.get(0));
            texParameteri(TEXTURE_2D, TEXTURE_MIN_FILTER, NEAREST);
            texParameteri(TEXTURE_2D, TEXTURE_MAG_FILTER, NEAREST);
            texParameteri(TEXTURE_2D, TEXTURE_WRAP_S, CLAMP_TO_EDGE);
            texParameteri(TEXTURE_2D, TEXTURE_WRAP_T, CLAMP_TO_EDGE);
            texImage2D(TEXTURE_2D, 0, RGBA, stopButtonWidth, stopButtonHeight, 0, RGBA, UNSIGNED_BYTE, null);

            int[] color = new int[closeButtonPix.length];
            PApplet.arrayCopy(closeButtonPix, color);


            // Multiply the texture by the button color
            float ba = ((stopButtonColor >> 24) & 0xFF) / 255f;
            float br = ((stopButtonColor >> 16) & 0xFF) / 255f;
            float bg = ((stopButtonColor >> 8) & 0xFF) / 255f;
            float bb = (stopButtonColor & 0xFF) / 255f;
            for (int i = 0; i < color.length; i++) {
              int c = closeButtonPix[i];
              int a = (int)(ba * ((c >> 24) & 0xFF));
              int r = (int)(br * ((c >> 16) & 0xFF));
              int g = (int)(bg * ((c >> 8) & 0xFF));
              int b = (int)(bb * (c & 0xFF));
              color[i] = javaToNativeARGB((a << 24) | (r << 16) | (g << 8) | b);
            }
            IntBuffer buf = allocateIntBuffer(color);
            copyToTexture(TEXTURE_2D, RGBA, closeButtonTex.get(0), 0, 0, stopButtonWidth, stopButtonHeight, buf);
            bindTexture(TEXTURE_2D, 0);
          }
          drawTexture(TEXTURE_2D, closeButtonTex.get(0), stopButtonWidth, stopButtonHeight,
                      0, 0, stopButtonX + stopButtonWidth, closeButtonY + stopButtonHeight,
                      0, stopButtonHeight, stopButtonWidth, 0,
                      stopButtonX, closeButtonY, stopButtonX + stopButtonWidth, closeButtonY + stopButtonHeight);
          }
      } else {
        clearDepth(1);
        clearColor(0, 0, 0, 0);
        clear(COLOR_BUFFER_BIT | DEPTH_BUFFER_BIT);
      }

      // Render current back texture to screen, without blending.
      disable(BLEND);
      int x = 0;
      int y = 0;
      if (presentMode) {
        x = (int)presentX;
        y = (int)presentY;
      }
      float scale = getPixelScale();
      drawTexture(TEXTURE_2D, glColorTex.get(backTex),
                  fboWidth, fboHeight,
                  x, y, graphics.width, graphics.height,
                  0, 0, (int)(scale * graphics.width), (int)(scale * graphics.height),
                  0, 0, graphics.width, graphics.height);

      // Swapping front and back textures.
      int temp = frontTex;
      frontTex = backTex;
      backTex = temp;

      if (fboLayerDisableReq) {
        fboLayerEnabled = false;
        fboLayerDisableReq = false;
      }
    } else {
      if (SINGLE_BUFFERED && sketch.frameCount == 0) {
        saveFirstFrame();
      }

      if (isFboAllowed()) {
        if (!clearColor && 0 < sketch.frameCount || !sketch.isLooping()) {
          enableFBOLayer();
          if (SINGLE_BUFFERED) {
            createFBOLayer();
          }
        }
      }
    }
  }


  protected abstract void getGL(PGL pgl);


  protected abstract boolean canDraw();


  protected abstract void requestFocus();


  protected abstract void requestDraw();


  protected abstract void swapBuffers();


  public boolean threadIsCurrent()  {
    return Thread.currentThread() == glThread;
  }


  public void setThread(Thread thread) {
    glThread = thread;
  }


  protected void beginGL() { }


  protected void endGL() { }


  private void createFBOLayer() {
    float scale = getPixelScale();

    if (hasNpotTexSupport()) {
      fboWidth = (int)(scale * graphics.width);
      fboHeight = (int)(scale * graphics.height);
    } else {
      fboWidth = nextPowerOfTwo((int)(scale * graphics.width));
      fboHeight = nextPowerOfTwo((int)(scale * graphics.height));
    }

    if (hasFboMultisampleSupport()) {
      int maxs = maxSamples();
      numSamples = PApplet.min(reqNumSamples, maxs);
    } else {
      numSamples = 1;
    }
    boolean multisample = 1 < numSamples;

    boolean packed = hasPackedDepthStencilSupport();
    int depthBits = PApplet.min(REQUESTED_DEPTH_BITS, getDepthBits());
    int stencilBits = PApplet.min(REQUESTED_STENCIL_BITS, getStencilBits());

    genTextures(2, glColorTex);
    for (int i = 0; i < 2; i++) {
      bindTexture(TEXTURE_2D, glColorTex.get(i));
      texParameteri(TEXTURE_2D, TEXTURE_MIN_FILTER, NEAREST);
      texParameteri(TEXTURE_2D, TEXTURE_MAG_FILTER, NEAREST);
      texParameteri(TEXTURE_2D, TEXTURE_WRAP_S, CLAMP_TO_EDGE);
      texParameteri(TEXTURE_2D, TEXTURE_WRAP_T, CLAMP_TO_EDGE);
      texImage2D(TEXTURE_2D, 0, RGBA, fboWidth, fboHeight, 0,
                 RGBA, UNSIGNED_BYTE, null);
      initTexture(TEXTURE_2D, RGBA, fboWidth, fboHeight, graphics.backgroundColor);
    }
    bindTexture(TEXTURE_2D, 0);

    backTex = 0;
    frontTex = 1;

    genFramebuffers(1, glColorFbo);
    bindFramebufferImpl(FRAMEBUFFER, glColorFbo.get(0));
    framebufferTexture2D(FRAMEBUFFER, COLOR_ATTACHMENT0, TEXTURE_2D,
                         glColorTex.get(backTex), 0);

    if (!multisample || graphics.getHint(PConstants.ENABLE_BUFFER_READING)) {
      // If not multisampled, this is the only depth and stencil buffer.
      // If multisampled and depth reading enabled, these are going to
      // hold downsampled depth and stencil buffers.
      createDepthAndStencilBuffer(false, depthBits, stencilBits, packed);
    }

    if (multisample) {
      // Creating multisampled FBO
      genFramebuffers(1, glMultiFbo);
      bindFramebufferImpl(FRAMEBUFFER, glMultiFbo.get(0));

      // color render buffer...
      genRenderbuffers(1, glMultiColor);
      bindRenderbuffer(RENDERBUFFER, glMultiColor.get(0));
      renderbufferStorageMultisample(RENDERBUFFER, numSamples,
                                     RGBA8, fboWidth, fboHeight);
      framebufferRenderbuffer(FRAMEBUFFER, COLOR_ATTACHMENT0,
                              RENDERBUFFER, glMultiColor.get(0));

      // Creating multisampled depth and stencil buffers
      createDepthAndStencilBuffer(true, depthBits, stencilBits, packed);
    }

    int status = validateFramebuffer();

    if (status == FRAMEBUFFER_INCOMPLETE_MULTISAMPLE && 1 < numSamples) {
      System.err.println("Continuing with multisampling disabled");
      reqNumSamples = 1;
      destroyFBOLayer();
      // try again
      createFBOLayer();
      return;
    }

    // Clear all buffers.
    clearDepth(1);
    clearStencil(0);
    int argb = graphics.backgroundColor;
    float ba = ((argb >> 24) & 0xff) / 255.0f;
    float br = ((argb >> 16) & 0xff) / 255.0f;
    float bg = ((argb >> 8) & 0xff) / 255.0f;
    float bb = ((argb) & 0xff) / 255.0f;
    clearColor(br, bg, bb, ba);
    clear(DEPTH_BUFFER_BIT | STENCIL_BUFFER_BIT | COLOR_BUFFER_BIT);

    bindFramebufferImpl(FRAMEBUFFER, 0);
    initFBOLayer();

    fboLayerCreated = true;
  }

  protected abstract void initFBOLayer();


  protected void saveFirstFrame() {
    firstFrame = allocateDirectIntBuffer(graphics.width * graphics.height);
    if (hasReadBuffer()) readBuffer(BACK);
    readPixelsImpl(0, 0, graphics.width, graphics.height, RGBA, UNSIGNED_BYTE, firstFrame);
  }


  protected void restoreFirstFrame() {
    if (firstFrame == null) return;

    IntBuffer tex = allocateIntBuffer(1);
    genTextures(1, tex);

    int w, h;
    float scale = getPixelScale();
    if (hasNpotTexSupport()) {
      w = (int)(scale * graphics.width);
      h = (int)(scale * graphics.height);
    } else {
      w = nextPowerOfTwo((int)(scale * graphics.width));
      h = nextPowerOfTwo((int)(scale * graphics.height));
    }

    bindTexture(TEXTURE_2D, tex.get(0));
    texParameteri(TEXTURE_2D, TEXTURE_MIN_FILTER, NEAREST);
    texParameteri(TEXTURE_2D, TEXTURE_MAG_FILTER, NEAREST);
    texParameteri(TEXTURE_2D, TEXTURE_WRAP_S, CLAMP_TO_EDGE);
    texParameteri(TEXTURE_2D, TEXTURE_WRAP_T, CLAMP_TO_EDGE);
    texImage2D(TEXTURE_2D, 0, RGBA, w, h, 0, RGBA, UNSIGNED_BYTE, null);
    texSubImage2D(TEXTURE_2D, 0, 0, 0, graphics.width, graphics.height, RGBA, UNSIGNED_BYTE, firstFrame);

    drawTexture(TEXTURE_2D, tex.get(0), w, h,
                0, 0, graphics.width, graphics.height,
                0, 0, (int)(scale * graphics.width), (int)(scale * graphics.height),
                0, 0, graphics.width, graphics.height);

    deleteTextures(1, tex);
    firstFrame.clear();
    firstFrame = null;
  }

  protected void destroyFBOLayer() {
    if (threadIsCurrent() && fboLayerCreated) {
      deleteFramebuffers(1, glColorFbo);
      deleteTextures(2, glColorTex);
      deleteRenderbuffers(1, glDepthStencil);
      deleteRenderbuffers(1, glDepth);
      deleteRenderbuffers(1, glStencil);

      deleteFramebuffers(1, glMultiFbo);
      deleteRenderbuffers(1, glMultiColor);
      deleteRenderbuffers(1, glMultiDepthStencil);
      deleteRenderbuffers(1, glMultiDepth);
      deleteRenderbuffers(1, glMultiStencil);
    }
    fboLayerCreated = false;
  }


  private void createDepthAndStencilBuffer(boolean multisample, int depthBits,
                                           int stencilBits, boolean packed) {
    // Creating depth and stencil buffers
    if (packed && depthBits == 24 && stencilBits == 8) {
      // packed depth+stencil buffer
      IntBuffer depthStencilBuf =
          multisample ? glMultiDepthStencil : glDepthStencil;
      genRenderbuffers(1, depthStencilBuf);
      bindRenderbuffer(RENDERBUFFER, depthStencilBuf.get(0));
      if (multisample) {
        renderbufferStorageMultisample(RENDERBUFFER, numSamples,
                                       DEPTH24_STENCIL8, fboWidth, fboHeight);
      } else {
        renderbufferStorage(RENDERBUFFER, DEPTH24_STENCIL8,
                            fboWidth, fboHeight);
      }
      framebufferRenderbuffer(FRAMEBUFFER, DEPTH_ATTACHMENT, RENDERBUFFER,
                              depthStencilBuf.get(0));
      framebufferRenderbuffer(FRAMEBUFFER, STENCIL_ATTACHMENT, RENDERBUFFER,
                              depthStencilBuf.get(0));
    } else {
      // separate depth and stencil buffers
      if (0 < depthBits) {
        int depthComponent = DEPTH_COMPONENT16;
        if (depthBits == 32) {
          depthComponent = DEPTH_COMPONENT32;
        } else if (depthBits == 24) {
          depthComponent = DEPTH_COMPONENT24;
        //} else if (depthBits == 16) {
          //depthComponent = DEPTH_COMPONENT16;
        }

        IntBuffer depthBuf = multisample ? glMultiDepth : glDepth;
        genRenderbuffers(1, depthBuf);
        bindRenderbuffer(RENDERBUFFER, depthBuf.get(0));
        if (multisample) {
          renderbufferStorageMultisample(RENDERBUFFER, numSamples,
                                         depthComponent, fboWidth, fboHeight);
        } else {
          renderbufferStorage(RENDERBUFFER, depthComponent,
                              fboWidth, fboHeight);
        }
        framebufferRenderbuffer(FRAMEBUFFER, DEPTH_ATTACHMENT,
                                RENDERBUFFER, depthBuf.get(0));
      }

      if (0 < stencilBits) {
        int stencilIndex = STENCIL_INDEX1;
        if (stencilBits == 8) {
          stencilIndex = STENCIL_INDEX8;
        } else if (stencilBits == 4) {
          stencilIndex = STENCIL_INDEX4;
        //} else if (stencilBits == 1) {
          //stencilIndex = STENCIL_INDEX1;
        }

        IntBuffer stencilBuf = multisample ? glMultiStencil : glStencil;
        genRenderbuffers(1, stencilBuf);
        bindRenderbuffer(RENDERBUFFER, stencilBuf.get(0));
        if (multisample) {
          renderbufferStorageMultisample(RENDERBUFFER, numSamples,
                                         stencilIndex, fboWidth, fboHeight);
        } else {
          renderbufferStorage(RENDERBUFFER, stencilIndex,
                              fboWidth, fboHeight);
        }
        framebufferRenderbuffer(FRAMEBUFFER, STENCIL_ATTACHMENT,
                                RENDERBUFFER, stencilBuf.get(0));
      }
    }
  }


  ///////////////////////////////////////////////////////////

  // Context interface


  protected int createEmptyContext() {
    return -1;
  }


  protected int getCurrentContext() {
    return glContext;
  }


  ///////////////////////////////////////////////////////////

  // Utility functions


  protected boolean contextIsCurrent(int other) {
    return other == -1 || other == glContext;
  }


  protected void enableTexturing(int target) {
    if (target == TEXTURE_2D) {
      texturingTargets[0] = true;
    } else if (target == TEXTURE_RECTANGLE) {
      texturingTargets[1] = true;
    }
  }


  protected void disableTexturing(int target) {
    if (target == TEXTURE_2D) {
      texturingTargets[0] = false;
    } else if (target == TEXTURE_RECTANGLE) {
      texturingTargets[1] = false;
    }
  }


  protected boolean texturingIsEnabled(int target) {
    if (target == TEXTURE_2D) {
      return texturingTargets[0];
    } else if (target == TEXTURE_RECTANGLE) {
      return texturingTargets[1];
    } else {
      return false;
    }
  }


  protected boolean textureIsBound(int target, int id) {
    if (boundTextures == null) return false;

    if (target == TEXTURE_2D) {
      return boundTextures[activeTexUnit][0] == id;
    } else if (target == TEXTURE_RECTANGLE) {
      return boundTextures[activeTexUnit][1] == id;
    } else {
      return false;
    }
  }


  protected void initTexture(int target, int format, int width, int height) {
    initTexture(target, format, width, height, 0);
  }


  protected void initTexture(int target, int format, int width, int height,
                            int initColor) {
    int[] glcolor = new int[16 * 16];
    Arrays.fill(glcolor, javaToNativeARGB(initColor));
    IntBuffer texels = allocateDirectIntBuffer(16 * 16);
    texels.put(glcolor);
    texels.rewind();
    for (int y = 0; y < height; y += 16) {
      int h = PApplet.min(16, height - y);
      for (int x = 0; x < width; x += 16) {
        int w = PApplet.min(16, width - x);
        texSubImage2D(target, 0, x, y, w, h, format, UNSIGNED_BYTE, texels);
      }
    }
  }


  protected void copyToTexture(int target, int format, int id, int x, int y,
                               int w, int h, int[] buffer) {
    copyToTexture(target, format, id, x, y, w, h, IntBuffer.wrap(buffer));

  }

  protected void copyToTexture(int target, int format, int id, int x, int y,
                               int w, int h, IntBuffer buffer) {
    activeTexture(TEXTURE0);
    boolean enabledTex = false;
    if (!texturingIsEnabled(target)) {
      enableTexturing(target);
      enabledTex = true;
    }
    bindTexture(target, id);
    texSubImage2D(target, 0, x, y, w, h, format, UNSIGNED_BYTE, buffer);
    bindTexture(target, 0);
    if (enabledTex) {
      disableTexturing(target);
    }
  }


  /**
   * Not an approved function, this will change or be removed in the future.
   */
  public void drawTexture(int target, int id, int width, int height,
                          int X0, int Y0, int X1, int Y1) {
    // If a texture is drawing on a viewport of the same size as its resolution,
    // the pixel factor is 1:1, so we override the surface's pixel factor.
    drawTexture(target, id, width, height,
                0, 0, width, height, 1,
                X0, Y0, X1, Y1,
                X0, Y0, X1, Y1);
  }


  /**
   * Not an approved function, this will change or be removed in the future.
   */
  public void drawTexture(int target, int id,int texW, int texH,
                          int viewX, int viewY, int viewW, int viewH,
                          int texX0, int texY0, int texX1, int texY1,
                          int scrX0, int scrY0, int scrX1, int scrY1) {
    int viewF = (int)getPixelScale();
    drawTexture(target, id, texW, texH,
                viewX, viewY, viewW, viewH, viewF,
                texX0, texY0, texX1, texY1,
                scrX0, scrY0, scrX1, scrY1);
  }


  public void drawTexture(int target, int id,int texW, int texH,
                          int viewX, int viewY, int viewW, int viewH, int viewF,
                          int texX0, int texY0, int texX1, int texY1,
                          int scrX0, int scrY0, int scrX1, int scrY1) {
    if (target == TEXTURE_2D) {
      drawTexture2D(id, texW, texH,
                    viewX, viewY, viewW, viewH, viewF,
                    texX0, texY0, texX1, texY1,
                    scrX0, scrY0, scrX1, scrY1);
    } else if (target == TEXTURE_RECTANGLE) {
      drawTextureRect(id, texW, texH,
                      viewX, viewY, viewW, viewH, viewF,
                      texX0, texY0, texX1, texY1,
                      scrX0, scrY0, scrX1, scrY1);
    }
  }


  protected PGL initTex2DShader() {
    PGL ppgl = primaryPGL ? this : graphics.getPrimaryPGL();

    if (!ppgl.loadedTex2DShader || ppgl.tex2DShaderContext != ppgl.glContext) {
      String[] preprocVertSrc = preprocessVertexSource(texVertShaderSource, getGLSLVersion(), getGLSLVersionSuffix());
      String vertSource = PApplet.join(preprocVertSrc, "\n");
      String[] preprocFragSrc = preprocessFragmentSource(tex2DFragShaderSource, getGLSLVersion(), getGLSLVersionSuffix());
      String fragSource = PApplet.join(preprocFragSrc, "\n");
      ppgl.tex2DVertShader = createShader(VERTEX_SHADER, vertSource);
      ppgl.tex2DFragShader = createShader(FRAGMENT_SHADER, fragSource);
      if (0 < ppgl.tex2DVertShader && 0 < ppgl.tex2DFragShader) {
        ppgl.tex2DShaderProgram = createProgram(ppgl.tex2DVertShader, ppgl.tex2DFragShader);
      }
      if (0 < ppgl.tex2DShaderProgram) {
        ppgl.tex2DVertLoc = getAttribLocation(ppgl.tex2DShaderProgram, "position");
        ppgl.tex2DTCoordLoc = getAttribLocation(ppgl.tex2DShaderProgram, "texCoord");
        ppgl.tex2DSamplerLoc = getUniformLocation(ppgl.tex2DShaderProgram, "texMap");
      }
      ppgl.loadedTex2DShader = true;
      ppgl.tex2DShaderContext = ppgl.glContext;

      genBuffers(1, intBuffer);
      ppgl.tex2DGeoVBO = intBuffer.get(0);
      bindBuffer(ARRAY_BUFFER, ppgl.tex2DGeoVBO);
      bufferData(ARRAY_BUFFER, 16 * SIZEOF_FLOAT, null, STATIC_DRAW);
    }

    if (texData == null) {
      texData = allocateDirectFloatBuffer(texCoords.length);
    }

    return ppgl;
  }


  protected void drawTexture2D(int id, int texW, int texH,
                               int viewX, int viewY, int viewW, int viewH, int viewF,
                               int texX0, int texY0, int texX1, int texY1,
                               int scrX0, int scrY0, int scrX1, int scrY1) {
    PGL ppgl = initTex2DShader();

    if (0 < ppgl.tex2DShaderProgram) {
      // The texture overwrites anything drawn earlier.
      boolean depthTest = getDepthTest();
      disable(DEPTH_TEST);

      // When drawing the texture we don't write to the
      // depth mask, so the texture remains in the background
      // and can be occluded by anything drawn later, even if
      // if it is behind it.
      boolean depthMask = getDepthWriteMask();
      depthMask(false);

      // Making sure that the viewport matches the provided screen dimensions
      viewBuffer.rewind();
      getIntegerv(VIEWPORT, viewBuffer);
      viewportImpl(viewF * viewX, viewF * viewY, viewF * viewW, viewF * viewH);

      useProgram(ppgl.tex2DShaderProgram);

      enableVertexAttribArray(ppgl.tex2DVertLoc);
      enableVertexAttribArray(ppgl.tex2DTCoordLoc);

      // Vertex coordinates of the textured quad are specified
      // in normalized screen space (-1, 1):
      // Corner 1
      texCoords[ 0] = 2 * (float)scrX0 / viewW - 1;
      texCoords[ 1] = 2 * (float)scrY0 / viewH - 1;
      texCoords[ 2] = (float)texX0 / texW;
      texCoords[ 3] = (float)texY0 / texH;
      // Corner 2
      texCoords[ 4] = 2 * (float)scrX1 / viewW - 1;
      texCoords[ 5] = 2 * (float)scrY0 / viewH - 1;
      texCoords[ 6] = (float)texX1 / texW;
      texCoords[ 7] = (float)texY0 / texH;
      // Corner 3
      texCoords[ 8] = 2 * (float)scrX0 / viewW - 1;
      texCoords[ 9] = 2 * (float)scrY1 / viewH - 1;
      texCoords[10] = (float)texX0 / texW;
      texCoords[11] = (float)texY1 / texH;
      // Corner 4
      texCoords[12] = 2 * (float)scrX1 / viewW - 1;
      texCoords[13] = 2 * (float)scrY1 / viewH - 1;
      texCoords[14] = (float)texX1 / texW;
      texCoords[15] = (float)texY1 / texH;

      texData.rewind();
      texData.put(texCoords);

      activeTexture(TEXTURE0);
      boolean enabledTex = false;
      if (!texturingIsEnabled(TEXTURE_2D)) {
        enableTexturing(TEXTURE_2D);
        enabledTex = true;
      }
      bindTexture(TEXTURE_2D, id);
      uniform1i(ppgl.tex2DSamplerLoc, 0);

      texData.position(0);
      bindBuffer(ARRAY_BUFFER, ppgl.tex2DGeoVBO);
      bufferData(ARRAY_BUFFER, 16 * SIZEOF_FLOAT, texData, STATIC_DRAW);

      vertexAttribPointer(ppgl.tex2DVertLoc, 2, FLOAT, false, 4 * SIZEOF_FLOAT, 0);
      vertexAttribPointer(ppgl.tex2DTCoordLoc, 2, FLOAT, false, 4 * SIZEOF_FLOAT, 2 * SIZEOF_FLOAT);

      drawArrays(TRIANGLE_STRIP, 0, 4);

      bindBuffer(ARRAY_BUFFER, 0); // Making sure that no VBO is bound at this point.

      bindTexture(TEXTURE_2D, 0);
      if (enabledTex) {
        disableTexturing(TEXTURE_2D);
      }

      disableVertexAttribArray(ppgl.tex2DVertLoc);
      disableVertexAttribArray(ppgl.tex2DTCoordLoc);

      useProgram(0);

      if (depthTest) {
        enable(DEPTH_TEST);
      } else {
        disable(DEPTH_TEST);
      }
      depthMask(depthMask);

      viewportImpl(viewBuffer.get(0), viewBuffer.get(1),
                   viewBuffer.get(2), viewBuffer.get(3));
    }
  }


  protected PGL initTexRectShader() {
    PGL ppgl = primaryPGL ? this : graphics.getPrimaryPGL();

    if (!ppgl.loadedTexRectShader || ppgl.texRectShaderContext != ppgl.glContext) {
      String[] preprocVertSrc = preprocessVertexSource(texVertShaderSource, getGLSLVersion(), getGLSLVersionSuffix());
      String vertSource = PApplet.join(preprocVertSrc, "\n");
      String[] preprocFragSrc = preprocessFragmentSource(texRectFragShaderSource, getGLSLVersion(), getGLSLVersionSuffix());
      String fragSource = PApplet.join(preprocFragSrc, "\n");
      ppgl.texRectVertShader = createShader(VERTEX_SHADER, vertSource);
      ppgl.texRectFragShader = createShader(FRAGMENT_SHADER, fragSource);
      if (0 < ppgl.texRectVertShader && 0 < ppgl.texRectFragShader) {
        ppgl.texRectShaderProgram = createProgram(ppgl.texRectVertShader,
                                                  ppgl.texRectFragShader);
      }
      if (0 < ppgl.texRectShaderProgram) {
        ppgl.texRectVertLoc = getAttribLocation(ppgl.texRectShaderProgram, "position");
        ppgl.texRectTCoordLoc = getAttribLocation(ppgl.texRectShaderProgram, "texCoord");
        ppgl.texRectSamplerLoc = getUniformLocation(ppgl.texRectShaderProgram, "texMap");
      }
      ppgl.loadedTexRectShader = true;
      ppgl.texRectShaderContext = ppgl.glContext;

      genBuffers(1, intBuffer);
      ppgl.texRectGeoVBO = intBuffer.get(0);
      bindBuffer(ARRAY_BUFFER, ppgl.texRectGeoVBO);
      bufferData(ARRAY_BUFFER, 16 * SIZEOF_FLOAT, null, STATIC_DRAW);
    }

    return ppgl;
  }


  protected void drawTextureRect(int id, int texW, int texH,
                                 int viewX, int viewY, int viewW, int viewH, int viewF,
                                 int texX0, int texY0, int texX1, int texY1,
                                 int scrX0, int scrY0, int scrX1, int scrY1) {
    PGL ppgl = initTexRectShader();

    if (texData == null) {
      texData = allocateDirectFloatBuffer(texCoords.length);
    }

    if (0 < ppgl.texRectShaderProgram) {
      // The texture overwrites anything drawn earlier.
      boolean depthTest = getDepthTest();
      disable(DEPTH_TEST);

      // When drawing the texture we don't write to the
      // depth mask, so the texture remains in the background
      // and can be occluded by anything drawn later, even if
      // if it is behind it.
      boolean depthMask = getDepthWriteMask();
      depthMask(false);

      // Making sure that the viewport matches the provided screen dimensions
      viewBuffer.rewind();
      getIntegerv(VIEWPORT, viewBuffer);
      viewportImpl(viewF * viewX, viewF * viewY, viewF * viewW, viewF * viewH);

      useProgram(ppgl.texRectShaderProgram);

      enableVertexAttribArray(ppgl.texRectVertLoc);
      enableVertexAttribArray(ppgl.texRectTCoordLoc);

      // Vertex coordinates of the textured quad are specified
      // in normalized screen space (-1, 1):
      // Corner 1
      texCoords[ 0] = 2 * (float)scrX0 / viewW - 1;
      texCoords[ 1] = 2 * (float)scrY0 / viewH - 1;
      texCoords[ 2] = texX0;
      texCoords[ 3] = texY0;
      // Corner 2
      texCoords[ 4] = 2 * (float)scrX1 / viewW - 1;
      texCoords[ 5] = 2 * (float)scrY0 / viewH - 1;
      texCoords[ 6] = texX1;
      texCoords[ 7] = texY0;
      // Corner 3
      texCoords[ 8] = 2 * (float)scrX0 / viewW - 1;
      texCoords[ 9] = 2 * (float)scrY1 / viewH - 1;
      texCoords[10] = texX0;
      texCoords[11] = texY1;
      // Corner 4
      texCoords[12] = 2 * (float)scrX1 / viewW - 1;
      texCoords[13] = 2 * (float)scrY1 / viewH - 1;
      texCoords[14] = texX1;
      texCoords[15] = texY1;

      texData.rewind();
      texData.put(texCoords);

      activeTexture(TEXTURE0);
      boolean enabledTex = false;
      if (!texturingIsEnabled(TEXTURE_RECTANGLE)) {
        enableTexturing(TEXTURE_RECTANGLE);
        enabledTex = true;
      }
      bindTexture(TEXTURE_RECTANGLE, id);
      uniform1i(ppgl.texRectSamplerLoc, 0);

      texData.position(0);
      bindBuffer(ARRAY_BUFFER, ppgl.texRectGeoVBO);
      bufferData(ARRAY_BUFFER, 16 * SIZEOF_FLOAT, texData, STATIC_DRAW);

      vertexAttribPointer(ppgl.texRectVertLoc, 2, FLOAT, false, 4 * SIZEOF_FLOAT, 0);
      vertexAttribPointer(ppgl.texRectTCoordLoc, 2, FLOAT, false, 4 * SIZEOF_FLOAT, 2 * SIZEOF_FLOAT);

      drawArrays(TRIANGLE_STRIP, 0, 4);

      bindBuffer(ARRAY_BUFFER, 0); // Making sure that no VBO is bound at this point.

      bindTexture(TEXTURE_RECTANGLE, 0);
      if (enabledTex) {
        disableTexturing(TEXTURE_RECTANGLE);
      }

      disableVertexAttribArray(ppgl.texRectVertLoc);
      disableVertexAttribArray(ppgl.texRectTCoordLoc);

      useProgram(0);

      if (depthTest) {
        enable(DEPTH_TEST);
      } else {
        disable(DEPTH_TEST);
      }
      depthMask(depthMask);

      viewportImpl(viewBuffer.get(0), viewBuffer.get(1),
                   viewBuffer.get(2), viewBuffer.get(3));
    }
  }


  protected int getColorValue(int scrX, int scrY) {
    if (colorBuffer == null) {
      colorBuffer = IntBuffer.allocate(1);
    }
    colorBuffer.rewind();
    readPixels(scrX, graphics.height - scrY - 1, 1, 1, RGBA, UNSIGNED_BYTE,
               colorBuffer);
    return colorBuffer.get();
  }


  protected float getDepthValue(int scrX, int scrY) {
    if (depthBuffer == null) {
      depthBuffer = FloatBuffer.allocate(1);
    }
    depthBuffer.rewind();
    readPixels(scrX, graphics.height - scrY - 1, 1, 1, DEPTH_COMPONENT, FLOAT,
               depthBuffer);
    return depthBuffer.get(0);
  }


  protected byte getStencilValue(int scrX, int scrY) {
    if (stencilBuffer == null) {
      stencilBuffer = ByteBuffer.allocate(1);
    }
    stencilBuffer.rewind();
    readPixels(scrX, graphics.height - scrY - 1, 1, 1, STENCIL_INDEX,
               UNSIGNED_BYTE, stencilBuffer);
    return stencilBuffer.get(0);
  }


  protected static boolean isPowerOfTwo(int val) {
    return (val & (val - 1)) == 0;
  }


  // bit shifting this might be more efficient
  protected static int nextPowerOfTwo(int val) {
    int ret = 1;
    while (ret < val) {
      ret <<= 1;
    }
    return ret;
  }


  /**
   * Converts input native OpenGL value (RGBA on big endian, ABGR on little
   * endian) to Java ARGB.
   */
  protected static int nativeToJavaARGB(int color) {
    if (BIG_ENDIAN) { // RGBA to ARGB
      return (color >>> 8) | (color << 24);
    } else { // ABGR to ARGB
      int rb = color & 0x00FF00FF;
      return (color & 0xFF00FF00) | (rb << 16) | (rb >> 16);
    }
  }


  /**
   * Converts input array of native OpenGL values (RGBA on big endian, ABGR on
   * little endian) representing an image of width x height resolution to Java
   * ARGB. It also rearranges the elements in the array so that the image is
   * flipped vertically.
   */
  protected static void nativeToJavaARGB(int[] pixels, int width, int height) {
    int index = 0;
    int yindex = (height - 1) * width;
    for (int y = 0; y < height / 2; y++) {
      for (int x = 0; x < width; x++) {
        int pixy = pixels[yindex];
        int pixi = pixels[index];
        if (BIG_ENDIAN) { // RGBA to ARGB
          pixels[index] = (pixy >>> 8) | (pixy << 24);
          pixels[yindex] = (pixi >>> 8) | (pixi << 24);
        } else { // ABGR to ARGB
          int rbi = pixi & 0x00FF00FF;
          int rby = pixy & 0x00FF00FF;
          pixels[index] = (pixy & 0xFF00FF00) | (rby << 16) | (rby >> 16);
          pixels[yindex] = (pixi & 0xFF00FF00) | (rbi << 16) | (rbi >> 16);
        }
        index++;
        yindex++;
      }
      yindex -= width * 2;
    }

    if ((height % 2) == 1) { // Converts center row
      index = (height / 2) * width;
      for (int x = 0; x < width; x++) {
        int pixi = pixels[index];
        if (BIG_ENDIAN) { // RGBA to ARGB
          pixels[index] = (pixi >>> 8) | (pixi << 24);
        } else { // ABGR to ARGB
          int rbi = pixi & 0x00FF00FF;
          pixels[index] = (pixi & 0xFF00FF00) | (rbi << 16) | (rbi >> 16);
        }
        index++;
      }
    }
  }


  /**
   * Converts input native OpenGL value (RGBA on big endian, ABGR on little
   * endian) to Java RGB, so that the alpha component of the result is set
   * to opaque (255).
   */
  protected static int nativeToJavaRGB(int color) {
    if (BIG_ENDIAN) { // RGBA to ARGB
      return (color >>> 8) | 0xFF000000;
    } else { // ABGR to ARGB
      int rb = color & 0x00FF00FF;
      return 0xFF000000 | (rb << 16) |
             (color & 0x0000FF00) | (rb >> 16);
    }
  }


  /**
   * Converts input array of native OpenGL values (RGBA on big endian, ABGR on
   * little endian) representing an image of width x height resolution to Java
   * RGB, so that the alpha component of all pixels is set to opaque (255). It
   * also rearranges the elements in the array so that the image is flipped
   * vertically.
   */
  protected static void nativeToJavaRGB(int[] pixels, int width, int height) {
    int index = 0;
    int yindex = (height - 1) * width;
    for (int y = 0; y < height / 2; y++) {
      for (int x = 0; x < width; x++) {
        int pixy = pixels[yindex];
        int pixi = pixels[index];
        if (BIG_ENDIAN) { // RGBA to ARGB
          pixels[index] = (pixy >>> 8) | 0xFF000000;
          pixels[yindex] = (pixi >>> 8) | 0xFF000000;
        } else { // ABGR to ARGB
          int rbi = pixi & 0x00FF00FF;
          int rby = pixy & 0x00FF00FF;
          pixels[index] = 0xFF000000 | (rby << 16) |
                          (pixy & 0x0000FF00) | (rby >> 16);
          pixels[yindex] = 0xFF000000 | (rbi << 16) |
                           (pixi & 0x0000FF00) | (rbi >> 16);
        }
        index++;
        yindex++;
      }
      yindex -= width * 2;
    }

    if ((height % 2) == 1) { // Converts center row
      index = (height / 2) * width;
      for (int x = 0; x < width; x++) {
        int pixi = pixels[index];
        if (BIG_ENDIAN) { // RGBA to ARGB
          pixels[index] = (pixi >>> 8) | 0xFF000000;
        } else { // ABGR to ARGB
          int rbi = pixi & 0x00FF00FF;
          pixels[index] = 0xFF000000 | (rbi << 16) |
                          (pixi & 0x000FF00) | (rbi >> 16);
        }
        index++;
      }
    }
  }


  /**
   * Converts input Java ARGB value to native OpenGL format (RGBA on big endian,
   * BGRA on little endian).
   */
  protected static int javaToNativeARGB(int color) {
    if (BIG_ENDIAN) { // ARGB to RGBA
      return (color >>> 24) | (color << 8);
    } else { // ARGB to ABGR
      int rb = color & 0x00FF00FF;
      return (color & 0xFF00FF00) | (rb << 16) | (rb >> 16);
    }
  }


  /**
   * Converts input array of Java ARGB values representing an image of width x
   * height resolution to native OpenGL format (RGBA on big endian, BGRA on
   * little endian). It also rearranges the elements in the array so that the
   * image is flipped vertically.
   */
  protected static void javaToNativeARGB(int[] pixels, int width, int height) {
    int index = 0;
    int yindex = (height - 1) * width;
    for (int y = 0; y < height / 2; y++) {
      for (int x = 0; x < width; x++) {
        int pixy = pixels[yindex];
        int pixi = pixels[index];
        if (BIG_ENDIAN) { // ARGB to RGBA
          pixels[index] = (pixy >>> 24) | (pixy << 8);
          pixels[yindex] = (pixi >>> 24) | (pixi << 8);
        } else { // ARGB to ABGR
          int rbi = pixi & 0x00FF00FF;
          int rby = pixy & 0x00FF00FF;
          pixels[index] = (pixy & 0xFF00FF00) | (rby << 16) | (rby >> 16);
          pixels[yindex] = (pixi & 0xFF00FF00) | (rbi << 16) | (rbi >> 16);
        }
        index++;
        yindex++;
      }
      yindex -= width * 2;
    }

    if ((height % 2) == 1) { // Converts center row
      index = (height / 2) * width;
      for (int x = 0; x < width; x++) {
        int pixi = pixels[index];
        if (BIG_ENDIAN) { // ARGB to RGBA
          pixels[index] = (pixi >>> 24) | (pixi << 8);
        } else { // ARGB to ABGR
          int rbi = pixi & 0x00FF00FF;
          pixels[index] = (pixi & 0xFF00FF00) | (rbi << 16) | (rbi >> 16);
        }
        index++;
      }
    }
  }


  /**
   * Converts input Java ARGB value to native OpenGL format (RGBA on big endian,
   * BGRA on little endian), setting alpha component to opaque (255).
   */
  protected static int javaToNativeRGB(int color) {
    if (BIG_ENDIAN) { // ARGB to RGB
      return 0xFF | (color << 8);
    } else { // ARGB to BGR
      int rb = color & 0x00FF00FF;
      return 0xFF000000 | (rb << 16) | (color & 0x0000FF00) | (rb >> 16);
    }
  }


  /**
   * Converts input array of Java ARGB values representing an image of width x
   * height resolution to native OpenGL format (RGBA on big endian, BGRA on
   * little endian), while setting alpha component of all pixels to opaque
   * (255). It also rearranges the elements in the array so that the image is
   * flipped vertically.
   */
  protected static void javaToNativeRGB(int[] pixels, int width, int height) {
    int index = 0;
    int yindex = (height - 1) * width;
    for (int y = 0; y < height / 2; y++) {
      for (int x = 0; x < width; x++) {
        int pixy = pixels[yindex];
        int pixi = pixels[index];
        if (BIG_ENDIAN) { // ARGB to RGB
          pixels[index] = 0xFF | (pixy << 8);
          pixels[yindex] = 0xFF | (pixi << 8);
        } else { // ARGB to BGR
          int rbi = pixi & 0x00FF00FF;
          int rby = pixy & 0x00FF00FF;
          pixels[index] = 0xFF000000 | (rby << 16) |
                          (pixy & 0x0000FF00) | (rby >> 16);
          pixels[yindex] = 0xFF000000 | (rbi << 16) |
                           (pixi & 0x0000FF00) | (rbi >> 16);
        }
        index++;
        yindex++;
      }
      yindex -= width * 2;
    }

    if ((height % 2) == 1) { // Converts center row
      index = (height / 2) * width;
      for (int x = 0; x < width; x++) {
        int pixi = pixels[index];
        if (BIG_ENDIAN) { // ARGB to RGB
          pixels[index] = 0xFF | (pixi << 8);
        } else { // ARGB to BGR
          int rbi = pixi & 0x00FF00FF;
          pixels[index] = 0xFF000000 | (rbi << 16) |
                          (pixi & 0x0000FF00) | (rbi >> 16);
        }
        index++;
      }
    }
  }


  protected static int qualityToSamples(int quality) {
    if (quality <= 1) {
      return 1;
    } else {
      // Number of samples is always an even number:
      return 2 * (quality / 2);
    }
  }


  abstract protected int getGLSLVersion();
  abstract protected String getGLSLVersionSuffix();


  protected String[] loadVertexShader(String filename) {
    return sketch.loadStrings(filename);
  }


  protected String[] loadFragmentShader(String filename) {
    return sketch.loadStrings(filename);
  }


  protected String[] loadFragmentShader(URL url) {
    try {
      return PApplet.loadStrings(url.openStream());
    } catch (IOException e) {
      PGraphics.showException("Cannot load fragment shader " + url.getFile());
    }
    return null;
  }


  protected String[] loadVertexShader(URL url) {
    try {
      return PApplet.loadStrings(url.openStream());
    } catch (IOException e) {
      PGraphics.showException("Cannot load vertex shader " + url.getFile());
    }
    return null;
  }


  protected String[] loadVertexShader(String filename, int version, String versionSuffix) {
    return loadVertexShader(filename);
  }


  protected String[] loadFragmentShader(String filename, int version, String versionSuffix) {
    return loadFragmentShader(filename);
  }


  protected String[] loadFragmentShader(URL url, int version, String versionSuffix) {
    return loadFragmentShader(url);
  }


  protected String[] loadVertexShader(URL url, int version, String versionSuffix) {
    return loadVertexShader(url);
  }


  protected static String[] preprocessFragmentSource(String[] fragSrc0,
                                                     int version,
                                                     String versionSuffix) {
    if (containsVersionDirective(fragSrc0)) {
      // The user knows what she or he is doing
      return fragSrc0;
    }

    String[] fragSrc;

    if (version < 130) {
      Pattern[] search = { };
      String[] replace = { };
      int offset = 1;

      fragSrc = preprocessShaderSource(fragSrc0, search, replace, offset);
      fragSrc[0] = "#version " + version + versionSuffix;
    } else {
      // We need to replace 'texture' uniform by 'texMap' uniform and
      // 'textureXXX()' functions by 'texture()' functions. Order of these
      // replacements is important to prevent collisions between these two.
      Pattern[] search = new Pattern[] {
          Pattern.compile(String.format(GLSL_ID_REGEX, "varying|attribute")),
          Pattern.compile(String.format(GLSL_ID_REGEX, "texture")),
          Pattern.compile(String.format(GLSL_FN_REGEX, "texture2DRect|texture2D|texture3D|textureCube")),
          Pattern.compile(String.format(GLSL_ID_REGEX, "gl_FragColor"))
      };
      String[] replace = new String[] {
          "in", "texMap", "texture", "_fragColor"
      };
      int offset = 2;

      fragSrc = preprocessShaderSource(fragSrc0, search, replace, offset);
      fragSrc[0] = "#version " + version + versionSuffix;
      if (" es".equals(versionSuffix)) {
        fragSrc[1] = "out mediump vec4 _fragColor;";
      } else {
        fragSrc[1] = "out vec4 _fragColor;";
      }
    }

    return fragSrc;
  }

  protected static String[] preprocessVertexSource(String[] vertSrc0,
                                                   int version,
                                                   String versionSuffix) {
    if (containsVersionDirective(vertSrc0)) {
      // The user knows what she or he is doing
      return vertSrc0;
    }

    String[] vertSrc;

    Pattern[] search;
    String[] replace;
    int offset = 1;
    if (version < 130) {
      search = new Pattern[] { };
      replace = new String[] { };

    } else {
      // We need to replace 'texture' uniform by 'texMap' uniform and
      // 'textureXXX()' functions by 'texture()' functions. Order of these
      // replacements is important to prevent collisions between these two.
      search = new Pattern[] {
        Pattern.compile(String.format(GLSL_ID_REGEX, "varying")),
        Pattern.compile(String.format(GLSL_ID_REGEX, "attribute")),
        Pattern.compile(String.format(GLSL_ID_REGEX, "texture")),
        Pattern.compile(String.format(GLSL_FN_REGEX, "texture2DRect|texture2D|texture3D|textureCube"))
      };
      replace = new String[]{
        "out", "in", "texMap", "texture",
      };
    }
    vertSrc = preprocessShaderSource(vertSrc0, search, replace, offset);
    vertSrc[0] = "#version " + version + versionSuffix;

    return vertSrc;
  }


  protected static final String GLSL_ID_REGEX = "(?<![0-9A-Z_a-z])(%s)(?![0-9A-Z_a-z]|\\s*\\()";
  protected static final String GLSL_FN_REGEX = "(?<![0-9A-Z_a-z])(%s)(?=\\s*\\()";


  protected static String[] preprocessShaderSource(String[] src0,
                                                   Pattern[] search,
                                                   String[] replace,
                                                   int offset) {
    String[] src = new String[src0.length+offset];
    for (int i = 0; i < src0.length; i++) {
      String line = src0[i];
      int versionIndex = line.indexOf("#version");
      if (versionIndex >= 0) {
        line = line.substring(0, versionIndex);
      }
      for (int j = 0; j < search.length; j++) {
        line = search[j].matcher(line).replaceAll(replace[j]);
      }
      src[i+offset] = line;
    }
    return src;
  }

  protected static boolean containsVersionDirective(String[] shSrc) {
    for (String line : shSrc) {
      int versionIndex = line.indexOf("#version");
      if (versionIndex >= 0) {
        int commentIndex = line.indexOf("//");
        if (commentIndex < 0 || versionIndex < commentIndex) {
          return true;
        }
      }
    }
    return false;
  }

  protected int createShader(int shaderType, String source) {
    int shader = createShader(shaderType);
    if (shader != 0) {
      shaderSource(shader, source);
      compileShader(shader);
      if (!compiled(shader)) {
        System.err.println("Could not compile shader " + shaderType + ":");
        System.err.println(getShaderInfoLog(shader));
        deleteShader(shader);
        shader = 0;
      }
    }
    return shader;
  }


  protected int createProgram(int vertexShader, int fragmentShader) {
    int program = createProgram();
    if (program != 0) {
      attachShader(program, vertexShader);
      attachShader(program, fragmentShader);
      linkProgram(program);
      if (!linked(program)) {
        System.err.println("Could not link program: ");
        System.err.println(getProgramInfoLog(program));
        deleteProgram(program);
        program = 0;
      }
    }
    return program;
  }


  protected boolean compiled(int shader) {
    intBuffer.rewind();
    getShaderiv(shader, COMPILE_STATUS, intBuffer);
    return intBuffer.get(0) != 0;
  }


  protected boolean linked(int program) {
    intBuffer.rewind();
    getProgramiv(program, LINK_STATUS, intBuffer);
    return intBuffer.get(0) != 0;
  }


  protected int validateFramebuffer() {
    int status = checkFramebufferStatus(FRAMEBUFFER);
    if (status == FRAMEBUFFER_COMPLETE) {
      return 0;
    } else if (status == FRAMEBUFFER_UNDEFINED) {
      System.err.printf((FRAMEBUFFER_ERROR) + "%n", "framebuffer undefined");
    } else if (status == FRAMEBUFFER_INCOMPLETE_ATTACHMENT) {
      System.err.printf((FRAMEBUFFER_ERROR) + "%n", "incomplete attachment");
    } else if (status == FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT) {
      System.err.printf((FRAMEBUFFER_ERROR) + "%n", "incomplete missing attachment");
    } else if (status == FRAMEBUFFER_INCOMPLETE_DIMENSIONS) {
      System.err.printf((FRAMEBUFFER_ERROR) + "%n", "incomplete dimensions");
    } else if (status == FRAMEBUFFER_INCOMPLETE_FORMATS) {
      System.err.printf((FRAMEBUFFER_ERROR) + "%n", "incomplete formats");
    } else if (status == FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER) {
      System.err.printf((FRAMEBUFFER_ERROR) + "%n", "incomplete draw buffer");
    } else if (status == FRAMEBUFFER_INCOMPLETE_READ_BUFFER) {
      System.err.printf((FRAMEBUFFER_ERROR) + "%n", "incomplete read buffer");
    } else if (status == FRAMEBUFFER_UNSUPPORTED) {
      System.err.printf((FRAMEBUFFER_ERROR) + "%n", "framebuffer unsupported");
    } else if (status == FRAMEBUFFER_INCOMPLETE_MULTISAMPLE) {
      System.err.printf((FRAMEBUFFER_ERROR) + "%n", "incomplete multisample buffer");
    } else if (status == FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS) {
      System.err.printf((FRAMEBUFFER_ERROR) + "%n", "incomplete layer targets");
    } else {
      System.err.printf((FRAMEBUFFER_ERROR) + "%n", "unknown error " + status);
    }
    return status;
  }

  protected boolean isES() {
    return getString(VERSION).trim().toLowerCase().contains("opengl es");
  }

  protected int[] getGLVersion() {
    String version = getString(VERSION).trim().toLowerCase();

    String ES = "opengl es";
    int esPosition = version.indexOf(ES);
    if (esPosition >= 0) {
      version = version.substring(esPosition + ES.length()).trim();
    }

    int[] res = {0, 0, 0};
    String[] parts = version.split(" ");
    for (String part : parts) {
      if (0 < part.indexOf(".")) {
        String[] nums = part.split("\\.");
        try {
          res[0] = Integer.parseInt(nums[0]);
        } catch (NumberFormatException e) {
          // ignored
        }
        if (1 < nums.length) {
          try {
            res[1] = Integer.parseInt(nums[1]);
          } catch (NumberFormatException e) {
            // ignored
          }
        }
        if (2 < nums.length) {
          try {
            res[2] = Integer.parseInt(nums[2]);
          } catch (NumberFormatException e) {
            // ignored
          }
        }
        break;
      }
    }
    return res;
  }


  protected boolean hasFBOs() {
    // FBOs might still be available through extensions.
    int major = getGLVersion()[0];
    if (major < 2) {
      String ext = getString(EXTENSIONS);
      return (ext.contains("_framebuffer_object") &&
              ext.contains("_vertex_shader") &&
              ext.contains("_shader_objects") &&
              ext.contains("_shading_language"));
    } else {
      return true;
    }
  }


  protected boolean hasShaders() {
    // GLSL might still be available through extensions. For instance,
    // GLContext.hasGLSL() gives false for older intel integrated chipsets on
    // OSX, where OpenGL is 1.4 but shaders are available.
    int major = getGLVersion()[0];
    if (major < 2) {
      String ext = getString(EXTENSIONS);
      return (ext.contains("_fragment_shader") &&
              ext.contains("_vertex_shader") &&
              ext.contains("_shader_objects") &&
              ext.contains("_shading_language"));
    } else {
      return true;
    }
  }


  protected boolean hasNpotTexSupport() {
    int major = getGLVersion()[0];
    if (major < 3) {
      String ext = getString(EXTENSIONS);
      if (isES()) {
        return ext.contains("_texture_npot");
      } else {
        return ext.contains("_texture_non_power_of_two");
      }
    } else {
      return true;
    }
  }


  protected boolean hasAutoMipmapGenSupport() {
    int major = getGLVersion()[0];
    if (isES() && major >= 2) {
      return true;
    } else if (!isES() && major >= 3) {
      return true;
    } else {
      String ext = getString(EXTENSIONS);
      return ext.contains("_generate_mipmap");
    }
  }


  protected boolean hasFboMultisampleSupport() {
    int major = getGLVersion()[0];
    if (major < 3) {
      String ext = getString(EXTENSIONS);
      return ext.contains("_framebuffer_multisample");
    } else {
      return true;
    }
  }


  protected boolean hasPackedDepthStencilSupport() {
    int major = getGLVersion()[0];
    if (major < 3) {
      String ext = getString(EXTENSIONS);
      return ext.contains("_packed_depth_stencil");
    } else {
      return true;
    }
  }


  protected boolean hasAnisoSamplingSupport() {
    int major = getGLVersion()[0];
    if (isES() || major < 3) {
      String ext = getString(EXTENSIONS);
      return ext.contains("_texture_filter_anisotropic");
    } else {
      return true;
    }
  }


  protected boolean hasSynchronization() {
    int[] version = getGLVersion();
    if (isES()) {
      return version[0] >= 3;
    }
    return (version[0] > 3) || (version[0] == 3 && version[1] >= 2);
  }


  protected boolean hasPBOs() {
    int[] version = getGLVersion();
    if (isES()) {
      return version[0] >= 3;
    }
    return (version[0] > 2) || (version[0] == 2 && version[1] >= 1);
  }


  protected boolean hasReadBuffer() {
    int[] version = getGLVersion();
    if (isES()) {
      return version[0] >= 3;
    }
    return version[0] >= 2;
  }


  protected boolean hasDrawBuffer() {
    int[] version = getGLVersion();
    if (isES()) {
      return version[0] >= 3;
    }
    return version[0] >= 2;
  }


  protected int maxSamples() {
    intBuffer.rewind();
    getIntegerv(MAX_SAMPLES, intBuffer);
    return intBuffer.get(0);
  }


  protected int getMaxTexUnits() {
    intBuffer.rewind();
    getIntegerv(MAX_TEXTURE_IMAGE_UNITS, intBuffer);
    return intBuffer.get(0);
  }


  public boolean isFboAllowed() {
    if (fboAllowed == null) {
      if (PApplet.platform == PConstants.MACOS) {
        try {
          String hardware = getString(PGL.RENDERER);
          if (hardware != null && hardware.contains("Intel HD Graphics 3000")) {
            fboAllowed = false;
            return false;
          }
        } catch (RuntimeException e) {
          System.err.println("Could not read renderer name. FBOs disabled. Reason: " + e);
          // disable for now, but will try again on next isFboAllowed() call
          return false;
        }
      }
      // all other scenarios allow for FBOs
      fboAllowed = true;
    }
    return fboAllowed;
  }


  protected static ByteBuffer allocateDirectByteBuffer(int size) {
    int bytes = PApplet.max(MIN_DIRECT_BUFFER_SIZE, size) * SIZEOF_BYTE;
    return ByteBuffer.allocateDirect(bytes).order(ByteOrder.nativeOrder());
  }


  protected static ByteBuffer allocateByteBuffer(int size) {
    if (USE_DIRECT_BUFFERS) {
      return allocateDirectByteBuffer(size);
    } else {
      return ByteBuffer.allocate(size);
    }
  }


  protected static ByteBuffer allocateByteBuffer(byte[] arr) {
    if (USE_DIRECT_BUFFERS) {
      ByteBuffer buf = allocateDirectByteBuffer(arr.length);
      buf.put(arr);
      buf.position(0);
      return buf;
    } else {
      return ByteBuffer.wrap(arr);
    }
  }


  protected static ByteBuffer updateByteBuffer(ByteBuffer buf, byte[] arr,
                                               boolean wrap) {
    if (USE_DIRECT_BUFFERS || (buf != null && buf.isDirect())) {
      if (buf == null || buf.capacity() < arr.length) {
        buf = allocateDirectByteBuffer(arr.length);
      }
      buf.position(0);
      buf.put(arr);
      buf.rewind();
    } else {
      if (wrap) {
        buf = ByteBuffer.wrap(arr);
      } else {
        if (buf == null || buf.capacity() < arr.length) {
          buf = ByteBuffer.allocate(arr.length);
        }
        buf.position(0);
        buf.put(arr);
        buf.rewind();
      }
    }
    return buf;
  }


  protected static void updateByteBuffer(ByteBuffer buf, byte[] arr,
                                         int offset, int size) {
    if (buf.isDirect() || (buf.hasArray() && buf.array() != arr)) {
      buf.position(offset);
      buf.put(arr, offset, size);
      buf.rewind();
    }
  }


  protected static void getByteArray(ByteBuffer buf, byte[] arr) {
    if (!buf.hasArray() || buf.array() != arr) {
      buf.position(0);
      buf.get(arr);
      buf.rewind();
    }
  }


  protected static void putByteArray(ByteBuffer buf, byte[] arr) {
    if (!buf.hasArray() || buf.array() != arr) {
      buf.position(0);
      buf.put(arr);
      buf.rewind();
    }
  }


  protected static void fillByteBuffer(ByteBuffer buf, int i0, int i1,
                                       byte val) {
    int n = i1 - i0;
    byte[] temp = new byte[n];
    Arrays.fill(temp, 0, n, val);
    buf.position(i0);
    buf.put(temp, 0, n);
    buf.rewind();
  }


  protected static ShortBuffer allocateDirectShortBuffer(int size) {
    int bytes = PApplet.max(MIN_DIRECT_BUFFER_SIZE, size) * SIZEOF_SHORT;
    return ByteBuffer.allocateDirect(bytes).order(ByteOrder.nativeOrder()).
           asShortBuffer();
  }


  protected static ShortBuffer allocateShortBuffer(int size) {
    if (USE_DIRECT_BUFFERS) {
      return allocateDirectShortBuffer(size);
    } else {
      return ShortBuffer.allocate(size);
    }
  }


  protected static ShortBuffer allocateShortBuffer(short[] arr) {
    if (USE_DIRECT_BUFFERS) {
      ShortBuffer buf = allocateDirectShortBuffer(arr.length);
      buf.put(arr);
      buf.position(0);
      return buf;
    } else {
      return ShortBuffer.wrap(arr);
    }
  }


  protected static ShortBuffer updateShortBuffer(ShortBuffer buf, short[] arr,
                                                 boolean wrap) {
    if (USE_DIRECT_BUFFERS || (buf != null && buf.isDirect())) {
      if (buf == null || buf.capacity() < arr.length) {
        buf = allocateDirectShortBuffer(arr.length);
      }
      buf.position(0);
      buf.put(arr);
      buf.rewind();
    } else {
      if (wrap) {
        buf = ShortBuffer.wrap(arr);
      } else {
        if (buf == null || buf.capacity() < arr.length) {
          buf = ShortBuffer.allocate(arr.length);
        }
        buf.position(0);
        buf.put(arr);
        buf.rewind();
      }
    }
    return buf;
  }


  protected static void updateShortBuffer(ShortBuffer buf, short[] arr,
                                          int offset, int size) {
    if (buf.isDirect() || (buf.hasArray() && buf.array() != arr)) {
      buf.position(offset);
      buf.put(arr, offset, size);
      buf.rewind();
    }
  }


  protected static void getShortArray(ShortBuffer buf, short[] arr) {
    if (!buf.hasArray() || buf.array() != arr) {
      buf.position(0);
      buf.get(arr);
      buf.rewind();
    }
  }


  protected static void putShortArray(ShortBuffer buf, short[] arr) {
    if (!buf.hasArray() || buf.array() != arr) {
      buf.position(0);
      buf.put(arr);
      buf.rewind();
    }
  }


  protected static void fillShortBuffer(ShortBuffer buf, int i0, int i1,
                                        short val) {
    int n = i1 - i0;
    short[] temp = new short[n];
    Arrays.fill(temp, 0, n, val);
    buf.position(i0);
    buf.put(temp, 0, n);
    buf.rewind();
  }


  protected static IntBuffer allocateDirectIntBuffer(int size) {
    int bytes = PApplet.max(MIN_DIRECT_BUFFER_SIZE, size) * SIZEOF_INT;
    return ByteBuffer.allocateDirect(bytes).order(ByteOrder.nativeOrder()).
           asIntBuffer();
  }


  protected static IntBuffer allocateIntBuffer(int size) {
    if (USE_DIRECT_BUFFERS) {
      return allocateDirectIntBuffer(size);
    } else {
      return IntBuffer.allocate(size);
    }
  }


  protected static IntBuffer allocateIntBuffer(int[] arr) {
    if (USE_DIRECT_BUFFERS) {
      IntBuffer buf = allocateDirectIntBuffer(arr.length);
      buf.put(arr);
      buf.position(0);
      return buf;
    } else {
      return IntBuffer.wrap(arr);
    }
  }


  protected static IntBuffer updateIntBuffer(IntBuffer buf, int[] arr,
                                             boolean wrap) {
    if (USE_DIRECT_BUFFERS || (buf != null && buf.isDirect())) {
      if (buf == null || buf.capacity() < arr.length) {
        buf = allocateDirectIntBuffer(arr.length);
      }
      buf.position(0);
      buf.put(arr);
      buf.rewind();
    } else {
      if (wrap) {
        buf = IntBuffer.wrap(arr);
      } else {
        if (buf == null || buf.capacity() < arr.length) {
          buf = IntBuffer.allocate(arr.length);
        }
        buf.position(0);
        buf.put(arr);
        buf.rewind();
      }
    }
    return buf;
  }


  protected static void updateIntBuffer(IntBuffer buf, int[] arr,
                                        int offset, int size) {
     if (buf.isDirect() || (buf.hasArray() && buf.array() != arr)) {
       buf.position(offset);
       buf.put(arr, offset, size);
       buf.rewind();
     }
   }


  protected static void getIntArray(IntBuffer buf, int[] arr) {
    if (!buf.hasArray() || buf.array() != arr) {
      buf.position(0);
      buf.get(arr);
      buf.rewind();
    }
  }


  protected static void putIntArray(IntBuffer buf, int[] arr) {
    if (!buf.hasArray() || buf.array() != arr) {
      buf.position(0);
      buf.put(arr);
      buf.rewind();
    }
  }


  protected static void fillIntBuffer(IntBuffer buf, int i0, int i1, int val) {
    int n = i1 - i0;
    int[] temp = new int[n];
    Arrays.fill(temp, 0, n, val);
    buf.position(i0);
    buf.put(temp, 0, n);
    buf.rewind();
  }


  protected static FloatBuffer allocateDirectFloatBuffer(int size) {
    int bytes = PApplet.max(MIN_DIRECT_BUFFER_SIZE, size) * SIZEOF_FLOAT;
    return ByteBuffer.allocateDirect(bytes).order(ByteOrder.nativeOrder()).asFloatBuffer();
  }


  protected static FloatBuffer allocateFloatBuffer(int size) {
    if (USE_DIRECT_BUFFERS) {
      return allocateDirectFloatBuffer(size);
    } else {
      return FloatBuffer.allocate(size);
    }
  }


  protected static FloatBuffer allocateFloatBuffer(float[] arr) {
    if (USE_DIRECT_BUFFERS) {
      FloatBuffer buf = allocateDirectFloatBuffer(arr.length);
      buf.put(arr);
      buf.position(0);
      return buf;
    } else {
      return FloatBuffer.wrap(arr);
    }
  }


  protected static FloatBuffer updateFloatBuffer(FloatBuffer buf, float[] arr,
                                                 boolean wrap) {
    if (USE_DIRECT_BUFFERS || (buf != null && buf.isDirect())) {
      if (buf == null || buf.capacity() < arr.length) {
        buf = allocateDirectFloatBuffer(arr.length);
      }
      buf.position(0);
      buf.put(arr);
      buf.rewind();
    } else {
      if (wrap) {
        buf = FloatBuffer.wrap(arr);
      } else {
        if (buf == null || buf.capacity() < arr.length) {
          buf = FloatBuffer.allocate(arr.length);
        }
        buf.position(0);
        buf.put(arr);
        buf.rewind();
      }
    }
    return buf;
  }


  protected static void updateFloatBuffer(FloatBuffer buf, float[] arr,
                                        int offset, int size) {
     if (buf.isDirect() || (buf.hasArray() && buf.array() != arr)) {
       buf.position(offset);
       buf.put(arr, offset, size);
       buf.rewind();
     }
   }


  protected static void getFloatArray(FloatBuffer buf, float[] arr) {
    if (!buf.hasArray() || buf.array() != arr) {
      buf.position(0);
      buf.get(arr);
      buf.rewind();
    }
  }


  protected static void putFloatArray(FloatBuffer buf, float[] arr) {
    if (!buf.hasArray() || buf.array() != arr) {
      buf.position(0);
      buf.put(arr);
      buf.rewind();
    }
  }


  protected static void fillFloatBuffer(FloatBuffer buf, int i0, int i1,
                                        float val) {
    int n = i1 - i0;
    float[] temp = new float[n];
    Arrays.fill(temp, 0, n, val);
    buf.position(i0);
    buf.put(temp, 0, n);
    buf.rewind();
  }


  // TODO: the next three functions shouldn't be here...
  // Uses 'Object' so that the API can be used w/ Android Typeface objects

  // abstract protected int getFontAscent(Object font);


  // abstract protected int getFontDescent(Object font);


  abstract protected int getTextWidth(Object font, char[] buffer, int start, int stop);


  abstract protected Object getDerivedFont(Object font, float size);


  ///////////////////////////////////////////////////////////

  // Tessellator interface


  protected abstract Tessellator createTessellator(TessellatorCallback callback);


  protected interface Tessellator {
    void setCallback(int flag);
    void setWindingRule(int rule);
    void setProperty(int property, int value);

    void beginPolygon();
    void beginPolygon(Object data);
    void endPolygon();
    void beginContour();
    void endContour();
    void addVertex(double[] v);
    void addVertex(double[] v, int n, Object data);
  }


  protected interface TessellatorCallback  {
    void begin(int type);
    void end();
    void vertex(Object data);
    void combine(double[] coords, Object[] data,
                 float[] weight, Object[] outData);
    void error(int errnum);
  }


  protected String tessError(int err) {
    return "";
  }


  ///////////////////////////////////////////////////////////

  // FontOutline interface


  protected static boolean SHAPE_TEXT_SUPPORTED;
  protected static int SEG_MOVETO;
  protected static int SEG_LINETO;
  protected static int SEG_QUADTO;
  protected static int SEG_CUBICTO;
  protected static int SEG_CLOSE;


  protected abstract FontOutline createFontOutline(char ch, Object font);


  protected interface FontOutline {
    boolean isDone();
    int currentSegment(float[] coords);
    void next();
  }


  //////////////////////////////////////////////////////////////////////////////
  //
  // OpenGL ES 2.0 API, with a few additional functions for multisampling and
  // and buffer mapping from OpenGL 2.1+.
  //
  // The functions are organized following the groups in the GLES 2.0 reference
  // card:
  // http://www.khronos.org/opengles/sdk/docs/reference_cards/OpenGL-ES-2_0-Reference-card.pdf
  //
  // The entire GLES 2.0 specification is available below:
  // http://www.khronos.org/opengles/2_X/
  //
  // Implementations of the PGL functions for specific OpenGL bindings (JOGL,
  // LWJGL) should simply call the corresponding GL function in the bindings.
  // readPixels(), activeTexture() and bindTexture() are special cases, please
  // read their comments.
  // Also, keep in mind the note about the PGL constants below.
  //
  //////////////////////////////////////////////////////////////////////////////

  ///////////////////////////////////////////////////////////

  // Constants
  // Very important note: set the GL constants in your PGL subclass by using an
  // static initialization block as follows:
  // static {
  //    FALSE = SUPER_DUPER_JAVA_OPENGL_BINDINGS.GL_FALSE;
  //    TRUE  = SUPER_DUPER_JAVA_OPENGL_BINDINGS.GL_TRUE;
  //    ...
  // }
  // and not by re-declaring the constants, because doing so will lead to
  // errors when the constants are accessed through PGL because they are not
  // overridden but hidden by the new declarations, and hence they keep their
  // initial values (all zeroes) when accessed through the superclass.

  public static int FALSE;
  public static int TRUE;

  public static int INT;
  public static int BYTE;
  public static int SHORT;
  public static int FLOAT;
  public static int BOOL;
  public static int UNSIGNED_INT;
  public static int UNSIGNED_BYTE;
  public static int UNSIGNED_SHORT;

  public static int RGB;
  public static int RGBA;
  public static int ALPHA;
  public static int LUMINANCE;
  public static int LUMINANCE_ALPHA;

  public static int UNSIGNED_SHORT_5_6_5;
  public static int UNSIGNED_SHORT_4_4_4_4;
  public static int UNSIGNED_SHORT_5_5_5_1;

  public static int RGBA4;
  public static int RGB5_A1;
  public static int RGB565;
  public static int RGB8;
  public static int RGBA8;
  public static int ALPHA8;

  public static int READ_ONLY;
  public static int WRITE_ONLY;
  public static int READ_WRITE;

  public static int TESS_WINDING_NONZERO;
  public static int TESS_WINDING_ODD;
  public static int TESS_EDGE_FLAG;

  public static int GENERATE_MIPMAP_HINT;
  public static int FASTEST;
  public static int NICEST;
  public static int DONT_CARE;

  public static int VENDOR;
  public static int RENDERER;
  public static int VERSION;
  public static int EXTENSIONS;
  public static int SHADING_LANGUAGE_VERSION;

  public static int MAX_SAMPLES;
  public static int SAMPLES;

  public static int ALIASED_LINE_WIDTH_RANGE;
  public static int ALIASED_POINT_SIZE_RANGE;

  public static int DEPTH_BITS;
  public static int STENCIL_BITS;

  public static int CCW;
  public static int CW;

  public static int VIEWPORT;

  public static int ARRAY_BUFFER;
  public static int ELEMENT_ARRAY_BUFFER;
  public static int PIXEL_PACK_BUFFER;

  public static int MAX_VERTEX_ATTRIBS;

  public static int STATIC_DRAW;
  public static int DYNAMIC_DRAW;
  public static int STREAM_DRAW;
  public static int STREAM_READ;

  public static int BUFFER_SIZE;
  public static int BUFFER_USAGE;

  public static int POINTS;
  public static int LINE_STRIP;
  public static int LINE_LOOP;
  public static int LINES;
  public static int TRIANGLE_FAN;
  public static int TRIANGLE_STRIP;
  public static int TRIANGLES;

  public static int CULL_FACE;
  public static int FRONT;
  public static int BACK;
  public static int FRONT_AND_BACK;

  public static int POLYGON_OFFSET_FILL;

  public static int UNPACK_ALIGNMENT;
  public static int PACK_ALIGNMENT;

  public static int TEXTURE_2D;
  public static int TEXTURE_RECTANGLE;

  public static int TEXTURE_BINDING_2D;
  public static int TEXTURE_BINDING_RECTANGLE;

  public static int MAX_TEXTURE_SIZE;
  public static int TEXTURE_MAX_ANISOTROPY;
  public static int MAX_TEXTURE_MAX_ANISOTROPY;

  public static int MAX_VERTEX_TEXTURE_IMAGE_UNITS;
  public static int MAX_TEXTURE_IMAGE_UNITS;
  public static int MAX_COMBINED_TEXTURE_IMAGE_UNITS;

  public static int NUM_COMPRESSED_TEXTURE_FORMATS;
  public static int COMPRESSED_TEXTURE_FORMATS;

  public static int NEAREST;
  public static int LINEAR;
  public static int LINEAR_MIPMAP_NEAREST;
  public static int LINEAR_MIPMAP_LINEAR;

  public static int CLAMP_TO_EDGE;
  public static int REPEAT;

  public static int TEXTURE0;
  public static int TEXTURE1;
  public static int TEXTURE2;
  public static int TEXTURE3;
  public static int TEXTURE_MIN_FILTER;
  public static int TEXTURE_MAG_FILTER;
  public static int TEXTURE_WRAP_S;
  public static int TEXTURE_WRAP_T;
  public static int TEXTURE_WRAP_R;

  public static int TEXTURE_CUBE_MAP;
  public static int TEXTURE_CUBE_MAP_POSITIVE_X;
  public static int TEXTURE_CUBE_MAP_POSITIVE_Y;
  public static int TEXTURE_CUBE_MAP_POSITIVE_Z;
  public static int TEXTURE_CUBE_MAP_NEGATIVE_X;
  public static int TEXTURE_CUBE_MAP_NEGATIVE_Y;
  public static int TEXTURE_CUBE_MAP_NEGATIVE_Z;

  public static int VERTEX_SHADER;
  public static int FRAGMENT_SHADER;
  public static int INFO_LOG_LENGTH;
  public static int SHADER_SOURCE_LENGTH;
  public static int COMPILE_STATUS;
  public static int LINK_STATUS;
  public static int VALIDATE_STATUS;
  public static int SHADER_TYPE;
  public static int DELETE_STATUS;

  public static int FLOAT_VEC2;
  public static int FLOAT_VEC3;
  public static int FLOAT_VEC4;
  public static int FLOAT_MAT2;
  public static int FLOAT_MAT3;
  public static int FLOAT_MAT4;
  public static int INT_VEC2;
  public static int INT_VEC3;
  public static int INT_VEC4;
  public static int BOOL_VEC2;
  public static int BOOL_VEC3;
  public static int BOOL_VEC4;
  public static int SAMPLER_2D;
  public static int SAMPLER_CUBE;

  public static int LOW_FLOAT;
  public static int MEDIUM_FLOAT;
  public static int HIGH_FLOAT;
  public static int LOW_INT;
  public static int MEDIUM_INT;
  public static int HIGH_INT;

  public static int CURRENT_VERTEX_ATTRIB;

  public static int VERTEX_ATTRIB_ARRAY_BUFFER_BINDING;
  public static int VERTEX_ATTRIB_ARRAY_ENABLED;
  public static int VERTEX_ATTRIB_ARRAY_SIZE;
  public static int VERTEX_ATTRIB_ARRAY_STRIDE;
  public static int VERTEX_ATTRIB_ARRAY_TYPE;
  public static int VERTEX_ATTRIB_ARRAY_NORMALIZED;
  public static int VERTEX_ATTRIB_ARRAY_POINTER;

  public static int BLEND;
  public static int ONE;
  public static int ZERO;
  public static int SRC_ALPHA;
  public static int DST_ALPHA;
  public static int ONE_MINUS_SRC_ALPHA;
  public static int ONE_MINUS_DST_COLOR;
  public static int ONE_MINUS_SRC_COLOR;
  public static int DST_COLOR;
  public static int SRC_COLOR;

  public static int SAMPLE_ALPHA_TO_COVERAGE;
  public static int SAMPLE_COVERAGE;

  public static int KEEP;
  public static int REPLACE;
  public static int INCR;
  public static int DECR;
  public static int INVERT;
  public static int INCR_WRAP;
  public static int DECR_WRAP;
  public static int NEVER;
  public static int ALWAYS;

  public static int EQUAL;
  public static int LESS;
  public static int LEQUAL;
  public static int GREATER;
  public static int GEQUAL;
  public static int NOTEQUAL;

  public static int FUNC_ADD;
  public static int FUNC_MIN;
  public static int FUNC_MAX;
  public static int FUNC_REVERSE_SUBTRACT;
  public static int FUNC_SUBTRACT;

  public static int DITHER;

  public static int CONSTANT_COLOR;
  public static int CONSTANT_ALPHA;
  public static int ONE_MINUS_CONSTANT_COLOR;
  public static int ONE_MINUS_CONSTANT_ALPHA;
  public static int SRC_ALPHA_SATURATE;

  public static int SCISSOR_TEST;
  public static int STENCIL_TEST;
  public static int DEPTH_TEST;
  public static int DEPTH_WRITEMASK;

  public static int COLOR_BUFFER_BIT;
  public static int DEPTH_BUFFER_BIT;
  public static int STENCIL_BUFFER_BIT;

  public static int FRAMEBUFFER;
  public static int COLOR_ATTACHMENT0;
  public static int COLOR_ATTACHMENT1;
  public static int COLOR_ATTACHMENT2;
  public static int COLOR_ATTACHMENT3;
  public static int RENDERBUFFER;
  public static int DEPTH_ATTACHMENT;
  public static int STENCIL_ATTACHMENT;
  public static int READ_FRAMEBUFFER;
  public static int DRAW_FRAMEBUFFER;

  public static int DEPTH24_STENCIL8;

  public static int DEPTH_COMPONENT;
  public static int DEPTH_COMPONENT16;
  public static int DEPTH_COMPONENT24;
  public static int DEPTH_COMPONENT32;

  public static int STENCIL_INDEX;
  public static int STENCIL_INDEX1;
  public static int STENCIL_INDEX4;
  public static int STENCIL_INDEX8;

  public static int DEPTH_STENCIL;

  public static int FRAMEBUFFER_COMPLETE;
  public static int FRAMEBUFFER_UNDEFINED;
  public static int FRAMEBUFFER_INCOMPLETE_ATTACHMENT;
  public static int FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT;
  public static int FRAMEBUFFER_INCOMPLETE_DIMENSIONS;
  public static int FRAMEBUFFER_INCOMPLETE_FORMATS;
  public static int FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER;
  public static int FRAMEBUFFER_INCOMPLETE_READ_BUFFER;
  public static int FRAMEBUFFER_UNSUPPORTED;
  public static int FRAMEBUFFER_INCOMPLETE_MULTISAMPLE;
  public static int FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS;

  public static int FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE;
  public static int FRAMEBUFFER_ATTACHMENT_OBJECT_NAME;
  public static int FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL;
  public static int FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE;

  public static int RENDERBUFFER_WIDTH;
  public static int RENDERBUFFER_HEIGHT;
  public static int RENDERBUFFER_RED_SIZE;
  public static int RENDERBUFFER_GREEN_SIZE;
  public static int RENDERBUFFER_BLUE_SIZE;
  public static int RENDERBUFFER_ALPHA_SIZE;
  public static int RENDERBUFFER_DEPTH_SIZE;
  public static int RENDERBUFFER_STENCIL_SIZE;
  public static int RENDERBUFFER_INTERNAL_FORMAT;

  public static int MULTISAMPLE;
  public static int LINE_SMOOTH;
  public static int POLYGON_SMOOTH;

  public static int SYNC_GPU_COMMANDS_COMPLETE;
  public static int ALREADY_SIGNALED;
  public static int CONDITION_SATISFIED;

  ///////////////////////////////////////////////////////////

  // Special Functions

  public abstract void flush();
  public abstract void finish();
  public abstract void hint(int target, int hint);

  ///////////////////////////////////////////////////////////

  // State and State Requests

  public abstract void enable(int value);
  public abstract void disable(int value);
  public abstract void getBooleanv(int value, IntBuffer data);
  public abstract void getIntegerv(int value, IntBuffer data);
  public abstract void getFloatv(int value, FloatBuffer data);
  public abstract boolean isEnabled(int value);
  public abstract String getString(int name);

  ///////////////////////////////////////////////////////////

  // Error Handling

  public abstract int getError();
  public abstract String errorString(int err);

  //////////////////////////////////////////////////////////////////////////////

  // Buffer Objects

  public abstract void genBuffers(int n, IntBuffer buffers);
  public abstract void deleteBuffers(int n, IntBuffer buffers);
  public abstract void bindBuffer(int target, int buffer);
  public abstract void bufferData(int target, int size, Buffer data, int usage);
  public abstract void bufferSubData(int target, int offset, int size, Buffer data);
  public abstract void isBuffer(int buffer);
  public abstract void getBufferParameteriv(int target, int value, IntBuffer data);
  public abstract ByteBuffer mapBuffer(int target, int access);
  public abstract ByteBuffer mapBufferRange(int target, int offset, int length, int access);
  public abstract void unmapBuffer(int target);

  //////////////////////////////////////////////////////////////////////////////

  // Synchronization

  public abstract long fenceSync(int condition, int flags);
  public abstract void deleteSync(long sync);
  public abstract int clientWaitSync(long sync, int flags, long timeout);

  //////////////////////////////////////////////////////////////////////////////

  // Viewport and Clipping

  public abstract void depthRangef(float n, float f);
  public abstract void viewport(int x, int y, int w, int h);
  protected abstract void viewportImpl(int x, int y, int w, int h);

  //////////////////////////////////////////////////////////////////////////////

  // Reading Pixels
  // This is a special case: because the renderer might be using an FBO even on
  // the main surface, some extra handling might be needed before and after
  // reading the pixels. To make this transparent to the user, the actual call
  // to glReadPixels() should be done in readPixelsImpl().

  public void readPixels(int x, int y, int width, int height, int format, int type, Buffer buffer){
    boolean multisampled = isMultisampled() || graphics.offscreenMultisample;
    boolean depthReadingEnabled = graphics.getHint(PConstants.ENABLE_BUFFER_READING);
    boolean depthRequested = format == STENCIL_INDEX || format == DEPTH_COMPONENT || format == DEPTH_STENCIL;

    if (multisampled && depthRequested && !depthReadingEnabled) {
      PGraphics.showWarning(DEPTH_READING_NOT_ENABLED_ERROR);
      return;
    }

    graphics.beginReadPixels();
    readPixelsImpl(x, y, width, height, format, type, buffer);
    graphics.endReadPixels();
  }

  public void readPixels(int x, int y, int width, int height, int format, int type, long offset){
    boolean multisampled = isMultisampled() || graphics.offscreenMultisample;
    boolean depthReadingEnabled = graphics.getHint(PConstants.ENABLE_BUFFER_READING);
    boolean depthRequested = format == STENCIL_INDEX || format == DEPTH_COMPONENT || format == DEPTH_STENCIL;

    if (multisampled && depthRequested && !depthReadingEnabled) {
      PGraphics.showWarning(DEPTH_READING_NOT_ENABLED_ERROR);
      return;
    }

    graphics.beginReadPixels();
    readPixelsImpl(x, y, width, height, format, type, offset);
    graphics.endReadPixels();
  }

  protected abstract void readPixelsImpl(int x, int y, int width, int height, int format, int type, Buffer buffer);
  protected abstract void readPixelsImpl(int x, int y, int width, int height, int format, int type, long offset);

  //////////////////////////////////////////////////////////////////////////////

  // Vertices

  public abstract void vertexAttrib1f(int index, float value);
  public abstract void vertexAttrib2f(int index, float value0, float value1);
  public abstract void vertexAttrib3f(int index, float value0, float value1, float value2);
  public abstract void vertexAttrib4f(int index, float value0, float value1, float value2, float value3);
  public abstract void vertexAttrib1fv(int index, FloatBuffer values);
  public abstract void vertexAttrib2fv(int index, FloatBuffer values);
  public abstract void vertexAttrib3fv(int index, FloatBuffer values);
  public abstract void vertexAttrib4fv(int index, FloatBuffer values);
  public abstract void vertexAttribPointer(int index, int size, int type, boolean normalized, int stride, int offset);
  public abstract void enableVertexAttribArray(int index);
  public abstract void disableVertexAttribArray(int index);

  public void drawArrays(int mode, int first, int count) {
    geomCount += count;
    drawArraysImpl(mode, first, count);
  }

  public abstract void drawArraysImpl(int mode, int first, int count);

  public void drawElements(int mode, int count, int type, int offset) {
    geomCount += count;
    drawElementsImpl(mode, count, type, offset);
  }

  public abstract void drawElementsImpl(int mode, int count, int type, int offset);

  //////////////////////////////////////////////////////////////////////////////

  // Rasterization

  public abstract void lineWidth(float width);
  public abstract void frontFace(int dir);
  public abstract void cullFace(int mode);
  public abstract void polygonOffset(float factor, float units);

  //////////////////////////////////////////////////////////////////////////////

  // Pixel Rectangles

  public abstract void pixelStorei(int pname, int param);

  ///////////////////////////////////////////////////////////

  // Texturing

  public abstract void texImage2D(int target, int level, int internalFormat, int width, int height, int border, int format, int type, Buffer data);
  public abstract void copyTexImage2D(int target, int level, int internalFormat, int x, int y, int width, int height, int border);
  public abstract void texSubImage2D(int target, int level, int xOffset, int yOffset, int width, int height, int format, int type, Buffer data);
  public abstract void copyTexSubImage2D(int target, int level, int xOffset, int yOffset, int x, int y, int width, int height);
  public abstract void compressedTexImage2D(int target, int level, int internalFormat, int width, int height, int border, int imageSize, Buffer data);
  public abstract void compressedTexSubImage2D(int target, int level, int xOffset, int yOffset, int width, int height, int format, int imageSize, Buffer data);
  public abstract void texParameteri(int target, int pname, int param);
  public abstract void texParameterf(int target, int pname, float param);
  public abstract void texParameteriv(int target, int pname, IntBuffer params);
  public abstract void texParameterfv(int target, int pname, FloatBuffer params);
  public abstract void generateMipmap(int target);
  public abstract void genTextures(int n, IntBuffer textures);
  public abstract void deleteTextures(int n, IntBuffer textures);
  public abstract void getTexParameteriv(int target, int pname, IntBuffer params);
  public abstract void getTexParameterfv(int target, int pname, FloatBuffer params);
  public abstract boolean isTexture(int texture);

  // activeTexture() and bindTexture() have some extra logic to keep track of
  // the bound textures, so the actual GL call should go in activeTextureImpl()
  // and bindTextureImpl().
  public void activeTexture(int texture) {
    activeTexUnit = texture - TEXTURE0;
    activeTextureImpl(texture);
  }

  protected abstract void activeTextureImpl(int texture);

  public void bindTexture(int target, int texture) {
    bindTextureImpl(target, texture);

    if (boundTextures == null) {
      maxTexUnits = getMaxTexUnits();
      boundTextures = new int[maxTexUnits][2];
    }

    if (maxTexUnits <= activeTexUnit) {
      throw new RuntimeException(TEXUNIT_ERROR);
    }

    if (target == TEXTURE_2D) {
      boundTextures[activeTexUnit][0] = texture;
    } else if (target == TEXTURE_RECTANGLE) {
      boundTextures[activeTexUnit][1] = texture;
    }
  }
  protected abstract void bindTextureImpl(int target, int texture);

  ///////////////////////////////////////////////////////////

  // Shaders and Programs

  public abstract int createShader(int type);
  public abstract void shaderSource(int shader, String source);
  public abstract void compileShader(int shader);
  public abstract void releaseShaderCompiler();
  public abstract void deleteShader(int shader);
  public abstract void shaderBinary(int count, IntBuffer shaders, int binaryFormat, Buffer binary, int length);
  public abstract int createProgram();
  public abstract void attachShader(int program, int shader);
  public abstract void detachShader(int program, int shader);
  public abstract void linkProgram(int program);
  public abstract void useProgram(int program);
  public abstract void deleteProgram(int program);
  public abstract String getActiveAttrib(int program, int index, IntBuffer size, IntBuffer type);
  public abstract int getAttribLocation(int program, String name);
  public abstract void bindAttribLocation(int program, int index, String name);
  public abstract int getUniformLocation(int program, String name);
  public abstract String getActiveUniform(int program, int index, IntBuffer size, IntBuffer type);
  public abstract void uniform1i(int location, int value);
  public abstract void uniform2i(int location, int value0, int value1);
  public abstract void uniform3i(int location, int value0, int value1, int value2);
  public abstract void uniform4i(int location, int value0, int value1, int value2, int value3);
  public abstract void uniform1f(int location, float value);
  public abstract void uniform2f(int location, float value0, float value1);
  public abstract void uniform3f(int location, float value0, float value1, float value2);
  public abstract void uniform4f(int location, float value0, float value1, float value2, float value3);
  public abstract void uniform1iv(int location, int count, IntBuffer v);
  public abstract void uniform2iv(int location, int count, IntBuffer v);
  public abstract void uniform3iv(int location, int count, IntBuffer v);
  public abstract void uniform4iv(int location, int count, IntBuffer v);
  public abstract void uniform1fv(int location, int count, FloatBuffer v);
  public abstract void uniform2fv(int location, int count, FloatBuffer v);
  public abstract void uniform3fv(int location, int count, FloatBuffer v);
  public abstract void uniform4fv(int location, int count, FloatBuffer v);
  public abstract void uniformMatrix2fv(int location, int count, boolean transpose, FloatBuffer mat);
  public abstract void uniformMatrix3fv(int location, int count, boolean transpose, FloatBuffer mat);
  public abstract void uniformMatrix4fv(int location, int count, boolean transpose, FloatBuffer mat);
  public abstract void validateProgram(int program);
  public abstract boolean isShader(int shader);
  public abstract void getShaderiv(int shader, int pname, IntBuffer params);
  public abstract void getAttachedShaders(int program, int maxCount, IntBuffer count, IntBuffer shaders);
  public abstract String getShaderInfoLog(int shader);
  public abstract String getShaderSource(int shader);
  public abstract void getShaderPrecisionFormat(int shaderType, int precisionType, IntBuffer range, IntBuffer precision);
  public abstract void getVertexAttribfv(int index, int pname, FloatBuffer params);
  public abstract void getVertexAttribiv(int index, int pname, IntBuffer params);
  public abstract void getVertexAttribPointerv(int index, int pname, ByteBuffer data);
  public abstract void getUniformfv(int program, int location, FloatBuffer params);
  public abstract void getUniformiv(int program, int location, IntBuffer params);
  public abstract boolean isProgram(int program);
  public abstract void getProgramiv(int program, int pname, IntBuffer params);
  public abstract String getProgramInfoLog(int program);

  ///////////////////////////////////////////////////////////

  // Per-Fragment Operations

  public abstract void scissor(int x, int y, int w, int h);
  public abstract void sampleCoverage(float value, boolean invert);
  public abstract void stencilFunc(int func, int ref, int mask);
  public abstract void stencilFuncSeparate(int face, int func, int ref, int mask);
  public abstract void stencilOp(int sfail, int dpfail, int dppass);
  public abstract void stencilOpSeparate(int face, int sfail, int dpfail, int dppass);
  public abstract void depthFunc(int func);
  public abstract void blendEquation(int mode);
  public abstract void blendEquationSeparate(int modeRGB, int modeAlpha);
  public abstract void blendFunc(int src, int dst);
  public abstract void blendFuncSeparate(int srcRGB, int dstRGB, int srcAlpha, int dstAlpha);
  public abstract void blendColor(float red, float green, float blue, float alpha);

  ///////////////////////////////////////////////////////////

  // Whole Framebuffer Operations

  public abstract void colorMask(boolean r, boolean g, boolean b, boolean a);
  public abstract void depthMask(boolean mask);
  public abstract void stencilMask(int mask);
  public abstract void stencilMaskSeparate(int face, int mask);
  public abstract void clearColor(float r, float g, float b, float a);
  public abstract void clearDepth(float d);
  public abstract void clearStencil(int s);
  public abstract void clear(int buf);

  ///////////////////////////////////////////////////////////

  // Framebuffers Objects

  public void bindFramebuffer(int target, int framebuffer) {
    graphics.beginBindFramebuffer(target, framebuffer);
    bindFramebufferImpl(target, framebuffer);
    graphics.endBindFramebuffer(target, framebuffer);
  }
  protected abstract void bindFramebufferImpl(int target, int framebuffer);

  public abstract void deleteFramebuffers(int n, IntBuffer framebuffers);
  public abstract void genFramebuffers(int n, IntBuffer framebuffers);
  public abstract void bindRenderbuffer(int target, int renderbuffer);
  public abstract void deleteRenderbuffers(int n, IntBuffer renderbuffers);
  public abstract void genRenderbuffers(int n, IntBuffer renderbuffers);
  public abstract void renderbufferStorage(int target, int internalFormat, int width, int height);
  public abstract void framebufferRenderbuffer(int target, int attachment, int rendbuferfTarget, int renderbuffer);
  public abstract void framebufferTexture2D(int target, int attachment, int texTarget, int texture, int level);
  public abstract int checkFramebufferStatus(int target);
  public abstract boolean isFramebuffer(int framebuffer);
  public abstract void getFramebufferAttachmentParameteriv(int target, int attachment, int pname, IntBuffer params);
  public abstract boolean isRenderbuffer(int renderbuffer);
  public abstract void getRenderbufferParameteriv(int target, int pname, IntBuffer params);
  public abstract void blitFramebuffer(int srcX0, int srcY0, int srcX1, int srcY1, int dstX0, int dstY0, int dstX1, int dstY1, int mask, int filter);
  public abstract void renderbufferStorageMultisample(int target, int samples, int format, int width, int height);
  public abstract void readBuffer(int buf);
  public abstract void drawBuffer(int buf);
}
