Skip to content

Commit cc56485

Browse files
authored
Properly convert Java char to C char in inputSwizzle (#876)
Fixes #875 As detailed in the issue: The Java `char[]` array for the input swizzle was casted to a `jbyteArray` in the JNI layer, in order to write it to the C/C++ `char[4]` array. This cast was invalid and caused data corruption. There could be two ways of tackling this: - One could change the type of the `inputSwizzle` in the Java layer to `byte[]` (which has 8 bits, and would make the cast valid). This would be a breaking change, and may require the inconvenient casting at the call site like `p.setInputSwizzle(new byte[]{ (byte)'b', (byte)'r', (byte)'g', (byte)'a' });` - One could fetch the Java `char[]` array as a `jcharArray`, and cast the elements to (C/C++) `char` individually This PR takes the second option (but we could do this either way, depending on the preferences from others).
1 parent e31b3e5 commit cc56485

File tree

4 files changed

+186
-20
lines changed

4 files changed

+186
-20
lines changed

interface/java_binding/src/main/cpp/libktx-jni.cpp

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -97,12 +97,14 @@ void copy_ktx_astc_params(JNIEnv *env, jobject params, ktxAstcParams &out)
9797
out.normalMap = env->GetBooleanField(params, normalMap);
9898
out.perceptual = env->GetBooleanField(params, perceptual);
9999

100-
env->GetByteArrayRegion(
101-
static_cast<jbyteArray>(env->GetObjectField(params, inputSwizzle)),
102-
0,
103-
4,
104-
reinterpret_cast<jbyte*>(&out.inputSwizzle)
105-
);
100+
jobject inputSwizzleObject = env->GetObjectField(params, inputSwizzle);
101+
jcharArray inputSwizzleArray = static_cast<jcharArray>(inputSwizzleObject);
102+
jchar *inputSwizzleValues = env->GetCharArrayElements( inputSwizzleArray, NULL);
103+
for (int i=0; i<4; i++)
104+
{
105+
out.inputSwizzle[i] = static_cast<char>(inputSwizzleValues[i]);
106+
}
107+
env->ReleaseCharArrayElements(inputSwizzleArray, inputSwizzleValues, JNI_ABORT);
106108
}
107109

108110
void copy_ktx_basis_params(JNIEnv *env, jobject params, ktxBasisParams &out)
@@ -145,12 +147,16 @@ void copy_ktx_basis_params(JNIEnv *env, jobject params, ktxBasisParams &out)
145147
out.endpointRDOThreshold = env->GetFloatField(params, endpointRDOThreshold);
146148
out.maxSelectors = env->GetIntField(params, maxSelectors);
147149
out.selectorRDOThreshold = env->GetFloatField(params, selectorRDOThreshold);
148-
env->GetByteArrayRegion(
149-
static_cast<jbyteArray>(env->GetObjectField(params, inputSwizzle)),
150-
0,
151-
4,
152-
reinterpret_cast<jbyte*>(&out.inputSwizzle)
153-
);
150+
151+
jobject inputSwizzleObject = env->GetObjectField(params, inputSwizzle);
152+
jcharArray inputSwizzleArray = static_cast<jcharArray>(inputSwizzleObject);
153+
jchar *inputSwizzleValues = env->GetCharArrayElements( inputSwizzleArray, NULL);
154+
for (int i=0; i<4; i++)
155+
{
156+
out.inputSwizzle[i] = static_cast<char>(inputSwizzleValues[i]);
157+
}
158+
env->ReleaseCharArrayElements(inputSwizzleArray, inputSwizzleValues, JNI_ABORT);
159+
154160
out.normalMap = env->GetBooleanField(params, normalMap);
155161
out.preSwizzle = env->GetBooleanField(params, preSwizzle);
156162
out.noEndpointRDO = env->GetBooleanField(params, noEndpointRDO);

interface/java_binding/src/test/java/org/khronos/ktx/test/KtxTestLibraryLoader.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,6 @@
1111
import java.io.File;
1212
import java.nio.file.Files;
1313
import java.nio.file.Path;
14-
import java.util.Arrays;
15-
16-
import static org.junit.jupiter.api.extension.ExtensionContext.Namespace.GLOBAL;
1714

