From f3902db35ed7fb375d8fdf9c4c38aa31b6778fd8 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Fri, 15 Nov 2024 01:58:36 +0100 Subject: [PATCH 1/2] Fix JNI function name for `getDataSizeUncompressed` (#957) A small fix for the JNI bindings: The function signature for `getDataSizeUncompressed` was given as `Java_org_khronos_KTXTexture_getDataSizeUncompressed` but must be `Java_org_khronos_ktx_KtxTexture_getDataSizeUncompressed` (matching the full package- and class name) Currently, calling this function would result in a `java.lang.UnsatisfiedLinkError: org.khronos.ktx.KtxTexture.getDataSizeUncompressed()J` I also added a test that _might_ look useless at the first glance: It just creates a texture, and calls all the `native` "getter" functions on that (without any assertion). The point of that test is that it would have _prevented_ this kind of error... --- .../java_binding/src/main/cpp/KtxTexture.cpp | 2 +- .../org/khronos/ktx/test/KtxTexture2Test.java | 43 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/interface/java_binding/src/main/cpp/KtxTexture.cpp b/interface/java_binding/src/main/cpp/KtxTexture.cpp index 0a6028f613..d8faf64a48 100644 --- a/interface/java_binding/src/main/cpp/KtxTexture.cpp +++ b/interface/java_binding/src/main/cpp/KtxTexture.cpp @@ -188,7 +188,7 @@ extern "C" JNIEXPORT jlong JNICALL Java_org_khronos_ktx_KtxTexture_getDataSize(J return static_cast(ktxTexture_GetDataSize(texture)); } -extern "C" JNIEXPORT jlong JNICALL Java_org_khronos_KTXTexture_getDataSizeUncompressed(JNIEnv *env, jobject thiz) +extern "C" JNIEXPORT jlong JNICALL Java_org_khronos_ktx_KtxTexture_getDataSizeUncompressed(JNIEnv *env, jobject thiz) { ktxTexture *texture = get_ktx_texture(env, thiz); if (texture == NULL) diff --git a/interface/java_binding/src/test/java/org/khronos/ktx/test/KtxTexture2Test.java b/interface/java_binding/src/test/java/org/khronos/ktx/test/KtxTexture2Test.java index 2518bc8cbd..0e00b1744c 100644 --- a/interface/java_binding/src/test/java/org/khronos/ktx/test/KtxTexture2Test.java +++ b/interface/java_binding/src/test/java/org/khronos/ktx/test/KtxTexture2Test.java @@ -599,4 +599,47 @@ public void testSupercompressionZLIB() throws IOException { t.destroy(); } + + @Test + public void testBindings() { + Path testKtxFile = Paths.get("") + .resolve("../../tests/testimages/astc_ldr_4x4_FlightHelmet_baseColor.ktx2") + .toAbsolutePath() + .normalize(); + + // The purpose of this test is to check the bindings for the 'native' + // functions that only return a value. When the binding for one of + // these functions is not implemented properly, then trying to call + // it will cause an 'UnsatisfiedLinkError'. + // This does not cover all 'native' functions: Some of them can only + // sensibly be called in the context of the other tests. + + KtxTexture2 texture = KtxTexture2.createFromNamedFile(testKtxFile.toString(), + KtxTextureCreateFlagBits.NO_FLAGS); + + // Native getter methods from the 'KtxTexture2' class + texture.getOETF(); + texture.getPremultipliedAlpha(); + texture.needsTranscoding(); + texture.getSupercompressionScheme(); + texture.getVkFormat(); + + // Native getter methods from the 'KtxTexture' class + texture.isArray(); + texture.isCubemap(); + texture.isCompressed(); + texture.getGenerateMipmaps(); + texture.getBaseWidth(); + texture.getBaseHeight(); + texture.getBaseDepth(); + texture.getNumDimensions(); + texture.getNumLevels(); + texture.getNumFaces(); + texture.getDataSize(); + texture.getDataSizeUncompressed(); + texture.getElementSize(); + texture.getRowPitch(0); + texture.getImageSize(0); + } + } From bd8ae318fa8fffd00e25ceea09b3b6ee1601d7cb Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Tue, 19 Nov 2024 12:02:36 +0100 Subject: [PATCH 2/2] Add GL upload function to Java interface (#959) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (This builds upon https://github.com/KhronosGroup/KTX-Software/pull/957, so that should be merged first) This adds bindings for the `ktxTexture_GLUpload` function to the Java interface. There certainly are a few degrees of freedom about how _exactly_ that should be offered. And there is a trade-off between "trying to closely resemble the existing API" and "trying to create a nice API for Java". The main point here being that the function receives three _pointers_ (to `int` values, essentially). And pointers don't exist in Java. A common (although not pretty) way is to emulate these with single-element arrays. So the call currently looks like this: ``` int texture[] = { 0 }; int target[] = { 0 }; int glError[] = { 0 }; int result = ktxTexture.glUpload(texture, target, glError); ``` This will fill the given arrays with the values that are returned from the native layer, accordingly. Proper testing may be difficult. The basic call conditions are checked in a unit test. But beyond that, really _using_ the function will require an OpenGL context to be current, so that 1. requires an OpenGL binding for the test, and 2. can hardly happen during the standard test runs. --- I tried it out in a _very_ basic experiment. This experiment currently used https://www.lwjgl.org/ , but I'll probably also try it with https://jogamp.org/ . Given that LWJGL is the library behind https://libgdx.com/ and https://jmonkeyengine.org/ , that's likely to be a primary goal. The result of that experiment is shown here... ![Khronos KTX in Java](https://github.com/user-attachments/assets/7c86d565-5fdf-4f31-a20c-f6616c2c99ed) ... and I frankly couldn't care less that it's upside down. It works 🙃 --- .../java_binding/src/main/cpp/KtxTexture.cpp | 110 ++++++++++++++++++ .../main/java/org/khronos/ktx/KtxTexture.java | 55 +++++++++ .../org/khronos/ktx/test/KtxTexture2Test.java | 54 +++++++++ 3 files changed, 219 insertions(+) diff --git a/interface/java_binding/src/main/cpp/KtxTexture.cpp b/interface/java_binding/src/main/cpp/KtxTexture.cpp index d8faf64a48..2b8cff1150 100644 --- a/interface/java_binding/src/main/cpp/KtxTexture.cpp +++ b/interface/java_binding/src/main/cpp/KtxTexture.cpp @@ -199,6 +199,116 @@ extern "C" JNIEXPORT jlong JNICALL Java_org_khronos_ktx_KtxTexture_getDataSizeUn return ktxTexture_GetDataSizeUncompressed(texture); } + +extern "C" JNIEXPORT jint JNICALL Java_org_khronos_ktx_KtxTexture_glUpload(JNIEnv *env, jobject thiz, jintArray javaTexture, jintArray javaTarget, jintArray javaGlError) +{ + ktxTexture *texture = get_ktx_texture(env, thiz); + if (texture == NULL) + { + ThrowDestroyed(env); + return 0; + } + + // The target array may not be NULL, and must have + // a size of at least 1 + if (javaTarget == NULL) + { + ThrowByName(env, "java/lang/NullPointerException", "Parameter 'target' is null for glUpload"); + return 0; + } + jsize javaTargetSize = env->GetArrayLength(javaTarget); + if (javaTargetSize == 0) + { + ThrowByName(env, "java/lang/IllegalArgumentException", "Parameter 'target' may not have length 0"); + return 0; + } + + // The texture array may be NULL, but if it is not NULL, + // then it must have a length of at least 1 + if (javaTexture != NULL) + { + jsize javaTextureSize = env->GetArrayLength(javaTexture); + if (javaTextureSize == 0) + { + ThrowByName(env, "java/lang/IllegalArgumentException", "Parameter 'texture' may not have length 0"); + return 0; + } + } + + // The GL error array may be NULL, but if it is not NULL, + // then it must have a length of at least 1 + if (javaGlError != NULL) + { + jsize javaGlErrorSize = env->GetArrayLength(javaGlError); + if (javaGlErrorSize == 0) + { + ThrowByName(env, "java/lang/IllegalArgumentException", "Parameter 'glError' may not have length 0"); + return 0; + } + } + + GLuint textureValue = 0; + if (javaTexture != NULL) + { + jint *javaTextureArrayElements = env->GetIntArrayElements(javaTexture, NULL); + if (javaTextureArrayElements == NULL) + { + // OutOfMemoryError is already pending + return 0; + } + textureValue = static_cast(javaTextureArrayElements[0]); + env->ReleaseIntArrayElements(javaTexture, javaTextureArrayElements, JNI_ABORT); + } + + GLenum target; + GLenum glError; + + KTX_error_code result = ktxTexture_GLUpload(texture, &textureValue, &target, &glError); + + // Write back the texture into the array + if (javaTexture != NULL) + { + jint *javaTextureArrayElements = env->GetIntArrayElements(javaTexture, NULL); + if (javaTextureArrayElements == NULL) + { + // OutOfMemoryError is already pending + return 0; + } + javaTextureArrayElements[0] = static_cast(textureValue); + env->ReleaseIntArrayElements(javaTexture, javaTextureArrayElements, JNI_COMMIT); + } + + // Write back the target into the array + if (javaTarget != NULL) + { + jint *javaTargetArrayElements = env->GetIntArrayElements(javaTarget, NULL); + if (javaTargetArrayElements == NULL) + { + // OutOfMemoryError is already pending + return 0; + } + javaTargetArrayElements[0] = static_cast(target); + env->ReleaseIntArrayElements(javaTarget, javaTargetArrayElements, JNI_COMMIT); + } + + // Write back the error into the array + if (javaGlError != NULL) + { + jint *javaGlErrorArrayElements = env->GetIntArrayElements(javaGlError, NULL); + if (javaGlErrorArrayElements == NULL) + { + // OutOfMemoryError is already pending + return 0; + } + javaGlErrorArrayElements[0] = static_cast(glError); + env->ReleaseIntArrayElements(javaGlError, javaGlErrorArrayElements, JNI_COMMIT); + } + return result; +} + + + + extern "C" JNIEXPORT jint JNICALL Java_org_khronos_ktx_KtxTexture_getElementSize(JNIEnv *env, jobject thiz) { ktxTexture *texture = get_ktx_texture(env, thiz); diff --git a/interface/java_binding/src/main/java/org/khronos/ktx/KtxTexture.java b/interface/java_binding/src/main/java/org/khronos/ktx/KtxTexture.java index 7f3f7fad4c..892ab3ab21 100644 --- a/interface/java_binding/src/main/java/org/khronos/ktx/KtxTexture.java +++ b/interface/java_binding/src/main/java/org/khronos/ktx/KtxTexture.java @@ -214,6 +214,61 @@ public boolean isDestroyed() { */ public native long getImageOffset(int level, int layer, int faceSlice); + /** + * Create a GL texture object from a {@link KtxTexture} object.
+ *
+ * This may only be called when a GL context is current.
+ *
+ * In order to ensure that the GL uploader is not linked into an application + * unless explicitly called, this is not a virtual function. It determines + * the texture type then dispatches to the correct function.
+ *
+ * Sets the texture object's GL_TEXTURE_MAX_LEVEL parameter + * according to the number of levels in the KTX data, provided the + * context supports this feature.
+ *
+ * Unpacks compressed {@link KtxInternalformat#GL_ETC1_RGB8_OES} and + * GL_ETC2_* format textures in software when the format + * is not supported by the GL context, provided the library has been + * compiled with SUPPORT_SOFTWARE_ETC_UNPACK defined as 1. + * + * It will also convert textures with legacy formats to their modern + * equivalents when the format is not supported by the GL context, + * provided the library has been compiled with + * SUPPORT_LEGACY_FORMAT_CONVERSION defined as 1. + * + * @param texture An array that is either null, or + * has a length of at least 1. It contains the name of the GL texture + * object to load. If it is null or contains 0, the + * function will generate a texture name. The function binds either + * the generated name or the name given in texture to + * the texture target returned in target, before + * loading the texture data. If pTexture is not null + * and a name was generated, the generated name will be returned + * in texture. + * @param target An array with a length of at least 1, where + * element 0 will receive the GL target value + * @param glError An array with a length of at least 1, where + * element 0 will receive any GL error information + * @return {@link KtxErrorCode#SUCCESS} on sucess. + * Returns {@link KtxErrorCode#GL_ERROR} when GL error was raised by + * glBindTexture, glGenTextures or + * gl*TexImage* The GL error will be returned in + * glError if glError is not + * null. Returns {@link KtxErrorCode#INVALID_VALUE} + * when target is null or the size of a mip level + * is greater than the size of the preceding level. Returns + * {@link KtxErrorCode#NOT_FOUND} when a dynamically loaded + * OpenGL function required by the loader was not found. + * Returns {@link KtxErrorCode#UNSUPPORTED_TEXTURE_TYPE} when + * the type of texture is not supported by the current OpenGL context. + * @throws NullPointerException If the given target + * array is null + * @throws IllegalArgumentException Any array that is not + * null has a length of 0 + */ + public native int glUpload(int texture[], int target[], int glError[]); + /** * Destroy the KTX texture and free memory image resources.
*
diff --git a/interface/java_binding/src/test/java/org/khronos/ktx/test/KtxTexture2Test.java b/interface/java_binding/src/test/java/org/khronos/ktx/test/KtxTexture2Test.java index 0e00b1744c..db2a1c4602 100644 --- a/interface/java_binding/src/test/java/org/khronos/ktx/test/KtxTexture2Test.java +++ b/interface/java_binding/src/test/java/org/khronos/ktx/test/KtxTexture2Test.java @@ -642,4 +642,58 @@ public void testBindings() { texture.getImageSize(0); } + @Test + public void testGlUpload() { + Path testKtxFile = Paths.get("") + .resolve("../../tests/testimages/astc_ldr_4x4_FlightHelmet_baseColor.ktx2") + .toAbsolutePath() + .normalize(); + + KtxTexture2 ktxTexture = KtxTexture2.createFromNamedFile(testKtxFile.toString(), + KtxTextureCreateFlagBits.NO_FLAGS); + ktxTexture.transcodeBasis(KtxTranscodeFormat.BC1_RGB, 0); + + // This test checks the error conditions that are supposed + // to be handled by the JNI layer by throwing exceptions. + // The test can NOT perform an actual, "valid" call that + // causes ktxTexture_GLUpload to be called internally, + // because that would require a GL context to be current. + int texture0[] = { }; + int target0[] = { }; + int glError0[] = { }; + int texture1[] = { 0 }; + int target1[] = { 0 }; + int glError1[] = { 0 }; + + // Expect NullPointerException when target is null + assertThrows(NullPointerException.class, + () -> { + ktxTexture.glUpload(texture1, null, glError1); + }, + "Expected to throw NullPointerException"); + + // Expect IllegalArgumentException when texture length is 0 + assertThrows(IllegalArgumentException.class, + () -> { + ktxTexture.glUpload(texture0, target1, glError1); + }, + "Expected to throw NullPointerException"); + + // Expect IllegalArgumentException when target length is 0 + assertThrows(IllegalArgumentException.class, + () -> { + ktxTexture.glUpload(texture1, target0, glError1); + }, + "Expected to throw NullPointerException"); + + // Expect IllegalArgumentException when glError length is 0 + assertThrows(IllegalArgumentException.class, + () -> { + ktxTexture.glUpload(texture1, target1, glError0); + }, + "Expected to throw NullPointerException"); + + ktxTexture.destroy(); + } + }