Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 46 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.
Expand All @@ -62,22 +65,22 @@ 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
is left of the destination, which makes this the form to use when the destination is already the size of the region you
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.
Expand All @@ -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

Expand Down
17 changes: 8 additions & 9 deletions src/main/java/be/twofold/tinybcdec/BC1.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand All @@ -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) {
Expand All @@ -90,5 +90,4 @@ private static int scale062(int i) {
private static int scale126(int i) {
return (i * 4145 + 1019) >>> 11;
}

}
4 changes: 2 additions & 2 deletions src/main/java/be/twofold/tinybcdec/BC2.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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;
}
Expand Down
19 changes: 16 additions & 3 deletions src/main/java/be/twofold/tinybcdec/BC3.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
}
}
}
32 changes: 20 additions & 12 deletions src/main/java/be/twofold/tinybcdec/BC4S.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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) {
Expand Down
30 changes: 18 additions & 12 deletions src/main/java/be/twofold/tinybcdec/BC4U.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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) {
Expand Down
22 changes: 18 additions & 4 deletions src/main/java/be/twofold/tinybcdec/BC5S.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,31 @@
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);
}

@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;
}
}
}
Loading