PGraphicsOpenGL - beginDraw, image, endDraw optimising

how would i optimize PGraphicsOpenGL.beginDraw(); and PGraphicsOpenGL.endDraw() in the context of being called 3,840 times per second or more

as i do have this which works for noLoop() but not for loop, im not sure why

  void imageFrameBuffer() {
    if (DirectRendererLogging) println("image frame buffer");
    flush();
    PGraphicsOpenGL ppg = getPrimaryPG();
    // all of the following is protected
    // lets use reflection to access these

    // fbStackDepth is protected
    
    int fbStackDepth = reflection.get_fbStackDepth(ppg);
    
    if (fbStackDepth == 0) {
      throw new RuntimeException("popFramebuffer call is unbalanced.");
    }
    
    fbStackDepth--;
    
    reflection.put_fbStackDepth(ppg, fbStackDepth);

    // fbStack is protected
    FrameBuffer[] fbStack = reflection.get_fbStack(ppg);
    
    FrameBuffer fbo = fbStack[fbStackDepth];
    
    // currentFramebuffer is protected
    FrameBuffer currentFramebuffer = reflection.get_currentFramebuffer(ppg);

    if (currentFramebuffer != fbo) {
      if (DirectRendererLogging) println("currentFramebuffer != fbo");
      reflection.put_currentFramebuffer(ppg, fbo);
      parent.g.image(this, 0, 0);
      reflection.put_currentFramebuffer(ppg, currentFramebuffer);

      //if (fbo != null) {
      //  if (DirectRendererLogging) println("bind framebuffer");
      //  fbo.bind();
      //}
    }

    fbStackDepth++;
    
    reflection.put_fbStackDepth(ppg, fbStackDepth);
  }
DirectRenderer g2;

void settings() {
  // 23 FPS
  //fullScreen(P3D);
  
  // 60FPS
  size(400, 400, P3D);
}

void setup() {
  background(0);
  g2 = new DirectRenderer(this, width, height);
  g2.beginDraw_();
  noLoop();
}

void exit() {
  g2.endDraw_();
  //g2.DirectRendererExit();
  super.exit();
}

void draw_() {
  // this makes no difference wether beginDraw is used or beginDraw_() is used
  g2.background(0);
  g2.lights();
  g2.noStroke();
  g2.translate(width/2, height/2);
  g2.rotateX(frameCount/100.0);
  g2.rotateY(frameCount/200.0);
  g2.box(40);
  g2.imageFrameBuffer();
}

void draw() {
  background(0);
  int oldColor = getGraphics().fillColor;
  fill(255);
  textSize(16);
  //for (int i = 0; i < 32; i++) draw_();
  draw_();
  //image(g2, 0, 0);
  text("FPS: " + frameRate, 10, 20);
  fill(oldColor);
}

specifically if i can find a way to grab the PGraphics framebuffer data as quickly as possible with as little performance impact as possible then i could in theory just directly render the framebuffer directly to g (sketch PGraphics) and then figure out a way to set up and switch to multiple framebuffers without needing to call endDraw(); as a way of quick switching the PGraphic framebuffer associated with the main PGraphics FBO stack

for example

switchFramebuffer(g2);
g2.draw();
g2.render();
switchFramebuffer(g3);
g3.draw();
g3.render();

