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
experimental
shadowislord 10 years ago
parent b83603cd8f
commit 7057e9cb18
  1. 3
      jme3-android-native/src/native/jme_stbi/Android.mk
  2. 324
      jme3-android-native/src/native/jme_stbi/com_jme3_texture_plugins_AndroidNativeImageLoader.c
  3. 142
      jme3-android/src/main/java/com/jme3/texture/plugins/AndroidNativeImageLoader.java

@ -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

@ -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);
}
*/

@ -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();
}

Loading…
Cancel
Save