diff --git a/README.md b/README.md index 8fc4494..8dfa40a 100644 --- a/README.md +++ b/README.md @@ -21,10 +21,14 @@ Currently, the following formats are supported: ## Output format -- BC1, BC2, BC3 and BC7 are decoded as RGBA (4 bytes per pixel). -- BC4 is decoded as R (1 byte per pixel). -- BC5 is decoded as RG (2 bytes per pixel). -- BC6H is decoded as RGB in little endian half-float (6 bytes per pixel). +Every format except BC6H is decoded as BGRA, 4 bytes per pixel. Read as little endian integers, that is AWT's +`TYPE_INT_ARGB` layout, so the result can go straight into a `BufferedImage` without a channel swap. + +- BC1, BC2, BC3 and BC7 fill all four channels. +- BC4 is a single channel, expanded to gray: blue, green and red all get the value, alpha is 255. +- BC5 is two channels: red and green carry the data, blue is 0 and alpha is 255. +- BC6H is the exception: it is decoded as RGBA in little endian half-float (8 bytes per pixel), in that channel order + rather than BGRA, with alpha set to 1.0. ## Usage @@ -34,9 +38,8 @@ The following features are present: - Partial decodes: The width and height do not need to be a multiple of the block size (4 in this case). The output width and height can be smaller than the input, and the library will handle this. -- BC6H: BC6H is decoded to a little endian half-float buffer. The library does not provide a way to convert this to full - float. This can be done with `Float.float16ToFloat` in Java 21, or with a library. This also means that the - output buffer is twice as big. +- BC6H: BC6H is decoded to a little endian half-float RGBA buffer, 8 bytes per pixel. The library does not provide a way + to convert this to full float. This can be done with `Float.float16ToFloat` in Java 21, or with a library. The library provides the `BlockDecoder` class, which can be used to decode. A new instance is created by one of the static factory methods. You can let the library create a new buffer, or pass an existing one to save allocations. @@ -62,14 +65,14 @@ ByteBuffer result = decoder.decode(src, 256, 256); To decode into a buffer you already have, pass it along with its dimensions. There is no return value. ```java -decoder.decode(src, 256,256,dst, 256,256); +decoder.decode(src, 256, 256, dst, 256, 256); ``` The destination may be smaller than the source, in which case the image is cropped to the top left. It may not be larger, as there would be nothing to fill the remainder with. ```java -decoder.decode(src, 256,256,dst, 64,64); // the top left 64x64 pixels +decoder.decode(src, 256, 256, dst, 64, 64); // the top left 64x64 pixels ``` To decode somewhere other than the top left, add the source and destination coordinates. The region decoded is whatever @@ -77,7 +80,7 @@ is left of the destination, which makes this the form to use when the destinatio want, such as a single tile. ```java -decoder.decode(src, 256,256,tile, 64,64,128,64,0,0); // the 64x64 tile at (128, 64) +decoder.decode(src, 256, 256, tile, 64, 64, 128, 64, 0, 0); // the 64x64 tile at (128, 64) ``` The general form takes the region size explicitly, and is the only way to decode into part of a larger destination. @@ -86,31 +89,55 @@ The general form takes the region size explicitly, and is the only way to decode decoder.decode( src, 256,256, // source buffer and its dimensions dst, 256,256, // destination buffer and its dimensions - 128,64,0,0, // decode from (128, 64) to (0, 0) - 64,64 // decoding a 64x64 region + 128, 64, 0, 0, // decode from (128, 64) to (0, 0) + 64,64 // decoding a 64x64 region ); ``` Both buffers must hold their entire image, not just the region, since the dimensions are what determine where a row starts. `encodedByteSize(width, height)` and `decodedByteSize(width, height)` return the sizes required. +## Converting to an image + +The BGRA output maps directly onto the common image types, so these snippets need no per-pixel work. They apply to every +format except BC6H, whose half-float output does not fit either. + +For an AWT `BufferedImage`, the bytes are `TYPE_INT_ARGB` when read as little endian integers: + +```java +ByteBuffer bgra = decoder.decode(src, width, height); +BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); +int[] pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); +bgra.order(ByteOrder.LITTLE_ENDIAN).asIntBuffer().get(pixels); +``` + +For a JavaFX `Image`, the bytes are straight-alpha BGRA, which is `PixelFormat.getByteBgraInstance()`: + +```java +ByteBuffer bgra = decoder.decode(src, width, height); +WritableImage image = new WritableImage(width, height); +image.getPixelWriter().setPixels( + 0,0,width, height, + PixelFormat.getByteBgraInstance(),bgra,width *4); +``` + ## Performance I've done some performance testing, and the library is quite fast. I've run some benchmarks on my machine (AMD 7840U). -Some quick benchmarks, tested on a Ryzen 7840U with Oracle Java 21 (MP/s stands for megapixels per second): +Some quick benchmarks, tested on a Ryzen 7840U with Oracle Java 25 (MP/s stands for megapixels per second): - BC1: ~1050MP/s -- BC2: ~750MP/s +- BC2: ~650MP/s - BC3: ~550MP/s -- BC4: ~1000MP/s -- BC5: ~550MP/s -- BC6: ~155MP/s -- BC7: ~175MP/s +- BC4: ~900MP/s +- BC5: ~450MP/s +- BC6: ~150MP/s +- BC7: ~170MP/s These numbers are just an estimate, and can vary depending on the hardware and the JVM. -To give you an idea, this means about 20 ms to decode a 4K texture in BC1, and about 100 ms for BC6 or BC7. +To give you an idea, this means about 16 ms to decode a 4K texture in BC1, and about 100 ms for BC6 or BC7. ## Accuracy diff --git a/src/main/java/be/twofold/tinybcdec/BC1.java b/src/main/java/be/twofold/tinybcdec/BC1.java index bcd87df..af162e9 100644 --- a/src/main/java/be/twofold/tinybcdec/BC1.java +++ b/src/main/java/be/twofold/tinybcdec/BC1.java @@ -3,7 +3,7 @@ import java.nio.*; final class BC1 extends BlockDecoder { - private static final int BPP = 4; + static final int BPP = 4; private final int[] colors = new int[4]; private final boolean bc2Or3; @@ -31,24 +31,24 @@ void decodeBlock(ByteBuffer src, int srcPos, ByteBuffer dst, int dstPos, int str int b1 = (c1/* */) & 0x1F; int[] colors = this.colors; - colors[0] = rgb(scale031(r0), scale063(g0), scale031(b0)); - colors[1] = rgb(scale031(r1), scale063(g1), scale031(b1)); + colors[0] = bgr(scale031(r0), scale063(g0), scale031(b0)); + colors[1] = bgr(scale031(r1), scale063(g1), scale031(b1)); if (c0 > c1 || bc2Or3) { int r2 = scale093(2 * r0 + r1); int g2 = scale189(2 * g0 + g1); int b2 = scale093(2 * b0 + b1); - colors[2] = rgb(r2, g2, b2); + colors[2] = bgr(r2, g2, b2); int r3 = scale093(r0 + 2 * r1); int g3 = scale189(g0 + 2 * g1); int b3 = scale093(b0 + 2 * b1); - colors[3] = rgb(r3, g3, b3); + colors[3] = bgr(r3, g3, b3); } else { int r2 = scale062(r0 + r1); int g2 = scale126(g0 + g1); int b2 = scale062(b0 + b1); - colors[2] = rgb(r2, g2, b2); + colors[2] = bgr(r2, g2, b2); colors[3] = color3; } @@ -63,8 +63,8 @@ void decodeBlock(ByteBuffer src, int srcPos, ByteBuffer dst, int dstPos, int str } } - private static int rgb(int r, int g, int b) { - return r | g << 8 | b << 16 | 0xFF000000; + private static int bgr(int r, int g, int b) { + return b | g << 8 | r << 16 | 0xFF000000; } private static int scale031(int i) { @@ -90,5 +90,4 @@ private static int scale062(int i) { private static int scale126(int i) { return (i * 4145 + 1019) >>> 11; } - } diff --git a/src/main/java/be/twofold/tinybcdec/BC2.java b/src/main/java/be/twofold/tinybcdec/BC2.java index 13653bd..a14993f 100644 --- a/src/main/java/be/twofold/tinybcdec/BC2.java +++ b/src/main/java/be/twofold/tinybcdec/BC2.java @@ -3,7 +3,7 @@ import java.nio.*; final class BC2 extends BlockDecoder { - private static final int BPP = 4; + static final int BPP = 4; private final BC1 colorDecoder = new BC1(BC1Mode.BC2OR3); @@ -22,7 +22,7 @@ private void decodeAlpha(ByteBuffer src, int srcPos, ByteBuffer dst, int dstPos, for (int y = 0; y < BLOCK_HEIGHT; y++) { for (int x = 0; x < BLOCK_WIDTH; x++) { - byte alpha = (byte) ((alphas & 15) * 17); + byte alpha = (byte) ((alphas & 0x0F) * 0x11); ByteIO.setByte(dst, dstPos + x * BPP, alpha); alphas >>>= 4; } diff --git a/src/main/java/be/twofold/tinybcdec/BC3.java b/src/main/java/be/twofold/tinybcdec/BC3.java index e0d1dc4..aab6b9e 100644 --- a/src/main/java/be/twofold/tinybcdec/BC3.java +++ b/src/main/java/be/twofold/tinybcdec/BC3.java @@ -3,10 +3,10 @@ import java.nio.*; final class BC3 extends BlockDecoder { - private static final int BPP = 4; + static final int BPP = 4; private final BC1 colorDecoder = new BC1(BC1Mode.BC2OR3); - private final BC4U alphaDecoder = new BC4U(BPP); + private final byte[] alphas = new byte[8]; BC3() { super(BPP, 16); @@ -15,6 +15,19 @@ final class BC3 extends BlockDecoder { @Override void decodeBlock(ByteBuffer src, int srcPos, ByteBuffer dst, int dstPos, int stride) { colorDecoder.decodeBlock(src, srcPos + 8, dst, dstPos/**/, stride); - alphaDecoder.decodeBlock(src, srcPos/**/, dst, dstPos + 3, stride); + decodeAlpha(src, srcPos/**/, dst, dstPos + 3, stride); + } + + private void decodeAlpha(ByteBuffer src, int srcPos, ByteBuffer dst, int dstPos, int stride) { + byte[] alphas = this.alphas; + long indices = BC4U.buildAlphas(ByteIO.getLong(src, srcPos), alphas); + + for (int y = 0; y < BLOCK_HEIGHT; y++) { + for (int x = 0; x < BLOCK_WIDTH; x++) { + ByteIO.setByte(dst, dstPos + x * BPP, alphas[(int) (indices & 0x07)]); + indices >>>= 3; + } + dstPos += stride; + } } } diff --git a/src/main/java/be/twofold/tinybcdec/BC4S.java b/src/main/java/be/twofold/tinybcdec/BC4S.java index cd24439..37f5152 100644 --- a/src/main/java/be/twofold/tinybcdec/BC4S.java +++ b/src/main/java/be/twofold/tinybcdec/BC4S.java @@ -3,20 +3,36 @@ import java.nio.*; final class BC4S extends BlockDecoder { + private static final int BPP = 4; + private final byte[] alphas = new byte[8]; - BC4S(int pixelStride) { - super(pixelStride, 8); + BC4S() { + super(BPP, 8); } @Override void decodeBlock(ByteBuffer src, int srcPos, ByteBuffer dst, int dstPos, int stride) { long block = ByteIO.getLong(src, srcPos); + byte[] alphas = this.alphas; + + buildAlphas(block, alphas); + + long indices = block >>> 16; + for (int y = 0; y < BLOCK_HEIGHT; y++) { + for (int x = 0; x < BLOCK_WIDTH; x++) { + int alpha = alphas[(int) (indices & 0x07)] & 0xFF; + ByteIO.setInt(dst, dstPos + x * BPP, alpha * 0x01_0101 | 0xFF00_0000); + indices >>>= 3; + } + dstPos += stride; + } + } + static long buildAlphas(long block, byte[] alphas) { int a0 = Math.max(-127, (byte) (block/* */)); int a1 = Math.max(-127, (byte) (block >>> 8)); - byte[] alphas = this.alphas; alphas[0] = scale127(a0); alphas[1] = scale127(a1); @@ -36,15 +52,7 @@ void decodeBlock(ByteBuffer src, int srcPos, ByteBuffer dst, int dstPos, int str alphas[7] = (byte) 0xFF; } - long indices = block >>> 16; - for (int y = 0; y < BLOCK_HEIGHT; y++) { - for (int x = 0; x < BLOCK_WIDTH; x++) { - byte alpha = alphas[(int) (indices & 0x07)]; - ByteIO.setByte(dst, dstPos + x * bytesPerPixel, alpha); - indices >>>= 3; - } - dstPos += stride; - } + return block >>> 16; } private static byte scale127(int i) { diff --git a/src/main/java/be/twofold/tinybcdec/BC4U.java b/src/main/java/be/twofold/tinybcdec/BC4U.java index ff41fa2..f4104d8 100644 --- a/src/main/java/be/twofold/tinybcdec/BC4U.java +++ b/src/main/java/be/twofold/tinybcdec/BC4U.java @@ -3,20 +3,34 @@ import java.nio.*; final class BC4U extends BlockDecoder { + static final int BPP = 4; + private final byte[] alphas = new byte[8]; - BC4U(int pixelStride) { - super(pixelStride, 8); + BC4U() { + super(BPP, 8); } @Override void decodeBlock(ByteBuffer src, int srcPos, ByteBuffer dst, int dstPos, int stride) { long block = ByteIO.getLong(src, srcPos); + byte[] alphas = this.alphas; + long indices = buildAlphas(block, alphas); + for (int y = 0; y < BLOCK_HEIGHT; y++) { + for (int x = 0; x < BLOCK_WIDTH; x++) { + int alpha = alphas[(int) (indices & 0x07)] & 0xFF; + ByteIO.setInt(dst, dstPos + x * BPP, alpha * 0x01_0101 | 0xFF00_0000); + indices >>>= 3; + } + dstPos += stride; + } + } + + static long buildAlphas(long block, byte[] alphas) { int a0 = (int) (block/* */) & 0xFF; int a1 = (int) (block >>> 8) & 0xFF; - byte[] alphas = this.alphas; alphas[0] = (byte) a0; alphas[1] = (byte) a1; @@ -36,15 +50,7 @@ void decodeBlock(ByteBuffer src, int srcPos, ByteBuffer dst, int dstPos, int str alphas[7] = (byte) 0xFF; } - long indices = block >>> 16; - for (int y = 0; y < BLOCK_HEIGHT; y++) { - for (int x = 0; x < BLOCK_WIDTH; x++) { - byte alpha = alphas[(int) (indices & 0x07)]; - ByteIO.setByte(dst, dstPos + x * bytesPerPixel, alpha); - indices >>>= 3; - } - dstPos += stride; - } + return block >>> 16; } private static byte scale1785(int i) { diff --git a/src/main/java/be/twofold/tinybcdec/BC5S.java b/src/main/java/be/twofold/tinybcdec/BC5S.java index 163d5a7..8b990db 100644 --- a/src/main/java/be/twofold/tinybcdec/BC5S.java +++ b/src/main/java/be/twofold/tinybcdec/BC5S.java @@ -3,9 +3,10 @@ import java.nio.*; final class BC5S extends BlockDecoder { - private static final int BPP = 2; + private static final int BPP = 4; - private final BC4S decoder = new BC4S(BPP); + private final byte[] rAlphas = new byte[8]; + private final byte[] gAlphas = new byte[8]; BC5S() { super(BPP, 16); @@ -13,7 +14,20 @@ final class BC5S extends BlockDecoder { @Override void decodeBlock(ByteBuffer src, int srcPos, ByteBuffer dst, int dstPos, int stride) { - decoder.decodeBlock(src, srcPos/**/, dst, dstPos/**/, stride); - decoder.decodeBlock(src, srcPos + 8, dst, dstPos + 1, stride); + byte[] rAlphas = this.rAlphas; + byte[] gAlphas = this.gAlphas; + + long rIndices = BC4S.buildAlphas(src.getLong(srcPos/**/), rAlphas); + long gIndices = BC4S.buildAlphas(src.getLong(srcPos + 8), gAlphas); + for (int y = 0; y < BLOCK_HEIGHT; y++) { + for (int x = 0; x < BLOCK_WIDTH; x++) { + int rAlpha = rAlphas[(int) (rIndices & 0x07)] & 0xFF; + int gAlpha = gAlphas[(int) (gIndices & 0x07)] & 0xFF; + ByteIO.setInt(dst, dstPos + x * BPP, gAlpha << 8 | rAlpha << 16 | 0xFF00_0000); + rIndices >>>= 3; + gIndices >>>= 3; + } + dstPos += stride; + } } } diff --git a/src/main/java/be/twofold/tinybcdec/BC5U.java b/src/main/java/be/twofold/tinybcdec/BC5U.java index 9a42354..83b90f8 100644 --- a/src/main/java/be/twofold/tinybcdec/BC5U.java +++ b/src/main/java/be/twofold/tinybcdec/BC5U.java @@ -3,9 +3,10 @@ import java.nio.*; final class BC5U extends BlockDecoder { - private static final int BPP = 2; + static final int BPP = 4; - private final BC4U decoder = new BC4U(BPP); + private final byte[] rAlphas = new byte[8]; + private final byte[] gAlphas = new byte[8]; BC5U() { super(BPP, 16); @@ -13,7 +14,20 @@ final class BC5U extends BlockDecoder { @Override void decodeBlock(ByteBuffer src, int srcPos, ByteBuffer dst, int dstPos, int stride) { - decoder.decodeBlock(src, srcPos/**/, dst, dstPos/**/, stride); - decoder.decodeBlock(src, srcPos + 8, dst, dstPos + 1, stride); + byte[] rAlphas = this.rAlphas; + byte[] gAlphas = this.gAlphas; + + long rIndices = BC4U.buildAlphas(src.getLong(srcPos/**/), rAlphas); + long gIndices = BC4U.buildAlphas(src.getLong(srcPos + 8), gAlphas); + for (int y = 0; y < BLOCK_HEIGHT; y++) { + for (int x = 0; x < BLOCK_WIDTH; x++) { + int rAlpha = rAlphas[(int) (rIndices & 0x07)] & 0xFF; + int gAlpha = gAlphas[(int) (gIndices & 0x07)] & 0xFF; + ByteIO.setInt(dst, dstPos + x * BPP, gAlpha << 8 | rAlpha << 16 | 0xFF00_0000); + rIndices >>>= 3; + gIndices >>>= 3; + } + dstPos += stride; + } } } diff --git a/src/main/java/be/twofold/tinybcdec/BC6H.java b/src/main/java/be/twofold/tinybcdec/BC6H.java index 3904569..86e646a 100644 --- a/src/main/java/be/twofold/tinybcdec/BC6H.java +++ b/src/main/java/be/twofold/tinybcdec/BC6H.java @@ -4,7 +4,7 @@ import java.util.*; final class BC6H extends BPTC { - private static final int BPP = 6; + static final int BPP = 8; private static final List MODES = List.of( new Mode(T, T, 10, +5, +5, +5, new short[]{0x0941, 0x0A41, 0x0E41, 0x000A, 0x010A, 0x020A, 0x0405, 0x0D41, 0x0904, 0x0505, 0x0E01, 0x0D04, 0x0605, 0x0E11, 0x0A04, 0x0805, 0x0E21, 0x0C05, 0x0E31}), @@ -94,15 +94,13 @@ void decodeBlock(ByteBuffer src, int srcPos, ByteBuffer dst, int dstPos, int str indexBits >>>= ib; int index = (partitions & 3) * 8; - short r = finalUnquantize(interpolate(colors[index/**/], colors[index + 4], weight), signed); - short g = finalUnquantize(interpolate(colors[index + 1], colors[index + 5], weight), signed); - short b = finalUnquantize(interpolate(colors[index + 2], colors[index + 6], weight), signed); + long r = finalUnquantize(interpolate(colors[index/**/], colors[index + 4], weight), signed) & 0xFFFFL; + long g = finalUnquantize(interpolate(colors[index + 1], colors[index + 5], weight), signed) & 0xFFFFL; + long b = finalUnquantize(interpolate(colors[index + 2], colors[index + 6], weight), signed) & 0xFFFFL; partitions >>>= 2; int o = dstPos + x * BPP; - ByteIO.setShort(dst, o/**/, r); - ByteIO.setShort(dst, o + 2, g); - ByteIO.setShort(dst, o + 4, b); + ByteIO.setLong(dst, o, r | g << 16 | b << 32 | 0x3C00_0000_0000_0000L); } dstPos += stride; } diff --git a/src/main/java/be/twofold/tinybcdec/BC7.java b/src/main/java/be/twofold/tinybcdec/BC7.java index da9c5b2..c033865 100644 --- a/src/main/java/be/twofold/tinybcdec/BC7.java +++ b/src/main/java/be/twofold/tinybcdec/BC7.java @@ -4,7 +4,7 @@ import java.util.*; final class BC7 extends BPTC { - private static final int BPP = 4; + static final int BPP = 4; private static final List MODES = List.of( new Mode(3, 4, F, F, 4, 0, T, F, 3, 0), @@ -152,8 +152,8 @@ void decodeBlock(ByteBuffer src, int srcPos, ByteBuffer dst, int dstPos, int str } } - int rgba = r | g << 8 | b << 16 | a << 24; - ByteIO.setInt(dst, dstPos + x * BPP, rgba); + int bgra = b | g << 8 | r << 16 | a << 24; + ByteIO.setInt(dst, dstPos + x * BPP, bgra); } dstPos += stride; } diff --git a/src/main/java/be/twofold/tinybcdec/BlockDecoder.java b/src/main/java/be/twofold/tinybcdec/BlockDecoder.java index 275cb32..11ea0e1 100644 --- a/src/main/java/be/twofold/tinybcdec/BlockDecoder.java +++ b/src/main/java/be/twofold/tinybcdec/BlockDecoder.java @@ -65,7 +65,7 @@ public static BlockDecoder bc3() { * @return The block decoder. */ public static BlockDecoder bc4(boolean signed) { - return signed ? new BC4S(1) : new BC4U(1); + return signed ? new BC4S() : new BC4U(); } /** diff --git a/src/main/java/be/twofold/tinybcdec/ByteIO.java b/src/main/java/be/twofold/tinybcdec/ByteIO.java index b80e4bc..7f6cbae 100644 --- a/src/main/java/be/twofold/tinybcdec/ByteIO.java +++ b/src/main/java/be/twofold/tinybcdec/ByteIO.java @@ -56,6 +56,14 @@ static void setInt(ByteBuffer buffer, int offset, int value) { } } + static void setLong(ByteBuffer buffer, int offset, long value) { + if (buffer.hasArray()) { + VH_LONG.set(buffer.array(), buffer.arrayOffset() + offset, value); + } else { + buffer.putLong(offset, value); + } + } + static long getLong(ByteBuffer buffer, int offset) { if (buffer.hasArray()) { return (long) VH_LONG.get(buffer.array(), buffer.arrayOffset() + offset); diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 01c3a07..188448d 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -17,4 +17,7 @@ requires static java.desktop; exports be.twofold.tinybcdec; + + // For testing in IntelliJ + opens be.twofold.tinybcdec to org.junit.platform.commons; } diff --git a/src/test/java/be/twofold/tinybcdec/BC1Benchmark.java b/src/test/java/be/twofold/tinybcdec/BC1Benchmark.java index 0433c04..459d112 100644 --- a/src/test/java/be/twofold/tinybcdec/BC1Benchmark.java +++ b/src/test/java/be/twofold/tinybcdec/BC1Benchmark.java @@ -18,7 +18,7 @@ public BC1State() { try { decoder = BlockDecoder.bc1(true); src = BCTestUtils.readResource("/bc1.dds"); - dst = ByteBuffer.allocate(256 * 256 * 4); + dst = ByteBuffer.allocate(256 * 256 * BC1.BPP); } catch (IOException e) { throw new UncheckedIOException(e); } diff --git a/src/test/java/be/twofold/tinybcdec/BC1Test.java b/src/test/java/be/twofold/tinybcdec/BC1Test.java index 59ca44d..8a4463d 100644 --- a/src/test/java/be/twofold/tinybcdec/BC1Test.java +++ b/src/test/java/be/twofold/tinybcdec/BC1Test.java @@ -5,8 +5,6 @@ import java.io.*; import java.nio.*; -import static org.assertj.core.api.Assertions.*; - class BC1Test { private final BlockDecoder decoder = BlockDecoder.bc1(false); @@ -18,7 +16,7 @@ void testBC1() throws IOException { ByteBuffer actual = decoder.decode(src.position(BCTestUtils.DDS_HEADER_SIZE), 256, 256); ByteBuffer expected = BCTestUtils.readPng("/bc1a.png"); - assertThat(actual).isEqualTo(expected); + BCTestUtils.assertBufferEquals(actual, expected); } @Test @@ -28,7 +26,7 @@ void testBC1NoAlpha() throws IOException { ByteBuffer actual = decoder.decode(src.position(BCTestUtils.DDS_HEADER_SIZE), 256, 256); ByteBuffer expected = BCTestUtils.readPng("/bc1.png"); - assertThat(actual).isEqualTo(expected); + BCTestUtils.assertBufferEquals(actual, expected); } } diff --git a/src/test/java/be/twofold/tinybcdec/BC2Benchmark.java b/src/test/java/be/twofold/tinybcdec/BC2Benchmark.java index 0be9bed..c390d89 100644 --- a/src/test/java/be/twofold/tinybcdec/BC2Benchmark.java +++ b/src/test/java/be/twofold/tinybcdec/BC2Benchmark.java @@ -18,7 +18,7 @@ public BC2State() { try { decoder = BlockDecoder.bc2(); src = BCTestUtils.readResource("/bc2.dds"); - dst = ByteBuffer.allocate(256 * 256 * 4); + dst = ByteBuffer.allocate(256 * 256 * BC2.BPP); } catch (IOException e) { throw new UncheckedIOException(e); } diff --git a/src/test/java/be/twofold/tinybcdec/BC2Test.java b/src/test/java/be/twofold/tinybcdec/BC2Test.java index 3cf7f20..6319318 100644 --- a/src/test/java/be/twofold/tinybcdec/BC2Test.java +++ b/src/test/java/be/twofold/tinybcdec/BC2Test.java @@ -5,8 +5,6 @@ import java.io.*; import java.nio.*; -import static org.assertj.core.api.Assertions.*; - class BC2Test { private final BlockDecoder decoder = BlockDecoder.bc2(); @@ -18,7 +16,7 @@ void testBC2() throws IOException { ByteBuffer actual = decoder.decode(src.position(BCTestUtils.DDS_HEADER_SIZE), 256, 256); ByteBuffer expected = BCTestUtils.readPng("/bc2.png"); - assertThat(actual).isEqualTo(expected); + BCTestUtils.assertBufferEquals(actual, expected); } } diff --git a/src/test/java/be/twofold/tinybcdec/BC3Benchmark.java b/src/test/java/be/twofold/tinybcdec/BC3Benchmark.java index e213fd2..4c4876f 100644 --- a/src/test/java/be/twofold/tinybcdec/BC3Benchmark.java +++ b/src/test/java/be/twofold/tinybcdec/BC3Benchmark.java @@ -18,7 +18,7 @@ public BC3State() { try { decoder = BlockDecoder.bc3(); src = BCTestUtils.readResource("/bc3.dds"); - dst = ByteBuffer.allocate(256 * 256 * 4); + dst = ByteBuffer.allocate(256 * 256 * BC3.BPP); } catch (IOException e) { throw new UncheckedIOException(e); } diff --git a/src/test/java/be/twofold/tinybcdec/BC3Test.java b/src/test/java/be/twofold/tinybcdec/BC3Test.java index 40f0f25..0c7571c 100644 --- a/src/test/java/be/twofold/tinybcdec/BC3Test.java +++ b/src/test/java/be/twofold/tinybcdec/BC3Test.java @@ -5,8 +5,6 @@ import java.io.*; import java.nio.*; -import static org.assertj.core.api.Assertions.*; - class BC3Test { private final BlockDecoder decoder = BlockDecoder.bc3(); @@ -18,7 +16,7 @@ void testBC3() throws IOException { ByteBuffer actual = decoder.decode(src.position(BCTestUtils.DDS_HEADER_SIZE), 256, 256); ByteBuffer expected = BCTestUtils.readPng("/bc3.png"); - assertThat(actual).isEqualTo(expected); + BCTestUtils.assertBufferEquals(actual, expected); } } diff --git a/src/test/java/be/twofold/tinybcdec/BC4STest.java b/src/test/java/be/twofold/tinybcdec/BC4STest.java index e966667..baff4be 100644 --- a/src/test/java/be/twofold/tinybcdec/BC4STest.java +++ b/src/test/java/be/twofold/tinybcdec/BC4STest.java @@ -5,8 +5,6 @@ import java.io.*; import java.nio.*; -import static org.assertj.core.api.Assertions.*; - class BC4STest { private final BlockDecoder decoder = BlockDecoder.bc4(true); @@ -18,7 +16,7 @@ void testBC4S() throws IOException { ByteBuffer actual = decoder.decode(src.position(BCTestUtils.DDS_HEADER_SIZE), 256, 256); ByteBuffer expected = BCTestUtils.readPng("/bc4s.png"); - assertThat(actual).isEqualTo(expected); + BCTestUtils.assertBufferEquals(actual, expected); } } diff --git a/src/test/java/be/twofold/tinybcdec/BC4UBenchmark.java b/src/test/java/be/twofold/tinybcdec/BC4UBenchmark.java index b695de3..427ebf9 100644 --- a/src/test/java/be/twofold/tinybcdec/BC4UBenchmark.java +++ b/src/test/java/be/twofold/tinybcdec/BC4UBenchmark.java @@ -18,7 +18,7 @@ public BC4UState() { try { decoder = BlockDecoder.bc4(false); src = BCTestUtils.readResource("/bc4u.dds"); - dst = ByteBuffer.allocate(256 * 256); + dst = ByteBuffer.allocate(256 * 256 * BC4U.BPP); } catch (IOException e) { throw new UncheckedIOException(e); } diff --git a/src/test/java/be/twofold/tinybcdec/BC4UTest.java b/src/test/java/be/twofold/tinybcdec/BC4UTest.java index a6bbfcc..fc429ec 100644 --- a/src/test/java/be/twofold/tinybcdec/BC4UTest.java +++ b/src/test/java/be/twofold/tinybcdec/BC4UTest.java @@ -5,8 +5,6 @@ import java.io.*; import java.nio.*; -import static org.assertj.core.api.Assertions.*; - class BC4UTest { private final BlockDecoder decoder = BlockDecoder.bc4(false); @@ -18,7 +16,7 @@ void testBC4U() throws IOException { ByteBuffer actual = decoder.decode(src.position(BCTestUtils.DDS_HEADER_SIZE), 256, 256); ByteBuffer expected = BCTestUtils.readPng("/bc4u.png"); - assertThat(actual).isEqualTo(expected); + BCTestUtils.assertBufferEquals(actual, expected); } } diff --git a/src/test/java/be/twofold/tinybcdec/BC5STest.java b/src/test/java/be/twofold/tinybcdec/BC5STest.java index def79ca..99d6961 100644 --- a/src/test/java/be/twofold/tinybcdec/BC5STest.java +++ b/src/test/java/be/twofold/tinybcdec/BC5STest.java @@ -12,13 +12,14 @@ class BC5STest { @Test void testBC5S() throws IOException { ByteBuffer src = BCTestUtils.readResource("/bc5s.dds"); + ByteBuffer actual = BlockDecoder.bc5(true) .decode(src.position(BCTestUtils.DDS_HEADER_SIZE), 256, 256); ByteBuffer expected = BCTestUtils.readPng("/bc5s.png"); - for (int i = 0, o = 0; i < expected.remaining(); i += 3, o += 2) { - assertThat(Math.abs((actual.get(o/**/) & 0xFF) - (expected.get(i/**/) & 0xFF))).isLessThanOrEqualTo(1); - assertThat(Math.abs((actual.get(o + 1) & 0xFF) - (expected.get(i + 1) & 0xFF))).isLessThanOrEqualTo(1); + for (int i = 0; i < expected.remaining(); i += 4) { + assertThat(Math.abs((actual.get(i + 1) & 0xFF) - (expected.get(i + 1) & 0xFF))).isLessThanOrEqualTo(1); + assertThat(Math.abs((actual.get(i + 2) & 0xFF) - (expected.get(i + 2) & 0xFF))).isLessThanOrEqualTo(1); } } diff --git a/src/test/java/be/twofold/tinybcdec/BC5UBenchmark.java b/src/test/java/be/twofold/tinybcdec/BC5UBenchmark.java index c269521..b1bc410 100644 --- a/src/test/java/be/twofold/tinybcdec/BC5UBenchmark.java +++ b/src/test/java/be/twofold/tinybcdec/BC5UBenchmark.java @@ -18,7 +18,7 @@ public BC5UState() { try { decoder = BlockDecoder.bc5(false); src = BCTestUtils.readResource("/bc5u.dds"); - dst = ByteBuffer.allocate(256 * 256 * 3); + dst = ByteBuffer.allocate(256 * 256 * BC5U.BPP); } catch (IOException e) { throw new UncheckedIOException(e); } diff --git a/src/test/java/be/twofold/tinybcdec/BC5UTest.java b/src/test/java/be/twofold/tinybcdec/BC5UTest.java index 00af5ce..90231d8 100644 --- a/src/test/java/be/twofold/tinybcdec/BC5UTest.java +++ b/src/test/java/be/twofold/tinybcdec/BC5UTest.java @@ -17,9 +17,9 @@ void testBC5U() throws IOException { .decode(src.position(BCTestUtils.DDS_HEADER_SIZE), 256, 256); ByteBuffer expected = BCTestUtils.readPng("/bc5u.png"); - for (int i = 0, o = 0; i < expected.remaining(); i += 3, o += 2) { - assertThat(actual.get(o/**/)).isEqualTo(expected.get(i/**/)); - assertThat(actual.get(o + 1)).isEqualTo(expected.get(i + 1)); + for (int i = 0; i < expected.remaining(); i += 4) { + assertThat(actual.get(i + 1)).isEqualTo(expected.get(i + 1)); + assertThat(actual.get(i + 2)).isEqualTo(expected.get(i + 2)); } } diff --git a/src/test/java/be/twofold/tinybcdec/BC6HBenchmark.java b/src/test/java/be/twofold/tinybcdec/BC6HBenchmark.java index 7cbe41f..284264a 100644 --- a/src/test/java/be/twofold/tinybcdec/BC6HBenchmark.java +++ b/src/test/java/be/twofold/tinybcdec/BC6HBenchmark.java @@ -18,7 +18,7 @@ public BC6HState() { try { decoder = BlockDecoder.bc6h(false); src = BCTestUtils.readResource("/bc6h_uf16.dds"); - dst = ByteBuffer.allocate(256 * 256 * 6); + dst = ByteBuffer.allocate(256 * 256 * BC6H.BPP); } catch (IOException e) { throw new UncheckedIOException(e); } diff --git a/src/test/java/be/twofold/tinybcdec/BC6HTest.java b/src/test/java/be/twofold/tinybcdec/BC6HTest.java index 35914c6..3d55c02 100644 --- a/src/test/java/be/twofold/tinybcdec/BC6HTest.java +++ b/src/test/java/be/twofold/tinybcdec/BC6HTest.java @@ -17,7 +17,7 @@ void testBC6H_UF16() throws IOException { ByteBuffer actual = decoder.decode(src.position(BCTestUtils.DDS_HEADER_SIZE), 256, 256); ByteBuffer expected = BCTestUtils.readDDSFP16("/bc6h_uf16_16.dds", 256, 256); - assertThat(actual).isEqualTo(expected); + BCTestUtils.assertBufferEquals(actual, expected); } @Test @@ -28,7 +28,7 @@ void testBC6H_SF16() throws IOException { ByteBuffer actual = decoder.decode(src.position(BCTestUtils.DDS_HEADER_SIZE), 256, 256); ByteBuffer expected = BCTestUtils.readDDSFP16("/bc6h_sf16_16.dds", 256, 256); - assertThat(actual).isEqualTo(expected); + BCTestUtils.assertBufferEquals(actual, expected); } @Test @@ -39,7 +39,7 @@ void testBC6HInvalidBlock() { src.put(0, invalidMode); BlockDecoder decoder = BlockDecoder.bc6h(false); ByteBuffer actual = decoder.decode(src, 4, 4); - ByteBuffer expected = ByteBuffer.allocate(16 * 2 * 3); + ByteBuffer expected = ByteBuffer.allocate(16 * 2 * 4); assertThat(actual).isEqualTo(expected); } } diff --git a/src/test/java/be/twofold/tinybcdec/BC7Benchmark.java b/src/test/java/be/twofold/tinybcdec/BC7Benchmark.java index cd176ff..6d1f5c2 100644 --- a/src/test/java/be/twofold/tinybcdec/BC7Benchmark.java +++ b/src/test/java/be/twofold/tinybcdec/BC7Benchmark.java @@ -18,7 +18,7 @@ public BC7State() { try { decoder = BlockDecoder.bc7(); src = BCTestUtils.readResource("/bc7.dds"); - dst = ByteBuffer.allocate(256 * 256 * 4); + dst = ByteBuffer.allocate(256 * 256 * BC7.BPP); } catch (IOException e) { throw new UncheckedIOException(e); } diff --git a/src/test/java/be/twofold/tinybcdec/BC7Test.java b/src/test/java/be/twofold/tinybcdec/BC7Test.java index 4b272f7..e65d1ce 100644 --- a/src/test/java/be/twofold/tinybcdec/BC7Test.java +++ b/src/test/java/be/twofold/tinybcdec/BC7Test.java @@ -5,8 +5,6 @@ import java.io.*; import java.nio.*; -import static org.assertj.core.api.Assertions.*; - class BC7Test { private final BlockDecoder decoder = BlockDecoder.bc7(); @@ -18,14 +16,15 @@ void testBC7() throws IOException { ByteBuffer actual = decoder.decode(src.position(BCTestUtils.DDS_HEADER_SIZE), 256, 256); ByteBuffer expected = BCTestUtils.readPng("/bc7.png"); - assertThat(actual).isEqualTo(expected); + BCTestUtils.assertBufferEquals(actual, expected); } @Test void testBC7InvalidBlock() { ByteBuffer src = ByteBuffer.allocate(16); ByteBuffer actual = decoder.decode(src, 4, 4); - assertThat(actual).isEqualTo(ByteBuffer.allocate(16 * 4)); + + BCTestUtils.assertBufferEquals(actual, ByteBuffer.allocate(16 * 4)); } } diff --git a/src/test/java/be/twofold/tinybcdec/BCTestUtils.java b/src/test/java/be/twofold/tinybcdec/BCTestUtils.java index 1f8e43c..9228668 100644 --- a/src/test/java/be/twofold/tinybcdec/BCTestUtils.java +++ b/src/test/java/be/twofold/tinybcdec/BCTestUtils.java @@ -1,6 +1,7 @@ package be.twofold.tinybcdec; import javax.imageio.*; +import java.awt.*; import java.awt.image.*; import java.io.*; import java.nio.*; @@ -17,56 +18,54 @@ public static ByteBuffer readResource(String path) throws IOException { } } + /** + * This conversion is actually just a byte level re-arrange and gray expansion. + */ static ByteBuffer readPng(String path) throws IOException { try (InputStream in = BCTestUtils.class.getResourceAsStream(path)) { BufferedImage image = ImageIO.read(in); - byte[] rawImage = ((DataBufferByte) image.getRaster().getDataBuffer()).getData(); + int width = image.getWidth(); + int height = image.getHeight(); - return ByteBuffer.wrap(decodeImage(rawImage, image.getType())); + BufferedImage argb = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + Graphics2D graphics = argb.createGraphics(); + graphics.setComposite(AlphaComposite.Src); // Needed for proper alpha copy + graphics.drawImage(image, 0, 0, null); + graphics.dispose(); + + int[] pixels = ((DataBufferInt) argb.getRaster().getDataBuffer()).getData(); + ByteBuffer result = ByteBuffer.allocate(pixels.length * 4).order(ByteOrder.LITTLE_ENDIAN); + result.asIntBuffer().put(pixels); + return result.order(ByteOrder.BIG_ENDIAN); } } static ByteBuffer readDDSFP16(String path, int width, int height) throws IOException { - ByteBuffer rawImage = readResource(path) + return readResource(path) .position(DDS_HEADER_SIZE) .limit(DDS_HEADER_SIZE + width * height * 8) .slice(); - ByteBuffer result = ByteBuffer.allocate(rawImage.remaining() * 6 / 8); - - for (int i = 0, o = 0; i < rawImage.remaining(); i += 8, o += 6) { - for (int j = 0; j < 6; j++) { - result.put(o + j, rawImage.get(i + j)); - } - } - return result; } - private static byte[] decodeImage(byte[] image, int type) { - switch (type) { - case BufferedImage.TYPE_3BYTE_BGR: { - for (int i = 0; i < image.length; i += 3) { - swap(image, i/**/, i + 2); - } - return image; - } - case BufferedImage.TYPE_4BYTE_ABGR: { - for (int i = 0; i < image.length; i += 4) { - swap(image, i/**/, i + 3); - swap(image, i + 1, i + 2); - } - return image; - } - case BufferedImage.TYPE_BYTE_GRAY: { - return image; + /** + * Compares two buffers byte for byte, starting at their current positions. + * Neither position is modified. + */ + static void assertBufferEquals(ByteBuffer actual, ByteBuffer expected) { + int actualBase = actual.position(); + int expectedBase = expected.position(); + int length = expected.remaining(); + + if (actual.remaining() != length) { + throw new AssertionError("Expected " + length + " bytes, but was " + actual.remaining()); + } + for (int i = 0; i < length; i++) { + byte a = actual.get(actualBase + i); + byte e = expected.get(expectedBase + i); + if (a != e) { + throw new AssertionError(String.format( + "Buffers differ at index %d: expected 0x%02X but was 0x%02X", i, e, a)); } - default: - throw new UnsupportedOperationException(type + " not supported"); } } - - private static void swap(byte[] array, int i, int j) { - byte tmp = array[i]; - array[i] = array[j]; - array[j] = tmp; - } } diff --git a/src/test/java/be/twofold/tinybcdec/BlockDecoderTest.java b/src/test/java/be/twofold/tinybcdec/BlockDecoderTest.java index c3fdf3b..8ea9b4d 100644 --- a/src/test/java/be/twofold/tinybcdec/BlockDecoderTest.java +++ b/src/test/java/be/twofold/tinybcdec/BlockDecoderTest.java @@ -31,7 +31,7 @@ void testPartialBlockCrop() throws IOException { int srcHeight = 119; int dstOffset = 31; - ByteBuffer dst = ByteBuffer.allocate(8 * 8 + dstOffset); + ByteBuffer dst = ByteBuffer.allocate((8 * 8 + dstOffset) * 4); BlockDecoder decoder = BlockDecoder.bc4(false); for (int h = 1; h <= 8; h++) { for (int w = 1; w <= 8; w++) { @@ -39,7 +39,11 @@ void testPartialBlockCrop() throws IOException { for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { - assertThat(dst.get(y * w + x + dstOffset)).isEqualTo(expected.get(y * srcWidth + x)); + int dstByte = dstOffset + (y * w + x) * 4; + int srcByte = (y * srcWidth + x) * 4; + for (int c = 0; c < 4; c++) { + assertThat(dst.get(dstByte + c)).isEqualTo(expected.get(srcByte + c)); + } } } } @@ -55,7 +59,7 @@ void testPartialBlockCropExtra() throws IOException { int srcHeight = 119; int dstOffset = 31; - ByteBuffer dst = ByteBuffer.allocate(8 * 8 + dstOffset); + ByteBuffer dst = ByteBuffer.allocate((8 * 8 + dstOffset) * 4); BlockDecoder decoder = BlockDecoder.bc4(false); // Test all offsets between 0 and 8 @@ -71,7 +75,11 @@ void testPartialBlockCropExtra() throws IOException { for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { - assertThat(dst.get(y * w + x + dstOffset)).isEqualTo(expected.get((srcY + y) * srcWidth + (srcX + x))); + int dstByte = dstOffset + (y * w + x) * 4; + int srcByte = ((srcY + y) * srcWidth + (srcX + x)) * 4; + for (int c = 0; c < 4; c++) { + assertThat(dst.get(dstByte + c)).isEqualTo(expected.get(srcByte + c)); + } } } }