1815
public class KtxTestLibraryLoader
1916
implements BeforeAllCallback, ExtensionContext.Store.CloseableResource {

interface/java_binding/src/test/java/org/khronos/ktx/test/KtxTexture2Test.java

Lines changed: 101 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,26 @@
55

66
package org.khronos.ktx.test;
77

8-
import org.junit.jupiter.api.Test;
9-
import org.junit.jupiter.api.extension.ExtendWith;
10-
import org.khronos.ktx.*;
8+
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
9+
import static org.junit.jupiter.api.Assertions.assertEquals;
10+
import static org.junit.jupiter.api.Assertions.assertNotNull;
1111

1212
import java.io.IOException;
1313
import java.nio.file.Files;
1414
import java.nio.file.Path;
1515
import java.nio.file.Paths;
1616

17-
import static org.junit.jupiter.api.Assertions.assertEquals;
18-
import static org.junit.jupiter.api.Assertions.assertNotNull;
17+
import org.junit.jupiter.api.Test;
18+
import org.junit.jupiter.api.extension.ExtendWith;
19+
import org.khronos.ktx.KtxBasisParams;
20+
import org.khronos.ktx.KtxCreateStorage;
21+
import org.khronos.ktx.KtxErrorCode;
22+
import org.khronos.ktx.KtxSupercmpScheme;
23+
import org.khronos.ktx.KtxTexture2;
24+
import org.khronos.ktx.KtxTextureCreateFlagBits;
25+
import org.khronos.ktx.KtxTextureCreateInfo;
26+
import org.khronos.ktx.KtxTranscodeFormat;
27+
import org.khronos.ktx.VkFormat;
1928

2029
@ExtendWith({ KtxTestLibraryLoader.class })
2130
public class KtxTexture2Test {
@@ -227,4 +236,91 @@ public void testCreate() {
227236

228237
texture.destroy();
229238
}
239+
240+
@Test
241+
public void testInputSwizzleBasisEx() throws IOException {
242+
243+
int sizeX = 32;
244+
int sizeY = 32;
245+
int outputFormat = KtxTranscodeFormat.RGBA32;
246+
int transcodeFlags = 0;
247+
248+
// Create the actual texture data:
249+
// - create RGBA pixels
250+
// - create texture
251+
// - compress with BRGA input swizzling
252+
// - obtain resulting RGBA values
253+
254+
// Create a RGBA pixels for an image filled with
255+
// 8 rows of red pixels
256+
// 8 rows of green pixels
257+
// 8 rows of blue pixels
258+
// 8 rows of white pixels
259+
byte[] input = new byte[sizeX * sizeY * 4];
260+
TestUtils.fillRows(input, sizeX, sizeY, 0, 8, 255, 0, 0, 255); // Red
261+
TestUtils.fillRows(input, sizeX, sizeY, 8, 16, 0, 255, 0, 255); // Green
262+
TestUtils.fillRows(input, sizeX, sizeY, 16, 24, 0, 0, 255, 255); // Blue
263+
TestUtils.fillRows(input, sizeX, sizeY, 24, 32, 255, 255, 255, 255); // White
264+
265+
// Create the input texture from the pixels
266+
KtxTextureCreateInfo inputInfo = new KtxTextureCreateInfo();
267+
inputInfo.setBaseWidth(sizeX);
268+
inputInfo.setBaseHeight(sizeY);
269+
inputInfo.setVkFormat(VkFormat.VK_FORMAT_R8G8B8A8_SRGB);
270+
KtxTexture2 inputTexture = KtxTexture2.create(inputInfo, KtxCreateStorage.ALLOC);
271+
inputTexture.setImageFromMemory(0, 0, 0, input);
272+
273+
// Apply basis compression to the input, with an input swizzle BRGA,
274+
// so that
275+
// the former B channel becomes the R channel
276+
// the former R channel becomes the G channel
277+
// the former G channel becomes the B channel
278+
// the former A channel remains the A channel
279+
KtxBasisParams inputParams = new KtxBasisParams();
280+
inputParams.setUastc(false);
281+
inputParams.setInputSwizzle(new char[] { 'b', 'r', 'g', 'a' });
282+
inputTexture.compressBasisEx(inputParams);
283+
284+
// Transcode the input texture to RGBA32
285+
inputTexture.transcodeBasis(outputFormat, transcodeFlags);
286+
byte[] actualRgba = inputTexture.getData();
287+
288+
// Create the expected reference data:
289+
// - create RGBA pixels, swizzled with BRGA
290+
// - create texture
291+
// - compress without input swizzling
292+
// - obtain resulting RGBA values
293+
294+
// Create "golden" reference pixels, where a BRGA
295+
// swizzling was already applied
296+
byte[] gold = new byte[sizeX * sizeY * 4];
297+
TestUtils.fillRows(gold, sizeX, sizeY, 0, 8, 0, 255, 0, 255); // Green
298+
TestUtils.fillRows(gold, sizeX, sizeY, 8, 16, 0, 0, 255, 255); // Blue
299+
TestUtils.fillRows(gold, sizeX, sizeY, 16, 24, 255, 0, 0, 255); // Red
300+
TestUtils.fillRows(gold, sizeX, sizeY, 24, 32, 255, 255, 255, 255); // White
301+
302+
// Create the reference texture from the swizzled pixels
303+
KtxTextureCreateInfo goldInfo = new KtxTextureCreateInfo();
304+
goldInfo.setBaseWidth(sizeX);
305+
goldInfo.setBaseHeight(sizeY);
306+
goldInfo.setVkFormat(VkFormat.VK_FORMAT_R8G8B8A8_SRGB);
307+
KtxTexture2 goldTexture = KtxTexture2.create(goldInfo, KtxCreateStorage.ALLOC);
308+
goldTexture.setImageFromMemory(0, 0, 0, gold);
309+
310+
// Apply basis compression to the reference, without swizzling
311+
KtxBasisParams goldParams = new KtxBasisParams();
312+
goldParams.setUastc(false);
313+
goldTexture.compressBasisEx(goldParams);
314+
315+
// Transcode the reference texture to RGBA32
316+
goldTexture.transcodeBasis(outputFormat, transcodeFlags);
317+
byte[] expectedRgba = goldTexture.getData();
318+
319+
// Compare the resulting data to the expected RGBA values.
320+
assertArrayEquals(expectedRgba, actualRgba);
321+
322+
inputTexture.destroy();
323+
goldTexture.destroy();
324+
}
230325
}
326+
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright (c) 2024, Khronos Group and Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package org.khronos.ktx.test;
6+
7+
import java.util.Locale;
8+
9+
/**
10+
* Utilities for the test package
11+
*/
12+
class TestUtils {
13+
14+
/**
15+
* Fill the specified range of rows of the given RGBA pixels array with the
16+
* given RGBA components
17+
*
18+
* @param rgba The RGBA pixels array
19+
* @param sizeX The size of the image in x-direction
20+
* @param sizeY The size of the image in y-direction
21+
* @param minRow The minimum row, inclusive
22+
* @param maxRow The maximum row, exclusive
23+
* @param r The red component (in [0,255])
24+
* @param g The green component (in [0,255])
25+
* @param b The blue component (in [0,255])
26+
* @param a The alpha component (in [0,255])
27+
*/
28+
static void fillRows(byte rgba[], int sizeX, int sizeY,
29+
int minRow, int maxRow,
30+
int r, int g, int b, int a) {
31+
for (int y = minRow; y < maxRow; y++) {
32+
for (int x = 0; x < sizeX; x++) {
33+
int index = (y * sizeX) + x;
34+
rgba[index * 4 + 0] = (byte) r;
35+
rgba[index * 4 + 1] = (byte) g;
36+
rgba[index * 4 + 2] = (byte) b;
37+
rgba[index * 4 + 3] = (byte) a;
38+
}
39+
}
40+
}
41+
42+
/**
43+
* Create a string representation of the RGBA components of the specified pixel
44+
* in the given RGBA pixels array.
45+
*
46+
* This is mainly intended for debugging. Some details of the resulting string
47+
* are not specified.
48+
*
49+
* @param rgba The RGBA pixels array
50+
* @param pixelIndex The pixel index
51+
* @return The string
52+
*/
53+
static String createRgbaString(byte rgba[], int pixelIndex) {
54+
byte r = rgba[pixelIndex * 4 + 0];
55+
byte g = rgba[pixelIndex * 4 + 1];
56+
byte b = rgba[pixelIndex * 4 + 2];
57+
byte a = rgba[pixelIndex * 4 + 3];
58+
int ir = Byte.toUnsignedInt(r);
59+
int ig = Byte.toUnsignedInt(g);
60+
int ib = Byte.toUnsignedInt(b);
61+
int ia = Byte.toUnsignedInt(a);
62+
String s = String.format(Locale.ENGLISH, "%3d, %3d, %3d, %3d", ir, ig, ib, ia);
63+
return s;
64+
65+
}
66+
67+
}

0 commit comments

Comments
 (0)