/* * Copyright (C) 2015 Jorge Ruesga * * 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 com.ruesga.android.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.GLES20; import android.opengl.GLException; import android.opengl.GLUtils; import android.support.design.BuildConfig; import android.util.Log; import com.ruesga.android.wallpapers.photophase.AndroidHelper; import com.ruesga.android.wallpapers.photophase.borders.Border; import com.ruesga.android.wallpapers.photophase.preferences.PreferencesProvider; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.nio.ByteBuffer; import java.nio.IntBuffer; import javax.microedition.khronos.egl.EGL10; import javax.microedition.khronos.egl.EGLContext; /** * 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; private static final boolean NATIVE_TEXTURE_BIND = true; 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 IntBuffer sNativeBuffer; private static final int MAX_GLES_ERRORS = 50; private static int sGlErrors = 0; // Load the native library static { if (NATIVE_TEXTURE_BIND) { System.loadLibrary("photophase"); sNativeBuffer = null; } } /** * A helper class to deal with OpenGL float colors. */ public static class GLColor { private static final float MAX_COLOR = 255.0f; /** * Red */ public final float r; /** * Green */ public final float g; /** * Blue */ public final float b; /** * Alpha */ public float a; /** * Constructor of <code>GLColor</code> 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 <code>GLColor</code> 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; } public GLColor(GLColor color) { this.a = color.a; this.r = color.r; this.g = color.g; this.b = color.b; } /** * Constructor of <code>GLColor</code> 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 @SuppressWarnings("SimplifiableIfStatement") 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; return Float.floatToIntBits(r) == Float.floatToIntBits(other.r); } /** * {@inheritDoc} */ @Override public String toString() { return "#"+Integer.toHexString(Color.argb( (int)a * 255, (int)r * 255, (int)g * 255, (int)b * 255)); } public float[] asVec4() { return new float[]{r, g, b, a}; } } /** * 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; /** * The border to apply */ public Border border; } /** * Method that load a vertex shader and returns its handler identifier. * * @param src The source shader * @return int The handler identifier of the shader */ private 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 */ private 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; 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); // If something is wrong repeatedly, then restart the wallpaper sGlErrors++; if (sGlErrors > MAX_GLES_ERRORS) { AndroidHelper.restartWallpaper(); } return 0; } sGlErrors = 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 fake texture (the bitmap but no gles data) from a file. * * @param file The image file * @param dimensions The desired dimensions * @return GLESTextureInfo The texture info */ public static GLESTextureInfo loadFadeTexture(File file, Rect dimensions) { Bitmap bitmap = null; try { // Decode and associate the bitmap (invert the desired dimensions) bitmap = BitmapUtils.decodeBitmap(file, dimensions.width(), dimensions.height()); 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 = new GLESTextureInfo(); ti.bitmap = bitmap; ti.path = file; return ti; } catch (Exception e) { if (DEBUG) { String msg = "Failed to generate a valid texture from file: " + file.getAbsolutePath(); Log.e(TAG, msg, e); } if (bitmap != null) { bitmap.recycle(); } return new GLESTextureInfo(); } } /** * 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 border The border to apply to the image or null if no border was defined * @param dimen The new dimensions * @param recycle If the bitmap should be recycled * @return GLESTextureInfo The texture info */ public static GLESTextureInfo loadTexture(Context ctx, int resourceId, Effect effect, Border border, Rect dimen, boolean recycle) { 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); return loadTexture(ctx, bitmap, effect, border, dimen); } 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(); } } } /** * 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 border The border to apply to the image or null if no border was defined * @param dimen The new dimensions * @return GLESTextureInfo The texture info */ public static synchronized GLESTextureInfo loadTexture(Context context, Bitmap bitmap, Effect effect, Border border, Rect dimen) { // Check that we have a valid image name reference if (bitmap == null) { return new GLESTextureInfo(); } Bitmap texture = ensurePowerOfTwoTexture(context, bitmap); int num = 1; if (effect != null) { num++; } if (border != null) { num++; } 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 if (NATIVE_TEXTURE_BIND) { // Create a buffer from the image int width = texture.getWidth(); int height = texture.getHeight(); final int size = height * texture.getRowBytes(); if (sNativeBuffer == null || sNativeBuffer.capacity() < size) { sNativeBuffer = ByteBuffer.allocateDirect(size * 4).asIntBuffer(); } else { sNativeBuffer.clear(); } texture.copyPixelsToBuffer(sNativeBuffer); nativeGlTexImage2D(sNativeBuffer, width, height); } else { GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, texture, 0); } if (!GLES20.glIsTexture(textureHandles[0])) { Log.e(TAG, "Failed to load a valid texture"); return new GLESTextureInfo(); } // Apply effects and borders. Don't apply effects if there is not a valid context int handle = textureHandles[0]; if (hasValidEglContext()) { int n = 0; if (effect != null) { handle = applyEffect(textureHandles, n, effect, dimen); n++; } if (border != null) { handle = applyEffect(textureHandles, n, border, dimen); } } // Return the texture handle identifier and the associated info GLESTextureInfo ti = new GLESTextureInfo(); ti.handle = handle; ti.bitmap = texture; ti.path = null; return ti; } private static int applyEffect(int[] textureHandles, int n, Effect effect, Rect dimen) { // Apply the border (we need a thread-safe call here) synchronized (SYNC) { // No more than 1024 (the minimum supported by all the gles20 devices) effect.apply(textureHandles[n], dimen.width(), dimen.height(), textureHandles[n + 1]); } // Delete the unused texture if (GLESUtil.DEBUG_GL_MEMOBJS) { Log.d(GLESUtil.DEBUG_GL_MEMOBJS_DEL_TAG, "glDeleteTextures: [" + textureHandles[n] + "]"); } GLES20.glDeleteTextures(1, textureHandles, n); GLESUtil.glesCheckError("glDeleteTextures"); return textureHandles[n + 1]; } /** * Ensure that the passed bitmap can be used a as power of two texture * * @param src The source bitmap * @return A bitmap which is power of two */ private static Bitmap ensurePowerOfTwoTexture(Context context, Bitmap src) { if (!BitmapUtils.isPowerOfTwo(src) && PreferencesProvider.Preferences.General.isPowerOfTwo(context)) { int powerOfTwo = BitmapUtils.calculateUpperPowerOfTwo( Math.min(src.getWidth(), src.getHeight())); // Create a power of two bitmap Bitmap out = Bitmap.createScaledBitmap(src, powerOfTwo, powerOfTwo, false); src.recycle(); return out; } return src; } /** * Method that checks if an GLES error is present * * @param func The GLES function to check */ public static void glesCheckError(String func) { // Log when a call happens without a current context or outside the GLThread if (BuildConfig.DEBUG) { if (!hasValidEglContext()) { try { throw new GLException(-1, "call to OpenGL ES API with no current context"); } catch (GLException ex) { Log.w(TAG, "GLES20 Error (" + glesGetErrorModule() + ") (" + func + "): call to " + "OpenGL ES API with no current context", ex); } } else if (!Thread.currentThread().getName().startsWith("GLThread")) { try { throw new GLException(-1, "call to OpenGL ES API outside GLThread"); } catch (GLException ex) { Log.w(TAG, "GLES20 Error (" + glesGetErrorModule() + ") (" + func + "): call to " + "OpenGL ES API outside GLThread", ex); } } } int error = GLES20.glGetError(); if (error != 0) { if (BuildConfig.DEBUG) { try { throw new GLException(error); } catch (GLException ex) { Log.e(TAG, "GLES20 Error (" + glesGetErrorModule() + ") (" + func + "): " + GLUtils.getEGLErrorString(error), ex); } } else { Log.e(TAG, "GLES20 Error (" + glesGetErrorModule() + ") (" + func + "): " + GLUtils.getEGLErrorString(error)); } } } /** * 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 ""; } /** * Return whether a valid Egl context exists * * @return boolean If a valid Egl context exists */ public static boolean hasValidEglContext() { final EGL10 egl = (EGL10) EGLContext.getEGL(); return egl != null && egl.eglGetCurrentContext() != null && !egl.eglGetCurrentContext().equals(EGL10.EGL_NO_CONTEXT); } /** * Method that read a resource. * * @param res The resources reference * @param resId The resource identifier * @return String The shader source */ 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; 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 } } } /** * Link the image via native code * * @param image The image buffer to bind * @param width The width of the image * @param height The height of the image */ @SuppressWarnings("JniMissingFunction") private static native void nativeGlTexImage2D(IntBuffer image, int width, int height); }