Texture compression on mobile demystified

If you’re developing a game for mobile devices, chances are you have run into the words “texture compression”. Texture compression helps to keep video memory usage down, reduce download sizes and loading times, and may even reduce power consumption. In this article, I take a comprehensive look at what the options are.

This post was prompted by a discussion on Reddit whether you should use pngquant to compress your game’s PNG assets. Because I’ve been able to get away with PNG so far, I have never really looked into texture compression. Now seemed as good a time as any to learn something new that I might be able to apply in the future. Note that this is all sourced from various websites, not from first-hand experience.

Everything in this article is up to date as of February 2017, but is bound to change as new formats are invented and support for existing formats is extended or dropped. If you have any updates or requests for updates, please post them in the comments, and I will do my best to incorporate them.

Technicalities

All the formats I’m talking about are lossy compression formats: compression is achieved by sacrificing some image quality. We have to distinguish between compression on disk versus compression in video memory (VRAM), where it is directly accessed by the graphics processing unit (GPU).

Unlike JPEG, PNG or GIF, all GPU texture compression formats work with a fixed bitrate (number of bits per pixel). This means you have little to no control over image quality. It’s a tradeoff to allow the GPU random access to texels; with a variable bitrate, it would be much more difficult for the hardware to know where in memory a particular texel resides.

Different GPUs support different texture compression formats. Textures compressed in one format cannot be directly uploaded to a GPU that does not support that format. All GPUs understand uncompressed textures (for example 24-bit RGB, 32-bit RGBA, or 16-bit RGB565), but these take up more VRAM than necessary.

On disk, you have more liberty with which formats you use, as long as you can somehow convert them into something that the GPU understands. PNG or even JPEG are both fine choices depending on circumstance, but they have one major drawback: no GPU understands them. So at load time, you either convert these into (big) uncompressed textures, or you recompress them using a GPU-specific format. Neither is a great option, so ideally, you should store the textures on disk in a format that the GPU supports.

But with texture formats and GPU support being all over the map, which format do you choose for on-disk storage? The Google Play Store is helpful here: it lets you specify different app packages for different target platforms, so devices will only download textures in a compression format they natively support. Apple takes a different approach: you upload a single IPA file that contains all assets, but since iOS 9 you can use app thinning (slicing in particular) to choose which textures are delivered to which devices.

That only leaves the trivial matter of selecting the particular compression format for your game. Or formats – there’s no reason you couldn’t mix and match, playing to each format’s strengths.

GPU compression formats

The table below summarises the most common texture compression formats that are directly supported by GPUs. It was surprisingly hard to scrape all this data together, and I hope I got it right. As said, any corrections are welcome.

FormatCompression ratioAlpha channelDevice support
ETC16x for RGBnoneAndroid 2.2 and up, on devices supporting OpenGL ES 2.0 (source); iOS 7 and up on devices supporting OpenGL ES 3.0: iPhone 5s or later, iPad 5th generation or later, iPad Pro, iPad Air, iPad Mini 2 or later, and iPod Touch 6th generation or later (source, source, source)
ETC26x for RGB, 4x for RGBA8 bitsOpenGL ES 3.0: Android 4.3, on devices with appropriate hardware (source, source); iOS 7 and up on devices supporting OpenGL ES 3.0: iPhone 5s or later, iPad 5th generation or later, iPad Pro, iPad Air, iPad Mini 2 or later, and iPod Touch 6th generation or later (source, source, source)
PVRTC8x or 16x?Android devices running PowerVR Series 5 and up, such as Motorola DROID series, Samsung Galaxy S, Nexus S, Samsung Galaxy Tab, and others (source); All iPhone, iPad and iPod devices (source)
PVRTC28x or 16x?Android devices running PowerVR Series 5X and up (source); Not supported by iOS (though supported by recent hardware) (source)
S3TC a.k.a. DXT or DXTC6x for RGB, 4x for RGBA8 bitsAndroid devices running Nvidia Tegra2 platform, including Motorala Xoom, Motorola Atrix, Droid Bionic, and others (source); Not supported by iOS (source)
ATC a.k.a. ATITC6x for RGB, 4x for RGBA8 bitsAndroid devices with Adreno GPU, including HTC Nexus One, Droid Incredible, EVO, and others (source); Not supported by iOS
ASTC3x–27x (configurable)8 bitsAndroid 5.0, on devices with appropriate hardware (source); iOS 8 or up on devices with an A8 processor or better: iPhone 6 and later, iPad 5th generation, iPad Air 2, iPad mini 4, iPad Pro, iPod Touch 6th generation and later (source, source)

I didn’t include 3Dc because it’s specifically optimised for normal maps.

What to use?

Most recommendations around the web point towards ETC1, as it’s nearly universally supported. The biggest drawback is that ETC1 does not support an alpha channel, so you will have to supply that in a second texture, then do some magic in the fragment shader to piece the two together again.

However, there is a reason for the existence of all these other formats: ETC1 does not preserve image quality particularly well. If this is a problem for you, move to ETC2 if you can afford to only support newer devices.

If you also need to support older devices, it gets trickier. I couldn’t find any statistics on how well particular formats are supported on devices “in the wild”, but from the table, it looks like you’d want to use at least PVRTC for iOS and some Android devices, and a combination of ETC2, S3TC and maybe ATC to cover most of the Android spectrum.

Another option might be to use PNG inside your app package to get the smallest possible download size, then convert this to an appropriate format for the GPU when the app is first run, caching the converted textures on the device. This could work well for PNG, but for JPEG and other lossy input formats you need to keep in mind that the texture will be compressed lossily twice.

Bottom line: there is no one-size-fits-all solution, but I hope this article has given you the data you need to make an informed decision.

References