From 7057e9cb183055955cda322d09198e0a53d6616e Mon Sep 17 00:00:00 2001 From: shadowislord Date: Sat, 8 Nov 2014 17:18:37 -0500 Subject: [PATCH] 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 --- .../src/native/jme_stbi/Android.mk | 3 +- ...texture_plugins_AndroidNativeImageLoader.c | 324 +++++++++++++++++- .../plugins/AndroidNativeImageLoader.java | 142 +------- 3 files changed, 328 insertions(+), 141 deletions(-) diff --git a/jme3-android-native/src/native/jme_stbi/Android.mk b/jme3-android-native/src/native/jme_stbi/Android.mk index 469c63e4d..1eddbaeee 100644 --- a/jme3-android-native/src/native/jme_stbi/Android.mk +++ b/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 diff --git a/jme3-android-native/src/native/jme_stbi/com_jme3_texture_plugins_AndroidNativeImageLoader.c b/jme3-android-native/src/native/jme_stbi/com_jme3_texture_plugins_AndroidNativeImageLoader.c index a4064a622..7255066c0 100644 --- a/jme3-android-native/src/native/jme_stbi/com_jme3_texture_plugins_AndroidNativeImageLoader.c +++ b/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 -#include -#include #include -#include -#include + +#ifdef DEBUG +#include +#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, "", + "(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); } +*/ diff --git a/jme3-android/src/main/java/com/jme3/texture/plugins/AndroidNativeImageLoader.java b/jme3-android/src/main/java/com/jme3/texture/plugins/AndroidNativeImageLoader.java index 5e6d20e93..0a72b37db 100644 --- a/jme3-android/src/main/java/com/jme3/texture/plugins/AndroidNativeImageLoader.java +++ b/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(); }