/*
* Copyright (C) 2014 The CyanogenMod Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.cyanogenmod.wallpapers.photophase.utils;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.Rect;
import android.media.effect.Effect;
import android.opengl.ETC1Util;
import android.opengl.ETC1Util.ETC1Texture;
import android.opengl.ETC1;
import android.opengl.GLES20;
import android.opengl.GLUtils;
import android.util.Log;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* A helper class with some useful methods for deal with GLES.
*/
public final class GLESUtil {
private static final String TAG = "GLESUtil";
private static final boolean DEBUG = false;
public static final boolean DEBUG_GL_MEMOBJS = false;
public static final String DEBUG_GL_MEMOBJS_NEW_TAG = "MEMOBJS_NEW";
public static final String DEBUG_GL_MEMOBJS_DEL_TAG = "MEMOBJS_DEL";
private static final Object SYNC = new Object();
private static final int MAX_ACEPTABLE_COMPRESSION_TIME = 2500;
private static boolean sDisabledTextureCompression = false;
/**
* A helper class to deal with OpenGL float colors.
*/
public static class GLColor {
private static final float MAX_COLOR = 255.0f;
/**
* Red
*/
public float r;
/**
* Green
*/
public float g;
/**
* Blue
*/
public float b;
/**
* Alpha
*/
public float a;
/**
* Constructor of GLColor
from ARGB
*
* @param a Alpha
* @param r Red
* @param g Green
* @param b Alpha
*/
public GLColor(int a, int r, int g, int b) {
this.a = a / MAX_COLOR;
this.r = r / MAX_COLOR;
this.g = g / MAX_COLOR;
this.b = b / MAX_COLOR;
}
/**
* Constructor of GLColor
from ARGB.
*
* @param argb An #AARRGGBB string
*/
public GLColor(String argb) {
int color = Color.parseColor(argb);
this.a = Color.alpha(color) / MAX_COLOR;
this.r = Color.red(color) / MAX_COLOR;
this.g = Color.green(color) / MAX_COLOR;
this.b = Color.blue(color) / MAX_COLOR;
}
/**
* Constructor of GLColor
from ARGB.
*
* @param argb An #AARRGGBB number
*/
public GLColor(int argb) {
this.a = Color.alpha(argb) / MAX_COLOR;
this.r = Color.red(argb) / MAX_COLOR;
this.g = Color.green(argb) / MAX_COLOR;
this.b = Color.blue(argb) / MAX_COLOR;
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + Float.floatToIntBits(a);
result = prime * result + Float.floatToIntBits(b);
result = prime * result + Float.floatToIntBits(g);
result = prime * result + Float.floatToIntBits(r);
return result;
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
GLColor other = (GLColor) obj;
if (Float.floatToIntBits(a) != Float.floatToIntBits(other.a))
return false;
if (Float.floatToIntBits(b) != Float.floatToIntBits(other.b))
return false;
if (Float.floatToIntBits(g) != Float.floatToIntBits(other.g))
return false;
if (Float.floatToIntBits(r) != Float.floatToIntBits(other.r))
return false;
return true;
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
return "#"+Integer.toHexString(Color.argb((int)a, (int)r, (int)g, (int)b));
}
}
/**
* Class that holds some information about a GLES texture
*/
public static class GLESTextureInfo {
/**
* Handle of the texture
*/
public int handle = 0;
/**
* The bitmap reference
*/
public Bitmap bitmap;
/**
* The path to the texture
*/
public File path;
/**
* The effect to apply
*/
public Effect effect;
}
/**
* Method that load a vertex shader and returns its handler identifier.
*
* @param src The source shader
* @return int The handler identifier of the shader
*/
public static int loadVertexShader(String src) {
return loadShader(src, GLES20.GL_VERTEX_SHADER);
}
/**
* Method that load a fragment shader and returns its handler identifier.
*
* @param src The source shader
* @return int The handler identifier of the shader
*/
public static int loadFragmentShader(String src) {
return loadShader(src, GLES20.GL_FRAGMENT_SHADER);
}
/**
* Method that load a shader and returns its handler identifier.
*
* @param src The source shader
* @param type The type of shader
* @return int The handler identifier of the shader
*/
public static int loadShader(String src, int type) {
int[] compiled = new int[1];
// Create, load and compile the shader
int shader = GLES20.glCreateShader(type);
if (GLESUtil.DEBUG_GL_MEMOBJS) {
Log.d(GLESUtil.DEBUG_GL_MEMOBJS_NEW_TAG, "glCreateShader (" + type + "): " + shader);
}
GLESUtil.glesCheckError("glCreateShader");
if (shader <= 0) {
Log.e(TAG, "Cannot create a shader");
return 0;
}
GLES20.glShaderSource(shader, src);
GLESUtil.glesCheckError("glShaderSource");
GLES20.glCompileShader(shader);
GLESUtil.glesCheckError("glesCheckError");
GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);
GLESUtil.glesCheckError("glesCheckError");
if (compiled[0] <= 0) {
String msg = "Shader compilation error trace:\n" + GLES20.glGetShaderInfoLog(shader);
Log.e(TAG, msg);
return 0;
}
return shader;
}
/**
* Method that create a new program from its shaders (vertex and fragment)
*
* @param res A resources reference
* @param vertexShaderId The vertex shader glsl resource
* @param fragmentShaderId The fragment shader glsl resource
* @return int The handler identifier of the program
*/
public static int createProgram(Resources res, int vertexShaderId, int fragmentShaderId) {
return createProgram(
readResource(res, vertexShaderId),
readResource(res, fragmentShaderId));
}
/**
* Method that create a new program from its shaders (vertex and fragment)
*
* @param vertexShaderSrc The vertex shader
* @param fragmentShaderSrc The fragment shader
* @return int The handler identifier of the program.
*/
public static int createProgram(String vertexShaderSrc, String fragmentShaderSrc) {
int vshader = 0;
int fshader = 0;
int progid = 0;
int[] link = new int[1];
try {
// Check that we have valid shaders
if (vertexShaderSrc == null || fragmentShaderSrc == null) {
return 0;
}
// Load the vertex and fragment shaders
vshader = loadVertexShader(vertexShaderSrc);
fshader = loadFragmentShader(fragmentShaderSrc);
// Create the programa ref
progid = GLES20.glCreateProgram();
if (GLESUtil.DEBUG_GL_MEMOBJS) {
Log.d(GLESUtil.DEBUG_GL_MEMOBJS_NEW_TAG, "glCreateProgram: " + progid);
}
GLESUtil.glesCheckError("glCreateProgram");
if (progid <= 0) {
String msg = "Cannot create a program";
Log.e(TAG, msg);
return 0;
}
// Attach the shaders
GLES20.glAttachShader(progid, vshader);
GLESUtil.glesCheckError("glAttachShader");
GLES20.glAttachShader(progid, fshader);
GLESUtil.glesCheckError("glAttachShader");
// Link the program
GLES20.glLinkProgram(progid);
GLESUtil.glesCheckError("glLinkProgram");
GLES20.glGetProgramiv(progid, GLES20.GL_LINK_STATUS, link, 0);
GLESUtil.glesCheckError("glGetProgramiv");
if (link[0] <= 0) {
String msg = "Program compilation error trace:\n" + GLES20.glGetProgramInfoLog(progid);
Log.e(TAG, msg);
return 0;
}
// Return the program
return progid;
} finally {
// Delete the shaders
if (vshader != 0) {
if (GLESUtil.DEBUG_GL_MEMOBJS) {
Log.d(GLESUtil.DEBUG_GL_MEMOBJS_DEL_TAG, "glDeleteShader (v): " + vshader);
}
GLES20.glDeleteShader(vshader);
GLESUtil.glesCheckError("glDeleteShader");
}
if (fshader != 0) {
if (GLESUtil.DEBUG_GL_MEMOBJS) {
Log.d(GLESUtil.DEBUG_GL_MEMOBJS_DEL_TAG, "glDeleteShader (f): " + fshader);
}
GLES20.glDeleteShader(fshader);
GLESUtil.glesCheckError("glDeleteShader");
}
}
}
/**
* Method that loads a texture from a file.
*
* @param file The image file
* @param dimensions The desired dimensions
* @param effect The effect to apply to the image or null if no effect is needed
* @param dimen The new dimensions
* @param recycle If the bitmap should be recycled
* @param compress Try to compress the bitmap into ETC1
* @return GLESTextureInfo The texture info
*/
public static GLESTextureInfo loadTexture(File file, Rect dimensions, Effect effect,
Rect dimen, boolean recycle, boolean compress) {
Bitmap bitmap = null;
try {
// Decode and associate the bitmap (invert the desired dimensions)
bitmap = BitmapUtils.decodeBitmap(file, dimensions.height(), dimensions.width());
if (bitmap == null) {
Log.e(TAG, "Failed to decode the file bitmap");
return new GLESTextureInfo();
}
if (DEBUG) Log.d(TAG, "image: " + file.getAbsolutePath());
GLESTextureInfo ti = loadTexture(bitmap, effect, dimen, compress);
ti.path = file;
return ti;
} catch (Exception e) {
String msg = "Failed to generate a valid texture from file: " + file.getAbsolutePath();
Log.e(TAG, msg, e);
return new GLESTextureInfo();
} finally {
// Recycle the bitmap
if (bitmap != null && recycle) {
bitmap.recycle();
bitmap = null;
}
}
}
/**
* Method that loads a texture from a resource context.
*
* @param ctx The current context
* @param resourceId The resource identifier
* @param effect The effect to apply to the image or null if no effect is needed
* @param dimen The new dimensions
* @param recycle If the bitmap should be recycled
* @param compress Try to compress the bitmap into ETC1
* @return GLESTextureInfo The texture info
*/
public static GLESTextureInfo loadTexture(Context ctx, int resourceId, Effect effect,
Rect dimen, boolean recycle, boolean compress) {
Bitmap bitmap = null;
InputStream raw = null;
try {
// Decode and associate the bitmap
raw = ctx.getResources().openRawResource(resourceId);
bitmap = BitmapUtils.decodeBitmap(raw);
if (bitmap == null) {
String msg = "Failed to decode the resource bitmap";
Log.e(TAG, msg);
return new GLESTextureInfo();
}
if (DEBUG) Log.d(TAG, "resourceId: " + resourceId);
GLESTextureInfo ti = loadTexture(bitmap, effect, dimen, compress);
return ti;
} catch (Exception e) {
String msg = "Failed to generate a valid texture from resource: " + resourceId;
Log.e(TAG, msg, e);
return new GLESTextureInfo();
} finally {
// Close the buffer
try {
if (raw != null) {
raw.close();
}
} catch (IOException e) {
// Ignore.
}
// Recycle the bitmap
if (bitmap != null && recycle) {
bitmap.recycle();
bitmap = null;
}
}
}
/**
* Method that loads texture from a bitmap reference.
*
* @param bitmap The bitmap reference
* @param effect The effect to apply to the image or null if no effect is needed
* @param dimen The new dimensions
* @param compress Try to compress the bitmap into ETC1
* @return GLESTextureInfo The texture info
*/
public static GLESTextureInfo loadTexture(Bitmap bitmap, Effect effect, Rect dimen,
boolean compress) {
// Check that we have a valid image name reference
if (bitmap == null) {
return new GLESTextureInfo();
}
int num = effect == null ? 1 : 2;
int[] textureHandles = new int[num];
GLES20.glGenTextures(num, textureHandles, 0);
GLESUtil.glesCheckError("glGenTextures");
for (int i = 0; i < num; i++) {
if (GLESUtil.DEBUG_GL_MEMOBJS) {
Log.d(GLESUtil.DEBUG_GL_MEMOBJS_NEW_TAG, "glGenTextures: " + textureHandles[i]);
}
}
if (textureHandles[0] <= 0 || (effect != null && textureHandles[1] <= 0)) {
Log.e(TAG, "Failed to generate a valid texture");
return new GLESTextureInfo();
}
// Bind the texture to the name
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureHandles[0]);
GLESUtil.glesCheckError("glBindTexture");
// Set the texture properties
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
GLESUtil.glesCheckError("glTexParameteri");
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST);
GLESUtil.glesCheckError("glTexParameteri");
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
GLESUtil.glesCheckError("glTexParameteri");
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
GLESUtil.glesCheckError("glTexParameteri");
// Load the texture. Check weather the device supports ETC1 compressed format
// AOSP ETC1 implementation doesn't support alpha channel
boolean fallback = true;
int bytesPerPixel = bitmap.getRowBytes() / bitmap.getWidth();
boolean hasAlpha = bitmap.hasAlpha() || (bytesPerPixel < 2 || bytesPerPixel > 3);
if (!sDisabledTextureCompression && compress && ETC1Util.isETC1Supported() && !hasAlpha) {
// Compress the texture
long start = System.currentTimeMillis();
ETC1Texture texture = createETC1CompressedTextureFromBitmap(bitmap);
if (texture != null) {
try {
long time = System.currentTimeMillis() - start;
if (DEBUG) {
Log.d(TAG, "Compression time: " + time + " ms");
}
if (time > MAX_ACEPTABLE_COMPRESSION_TIME) {
sDisabledTextureCompression = true;
Log.e(TAG, "Excessive compression time (" + time + " ms). " +
"Disabling compression");
}
// Load the compressed texture
int width = texture.getWidth();
int height = texture.getHeight();
Buffer data = texture.getData();
int imageSize = data.remaining();
GLES20.glCompressedTexImage2D(GLES20.GL_TEXTURE_2D, 0, ETC1.ETC1_RGB8_OES,
width, height, 0, imageSize, data);
GLESUtil.glesCheckError("glCompressedTexImage2D");
fallback = !GLES20.glIsTexture(textureHandles[0]);
} finally {
texture = null;
}
}
}
if (fallback) {
if (DEBUG) {
Log.d(TAG, "Fallback to uncompressed texture.");
}
// Fallback to uncompressed texture
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
if (!GLES20.glIsTexture(textureHandles[0])) {
Log.e(TAG, "Failed to load a valid texture");
return new GLESTextureInfo();
}
}
// Has a effect?
int handle = textureHandles[0];
if (effect != null) {
// Apply the effect (we need a thread-safe call here)
synchronized (SYNC) {
// No more than 1024 (the minimum supported by all the gles20 devices)
int w = Math.min(dimen.width(), 1024);
int h = Math.min(dimen.width(), 1024);
effect.apply(textureHandles[0], w, h, textureHandles[1]);
}
handle = textureHandles[1];
// Delete the unused texture
int[] textures = {textureHandles[0]};
if (GLESUtil.DEBUG_GL_MEMOBJS) {
Log.d(GLESUtil.DEBUG_GL_MEMOBJS_DEL_TAG, "glDeleteTextures: ["
+ textureHandles[0] + "]");
}
GLES20.glDeleteTextures(1, textures, 0);
GLESUtil.glesCheckError("glDeleteTextures");
}
// Return the texture handle identifier and the associated info
GLESTextureInfo ti = new GLESTextureInfo();
ti.handle = handle;
ti.bitmap = bitmap;
ti.path = null;
return ti;
}
/**
* Method that checks if an GLES error is present
*
* @param func The GLES function to check
* @return boolean If there was an error
*/
public static boolean glesCheckError(String func) {
int error = GLES20.glGetError();
if (error != 0) {
Log.e(TAG, "GLES20 Error (" + glesGetErrorModule() + ") (" + func + "): " +
GLUtils.getEGLErrorString(error));
return true;
}
return false;
}
/**
* Method that returns the line and module that generates the current error
*
* @return String The line and module
*/
private static String glesGetErrorModule() {
try {
return String.valueOf(Thread.currentThread().getStackTrace()[4]);
} catch (IndexOutOfBoundsException ioobEx) {
// Ignore
}
return "";
}
/**
* Method that read a resource.
*
* @param res The resources reference
* @param resId The resource identifier
* @return String The shader source
* @throws IOException If an error occurs while loading the resource
*/
private static String readResource(Resources res, int resId) {
Reader reader = new InputStreamReader(res.openRawResource(resId));
try {
final int BUFFER = 1024;
char[] data = new char[BUFFER];
int read = 0;
StringBuilder sb = new StringBuilder();
while ((read = reader.read(data, 0, BUFFER)) != -1) {
sb.append(data, 0, read);
}
return sb.toString();
} catch (Exception e) {
Log.e(TAG, "Failed to read the resource " + resId);
return null;
} finally {
try {
reader.close();
} catch (Exception ex) {
// Ignore
}
}
}
/**
* Method that compress a uncompressed bitmap to an compressed ETC1 texture
*
* @param bitmap The uncompressed bitmap
* @return ETC1Texture The ETC1 compressed texture
*/
private static ETC1Texture createETC1CompressedTextureFromBitmap(Bitmap bitmap) {
int width = bitmap.getWidth();
int height = bitmap.getHeight();
int dataSize = bitmap.getRowBytes() * height;
int bytesPerPixel = bitmap.getRowBytes() / width;
int stride = bitmap.getRowBytes();
ByteBuffer dataBuffer = ByteBuffer.allocateDirect(dataSize).order(ByteOrder.nativeOrder());
try {
bitmap.copyPixelsToBuffer(dataBuffer);
dataBuffer.position(0);
int encodedImageSize = ETC1.getEncodedDataSize(width, height);
ByteBuffer compressedImage = ByteBuffer.allocateDirect(encodedImageSize).
order(ByteOrder.nativeOrder());
try {
ETC1.encodeImage(dataBuffer, width, height, bytesPerPixel, stride, compressedImage);
return new ETC1Util.ETC1Texture(width, height, compressedImage);
} catch (IllegalArgumentException ex) {
compressedImage = null;
}
} finally {
dataBuffer = null;
}
return null;
}
}