Using Basis Universal Texture Compression with Metal

In this short article, we’ll take a look at a relatively new compressed texture format called Basis. Basis is developed by Binomial, LLC, a company founded by Rich Geldreich (of crunch fame) and Stephanie Hurlburt.

Basis is unique among compression formats in that it emphasizes efficient transcoding between compressed formats. This means that a single .basis file can be transformed, at runtime, into a format that’s optimal for the target platform, without decompressing it in memory, saving space and bandwidth.

Although Basis is a commercial product, targeted predominantly at game developers, Binomial has contributed their Basis Universal reference encoder and transcoder to the Khronos Group in support of the glTF model format, under the Apache License 2.0.

As of this writing, Basis Universal supports transcoding to PVRTC1 4bpp RGB, BC7 mode 6 RGB, BC1-5, ETC1, and ETC2 formats. If you’re familiar with Metal’s compressed texture formats, some of these will look familiar. In particular, all iOS devices that support Metal also support PVRTC1 and ETC2. All Macs that support Metal also support BC pixel formats.

Using Basis-Compressed Textures in Metal

Now that we have some Basis Universal files, let’s talk about loading them and using them with Metal.

The Basis Transcoder API

The API of the Basis Universal transcoder is fairly straightforward. Currently, only a C++ API is available, which means it doesn’t play nicely with Swift. In order to work around this, I wrote a utility class (MBEBasisTextureLoader) in Objective-C++ that can be used from Swift.

Because the API is still under active development at the time of this writing, I won’t talk about the parts that still seem to be evolving, just the core functions.

To start loading a Basis file, you create a basisu_transcoder object. All transcoder-related types live in the basist namespace, so that looks like this:

auto transcoder = new basist::basisu_transcoder(sel_codebook);

where the sel_codebook is an implementation detail that’s likely to go away in the future. Consult the Basis Universal documentation for the gory details.

To get ready to transcode the texture into your desired runtime format, load the texture file into memory, then call start_transcoding:

bool success = transcoder->start_transcoding(data, dataSizeInBytes);

At this point, you can start using the API to learn about the contents of the file. The get_texture_type function tells you whether the file contains a 2D texture, a 3D texture, a cubemap, etc.:

The get_total_images function allows you to determine how many images are in the file. In the case of a cubemap texture, this will be a multiple of 6, and in the case of a texture array, it will be the number of array slices:

The image info struct contains various bits of information, such as the width and height of the image and the number of mipmap levels. With this information, we can create a MTLTexture of the appropriate type. After that, it’s a matter of transcoding the texture data on a level-by-level basis.

For each level of the image, we can query its properties with the get_image_level_info function:

At long last, we’re ready to get the data for the level and request that it be transcoded into our desired pixel format. To do that, we allocate a buffer large enough to hold the level (levelData), then call the transcode_image_level function to get the transcoded texture data:

We repeat this for every level for every slice in the texture, after which we’re done loading.

Loading Compressed Textures with Metal

The MBEBasisTextureLoader in the sample code provides a somewhat simpler API for doing this work. It’s closely modeled after the MTKTextureLoader class from MetalKit and has the appropriate Swift annotations to make it work well with Swift.

For example, here’s how we might load a Basis texture from our app bundle, requesting that it be transcoded into the BC7 Mode 6 format for use on macOS:

Note that we’re using an asynchronous loading method here. The loader class also provides synchronous methods. The loader uses concurrent NSOperations to load textures on a background thread when requested.

Results and Conclusion

In this article, we’ve looked at the Basis Universal compressed texture format and how to use it with Metal. With the open-sourced encoder and transcoder, Binomial has made efficient transcoding to various target platforms simple. And although the commercial suite of Basis products will be most useful to game studios, the Basis Universal offering is worth considering for your cross-platform texture compression needs.