Browse Source

Android native image loader rewritten from scratch
* Now supports reading directly from Java InputStream instead of having to read image file into memory first
* Optimized native code - reduced unneccessary memory copies

shadowislord 11 years ago
parent
commit
7057e9cb18

+ 2 - 1
jme3-android-native/src/native/jme_stbi/Android.mk

@@ -7,7 +7,8 @@ include $(CLEAR_VARS)
 LOCAL_MODULE    := stbijme
 	
 LOCAL_C_INCLUDES  += $(LOCAL_PATH)
-	
+
+LOCAL_CFLAGS := -std=c99
 LOCAL_LDLIBS := -lz -llog -Wl,-s
 	
 LOCAL_SRC_FILES := com_jme3_texture_plugins_AndroidNativeImageLoader.c

+ 316 - 8
jme3-android-native/src/native/jme_stbi/com_jme3_texture_plugins_AndroidNativeImageLoader.c

@@ -1,24 +1,331 @@
 #include "com_jme3_texture_plugins_AndroidNativeImageLoader.h"
-// for __android_log_print(ANDROID_LOG_INFO, "YourApp", "formatted message");
-#include <android/log.h>
-#include <stddef.h>
-#include <stdio.h>
 #include <assert.h>
-#include <string.h>
-#include <time.h>
+
+#ifdef DEBUG
+#include <android/log.h>
+#define LOGI(fmt, ...) __android_log_print(ANDROID_LOG_INFO, \
+                       "NativeImageLoader", fmt, ##__VA_ARGS__);
+#else
+#define LOGI(fmt, ...)
+#endif
 
 #define STB_IMAGE_IMPLEMENTATION
+#define STBI_NO_STDIO
+#define STBI_NO_HDR
 #include "stb_image.h"
 
-typedef unsigned int    uint32;
+typedef struct 
+{
+    JNIEnv* env;
+    jbyteArray tmp;
+    int tmpSize;
+    jobject isObject;
+    jmethodID isReadMethod;
+    jmethodID isSkipMethod;
+    int isEOF;
+    char* errorMsg;
+}
+JavaInputStreamWrapper;
+
+static void throwIOException(JNIEnv* env, const char* message)
+{
+    jclass ioExClazz = (*env)->FindClass(env, "java/io/IOException");
+    (*env)->ThrowNew(env, ioExClazz, message);
+}
+
+static int InputStream_read(void *user, char *nativeData, int nativeSize) {
+    JavaInputStreamWrapper* wrapper = (JavaInputStreamWrapper*) user;
+    JNIEnv* env = wrapper->env;
+    
+    if (nativeSize <= 0)
+    {
+        wrapper->isEOF = 1;
+        wrapper->errorMsg = "read() requested negative or zero size";
+        return 0;
+    }
+
+    jbyteArray tmp = wrapper->tmp;
+    jint tmpSize = wrapper->tmpSize;
+    jint remaining = nativeSize;
+    jint offset = 0;
+    
+    while (offset < nativeSize)
+    {
+        // Read data into Java array.
+        jint toRead = tmpSize < remaining ? tmpSize : remaining;
+        jint read = (*env)->CallIntMethod(env, wrapper->isObject,
+                                          wrapper->isReadMethod, 
+                                          tmp, (jint)0, (jint)toRead);
+        
+        // Check IOException
+        if ((*env)->ExceptionCheck(env))
+        {
+            wrapper->isEOF = 1;
+            wrapper->errorMsg = NULL;
+            return 0;
+        }
+        
+        LOGI("InputStream->read(tmp, 0, %d) = %d", toRead, read);
+        
+        // Read -1 bytes = EOF. 
+        if (read < 0)
+        {
+            wrapper->isEOF = 1;
+            wrapper->errorMsg = NULL;
+            break;
+        }
+        else if (read == 0)
+        {
+            // Read 0 bytes, give it another try.
+            continue;
+        }
+        
+        // Read 1 byte or more.
+        
+        LOGI("memcpy(native[%d], java, %d)", offset, read);
+        
+        // Copy contents of Java array to native array.
+        jbyte* nativeTmp = (*env)->GetPrimitiveArrayCritical(env, tmp, 0);
+        
+        if (nativeTmp == NULL)
+        {
+            wrapper->isEOF = 1;
+            wrapper->errorMsg = "Failed to acquire Java array contents";
+            return 0;
+        }
+        
+        memcpy(&nativeData[offset], nativeTmp, read);
+        
+        (*env)->ReleasePrimitiveArrayCritical(env, tmp, nativeTmp, 0);
+        
+        offset += read;
+        remaining -= read;
+        
+        assert(remaining >= 0);
+        assert(offset <= nativeSize);
+    }
+
+    return offset;
+}
 
