@Override
    public String toString() {
      final String ppTxt =
          null == iVBO
              ? ", no post-processing"
              : ", uvScale["
                  + eyeToSourceUVScale.floatBufferValue().get(0)
                  + ", "
                  + eyeToSourceUVScale.floatBufferValue().get(1)
                  + "], uvOffset["
                  + eyeToSourceUVOffset.floatBufferValue().get(0)
                  + ", "
                  + eyeToSourceUVOffset.floatBufferValue().get(1)
                  + "]";

      return "Eye["
          + eyeName
          + ", viewport "
          + viewport
          + ", "
          + eyeParameter
          + ", vertices "
          + vertexCount
          + ", indices "
          + indexCount
          + ppTxt
          + ", desc "
          + eyeParameter
          + "]";
    }
    private void linkData(final GL2ES2 gl, final ShaderProgram sp) {
      if (null == iVBO) return;

      if (0 > vboPos.setLocation(gl, sp.program())) {
        throw new GLException("Couldn't locate " + vboPos);
      }
      if (0 > vboParams.setLocation(gl, sp.program())) {
        throw new GLException("Couldn't locate " + vboParams);
      }
      if (0 > vboTexCoordsR.setLocation(gl, sp.program())) {
        throw new GLException("Couldn't locate " + vboTexCoordsR);
      }
      if (StereoUtil.usesChromaticDistortion(distortionBits)) {
        if (0 > vboTexCoordsG.setLocation(gl, sp.program())) {
          throw new GLException("Couldn't locate " + vboTexCoordsG);
        }
        if (0 > vboTexCoordsB.setLocation(gl, sp.program())) {
          throw new GLException("Couldn't locate " + vboTexCoordsB);
        }
      }
      if (0 > eyeToSourceUVScale.setLocation(gl, sp.program())) {
        throw new GLException("Couldn't locate " + eyeToSourceUVScale);
      }
      if (0 > eyeToSourceUVOffset.setLocation(gl, sp.program())) {
        throw new GLException("Couldn't locate " + eyeToSourceUVOffset);
      }
      if (StereoUtil.usesTimewarpDistortion(distortionBits)) {
        if (0 > eyeRotationStart.setLocation(gl, sp.program())) {
          throw new GLException("Couldn't locate " + eyeRotationStart);
        }
        if (0 > eyeRotationEnd.setLocation(gl, sp.program())) {
          throw new GLException("Couldn't locate " + eyeRotationEnd);
        }
      }
      iVBO.seal(gl, true);
      iVBO.enableBuffer(gl, false);
      indices.seal(gl, true);
      indices.enableBuffer(gl, false);
    }
 @Override
 public String toString() {
   return "GenericStereo[distortion["
       + StereoUtil.distortionBitsToString(distortionBits)
       + "], eyeTexSize "
       + Arrays.toString(eyeTextureSizes)
       + ", sbsSize "
       + totalTextureSize
       + ", texCount "
       + textureCount
       + ", texUnit "
       + (null != texUnit0 ? texUnit0.intValue() : "n/a")
       + ", "
       + PlatformPropsImpl.NEWLINE
       + "  "
       + (0 < eyes.length ? eyes[0] : "none")
       + ", "
       + PlatformPropsImpl.NEWLINE
       + "  "
       + (1 < eyes.length ? eyes[1] : "none")
       + "]";
 }
    /* pp */ GenericEye(
        final GenericStereoDevice device,
        final int distortionBits,
        final float[] eyePositionOffset,
        final EyeParameter eyeParam,
        final DimensionImmutable textureSize,
        final RectangleImmutable eyeViewport) {
      this.eyeName = eyeParam.number;
      this.distortionBits = distortionBits;
      this.viewport = eyeViewport;

      final boolean usePP = null != device.config.distortionMeshProducer && 0 != distortionBits;

      final boolean usesTimewarp = usePP && StereoUtil.usesTimewarpDistortion(distortionBits);
      final FloatBuffer fstash = Buffers.newDirectFloatBuffer(2 + 2 + (usesTimewarp ? 16 + 16 : 0));

      if (usePP) {
        eyeToSourceUVScale =
            new GLUniformData("svr_EyeToSourceUVScale", 2, Buffers.slice2Float(fstash, 0, 2));
        eyeToSourceUVOffset =
            new GLUniformData("svr_EyeToSourceUVOffset", 2, Buffers.slice2Float(fstash, 2, 2));
      } else {
        eyeToSourceUVScale = null;
        eyeToSourceUVOffset = null;
      }

      if (usesTimewarp) {
        eyeRotationStart =
            new GLUniformData("svr_EyeRotationStart", 4, 4, Buffers.slice2Float(fstash, 4, 16));
        eyeRotationEnd =
            new GLUniformData("svr_EyeRotationEnd", 4, 4, Buffers.slice2Float(fstash, 20, 16));
      } else {
        eyeRotationStart = null;
        eyeRotationEnd = null;
      }

      this.eyeParameter = eyeParam;

      // Setup: eyeToSourceUVScale, eyeToSourceUVOffset
      if (usePP) {
        final ScaleAndOffset2D textureScaleAndOffset =
            new ScaleAndOffset2D(eyeParam.fovhv, textureSize, eyeViewport);
        if (StereoDevice.DEBUG) {
          System.err.println("XXX." + eyeName + ": eyeParam      " + eyeParam);
          System.err.println("XXX." + eyeName + ": uvScaleOffset " + textureScaleAndOffset);
          System.err.println("XXX." + eyeName + ": textureSize   " + textureSize);
          System.err.println("XXX." + eyeName + ": viewport      " + eyeViewport);
        }
        final FloatBuffer eyeToSourceUVScaleFB = eyeToSourceUVScale.floatBufferValue();
        eyeToSourceUVScaleFB.put(0, textureScaleAndOffset.scale[0]);
        eyeToSourceUVScaleFB.put(1, textureScaleAndOffset.scale[1]);
        final FloatBuffer eyeToSourceUVOffsetFB = eyeToSourceUVOffset.floatBufferValue();
        eyeToSourceUVOffsetFB.put(0, textureScaleAndOffset.offset[0]);
        eyeToSourceUVOffsetFB.put(1, textureScaleAndOffset.offset[1]);
      } else {
        vertexCount = 0;
        indexCount = 0;
        iVBO = null;
        vboPos = null;
        vboParams = null;
        vboTexCoordsR = null;
        vboTexCoordsG = null;
        vboTexCoordsB = null;
        indices = null;
        if (StereoDevice.DEBUG) {
          System.err.println("XXX." + eyeName + ": " + this);
        }
        return;
      }
      final DistortionMesh meshData =
          device.config.distortionMeshProducer.create(eyeParam, distortionBits);
      if (null == meshData) {
        throw new GLException(
            "Failed to create meshData for eye "
                + eyeParam
                + ", and "
                + StereoUtil.distortionBitsToString(distortionBits));
      }

      vertexCount = meshData.vertexCount;
      indexCount = meshData.indexCount;

      /**
       * 2+2+2+2+2: { vec2 position, vec2 color, vec2 texCoordR, vec2 texCoordG, vec2 texCoordB }
       */
      final boolean useChromatic = StereoUtil.usesChromaticDistortion(distortionBits);
      final boolean useVignette = StereoUtil.usesVignetteDistortion(distortionBits);

      final int compsPerElement =
          2 + 2 + 2 + (useChromatic ? 2 + 2 /* texCoordG + texCoordB */ : 0);
      iVBO =
          GLArrayDataServer.createGLSLInterleaved(
              compsPerElement, GL.GL_FLOAT, false, vertexCount, GL.GL_STATIC_DRAW);
      vboPos = iVBO.addGLSLSubArray("svr_Position", 2, GL.GL_ARRAY_BUFFER);
      vboParams = iVBO.addGLSLSubArray("svr_Params", 2, GL.GL_ARRAY_BUFFER);
      vboTexCoordsR = iVBO.addGLSLSubArray("svr_TexCoordR", 2, GL.GL_ARRAY_BUFFER);
      if (useChromatic) {
        vboTexCoordsG = iVBO.addGLSLSubArray("svr_TexCoordG", 2, GL.GL_ARRAY_BUFFER);
        vboTexCoordsB = iVBO.addGLSLSubArray("svr_TexCoordB", 2, GL.GL_ARRAY_BUFFER);
      } else {
        vboTexCoordsG = null;
        vboTexCoordsB = null;
      }
      indices =
          GLArrayDataServer.createData(
              1, GL.GL_SHORT, indexCount, GL.GL_STATIC_DRAW, GL.GL_ELEMENT_ARRAY_BUFFER);

      /**
       * 2+2+2+2+2: { vec2 position, vec2 color, vec2 texCoordR, vec2 texCoordG, vec2 texCoordB }
       */
      final FloatBuffer iVBOFB = (FloatBuffer) iVBO.getBuffer();

      for (int vertNum = 0; vertNum < vertexCount; vertNum++) {
        final DistortionMesh.DistortionVertex v = meshData.vertices[vertNum];
        int dataIdx = 0;

        if (StereoDevice.DUMP_DATA) {
          System.err.println("XXX." + eyeName + ": START VERTEX " + vertNum + " / " + vertexCount);
        }
        // pos
        if (v.pos_size >= 2) {
          if (StereoDevice.DUMP_DATA) {
            System.err.println(
                "XXX." + eyeName + ": pos [" + v.data[dataIdx] + ", " + v.data[dataIdx + 1] + "]");
          }
          iVBOFB.put(v.data[dataIdx]);
          iVBOFB.put(v.data[dataIdx + 1]);
        } else {
          iVBOFB.put(0f);
          iVBOFB.put(0f);
        }
        dataIdx += v.pos_size;

        // params
        if (v.vignetteFactor_size >= 1 && useVignette) {
          if (StereoDevice.DUMP_DATA) {
            System.err.println("XXX." + eyeName + ": vignette " + v.data[dataIdx]);
          }
          iVBOFB.put(v.data[dataIdx]);
        } else {
          iVBOFB.put(1.0f);
        }
        dataIdx += v.vignetteFactor_size;

        if (v.timewarpFactor_size >= 1) {
          if (StereoDevice.DUMP_DATA) {
            System.err.println("XXX." + eyeName + ": timewarp " + v.data[dataIdx]);
          }
          iVBOFB.put(v.data[dataIdx]);
        } else {
          iVBOFB.put(1.0f);
        }
        dataIdx += v.timewarpFactor_size;

        // texCoordR
        if (v.texR_size >= 2) {
          if (StereoDevice.DUMP_DATA) {
            System.err.println(
                "XXX." + eyeName + ": texR [" + v.data[dataIdx] + ", " + v.data[dataIdx + 1] + "]");
          }
          iVBOFB.put(v.data[dataIdx]);
          iVBOFB.put(v.data[dataIdx + 1]);
        } else {
          iVBOFB.put(1f);
          iVBOFB.put(1f);
        }
        dataIdx += v.texR_size;

        if (useChromatic) {
          // texCoordG
          if (v.texG_size >= 2) {
            if (StereoDevice.DUMP_DATA) {
              System.err.println(
                  "XXX."
                      + eyeName
                      + ": texG ["
                      + v.data[dataIdx]
                      + ", "
                      + v.data[dataIdx + 1]
                      + "]");
            }
            iVBOFB.put(v.data[dataIdx]);
            iVBOFB.put(v.data[dataIdx + 1]);
          } else {
            iVBOFB.put(1f);
            iVBOFB.put(1f);
          }
          dataIdx += v.texG_size;

          // texCoordB
          if (v.texB_size >= 2) {
            if (StereoDevice.DUMP_DATA) {
              System.err.println(
                  "XXX."
                      + eyeName
                      + ": texB ["
                      + v.data[dataIdx]
                      + ", "
                      + v.data[dataIdx + 1]
                      + "]");
            }
            iVBOFB.put(v.data[dataIdx]);
            iVBOFB.put(v.data[dataIdx + 1]);
          } else {
            iVBOFB.put(1f);
            iVBOFB.put(1f);
          }
          dataIdx += v.texB_size;
        } else {
          dataIdx += v.texG_size;
          dataIdx += v.texB_size;
        }
      }
      if (StereoDevice.DUMP_DATA) {
        System.err.println("XXX." + eyeName + ": iVBO " + iVBO);
      }
      {
        if (StereoDevice.DUMP_DATA) {
          System.err.println("XXX." + eyeName + ": idx " + indices + ", count " + indexCount);
          for (int i = 0; i < indexCount; i++) {
            if (0 == i % 16) {
              System.err.printf("%n%5d: ", i);
            }
            System.err.printf("%5d, ", (int) meshData.indices[i]);
          }
          System.err.println();
        }
        final ShortBuffer out = (ShortBuffer) indices.getBuffer();
        out.put(meshData.indices, 0, meshData.indexCount);
      }
      if (StereoDevice.DEBUG) {
        System.err.println("XXX." + eyeName + ": " + this);
      }
    }
  @Override
  public final void init(final GL gl) {
    if (StereoDevice.DEBUG) {
      System.err.println(JoglVersion.getGLInfo(gl, null).toString());
    }
    if (null != sp) {
      throw new IllegalStateException("Already initialized");
    }
    if (!ppAvailable()) {
      return;
    }
    final GL2ES2 gl2es2 = gl.getGL2ES2();

    final String vertexShaderBasename;
    final String fragmentShaderBasename;
    {
      final boolean usesTimewarp = StereoUtil.usesTimewarpDistortion(distortionBits);
      final boolean usesChromatic = StereoUtil.usesChromaticDistortion(distortionBits);

      final StringBuilder sb = new StringBuilder();
      sb.append(shaderPrefix01);
      if (!usesChromatic && !usesTimewarp) {
        sb.append(shaderPlainSuffix);
      } else if (usesChromatic && !usesTimewarp) {
        sb.append(shaderChromaSuffix);
      } else if (usesTimewarp) {
        sb.append(shaderTimewarpSuffix);
        if (usesChromatic) {
          sb.append(shaderChromaSuffix);
        }
      }
      vertexShaderBasename = sb.toString();
      sb.setLength(0);
      sb.append(shaderPrefix01);
      if (usesChromatic) {
        sb.append(shaderChromaSuffix);
      } else {
        sb.append(shaderPlainSuffix);
      }
      fragmentShaderBasename = sb.toString();
    }
    final ShaderCode vp0 =
        ShaderCode.create(
            gl2es2,
            GL2ES2.GL_VERTEX_SHADER,
            GenericStereoDeviceRenderer.class,
            "shader",
            "shader/bin",
            vertexShaderBasename,
            true);
    final ShaderCode fp0 =
        ShaderCode.create(
            gl2es2,
            GL2ES2.GL_FRAGMENT_SHADER,
            GenericStereoDeviceRenderer.class,
            "shader",
            "shader/bin",
            fragmentShaderBasename,
            true);
    vp0.defaultShaderCustomization(gl2es2, true, true);
    fp0.defaultShaderCustomization(gl2es2, true, true);

    sp = new ShaderProgram();
    sp.add(gl2es2, vp0, System.err);
    sp.add(gl2es2, fp0, System.err);
    if (!sp.link(gl2es2, System.err)) {
      throw new GLException("could not link program: " + sp);
    }
    sp.useProgram(gl2es2, true);
    if (0 > texUnit0.setLocation(gl2es2, sp.program())) {
      throw new GLException("Couldn't locate " + texUnit0);
    }
    for (int i = 0; i < eyes.length; i++) {
      eyes[i].linkData(gl2es2, sp);
    }
    sp.useProgram(gl2es2, false);
  }
 @Override
 public final int getTextureUnit() {
   return ppAvailable() ? texUnit0.intValue() : 0;
 }