keep in mind that my implementation(copy) which is suffixed with _ achieves the exact same FPS rate as the built-in implementations (as long as

    DirectRendererImage = null;
    if (DirectRendererImage != null) {

is used)

specifically i am trying to optimize this to reduce the time taken to execute

      DirectRendererImage.beginDraw_(); // beginDraw();
      DirectRendererImage.image_(this, 0, 0); // image(PImage, float, float);
      DirectRendererImage.endDraw_(); // endDraw();

in that exact order, nothing else in between, specifically i am trying to render its framebuffer to my sketch canvas as quickly as possible

with 3,840 being this is called twice per frame, 60*2 = 120, for 32 objects, 120*32 = 3,840

public class DirectRenderer extends PGraphics3D {
  // ...
  void endDraw_() {
    // ...
    // 27 - 30 FPS if not null, otherwise 60 FPS
    
    if (DirectRendererImage != null) {
      
      // fast - no FPS reduction
      DirectRendererImage.beginDraw();
  
      // slow - reduces FPS by a range from 10 to 16 -- 27 to 30 FPS
      // image draws of the texture of the argument given to it
      DirectRendererImage.image_(this, 0, 0);
      
      // slow - reduces FPS by a range from 13 to 20 -- 40 to 47 FPS
      // if DirectRendererImage is not null this will recursive loop untill null
      DirectRendererImage.endDraw_();
    }
    // ...
  }
  // ...
}

CODE:

DirectRenderer g2;

void settings() {
  // 23 FPS
  //fullScreen(P3D);
  
  // 60FPS
  size(400, 400, P3D);
}

void setup() {
  background(0);
  g2 = new DirectRenderer(this, 500,500);
  //noLoop();
}

void exit() {
  //g2.DirectRendererExit();
  super.exit();
}

void draw_() {
  // this makes no difference wether beginDraw is used or beginDraw_() is used
  g2.beginDraw_();
  g2.background(0);
  g2.lights();
  g2.noStroke();
  g2.translate(width/2, height/2);
  g2.rotateX(frameCount/100.0);
  g2.rotateY(frameCount/200.0);
  g2.box(40);
  g2.endDraw_();
}

void draw() {
  background(0);
  int oldColor = getGraphics().fillColor;
  fill(255);
  textSize(16);
  for (int i = 0; i < 32; i++) draw_();
  image(g2.DirectRendererImage, 0, 0);
  text("FPS: " + frameRate, 10, 20);
  fill(oldColor);
}

this is my class, with as much implemented (and copied from processing/opengl/PGraphicsOpenGL.java and other files) as i see required to help aid optimization

NOTE i have confirmed my implementation to be working with P3D renderer type, have not tested for other renderer types such as P2D or JAVAFX

/**
 * OpenGL renderer.
 */
public class DirectRenderer extends PGraphics3D {
  
  // to be safe, prefix everything with DirectRenderer
  
  boolean DirectRendererLogging = false;
  
  // use java.lang.reflection wrappers class to simplify code
  
  // all methods in DirectRendererReflection will cache if possible
  
  // DirectRendererReflection no longer extends the PGraphicsOpenGL
  // class and instead calls texCache.getClass()
  
  DirectRendererReflection reflection = new DirectRendererReflection();
  
  DirectRenderer DirectRendererImage = null;
  
  private void DirectRendererInit(PApplet papplet, int w, int h, boolean createDirectRendererImage) {
    // PApplet cannot be cast
    setParent(papplet);
    setPrimary(false);
    setSize(w, h);
    if (createDirectRendererImage) {
      DirectRendererImage = new DirectRenderer(papplet, w, h, false);
    }
  }
  
  DirectRenderer(PApplet papplet, int w, int h, boolean createDirectRendererImage) {
    DirectRendererInit(papplet, w, h, createDirectRendererImage);
  }

  DirectRenderer(PApplet papplet, int w, int h) {
    DirectRendererInit(papplet, w, h, true);
  }

  void DirectRendererSetup() {
    beginDraw();
  }
  
  void DirectRendererExit() {
    DirectRendererImage.endDraw();
  }
    
  protected void pushFramebuffer_() {
    PGraphicsOpenGL ppg = getPrimaryPG();
    // all of the following is protected
    // lets use reflection to access these

    // fbStackDepth is protected
    int fbStackDepth = reflection.get_fbStackDepth(ppg);
    
    if (fbStackDepth == FB_STACK_DEPTH) {
      throw new RuntimeException("Too many pushFramebuffer calls");
    }
    
    // fbStack is protected
    FrameBuffer[] fbStack = reflection.get_fbStack(ppg);

    // currentFramebuffer is protected

    fbStack[fbStackDepth] = reflection.get_currentFramebuffer(ppg);
    
    reflection.put_fbStack(ppg, fbStack);
        
    fbStackDepth++;
    
    reflection.put_fbStackDepth(ppg, fbStackDepth);    
  }
  
  protected void setFramebuffer_(FrameBuffer fbo) {
    PGraphicsOpenGL ppg = getPrimaryPG();
    // all of the following is protected
    // lets use reflection to access these

    // currentFramebuffer is protected
    FrameBuffer currentFramebuffer = reflection.get_currentFramebuffer(ppg);
    
    if (currentFramebuffer != fbo) {
      currentFramebuffer = fbo;
      reflection.put_currentFramebuffer(ppg, currentFramebuffer);
      if (currentFramebuffer != null) currentFramebuffer.bind();
    }
  }
  
  /**
   * Flips intArray along the X axis.
   * @param intArray int[]
   * @param mult int
   */
  protected void flipArrayOnX(int[] intArray, int mult)  {
    int index = 0;
    int xindex = mult * (width - 1);
    for (int x = 0; x < width / 2; x++) {
      for (int y = 0; y < height; y++)  {
        int i = index + mult * y * width;
        int j = xindex + mult * y * width;

        for (int c = 0; c < mult; c++) {
          int temp = intArray[i];
          intArray[i] = intArray[j];
          intArray[j] = temp;

          i++;
          j++;
        }

      }
      index += mult;
      xindex -= mult;
    }
  }


  /**
   * Flips intArray along the Y axis.
   * @param intArray int[]
   * @param mult int
   */
  protected void flipArrayOnY(int[] intArray, int mult) {
    int index = 0;
    int yindex = mult * (height - 1) * width;
    for (int y = 0; y < height / 2; y++) {
      for (int x = 0; x < mult * width; x++) {
        int temp = intArray[index];
        intArray[index] = intArray[yindex];
        intArray[yindex] = temp;

        index++;
        yindex++;
      }
      yindex -= mult * width * 2;
    }
  }
  
  /**
   * Reorders an OpenGL pixel array (RGBA) into ARGB. The array must be
   * of size width * height.
   * @param pixels int[]
   */
  protected void convertToARGB(int[] pixels) {
    int t = 0;
    int p = 0;
    // BIG_Endian is private
    
    if (reflection.get_BigEndian()) {
      // RGBA to ARGB conversion: shifting RGB 8 bits to the right,
      // and placing A 24 bits to the left.
      for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
          int pixel = pixels[p++];
          pixels[t++] = (pixel >>> 8) | ((pixel << 24) & 0xFF000000);
        }
      }
    } else {
      // We have to convert ABGR into ARGB, so R and B must be swapped,
      // A and G just brought back in.
      for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
          int pixel = pixels[p++];
          pixels[t++] = ((pixel & 0xFF) << 16) | ((pixel & 0xFF0000) >> 16) |
                          (pixel & 0xFF00FF00);
        }
      }
    }
  }
  
  //@Override
  protected void popFramebuffer_() {
    PGraphicsOpenGL ppg = getPrimaryPG();
    // all of the following is protected
    // lets use reflection to access these

    // fbStackDepth is protected
    
    int fbStackDepth = reflection.get_fbStackDepth(ppg);
    
    if (fbStackDepth == 0) {
      throw new RuntimeException("popFramebuffer call is unbalanced.");
    }
    
    fbStackDepth--;
    
    reflection.put_fbStackDepth(ppg, fbStackDepth);

    // fbStack is protected
    FrameBuffer[] fbStack = reflection.get_fbStack(ppg);
    
    FrameBuffer fbo = fbStack[fbStackDepth];
    
    // currentFramebuffer is protected
    FrameBuffer currentFramebuffer = reflection.get_currentFramebuffer(ppg);

    if (currentFramebuffer != fbo) {
      if (DirectRendererLogging) println("framebuffer != fbo");
      currentFramebuffer.finish();
      currentFramebuffer = fbo;
      reflection.put_currentFramebuffer(ppg, currentFramebuffer);

      if (currentFramebuffer != null) {
        if (DirectRendererLogging) println("bind framebuffer");
        currentFramebuffer.bind();
      }
    }
  }
  
  protected void initOffscreen_() {
    // Getting the context and capabilities from the main renderer.
    loadTextureImpl(textureSampling, false);

    FrameBuffer ofb = offscreenFramebuffer;
    FrameBuffer mfb = multisampleFramebuffer;

    // dispose is private, obtain it via reflection
    // In case of re-initialization (for example, when the smooth level
    // is changed), we make sure that all the OpenGL resources associated
    // to the surface are released by calling delete().
    if (ofb != null || mfb != null) {
      reflection.get_dispose_begin();
      if (ofb != null) {
        reflection.invoke_dispose(ofb);
        ofb = null;
      }
      if (mfb != null) {
        reflection.invoke_dispose(mfb);
        mfb = null;
      }
    }
    boolean packed = depthBits == 24 && stencilBits == 8 &&
                     packedDepthStencilSupported;
    
    // FrameBuffer is private, obtain it via reflection
    
    reflection.get_FrameBuffer7Args_begin();
    
    if (PGraphicsOpenGL.fboMultisampleSupported && 1 < PGL.smoothToSamples(smooth)) {
      mfb = reflection.invoke_FrameBuffer7Args(this, texture.glWidth, texture.glHeight, PGL.smoothToSamples(smooth), 0,
                            depthBits, stencilBits, packed, false);
      mfb.clear();
      multisampleFramebuffer = mfb;
      offscreenMultisample = true;

      // The offscreen framebuffer where the multisampled image is finally drawn
      // to. If depth reading is disabled it doesn't need depth and stencil buffers
      // since they are part of the multisampled framebuffer.
      if (hints[ENABLE_BUFFER_READING]) {
        ofb = reflection.invoke_FrameBuffer7Args(this, texture.glWidth, texture.glHeight, 1, 1,
                              depthBits, stencilBits, packed, false);
      } else {
        ofb = reflection.invoke_FrameBuffer7Args(this, texture.glWidth, texture.glHeight, 1, 1,
                          0, 0, false, false);
      }
    } else {
      smooth = 0;
      ofb = reflection.invoke_FrameBuffer7Args(this, texture.glWidth, texture.glHeight, 1, 1,
                            depthBits, stencilBits, packed, false);
      offscreenMultisample = false;
    }
    
    ofb.setColorBuffer(texture);
    ofb.clear();
    offscreenFramebuffer = ofb;

    initialized = true;
  }
  
  protected void swapOffscreenTextures_() {
    FrameBuffer ofb = offscreenFramebuffer;
    if (texture != null && ptexture != null && ofb != null) {
      int temp = texture.glName;
      texture.glName = ptexture.glName;
      ptexture.glName = temp;
      ofb.setColorBuffer(texture);
    }
  }
  
  protected void drawPTexture_() {
    if (ptexture != null) {
      // No blend so the texure replaces wherever is on the screen,
      // irrespective of the alpha
      pgl.disable(PGL.BLEND);
      pgl.drawTexture(ptexture.glTarget, ptexture.glName,
                      ptexture.glWidth, ptexture.glHeight,
                      0, 0, width, height);
      pgl.enable(PGL.BLEND);
    }
  }
  
  protected void restartPGL_() {
    initialized = false;
  }
  
  protected void beginOffscreenDraw_() {
    if (!initialized) {
      if (DirectRendererLogging) println("not initialized");
      initOffscreen_();
    } else {
      if (DirectRendererLogging) println("initialized");
      FrameBuffer ofb = offscreenFramebuffer;
      FrameBuffer mfb = multisampleFramebuffer;
      // contextIsOutdated is private, obtain it via reflection
      
      reflection.get_contextIsOutdated_begin();
      boolean outdated = ofb != null && reflection.invoke_contextIsOutdated(ofb);
      boolean outdatedMulti = mfb != null && reflection.invoke_contextIsOutdated(mfb);
      
      if (outdated || outdatedMulti) {
        if (DirectRendererLogging) println("outdated");
        restartPGL_();
        initOffscreen_();
      } else {
        if (DirectRendererLogging) println("swapOffscreenTextures");
        // The back texture of the past frame becomes the front,
        // and the front texture becomes the new back texture where the
        // new frame is drawn to.
        swapOffscreenTextures_();
      }
    }
    
    if (DirectRendererLogging) println("pushFramebuffer");

    pushFramebuffer_();
    if (offscreenMultisample) {
      FrameBuffer mfb = multisampleFramebuffer;
      if (mfb != null) {
        if (DirectRendererLogging) println("set framebuffer mfb");
        setFramebuffer_(mfb);
      }
    } else {
      FrameBuffer ofb = offscreenFramebuffer;
      if (ofb != null) {
        if (DirectRendererLogging) println("set framebuffer ofb");
        setFramebuffer_(ofb);
      }
    }

    // Render previous back texture (now is the front) as background
    if (DirectRendererLogging) println("drawPTexture");
    drawPTexture_();

    // Restoring the clipping configuration of the offscreen surface.
    if (clip) {
      pgl.enable(PGL.SCISSOR_TEST);
      pgl.scissor(clipRect[0], clipRect[1], clipRect[2], clipRect[3]);
    } else {
      pgl.disable(PGL.SCISSOR_TEST);
    }
  }
  
  //@Override
  protected void endOffscreenDraw_() {
    // the only functions which appear to copy the texture are:
    //   popFramebuffer();
    //   if (texture != null) {
    //     texture.updateTexels(); // Mark all texels in screen texture as modified.
    //   }
    
    
    if (offscreenMultisample) {
      if (DirectRendererLogging) println("multi sample offscreen");
      FrameBuffer ofb = offscreenFramebuffer;
      FrameBuffer mfb = multisampleFramebuffer;
      if (ofb != null && mfb != null) {
        mfb.copyColor(ofb);
      }
    }

    if (DirectRendererLogging) println("pop frame buffer");
    popFramebuffer_();
    
    if (backgroundA == 1) {
      // this seems to have no effect
      if (DirectRendererLogging) println("backgroundA");
      // Set alpha channel to opaque in order to match behavior of JAVA2D, not
      // on the multisampled FBO because it leads to wrong background color
      // on some Macbooks with AMD graphics.
      pgl.colorMask(false, false, false, true);
      pgl.clearColor(0, 0, 0, backgroundA);
      pgl.clear(PGL.COLOR_BUFFER_BIT);
      pgl.colorMask(true, true, true, true);
    }

    if (texture != null) {
      texture.updateTexels(); // Mark all texels in screen texture as modified.
    }
    
    // the function restoreGL is protected, lets call it via Reflection
    
    reflection.get_restoreGL_begin();
    reflection.invoke_restoreGL(getPrimaryPG());
  }
  
  void texture_get(int[] pixels) {
    // texture.pg is protected
    PGraphicsOpenGL pg = reflection.get_texture_pg(texture);

    // FrameBuffer is private, obtain it via reflection
    FrameBuffer tempFbo = null;
    
    reflection.get_FrameBuffer3Args_begin();
    tempFbo = reflection.invoke_FrameBuffer3Args(pg, texture.glWidth, texture.glHeight);
    
    tempFbo.setColorBuffer(texture);
    
    if (DirectRendererLogging) println("pushFramebuffer");
    pushFramebuffer();

    if (DirectRendererLogging) println("setFramebuffer");
    setFramebuffer(tempFbo);
    
    tempFbo.readPixels();
    
    if (DirectRendererLogging) println("popFramebuffer");
    popFramebuffer();

    tempFbo.getPixels(pixels);
    convertToARGB(pixels);

    // texture.invertedX and texture.invertedY are private, obtain it via reflection
    if (reflection.get_texture_invertedX(texture)) {
      if (DirectRendererLogging) println("flipArrayOnX");
      flipArrayOnX(pixels, 1);
    }

    if (reflection.get_texture_invertedY(texture)) {
      if (DirectRendererLogging) println("flipArrayOnY");
      flipArrayOnY(pixels, 1);
    }
  }

  void beginDraw_() {
    // the function getGL is protected, lets call it via Reflection
    
    reflection.get_getGL_begin();
    reflection.invoke_getGL(pgl, getPrimaryPGL());
    
    // the function setCurrentPG is protected, lets call it via Reflection
    
    reflection.get_setCurrentPG1Arg_begin();
    reflection.invoke_setCurrentPG1Arg(getPrimaryPG(), this);

    // This has to go after the surface initialization, otherwise offscreen
    // surfaces will have a null gl object.
    report("top beginDraw()");

    if (!checkGLThread()) {
      if (DirectRendererLogging) println("if (!checkGLThread()) return;");
      return;
    }

    if (drawing) {
      if (DirectRendererLogging) println("if (drawing) return;");
      return;
    }
    
    // the method texCache is protected
    // lets access it via reflection
    
    TexCache texCache = (TexCache) reflection.get_texCache(getPrimaryPG());
    
    // the function texCache.containsTexture(this) is protected
    // lets call it via Reflection

    if (!primaryGraphics && reflection.invoke_containsTexture(texCache, this)) {
      // This offscreen surface is being used as a texture earlier in draw,
      // so we should update the rendering up to this point since it will be
      // modified.
      if (DirectRendererLogging) println("getPrimaryPG().flush();");
      getPrimaryPG().flush();
    }

    if (!glParamsRead) {
      if (DirectRendererLogging) println("getGLParameters();");
      getGLParameters();
    }

    setViewport();
    if (DirectRendererLogging) println("beginOffscreenDraw();");
    beginOffscreenDraw_();
    checkSettings();

    drawing = true;
    
    report("bot beginDraw()");
  }
  
  void endDraw_() {
    report("top endDraw()");
    
    if (!drawing) {
      return;
    }
    
    flush();
    endOffscreenDraw_();
    
    // the function setCurrentPG is protected, lets call it via Reflection
    
    reflection.get_setCurrentPG0Args_begin();
    reflection.invoke_setCurrentPG0Args(getPrimaryPG());
    
    drawing = false;
    
    // 27 - 30 FPS if not null, otherwise 60 FPS
    
    if (DirectRendererImage != null) {
      
      // fast - no FPS reduction
      DirectRendererImage.beginDraw();
  
      // slow - reduces FPS by a range from 10 to 16 -- 27 to 30 FPS
      // image draws of the texture of the argument given to it
      DirectRendererImage.image_(this, 0, 0);
      
      // slow - reduces FPS by a range from 13 to 20 -- 40 to 47 FPS
      // if DirectRendererImage is not null this will recursive loop untill null
      DirectRendererImage.endDraw_();
    }
    
    //DirectRendererImage.loadPixels();
    //image_(this, width, height);
    // IMPORTANT: without texture.get() the performance jumps up signifigantly
    
    // with: 11 FPS for 400x400 32 draws
    // without: 60 FPS for 400x400 32 draws
    
    // DirectRendererImage = get(); // THIS IS AS SLOW AS texture.get()
    
    //PImage test = createImage(500, 500, ARGB);
    //test.loadPixels();
    //texture_get(test.pixels);
    //test.updatePixels();
    //test = null;
    
    //DirectRendererImage.updatePixels();
    report("bot endDraw()");
  }
  
  /**
   * Expects x1, y1, x2, y2 coordinates where (x2 >= x1) and (y2 >= y1).
   * If tint() has been called, the image will be colored.
   * <p/>
   * The default implementation draws an image as a textured quad.
   * The (u, v) coordinates are in image space (they're ints, after all..)
   */
  protected void imageImpl_(PImage img,
                           float x1, float y1, float x2, float y2,
                           int u1, int v1, int u2, int v2) {
    boolean savedStroke = stroke;
    int savedTextureMode = textureMode;

    stroke = false;
    textureMode = IMAGE;


    u1 *= img.pixelDensity;
    u2 *= img.pixelDensity;
    v1 *= img.pixelDensity;
    v2 *= img.pixelDensity;

    beginShape(QUADS);
    //texture(img);
    // texture(img) just calls:
    // public void texture(PImage image) { textureImage = image; }
    textureImage = img;
    vertex(x1, y1, u1, v1);
    vertex(x1, y2, u1, v2);
    vertex(x2, y2, u2, v2);
    vertex(x2, y1, u2, v1);
    endShape();

    stroke = savedStroke;
    textureMode = savedTextureMode;
  }
  
  public void image_(PImage img, float a, float b) {
    // Starting in release 0144, image errors are simply ignored.
    // loadImageAsync() sets width and height to -1 when loading fails.
    if (img.width == -1 || img.height == -1) return;

    if (imageMode == CORNER || imageMode == CORNERS) {
      imageImpl_(img,
                a, b, a+img.width, b+img.height,
                0, 0, img.width, img.height);

    } else if (imageMode == CENTER) {
      float x1 = a - img.width/2;
      float y1 = b - img.height/2;
      imageImpl_(img,
                x1, y1, x1+img.width, y1+img.height,
                0, 0, img.width, img.height);
    }
  }
  
  /**
   * Report on anything from glError().
   * Don't use this inside glBegin/glEnd otherwise it'll
   * throw an GL_INVALID_OPERATION error.
   */
  @Override
  protected void report(String where) {
    if (DirectRendererLogging) println(where);
    if (!hints[DISABLE_OPENGL_ERRORS]) {
      int err = pgl.getError();
      if (err != 0) {
        String errString = pgl.errorString(err);
        String msg = "OpenGL error " + err + " at " + where + ": " + errString;
        PGraphics.showWarning(msg);
      }
    }
  }
}