+static void InputStream_skip(void *user, int n) {
+    JavaInputStreamWrapper* wrapper = (JavaInputStreamWrapper*) user;
+    JNIEnv* env = wrapper->env;
+    
+    if (n < 0)
+    {
+        wrapper->isEOF = 1;
+        wrapper->errorMsg = "Negative seek attempt detected";
+        return;
+    } 
+    else if (n == 0) 
+    {
+        return;
+    }
+
+    // InputStream.skip(n);
+    jlong result = (*env)->CallLongMethod(env, wrapper->isObject, 
+                                          wrapper->isSkipMethod, (jlong)n);
+
+    LOGI("InputStream->skip(%lld) = %lld", (jlong)n, result);
+    
+    // IOException
+    if ((*env)->ExceptionCheck(env))
+    {
+        wrapper->isEOF = 1;
+        wrapper->errorMsg = NULL;
+    }
+    else if ((int)result != n)
+    {
+        wrapper->isEOF = 1;
+        wrapper->errorMsg = "Could not skip requested number of bytes";
+    }
+}
+
+static int InputStream_eof(void *user) {
+    JavaInputStreamWrapper* wrapper = (JavaInputStreamWrapper*) user;
+    LOGI("InputStream->eof() = %s", wrapper->isEOF ? "true" : "false");
+    return wrapper->isEOF;
+}
+
+static stbi_io_callbacks JavaInputStreamCallbacks ={
+    InputStream_read,
+    InputStream_skip,
+    InputStream_eof,
+};
+
+static JavaInputStreamWrapper createInputStreamWrapper(JNIEnv* env, jobject is, jbyteArray tmpArray)
+{
+    JavaInputStreamWrapper wrapper;
+    jclass inputStreamClass = (*env)->FindClass(env, "java/io/InputStream");
+    
+    wrapper.env = env;
+    wrapper.isObject = is;
+    wrapper.isEOF = 0;
+    wrapper.errorMsg = NULL;
+    wrapper.isReadMethod = (*env)->GetMethodID(env, inputStreamClass, "read", "([BII)I");
+    wrapper.isSkipMethod = (*env)->GetMethodID(env, inputStreamClass, "skip", "(J)J");
+    wrapper.tmp = (jbyteArray) tmpArray;
+    wrapper.tmpSize = (*env)->GetArrayLength(env, tmpArray);
+    
+    return wrapper;
+}
+
+static jobject createJmeImage(JNIEnv* env, int width, int height, int comps, char* data)
+{
+    // Convert # of components to jME format.
+    jclass formatClass = (*env)->FindClass(env, "com/jme3/texture/Image$Format");
+    jfieldID formatFieldID;
+    
+    switch (comps)
+    {
+        case 1:
+            formatFieldID = (*env)->GetStaticFieldID(env, formatClass, 
+                                    "Luminance8", "Lcom/jme3/texture/Image$Format;");
+            break;
+        case 2:
+            formatFieldID = (*env)->GetStaticFieldID(env, formatClass, 
+                                    "Luminance8Alpha8", "Lcom/jme3/texture/Image$Format;");
+            break;
+        case 3:
+            formatFieldID = (*env)->GetStaticFieldID(env, formatClass, 
+                                    "RGB8", "Lcom/jme3/texture/Image$Format;");
+            break;
+        case 4:
+            formatFieldID = (*env)->GetStaticFieldID(env, formatClass, 
+                                    "RGBA8", "Lcom/jme3/texture/Image$Format;");
+            break;
+        default:
+            throwIOException(env, "Unrecognized number of components");
+            return NULL;
+    }
+    
+    jobject formatVal = (*env)->GetStaticObjectField(env, formatClass, formatFieldID);
+    
+    // Get colorspace sRGB
+    jclass colorSpaceClass = (*env)->FindClass(env, "com/jme3/texture/image/ColorSpace");
+    jfieldID sRGBFieldID = (*env)->GetStaticFieldID(env, colorSpaceClass, 
+                                    "sRGB", "Lcom/jme3/texture/image/ColorSpace;");
+    jobject sRGBVal = (*env)->GetStaticObjectField(env, colorSpaceClass, sRGBFieldID);
+    
+    int size = width * height * comps;
+    
+    // Stick it in a ByteBuffer
+    jobject directBuffer = (*env)->NewDirectByteBuffer(env, data, size);
+    
+    if (directBuffer == NULL)
+    {
+        throwIOException(env, "Failed to allocate ByteBuffer");
+        return NULL;
+    }
+    
+    // Create JME image.
+    jclass jmeImageClass   = (*env)->FindClass(env, "com/jme3/texture/Image");
+    
+    // Image(Format format, int width, int height, ByteBuffer data, ColorSpace colorSpace)
+    jmethodID newImageMethod = (*env)->GetMethodID(env, jmeImageClass, "<init>", 
+                                                   "(Lcom/jme3/texture/Image$Format;IILjava/nio/ByteBuffer;Lcom/jme3/texture/image/ColorSpace;)V");
+    
+    jobject jmeImage = (*env)->NewObject(env, jmeImageClass, newImageMethod, 
+                                         formatVal, (jint)width, (jint)height, 
+                                         directBuffer, sRGBVal);
+    
+    return jmeImage;
+}
+
+static void flipImage(int scanline, int height, char* data)
+{
+    char tmp[scanline];
+    
+    for (int y = 0; y < height / 2; y++)
+    {
+        int oppY = height - y - 1;
+        int yOff  = y * scanline;
+        int oyOff = oppY * scanline;
+        // Copy scanline at Y to tmp
+        memcpy(tmp, &data[yOff], scanline);
+        // Copy data at opposite Y to Y
+        memcpy(&data[yOff], &data[oyOff], scanline);
+        // Copy tmp to opposite Y
+        memcpy(&data[oyOff], tmp, scanline);
+    }
+}
+
+JNIEXPORT jobject JNICALL Java_com_jme3_texture_plugins_AndroidNativeImageLoader_load
+  (JNIEnv * env, jobject thisObj, jobject inputStream, jboolean flipY, jbyteArray tmpArray)
+{
+    JavaInputStreamWrapper wrapper = createInputStreamWrapper(env, inputStream, tmpArray);
+    stbi_uc* imageData;
+    int width, height, comps;
+    
+    LOGI("stbi_load_from_callbacks");
+    
+    imageData = stbi_load_from_callbacks(&JavaInputStreamCallbacks, &wrapper, &width, &height, &comps, STBI_default);
+    
+    if ((*env)->ExceptionCheck(env))
+    {
+        // IOException
+        goto problems;
+    }
+    else if (wrapper.errorMsg != NULL)
+    {
+        // Misc error
+        throwIOException(env, wrapper.errorMsg);
+        goto problems;
+    }
+    else if (imageData == NULL)
+    {
+        // STBI error
+        throwIOException(env, stbi_failure_reason());
+        goto problems;
+    }
+    
+    // No IOExceptions or errors encountered. We have image data!
+    
+    // Maybe we need to flip it.
+    LOGI("Flipping image");
+    if (flipY) 
+    {
+        flipImage(width * comps, height, imageData);
+    }
+    
+    // Create the jME3 image.
+    LOGI("Creating jME3 image");
+    return createJmeImage(env, width, height, comps, imageData);
+    
+problems:
+    if (imageData != NULL)
+    {
+        stbi_image_free(imageData);
+    }
+
+    return NULL;
+}
 
 JNIEXPORT jobject JNICALL Java_com_jme3_texture_plugins_AndroidNativeImageLoader_getFailureReason
   (JNIEnv * env, jclass clazz)
 {
-    return stbi_failure_reason();
+    return NULL;
 }
 
