From faa982e7beda54bf28e0dbf1488428093bbbf485 Mon Sep 17 00:00:00 2001 From: Jorge Ruesga Date: Wed, 10 Sep 2014 23:52:05 +0200 Subject: photophase: use etc1 compression when available When avaliable this help to decrease the memory footprint. ETC1 compression requieres to: - be supported by opengl - picture couldn't have an alpha channel - compression times shouldn't be too higher (< 1000 ms) - initial pictures doesn't be compressed (to speed up boot) Change-Id: I87e41db3ca7f2ccb82d4af2763609f11d7e67121 Signed-off-by: Jorge Ruesga --- .../wallpapers/photophase/TextureManager.java | 20 ++-- .../photophase/effects/PhotoPhaseEffect.java | 8 +- .../wallpapers/photophase/shapes/OopsShape.java | 5 +- .../wallpapers/photophase/utils/BitmapUtils.java | 3 +- .../wallpapers/photophase/utils/GLESUtil.java | 115 ++++++++++++++++++--- 5 files changed, 124 insertions(+), 27 deletions(-) diff --git a/src/org/cyanogenmod/wallpapers/photophase/TextureManager.java b/src/org/cyanogenmod/wallpapers/photophase/TextureManager.java index 417cfb9..65987c3 100644 --- a/src/org/cyanogenmod/wallpapers/photophase/TextureManager.java +++ b/src/org/cyanogenmod/wallpapers/photophase/TextureManager.java @@ -89,22 +89,27 @@ public class TextureManager implements OnMediaPictureDiscoveredListener { effect = mEffects.getNextEffect(); } + boolean enqueue = false; + synchronized (mSync) { + enqueue = mPendingRequests.size() == 0; + } + // Load and bind to the GLES context. The effect is applied when the image // is associated to the destination target (only if aspect ratio will be applied) if (!Preferences.General.isFixAspectRatio()) { ti = GLESUtil.loadTexture( - mImage, mDimensions, effect, mDimensions, false); + mImage, mDimensions, effect, mDimensions, false, enqueue); } else { - ti = GLESUtil.loadTexture(mImage, mDimensions, null, null, false); + ti = GLESUtil.loadTexture(mImage, mDimensions, null, null, false, enqueue); ti.effect = effect; } synchronized (mSync) { // Notify the new images to all pending frames - if (mPendingRequests.size() > 0) { + if (!enqueue) { // Invalid textures are also reported, so requestor can handle it TextureRequestor requestor = mPendingRequests.remove(0); - fixAspectRatio(requestor, ti); + fixAspectRatio(requestor, ti, false); requestor.setTextureHandle(ti); // Clean up memory @@ -247,7 +252,7 @@ public class TextureManager implements OnMediaPictureDiscoveredListener { synchronized (mSync) { try { GLESTextureInfo ti = mQueue.remove(); - fixAspectRatio(requestor, ti); + fixAspectRatio(requestor, ti, true); requestor.setTextureHandle(ti); // Clean up memory @@ -431,8 +436,9 @@ public class TextureManager implements OnMediaPictureDiscoveredListener { * @param request The requestor target * @param ti The original texture information * @param effect The effect to apply to the destination picture + * @param compress Compress the texture */ - void fixAspectRatio(TextureRequestor requestor, GLESTextureInfo ti) { + void fixAspectRatio(TextureRequestor requestor, GLESTextureInfo ti, boolean compress) { // Check if we have to apply any correction to the image if (Preferences.General.isFixAspectRatio()) { // Transform requestor dimensions to screen dimensions @@ -449,7 +455,7 @@ public class TextureManager implements OnMediaPictureDiscoveredListener { pixels.width(), pixels.height(), ThumbnailUtils.OPTIONS_RECYCLE_INPUT); - GLESTextureInfo dst = GLESUtil.loadTexture(thumb, ti.effect, pixels); + GLESTextureInfo dst = GLESUtil.loadTexture(thumb, ti.effect, pixels, compress); // Destroy references int[] textures = new int[]{ti.handle}; diff --git a/src/org/cyanogenmod/wallpapers/photophase/effects/PhotoPhaseEffect.java b/src/org/cyanogenmod/wallpapers/photophase/effects/PhotoPhaseEffect.java index 0900322..bbfb2bc 100644 --- a/src/org/cyanogenmod/wallpapers/photophase/effects/PhotoPhaseEffect.java +++ b/src/org/cyanogenmod/wallpapers/photophase/effects/PhotoPhaseEffect.java @@ -158,12 +158,14 @@ public abstract class PhotoPhaseEffect extends Effect { mIdentityEffect.apply(inputTexId, width, height, outputTexId); // Create the framebuffer - GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20. GL_TEXTURE_2D, outputTexId, 0); + GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, + GLES20. GL_TEXTURE_2D, outputTexId, 0); GLESUtil.glesCheckError("glFramebufferTexture2D"); // Check if the buffer was built successfully - if (GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER) != GLES20.GL_FRAMEBUFFER_COMPLETE) { - // Something when wrong. Throw an exception + final int status = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER); + if (status != GLES20.GL_FRAMEBUFFER_COMPLETE) { + // Something was wrong. Throw an exception GLESUtil.glesCheckError("glCheckFramebufferStatus"); int error = GLES20.glGetError(); throw new android.opengl.GLException(error, GLUtils.getEGLErrorString(error)); diff --git a/src/org/cyanogenmod/wallpapers/photophase/shapes/OopsShape.java b/src/org/cyanogenmod/wallpapers/photophase/shapes/OopsShape.java index 195a1e4..5ef4bd9 100644 --- a/src/org/cyanogenmod/wallpapers/photophase/shapes/OopsShape.java +++ b/src/org/cyanogenmod/wallpapers/photophase/shapes/OopsShape.java @@ -142,9 +142,10 @@ public class OopsShape implements DrawableShape { mMessage = ctx.getString(resourceMessageId); // Load the textures - mOopsImageTexture = GLESUtil.loadTexture(ctx, R.drawable.bg_cid_oops, null, null, false); + mOopsImageTexture = GLESUtil.loadTexture(ctx, R.drawable.bg_cid_oops, null, + null, false, false); Bitmap textBitmap = text2Bitmap(ctx, mMessage); - mOopsTextTexture = GLESUtil.loadTexture(textBitmap, null, null); + mOopsTextTexture = GLESUtil.loadTexture(textBitmap, null, null, false); // Recycle mOopsImageTexture.bitmap.recycle(); diff --git a/src/org/cyanogenmod/wallpapers/photophase/utils/BitmapUtils.java b/src/org/cyanogenmod/wallpapers/photophase/utils/BitmapUtils.java index bbd3dec..ebfb4f6 100644 --- a/src/org/cyanogenmod/wallpapers/photophase/utils/BitmapUtils.java +++ b/src/org/cyanogenmod/wallpapers/photophase/utils/BitmapUtils.java @@ -41,6 +41,7 @@ public class BitmapUtils { final BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferQualityOverSpeed = false; options.inPreferredConfig = Bitmap.Config.RGB_565; + options.inDither = false; return BitmapFactory.decodeStream(bitmap, null, options); } @@ -56,6 +57,7 @@ public class BitmapUtils { // First decode with inJustDecodeBounds=true to check dimensions final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; + options.inPreferredConfig = Bitmap.Config.RGB_565; BitmapFactory.decodeFile(file.getAbsolutePath(), options); // Calculate inSampleSize (use 1024 as maximum size, the minimum supported @@ -70,7 +72,6 @@ public class BitmapUtils { options.inPreferQualityOverSpeed = false; options.inPurgeable = true; options.inInputShareable = true; - options.inDither = true; Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath(), options); if (bitmap == null) { return null; diff --git a/src/org/cyanogenmod/wallpapers/photophase/utils/GLESUtil.java b/src/org/cyanogenmod/wallpapers/photophase/utils/GLESUtil.java index 8b34601..1d7c8bc 100644 --- a/src/org/cyanogenmod/wallpapers/photophase/utils/GLESUtil.java +++ b/src/org/cyanogenmod/wallpapers/photophase/utils/GLESUtil.java @@ -22,6 +22,9 @@ 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; @@ -31,6 +34,9 @@ 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; /** @@ -46,7 +52,10 @@ public final class GLESUtil { 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 sSync = new Object(); + private static final Object SYNC = new Object(); + + private static final int MAX_ACEPTABLE_COMPRESSION_TIME = 1000; + private static boolean sDisabledTextureCompression = false; /** * A helper class to deal with OpenGL float colors. @@ -330,10 +339,11 @@ public final class GLESUtil { * @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) { + 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) @@ -344,7 +354,7 @@ public final class GLESUtil { } if (DEBUG) Log.d(TAG, "image: " + file.getAbsolutePath()); - GLESTextureInfo ti = loadTexture(bitmap, effect, dimen); + GLESTextureInfo ti = loadTexture(bitmap, effect, dimen, compress); ti.path = file; return ti; @@ -370,10 +380,11 @@ public final class GLESUtil { * @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) { + public static GLESTextureInfo loadTexture(Context ctx, int resourceId, Effect effect, + Rect dimen, boolean recycle, boolean compress) { Bitmap bitmap = null; InputStream raw = null; try { @@ -387,7 +398,7 @@ public final class GLESUtil { } if (DEBUG) Log.d(TAG, "resourceId: " + resourceId); - GLESTextureInfo ti = loadTexture(bitmap, effect, dimen); + GLESTextureInfo ti = loadTexture(bitmap, effect, dimen, compress); return ti; } catch (Exception e) { @@ -418,9 +429,11 @@ public final class GLESUtil { * @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) { + 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(); @@ -455,18 +468,59 @@ public final class GLESUtil { GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); GLESUtil.glesCheckError("glTexParameteri"); - // Load the 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(); + // 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 (sSync) { + 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); @@ -553,4 +607,37 @@ public final class GLESUtil { } } + /** + * 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; + } + } -- cgit v1.2.3