A collection of optimized WebAssembly transcoders for Basis Universal compressed GPU texture formats.
These transcoders accept only low-level compressed payloads. Containers such as .basis
or .ktx2
(KTX) should first be parsed by other means, then transcoded to the target format with this library.
Target Format | BasisLZ / ETC1S | UASTC |
---|---|---|
RGBA8 | planned | ✔️ |
R8 / RG8 | planned | ✔️ |
ASTC 4x4 | planned | ✔️ |
BC7 | planned | ✔️ |
ETC | planned | planned |
BC1 / BC3 | planned | planned |
PVRTC | planned | planned |
-
Ensure that Node.js runtime is installed.
-
After cloning this repository, run
$ npm install
-
After all the dependencies are fetched, run
$ npm run asbuild
-
Built transcoders will be available in the
build/
directory.
All UASTC transcoders accept only raw UASTC blocks. Zstandard or zlib/deflate compression (if present) must be decoded in advance.
Like UASTC, both these GPU formats use 16 bytes per 4x4 block. The transcoders to ASTC and BC7 share the same API and overwrite UASTC data with ASTC or BC7 in place.
-
The transcoders operate on 4x4 blocks rather than on individual pixels so applications are free to schedule workloads as needed. For example, several small textures could be concatenated and transcoded at once or a large texture could be split into chunks. For a single texture, the number of blocks should be calculated as:
const nBlocks = ((width + 3) >> 2) * ((height + 3) >> 2);
-
Create a
WebAssembly.Memory
object large enough to contain the proper amount of texture blocks. Its size is given in pages, each page is 65536 bytes. The zeroth page is reserved for the transcoder's internal use, so the total amount of pages should be computed as:const texMemoryPages = (nBlocks * 16 + 65535) >> 16; const memory = new WebAssembly.Memory({ initial: texMemoryPages + 1 });
-
Create a view into the memory region that will be used for transferring texture data. This step must be repeated after calling
memory.grow
, such as when allocating space for another, larger, texture.let textureView = new Uint8Array(memory.buffer, 65536, nBlocks * 16);
-
The memory could be populated with the UASTC data even before the transcoder is ready.
textureView.set(compressedData /* Uint8Array */);
-
Fetch and instantiate the transcoder with the created memory. Note, that the example code uses Fetch and WebAssembly Web APIs. Other JavaScript environments (such as Node.js) would need slightly different steps.
const transcoder = ( await WebAssembly.instantiateStreaming( fetch('uastc_{bc7,astc}.wasm'), { env: { memory } } ) ).instance.exports;
-
For each new texture, call the exported
transcode
function with the number of compressed blocks to transcode. If the number of blocks is negative or exceeds the available memory, the function returns1
. Otherwise, it performs the transcoding and returns0
. The transcoded texture data will be available through the same memory view.textureView.set(compressedData); if (transcoder.transcode(nBlocks) === 0) { // Upload textureView data to the GPU } else { // Wrong nBlocks value }
-
In a case when a new texture does not fit into the existing memory, the latter could be expanded by calling
memory.grow
. Note that the memory view would need to be recreated afterwards, by repeating step (3).
Each 16-byte UASTC block is uncompressed to 64 bytes of 32bpp data, so the decoder writes decompressed data in another memory region, leaving the original UASTC data intact.
Since UASTC is a strict subset of ASTC and the latter has several decode modes, an application should know which ASTC decode mode to use before choosing the appropriate decoder module. Decode mode specifications could be found in the KDFS 1.3, Section 23.19.
Currently supported modes are:
-
uastc_rgba8_unorm.wasm
matches thedecode_unorm8
ASTC decode mode.OpenGL Note: Sampling UASTC data decoded with this mode and uploaded as
GL_RGBA8
should exactly match sampling UASTC data transcoded to ASTC and uploaded asGL_COMPRESSED_RGBA_ASTC_4x4_KHR
with theGL_TEXTURE_ASTC_DECODE_PRECISION_EXT
texture parameter set toGL_RGBA8
. -
uastc_rgba8_srgb.wasm
matches the sRGB ASTC decode mode.OpenGL Note: Sampling UASTC data decoded with this mode and uploaded as
GL_SRGB_ALPHA8
should exactly match sampling UASTC data transcoded to ASTC and uploaded asGL_COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR
.
Regardless of the decode mode, these decoders share the same API and memory requirements.
-
For a single texture, the amount of the required memory should be calculated as:
const xBlocks = (width + 3) >> 2; const yBlocks = (height + 3) >> 2; const compressedByteLength = xBlocks * yBlocks * 16; // Uncompressed texture padded to multiple-of-4 height const uncompressedByteLength = width * yBlocks * 4 * 4; const totalByteLength = compressedDataLength + uncompressedDataLength;
-
Create a
WebAssembly.Memory
object large enough to contain both the compressed and the uncompressed data. Its size is given in pages, each page is 65536 bytes. The zeroth page is reserved for the decoder's internal use, so the total amount of pages should be computed as:const texMemoryPages = (totalByteLength + 65535) >> 16; const memory = new WebAssembly.Memory({ initial: texMemoryPages + 1 });
-
Create a view into the memory region that will be used for transferring compressed texture data. This step must be repeated after calling
memory.grow
, such as when allocating space for another, larger, texture.let compressedTextureView = new Uint8Array(memory.buffer, 65536, compressedByteLength);
-
The memory could be populated with the UASTC data even before the decoder is ready.
compressedTextureView.set(compressedData /* Uint8Array */);
-
Create a view into the memory region that will be used for transferring decoded texture data. This step must be repeated after calling
memory.grow
, such as when allocating space for another, larger, texture.const textureByteLength = width * height * 4; let decodedTextureView = new Uint8Array(memory.buffer, 65536 + compressedByteLength, textureByteLength);
-
Fetch and instantiate the decoder with the created memory. Note, that the example code uses Fetch and WebAssembly Web APIs. Other JavaScript environments (such as Node.js) would need slightly different steps.
const decoder = ( await WebAssembly.instantiateStreaming( fetch('uastc_rgba8_{srgb,unorm}.wasm'), { env: { memory } } ) ).instance.exports;
-
For each new texture, call the exported
decode
function passing the texture dimensions. If they are negative, greater than16384
, or exceed the available memory, the function returns1
. Otherwise, it performs the decoding and returns0
. The transcoded texture data will be available through thedecodedTextureView
memory view.compressedTextureView.set(compressedData); if (decoder.decode(width, height) === 0) { // Upload decodedTextureView data to the GPU } else { // Wrong dimensions }
-
In a case when a new texture does not fit into the existing memory, the latter could be expanded by calling
memory.grow
. Note that the memory views would need to be recreated afterwards, by repeating step (3) and repeating step (5) in this case.
When it is known that only Red or Red and Green channels are used, e.g., the texture contains non-color data, and compressed targets are not supported, an application may decode it to R8 or RG8 formats to avoid wasting CPU cycles and GPU memory on the unused channels.
Each 16-byte UASTC block is uncompressed to 16 bytes of 8bpp or to 32 bytes of 16bpp data.
These target formats support only decode_unorm8
ASTC decode mode.
OpenGL Note: Sampling Red or Red-Green UASTC data decoded with this mode and uploaded as
GL_R8
orGL_RG8
should exactly match sampling Red or Red-Green UASTC data transcoded to ASTC and uploaded asGL_COMPRESSED_RGBA_ASTC_4x4_KHR
with theGL_TEXTURE_ASTC_DECODE_PRECISION_EXT
texture parameter set toGL_RGBA8
.
The two provided decoders, uastc_r8_unorm.wasm
and uastc_rg8_unorm.wasm
, share the API with the RGBA8 decoders but they have different memory requirements for the steps 1 and 5 above.
-
R8
uncompressedByteLength = width * yBlocks * 4 * 1
textureByteLength = width * height * 1
-
RG8
uncompressedByteLength = width * yBlocks * 4 * 2
textureByteLength = width * height * 2