+JNIEXPORT jint JNICALL Java_com_jme3_texture_plugins_AndroidNativeImageLoader_getImageInfo
+  (JNIEnv * env, jclass clazz, jobject inBuffer, jint bufSize, jobject outBuffer, jint outSize)
+{
+    return 0;
+}
+
+JNIEXPORT jint JNICALL Java_com_jme3_texture_plugins_AndroidNativeImageLoader_decodeBuffer
+  (JNIEnv * env, jclass clazz, jobject inBuffer, jint inSize, jboolean flipY, jobject outBuffer, jint outSize)
+{
+    return 0;
+}
+
+/*
+JNIEXPORT jobject JNICALL Java_com_jme3_texture_plugins_AndroidNativeImageLoader_getFailureReason
+  (JNIEnv * env, jclass clazz)
+{
+    return stbi_failure_reason();
+}
 
 JNIEXPORT jint JNICALL Java_com_jme3_texture_plugins_AndroidNativeImageLoader_getImageInfo
   (JNIEnv * env, jclass clazz, jobject inBuffer, jint bufSize, jobject outBuffer, jint outSize)
@@ -124,3 +431,4 @@ JNIEXPORT jint JNICALL Java_com_jme3_texture_plugins_AndroidNativeImageLoader_de
 
 //    stbi_uc *stbi_load_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *comp, int req_comp);
 }
+*/

