Android development: Loading ETC1 textures from NDK

This post is probably irrelevant to anyone but Android NDK game developers, nevertheless I wanted to share my findings to help out those with the same problem. (Yes, this is not the Jewels post-mortem I promised, it is still coming :))

As you may know, ETC1 is the common texture compression technique on all Android devices supporting OpenGL ES 2.0. Android SDK comes with this aptly named tool etc1tool, that allows compressing PNG images to the ETC1 format. Android SDK has couple of APIs for loading these textures from the Java side (ETC1Util), but what about the NDK (i.e. the native side, with C or C++)? I was adding ETC1 texture support to my project and wanted to load them from native code.

The tool packages the compressed ETC1 data into a container format called PKM. The problem is that the specs of that format are nowhere to be found! At least, I did not find them despite spending quite a bit of time searching. The docs state that "The PKM file format is of a 16-byte header that describes the image bounds followed by the encoded ETC1 texture data", which is the best I could find.

My first guess was that there would be a tag/id, two 32-bit integers for the bounds and some padding in those 16 bytes. That was close, but not quite. I had to actually install a hex editor (haven't needed one in years) and take a look at the file to see what actually was going on!

Indeed the identifier "PKM 10" was there, but I was puzzled about the bounds. Only after I read somewhere that the image dimensions for ETC1 must be multiplies of 4, it started to make sense. The bounds were not in two 32-bit numbers but in four 16-bit shorts. The bounds appeared twice: first rounded up to the nearest multiple of 4, and then in the original dimensions.

UPDATE: There is actually a format specifier after the "PKM 10" string (not padding), which specifies the number of mip maps (always zero). It is important to note that all the values are saved in big endian encoding.

So here is the format I used, in pseudoish-code:

struct ETC1Header {
    char tag[6];        // "PKM 10"
    U16 format;         // Format == number of mips (== zero)
    U16 texWidth;       // Texture dimensions, multiple of 4 (big-endian)
    U16 texHeight;
    U16 origWidth;      // Original dimensions (big-endian)
    U16 origHeight;

That totals to 16 bytes as stated in the docs. Hopefully this helps loading those textures from native code! :)

Disclaimer: all this is based on my findings and the code does work for the textures I currently use, but I take no responsibility if the specs are wrong or bound to change.