+ 10 - 132
jme3-android/src/main/java/com/jme3/texture/plugins/AndroidNativeImageLoader.java

@@ -5,13 +5,8 @@ import com.jme3.asset.AssetLoadException;
 import com.jme3.asset.AssetLoader;
 import com.jme3.asset.TextureKey;
 import com.jme3.texture.Image;
-import com.jme3.texture.image.ColorSpace;
-import com.jme3.util.BufferUtils;
-import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.nio.ByteBuffer;
-import java.util.logging.Level;
 import java.util.logging.Logger;
 
 /**
@@ -20,145 +15,28 @@ import java.util.logging.Logger;
  * loading.  This loader does not.
  *
  * @author iwgeric
+ * @author Kirill Vainer
  */
 public class AndroidNativeImageLoader  implements AssetLoader {
-    private static final Logger logger = Logger.getLogger(AndroidNativeImageLoader.class.getName());
-
-    public Image load(InputStream in, boolean flipY) throws IOException{
-        int result;
-        byte[] bytes = getBytes(in);
-        int origSize = bytes.length;
-//        logger.log(Level.INFO, "png file length: {0}", size);
-
-        ByteBuffer origDataBuffer = BufferUtils.createByteBuffer(origSize);
-        origDataBuffer.clear();
-        origDataBuffer.put(bytes, 0, origSize);
-        origDataBuffer.flip();
-
-        int headerSize = 12;
-        ByteBuffer headerDataBuffer = BufferUtils.createByteBuffer(headerSize);
-        headerDataBuffer.asIntBuffer();
-        headerDataBuffer.clear();
-
-        result = getImageInfo(origDataBuffer, origSize, headerDataBuffer, headerSize);
-        if (result != 0) {
-            logger.log(Level.SEVERE, "Image header could not be read: {0}", getFailureReason());
-            return null;
-        }
-        headerDataBuffer.rewind();
-
-//        logger.log(Level.INFO, "image header size: {0}", headerDataBuffer.capacity());
-//        int position = 0;
-//        while (headerDataBuffer.position() < headerDataBuffer.capacity()) {
-//            int value = headerDataBuffer.getInt();
-//            logger.log(Level.INFO, "position: {0}, value: {1}",
-//                    new Object[]{position, value});
-//            position++;
-//        }
-//        headerDataBuffer.rewind();
-
-
-        int width = headerDataBuffer.getInt();
-        int height = headerDataBuffer.getInt();
-        int numComponents = headerDataBuffer.getInt();
-        int imageDataSize = width * height * numComponents;
-//        logger.log(Level.INFO, "width: {0}, height: {1}, numComponents: {2}, imageDataSize: {3}",
-//                new Object[]{width, height, numComponents, imageDataSize});
-
-        ByteBuffer imageDataBuffer = BufferUtils.createByteBuffer(imageDataSize);
-        imageDataBuffer.clear();
-
-        result = decodeBuffer(origDataBuffer, origSize, flipY, imageDataBuffer, imageDataSize);
-        if (result != 0) {
-            logger.log(Level.SEVERE, "Image could not be decoded: {0}", getFailureReason());
-            return null;
-        }
-        imageDataBuffer.rewind();
-
-//        logger.log(Level.INFO, "png outSize: {0}", imageDataBuffer.capacity());
-//        int pixelNum = 0;
-//        while (imageDataBuffer.position() < imageDataBuffer.capacity()) {
-//            short r = (short) (imageDataBuffer.get() & 0xFF);
-//            short g = (short) (imageDataBuffer.get() & 0xFF);
-//            short b = (short) (imageDataBuffer.get() & 0xFF);
-//            short a = (short) (imageDataBuffer.get() & 0xFF);
-//            logger.log(Level.INFO, "pixel: {0}, r: {1}, g: {2}, b: {3}, a: {4}",
-//                    new Object[]{pixelNum, r, g, b, a});
-//            pixelNum++;
-//        }
-//        imageDataBuffer.rewind();
-
-        BufferUtils.destroyDirectBuffer(origDataBuffer);
-        BufferUtils.destroyDirectBuffer(headerDataBuffer);
-
-        Image img = new Image(getImageFormat(numComponents), width, height, imageDataBuffer, ColorSpace.sRGB);
-
-        return img;
+    
+    private final byte[] tmpArray = new byte[1024];
+    
+    static {
+         System.loadLibrary("stbijme");
     }
-
+    
+    private static native Image load(InputStream in, boolean flipY, byte[] tmpArray) throws IOException;
+    
     public Image load(AssetInfo info) throws IOException {
-//        logger.log(Level.INFO, "Loading texture: {0}", ((TextureKey)info.getKey()).toString());
         boolean flip = ((TextureKey) info.getKey()).isFlipY();
         InputStream in = null;
         try {
             in = info.openStream();
-            Image img = load(in, flip);
-            if (img == null){
-                throw new AssetLoadException("The given image cannot be loaded " + info.getKey());
-            }
-            return img;
+            return load(info.openStream(), flip, tmpArray);
         } finally {
             if (in != null){
                 in.close();
             }
         }
     }
-
-    private static Image.Format getImageFormat(int stbiNumComponents) {
-//     stb_image always returns 8 bit components
-//     N=#comp     components
-//       1           grey
-//       2           grey, alpha
-//       3           red, green, blue
-//       4           red, green, blue, alpha
-        Image.Format format = null;
-
-        if (stbiNumComponents == 1) {
-            format = Image.Format.Luminance8;
-        } else if (stbiNumComponents == 2) {
-            format = Image.Format.Luminance8Alpha8;
-        } else if (stbiNumComponents == 3) {
-            format = Image.Format.RGB8;
-        } else if (stbiNumComponents == 4) {
-            format = Image.Format.RGBA8;
-        } else {
-            throw new IllegalArgumentException("Format returned by stbi is not valid.  Returned value: " + stbiNumComponents);
-        }
-
-        return format;
-    }
-
-    public static byte[] getBytes(InputStream input) throws IOException {
-        byte[] buffer = new byte[32768];
-        int bytesRead;
-        ByteArrayOutputStream os = new ByteArrayOutputStream();
-        while ((bytesRead = input.read(buffer)) != -1)
-        {
-            os.write(buffer, 0, bytesRead);
-        }
-
-        byte[] output = os.toByteArray();
-        return output;
-    }
-
-
-
-    /** Load jni .so on initialization */
-    static {
-         System.loadLibrary("stbijme");
-    }
-
-    private static native int getImageInfo(ByteBuffer inBuffer, int inSize, ByteBuffer outBuffer, int outSize);
-    private static native int decodeBuffer(ByteBuffer inBuffer, int inSize, boolean flipY, ByteBuffer outBuffer, int outSize);
-    private static native String getFailureReason();
 }