Table of contents
- Table of Contents
- Intro
- Useful External References
- Khronos KTX-Software Codec Support Status
- KTX2 Validation, DFD's, and vk_format values
- Basis Universal Formats
- 1. ETC1S
- 2. UASTC LDR 4x4
- 3. UASTC HDR 4x4
- 4. ASTC HDR 6x6
- 5. UASTC HDR 6x6i (intermediate)
- 6. ASTC LDR 4x4-12x12
- 7. XUASTC LDR 4x4-12x12
- 8. XUBC7
- DFD - Linear vs. sRGB Determination
- DFD - REC 2020/BT2100 Color Gamut Determination
- DFD - Alpha Channel Determination
- Key Value Fields
- Using the basisu command line tool to dump basic KTX2 header info
- Using the KTX command line tool to validate KTX2 files
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This document is up to date as of Basis Universal v2.5. There is also the previous v2.0 version.
Table of Contents
- Intro
- Useful External References
- Khronos KTX-Software Codec Support Status
- KTX2 Validation, DFDs, and
vk_formatvalues - Basis Universal Formats
- DFD - Linear vs. sRGB Determination
- DFD - REC 2020/BT2100 Color Gamut Determination
- DFD - Alpha Channel Determination
- Key Value Fields
- Using the basisu command line tool to dump basic KTX2 header info
- Using the KTX command line tool to validate KTX2 files
Intro
KTX™ is a trademark of The Khronos Group Inc.
The Basis Universal transcoder only supports reading compressed texture data from .KTX2 container files in the below specific formats. Our transcoder is not yet a general purpose .KTX2 reader. However, for standard ASTC LDR 4x4-12x12 (all 14 standard ASTC block sizes, UNORM or sRGB variants), and standard ASTC HDR 6x6, the transcoder fully supports ASTC LDR/HDR blocks: i.e. blocks created by our encoder or external encoders such as ARM's astcenc, Intel's ispc_texcomp, etc.
Useful External References
- KTX-Software-Binomial-Fork
- Khronos Data Format Specification v1.4.0
- Khronos KTX2 File Format Specification
- dfdutils
- Vulkan Buffer and Image Formats
- The raw Khronos ASTC spec document (astc.txt). It's recommend that this spec is used by developers working on low-level ASTC decoders/encoders, due to known ASTC bugs and errata. See more technical info here.
- ASTC Texture Format Specification v1.1 or ASTC Texture Format Specification v1.4
Khronos KTX-Software Codec Support Status
KTX-Software supports the following supercompressed codecs:
| BasisU Codec Name | KTX-Software Name |
|---|---|
| ETC1S | "basis-lz" |
| UASTC LDR 4x4 | "uastc" or "uastc-ldr-4x4" |
| UASTC HDR 4x4 | "uastc-hdr-4x4" |
| UASTC HDR 6x6i | "uastc-hdr-6x6i" |
Our non-supercompressed ASTC formats, such as ASTC LDR 4x4-12x12 and ASTC HDR 6x6, are also supported for validation (using the info command) or extraction by ktx (using the extract command), although as of 2/26/2026 extracting plain ASTC HDR 6x6 isn't supported by ktx yet (it sounds like this feature is coming soon).
See the ktx tool's create command help text:
>ktx create --help
...
--encode <codec> Encode the created KTX file. Case insensitive.
Possible options are: basis-lz | uastc | uastc-ldr-4x4 |
uastc-hdr-4x4 | uastc-hdr-6x6i
The ktx tool's info command can validate our generated .KTX2 files in all formats except for XUASTC LDR (which is expected), and basisu can introspect, transcode and unpack .KTX2 files generated by ktx (in the codec formats we support).
Our XUASTC LDR supercompressed format is not yet standardized yet, as of late Feb. 2026.
Example ktx command lines for UASTC HDR 4x4 and UASTC HDR 6x6i:
ktx create --format R16G16B16_SFLOAT --generate-mipmap --zstd 2 --encode uastc-hdr-4x4 desk.exr 1.ktx2
ktx create --format R16G16B16_SFLOAT --generate-mipmap --encode uastc-hdr-6x6i desk.exr 2.ktx2
For LDR sRGB images using the ktx tool, be sure to use --format R8G8B8_SRGB, not the UNORM variants which can cause a destructive sRGB->linear conversion before compression (which in 8-bit loses an enormous amount of quality, essentially making the results unusable in practice).
Note if you want to build KTX-Software under Windows with Visual Studio/MSVC using cmake to create the .SLN, we had to pass in -D KTX_FEATURE_TOOLS=ON to cmake to get the command line tools (like ktx) to build. (Otherwise the command line tools won't be in the generated solution.)
The extract command (or extract --all) can be used to unpack a KTX2 file to individual image files.
KTX2 Validation, DFD's, and vk_format values
This document assumes familiarity with the KTX2 container format, Khronos Data Format Descriptors (DFDs), and GPU block-compressed texture formats. The Khronos KTX-Software ktx (or btx in our fork with fixes) "info" command can be used to validate and display detailed file information about our written .KTX2 files. Alternatively, basisu -validate file.ktx2 can be used too.
To see exactly how the compressor creates the output KTX2 file's DFD, see encoder/basisu_comp.cpp, method basis_compressor::get_dfd().
In the KTX2 header, the vk_format field must be KTX2_UNDEFINED (0), or one of these values:
// vk_format's basisu v2.0/v2.1 supports:
// ASTC HDR 4x4 (also see the DFD, or Data Format Descriptor, as our usage is constrained for fast transcoding to BC6H) - this is used for UASTC HDR 4x4
uint32_t KTX2_FORMAT_ASTC_4x4_SFLOAT_BLOCK = 1000066000;
// ASTC HDR 6x6
uint32_t KTX2_FORMAT_ASTC_6x6_SFLOAT_BLOCK = 1000066004;
// ASTC LDR 4x4-12x12 (UNORM and sRGB variants)
uint32_t KTX2_FORMAT_ASTC_4x4_UNORM_BLOCK = 157, KTX2_FORMAT_ASTC_4x4_SRGB_BLOCK = 158;
uint32_t KTX2_FORMAT_ASTC_5x4_UNORM_BLOCK = 159, KTX2_FORMAT_ASTC_5x4_SRGB_BLOCK = 160;
uint32_t KTX2_FORMAT_ASTC_5x5_UNORM_BLOCK = 161, KTX2_FORMAT_ASTC_5x5_SRGB_BLOCK = 162;
uint32_t KTX2_FORMAT_ASTC_6x5_UNORM_BLOCK = 163, KTX2_FORMAT_ASTC_6x5_SRGB_BLOCK = 164;
uint32_t KTX2_FORMAT_ASTC_6x6_UNORM_BLOCK = 165, KTX2_FORMAT_ASTC_6x6_SRGB_BLOCK = 166;
uint32_t KTX2_FORMAT_ASTC_8x5_UNORM_BLOCK = 167, KTX2_FORMAT_ASTC_8x5_SRGB_BLOCK = 168;
uint32_t KTX2_FORMAT_ASTC_8x6_UNORM_BLOCK = 169, KTX2_FORMAT_ASTC_8x6_SRGB_BLOCK = 170;
uint32_t KTX2_FORMAT_ASTC_10x5_UNORM_BLOCK = 173, KTX2_FORMAT_ASTC_10x5_SRGB_BLOCK = 174;
uint32_t KTX2_FORMAT_ASTC_10x6_UNORM_BLOCK = 175, KTX2_FORMAT_ASTC_10x6_SRGB_BLOCK = 176;
uint32_t KTX2_FORMAT_ASTC_8x8_UNORM_BLOCK = 171, KTX2_FORMAT_ASTC_8x8_SRGB_BLOCK = 172; // note the ASTC block size enum order differences between basisu and vkFormat
uint32_t KTX2_FORMAT_ASTC_10x8_UNORM_BLOCK = 177, KTX2_FORMAT_ASTC_10x8_SRGB_BLOCK = 178;
uint32_t KTX2_FORMAT_ASTC_10x10_UNORM_BLOCK = 179, KTX2_FORMAT_ASTC_10x10_SRGB_BLOCK = 180;
uint32_t KTX2_FORMAT_ASTC_12x10_UNORM_BLOCK = 181, KTX2_FORMAT_ASTC_12x10_SRGB_BLOCK = 182;
uint32_t KTX2_FORMAT_ASTC_12x12_UNORM_BLOCK = 183, KTX2_FORMAT_ASTC_12x12_SRGB_BLOCK = 184;
If vk_format is not KTX2_UNDEFINED, the transcoder expects the block data to be bit-compatible with the corresponding Vulkan/Khronos texture format, and the DFD must not contradict it. (Also, as a side note: in practice we've seen at least one major KTX2 reader ignore the DFD entirely, and only pay attention to the vk_format.)
The supercompression field must be:
uint32_t KTX2_SS_NONE = 0; // data is raw
uint32_t KTX2_SS_BASISLZ = 1; // ETC1S, file must have global supercompressed data (sgd_byte_offset/sgd_byte_length must be valid)
uint32_t KTX2_SS_ZSTANDARD = 2; // data is Zstd compressed
uint32_t KTX2_SS_UASTC_HDR_6x6I = 4, // UASTC HDR 6x6i (assigned by Khronos in early 2026)
uint32_t KTX2_SS_XUASTC_LDR = 5, // XUASTC LDR 4x4-12x12 (assignment in progress with Khronos as of early 2026)
uint32_t KTX2_SS_XUBC7 = 6 // XUBC7
Basis Universal doesn't currently support supercompression ID 3 (Deflate).
The transcoder supports 2D or cubemap textures, with or without mipmaps. Array textures of any type (2D/cubemap/texture video, with or without mipmaps) are supported. 3D textures are not supported (i.e. the header's pixel_depth must be 0). Cubemaps must have square dimensions.
Basis Universal Formats
The table below summarizes the KTX2 container settings for each of the 8 formats Basis Universal reads and writes. All fields are little-endian. Each format is described in detail in the sections that follow.
| # | Format | vk_format |
DFD color model | Block dims | Supercompression scheme | Alpha | Supercompression global data |
|---|---|---|---|---|---|---|---|
| 1 | ETC1S | UNDEFINED (0) |
ETC1S — 163 (0xA3) | 4x4 | BasisLZ (1) | optional | yes — compressed ETC1S codebooks |
| 2 | UASTC LDR 4x4 | UNDEFINED (0) |
UASTC_LDR_4X4 — 166 (0xA6) | 4x4 | Zstd (2) or None (0) | optional | none |
| 3 | UASTC HDR 4x4 | ASTC_4x4_SFLOAT (1000066000) |
UASTC_HDR_4X4 — 167 (0xA7) | 4x4 | Zstd (2) or None (0) | no | none |
| 4 | ASTC HDR 6x6 | ASTC_6x6_SFLOAT (1000066004) |
ASTC — 162 (0xA2) | 6x6 | Zstd (2) or None (0) | no | none |
| 5 | UASTC HDR 6x6i | UNDEFINED (0) |
UASTC_HDR_6X6_INTERMEDIATE — 168 (0xA8) | 6x6 | UASTC_HDR_6x6I (4) | no | yes — seek table |
| 6 | ASTC LDR 4x4-12x12 | ASTC_*_UNORM/_SRGB (157–184) |
ASTC — 162 (0xA2) | 4x4–12x12 | Zstd (2) or None (0) | optional | none |
| 7 | XUASTC LDR 4x4-12x12 | UNDEFINED (0) |
XUASTC_LDR_INTERMEDIATE — 169 (0xA9) | 4x4–12x12 | XUASTC_LDR (5) | optional | yes — seek table |
| 8 | XUBC7 | UNDEFINED (0) |
XUBC7 — 170 (0xAA) | 4x4 | XUBC7 (6) | optional | yes — seek table |
Notes:
- Alpha: "optional" means alpha presence is signaled per file — ETC1S via the DFD byte length (60 bytes = alpha), and the UASTC LDR / ASTC LDR / XUASTC LDR / XUBC7 family via the DFD
channelTypefield (see DFD - Alpha Channel Determination). "no" means the format does not carry an alpha channel. - Provisional values: DFD color models 168–170 and supercompression schemes 4–6 are Basis Universal / vendor values still being coordinated with Khronos (each such section is marked accordingly).
- Supercompression global data: the seek table (
ktx2_slice_offset_len_desc_stdarray) for the intermediate/supercompressed formats (5, 7, 8), the compressed codebooks for ETC1S (1), and none for the rest.
1. ETC1S
The vk_format field must be KTX2_UNDEFINED, and the DFD color model must be:
uint32_t KTX2_KDF_DF_MODEL_ETC1S = 163; // 0xA3
The block dimension fields in the DFD must be set to 4x4.
ETC1S textures may have an alpha channel (see below).
The supercompression type must be KTX2_SS_BASISLZ.
The supercompression global data contains compressed codebooks. See the ETC1S spec for details on the format.
2. UASTC LDR 4x4
The vk_format field must be KTX2_UNDEFINED, and the DFD color model must be:
uint32_t KTX2_KDF_DF_MODEL_UASTC_LDR_4X4 = 166; // 0xA6
The block dimension fields in the DFD must be set to 4x4.
UASTC LDR 4x4 textures may have an alpha channel. Here's the UASTC LDR 4x4 spec. Internally UASTC LDR 4x4 uses a conservative union of both the ASTC LDR 4x4 and BC7 latents, but the block packing is simplified to free up space for hint values to accelerate transcoding to other formats.
UASTC LDR 4x4 data may be supercompressed with Zstd.
UASTC LDR 4x4 has no supercompressed global data.
3. UASTC HDR 4x4
The vk_format field must be KTX2_FORMAT_ASTC_4x4_SFLOAT_BLOCK (see above), and the DFD color model must be:
uint32_t KTX2_KDF_DF_MODEL_UASTC_HDR_4X4 = 167; // 0xA7
The block dimension fields in the DFD must be set to 4x4.
UASTC HDR 4x4 data is 100% standard ASTC HDR 4x4 data, but constrained for fast transcoding to BC6H (unsigned variant only - ASTC HDR is unsigned). The UASTC HDR 4x4 spec describes the exact configurations supported. The texture data is standard ASTC HDR, but it cannot utilize configurations that are not supported by the UASTC HDR 4x4 spec, otherwise fast latent to latent transcoding from ASTC HDR to BC6H wouldn't be practical.
UASTC HDR 4x4 textures do not support alpha channels. ASTC HDR 4x4 does, but the constrained subset of ASTC HDR we support to enable fast, simple transcoding to BC6H does not. (BC6H doesn't support alpha.)
UASTC HDR 4x4 data may be supercompressed with Zstd.
UASTC HDR 4x4 has no supercompressed global data.
4. ASTC HDR 6x6
The vk_format field must be KTX2_FORMAT_ASTC_6x6_SFLOAT_BLOCK (see above), and the DFD color model must be:
uint32_t KTX2_KDF_DF_MODEL_ASTC = 162; // 0xA2
The block dimension fields in the DFD must be set to 6x6.
The transcoder uses a fully compliant ASTC HDR decoder whenever it has to decode this format to plain texels. If the ASTC HDR 6x6 block data has alpha, it will be decoded correctly but the alpha data may be ignored depending on the output transcode format. (BC6H doesn't support alpha.)
ASTC HDR 6x6 data may be supercompressed with Zstd.
ASTC HDR 6x6 has no supercompressed global data.
5. UASTC HDR 6x6i (intermediate)
The vk_format field must be KTX2_UNDEFINED, and the DFD color model must be:
uint32_t KTX2_KDF_DF_MODEL_UASTC_HDR_6X6_INTERMEDIATE = 168; // 0xA8
The supercompression type must be KTX2_SS_UASTC_HDR_6x6I (4).
The supercompressed global data contains a seek table: an array of ktx2_slice_offset_len_desc_std struct's, one per KTX2 "image" (the image count = max(header.layer_count, 1) * header.face_count * header.m_level_count):
struct ktx2_slice_offset_len_desc_std
{
uint32_t m_slice_byte_offset; // mipmap relative offset (NOT the absolute file offset)
uint32_t m_slice_byte_length; // byte length of this image's data
uint32_t m_profile; // codec-specific profile/version word; interpretation depends on the format (see below)
};
Here's how to compute the index to use within this array, given the mipmap level_index, the array layer_index, and the cubemap face_index:
const uint32_t image_index =
(level_index * basisu::maximum<uint32_t>(m_header.m_layer_count, 1) * m_header.m_face_count) +
layer_index * m_header.m_face_count +
face_index;
The m_profile field. This 32-bit little-endian word is codec-specific: each of the three formats that share this seek table (UASTC HDR 6x6i, XUASTC LDR, and XUBC7) packs it differently. For UASTC HDR 6x6i, the low 16 bits hold the stream's 16-bit signature/version marker — a copy of the first two bytes of the mipmap level's data — and the high 16 bits are 0:
uint32_t UASTC_6x6_HDR_SIG0 = 0xABCD; // original v1.6 stream format
uint32_t UASTC_6x6_HDR_SIG1 = 0xABCE; // v2.0 stream format (2x2->4x4 weight grid upsampling change;
// NOT backwards compatible with older decoders)
So m_profile is 0x0000ABCD for a v1.6-format stream or 0x0000ABCE for a v2.0-format stream. (KTX-Software surfaces this same field as rgbSliceType, printed as 0xabcd / 0xabce — see the ktx info dump near the bottom of this page.) The transcoder currently reads only m_slice_byte_offset and m_slice_byte_length and ignores m_profile; the signature/version is re-read from the slice payload at decode time. The field is written for forward compatibility and for external introspection tools.
The block dimension fields in the DFD must be set to 6x6.
UASTC HDR 6x6i doesn't support alpha channels. Here's the UASTC HDR 6x6i spec.
Note: UASTC HDR 6x6i is the "Codec". Each mipmap level has a small header or marker which defines the exact "version" (or variant, or semantic contract) used by that mipmap level. Each mipmap level may in theory use a different version. In practice, they'll very likely all use the same version. The version field allows Binomial to signal to the transcoder exactly which semantics should be used during transcoding. Binomial will always keep transcoder compatibility with all versions whenever possible. Currently there are 2 stream versions (those written by v1.6, and later by v2.0).
6. ASTC LDR 4x4-12x12
ASTC LDR data may be supercompressed with Zstd.
The vk_format field in the header must be between [KTX2_FORMAT_ASTC_4x4_UNORM_BLOCK, KTX2_FORMAT_ASTC_12x12_SRGB_BLOCK] (this range includes the linear and sRGB variants). During compression when computing candidate distortions, our encoder will configure its ASTC decoder to be compatible with this vk_format (sRGB or linear).
The DFD's model color field must be:
uint32_t KTX2_KDF_DF_MODEL_ASTC = 162; // 0xA2
The block dimension fields in the DFD must be set correctly and match the vk_format field.
The UNORM vk_format's will be emitted if the encoder was in linear mode, otherwise sRGB. This field slightly modifies how ASTC LDR blocks will be decoded by the transcoder (see the ASTC LDR specification).
The transcoder uses a fully compliant ASTC LDR decoder whenever it has to decode these formats.
ASTC LDR 4x4-12x12 may have an alpha channel.
ASTC LDR 4x4-12x12 data may be supercompressed with Zstd.
ASTC LDR 4x4-12x12 has no supercompressed global data.
7. XUASTC LDR 4x4-12x12
The vk_format field must be KTX2_UNDEFINED, and the DFD color model must be:
uint32_t KTX2_KDF_DF_MODEL_XUASTC_LDR_INTERMEDIATE = 169; // 0xA9
The standard ASTC/XUASTC block dimensions are written into the DFD.
The supercompression type must be KTX2_SS_XUASTC_LDR (5).
The supercompression global data contains the same format seek tables as UASTC HDR 6x6i — see the ktx2_slice_offset_len_desc_std struct and the image_index formula in that section.
The m_profile field for XUASTC LDR. XUASTC LDR packs the shared 32-bit little-endian m_profile word as:
bits 0..7 : entropy profile index = the first byte of the slice's payload
(the xuastc_ldr_syntax value chosen for this mipmap level):
0 = cFullArith (full arithmetic coding)
1 = cHybridArithZStd (hybrid arithmetic + Zstd)
2 = cFullZStd (full Zstd)
bits 8..15 : XUASTC LDR codec variant index ("version"), currently hardcoded to 0x01
(there is only one variant today)
bits 16..31 : reserved (0)
i.e. the encoder computes m_profile = payload_byte0 | (0x01 << 8). See the xuastc_ldr_syntax profile enum in the XUASTC LDR specification. Each mipmap level may use a different entropy profile (see below). As with the other formats sharing this seek table, the transcoder ignores m_profile and re-derives the entropy profile from the payload's first byte at decode time.
XUASTC LDR 4x4-12x12 may have an alpha channel.
Each mipmap level may use a different entropy profile. The encoder's default behavior is to always use the arithmetic profile on mipmap levels containing DISABLE_FASTER_FORMAT_TOTAL_BLOCKS_THRESH (64) total blocks or less. The Zstd profile's header overhead is too high for the smallest mipmap levels. The arithmetic profile is slower to transcode, but has very little header overhead. This behavior will likely be changing (and become more configurable) in future versions of the library.
XUASTC LDR is our newest format. Khronos standardization efforts are ongoing as of early 2026. We have aligned the global supercompression seek tables used by XUASTC LDR to be the same as UASTC HDR 6x6i's to assist standardization. It's likely we will be also standardizing several simple deblocking pixel shaders, and also adding at least one key value field that indicates the type of deblocking to apply (if any).
Note: XUASTC LDR is the "Codec". Each mipmap level has a small header which defines the exact XUASTC LDR entropy "profile" (arithmetic, hybrid, or full Zstd), and the "version" (or variant, or semantic contract) used by that mipmap level. Each mipmap level may use a different profile, and (in theory) a different version. (In practice, they'll very likely all use the same profile version.) The version field allows Binomial to signal to the transcoder exactly which semantics should be used during transcoding. Binomial will always keep transcoder compatibility with all versions whenever possible. Currently there is only a single version of XUASTC LDR for all profiles.
8. XUBC7
XUBC7 was released in basisu v2.50 (on 7/1/2026). It is supercompressed standard (i.e. full-featured) BC7: the transcoder decodes each block back to a standard BC7 block, and it transcodes near-losslessly to ASTC LDR 4x4, or to the other LDR GPU texture formats we support such as ETC1/2 etc. XUBC7 is an LDR-only format. XUBC7 supports the entire BC7 texture format: all modes, all mode features, single or dual plane, 2/3 subsets, and all possible partition patterns.
The vk_format field must be KTX2_UNDEFINED, and the DFD color model must be:
const uint32_t KTX2_KDF_DF_MODEL_XUBC7 = 170; // 0xAA, TODO - coordinate with Khronos on this
The supercompression type must be KTX2_SS_XUBC7 (6).
As this format is supercompressed standard BC7, the block dimensions are always 4x4. The encoder hardcodes the DFD's texel block dimension fields to 4x4, and the transcoder ignores them for XUBC7 and always uses 4x4.
Alpha
XUBC7 may have an alpha channel. As with UASTC LDR 4x4, ASTC LDR 4x4-12x12, and XUASTC LDR 4x4-12x12, alpha presence is signaled in the DFD's channelType field (see DFD - Alpha Channel Determination): the encoder writes KTX2_DF_CHANNEL_UASTC_RGBA (3) if any source image has alpha, otherwise KTX2_DF_CHANNEL_UASTC_RGB (0), and the transcoder reads this field to set its has_alpha flag. The DFD channelType value is provided for external callers and introspection tools; the authoritative alpha flag lives per slice — each (level, layer, face) image carries its own alpha flag inside its own payload.
Additionally, the first byte of each slice's payload is a format-dispatch marker that (for the smallest or "tiny" mipmap levels) also carries the alpha bit:
0xB7— a regular blob-container slice. Alpha is taken from the slice'sxbc7_header.m_flagsfield, where bit 0 isXBC7_FLAG_HAS_ALPHA(1). The decoder rejects any other flag bits as unknown (if (hdr.m_flags & ~XBC7_FLAG_HAS_ALPHA) return false;).0xB8/0xB9— a "tiny mip" slice: the smallest mip levels are stored as a short marker followed by 16 raw BC7 bytes per block, with no header flags byte. Here the alpha bit rides in the marker itself:0xB8= no alpha,0xB9= has alpha.
This first payload byte is exactly what the seek table's m_profile low 8 bits record (see below).
Transfer function (sRGB vs. linear)
As an LDR format, the DFD's transfer function field will be either KTX2_KHR_DF_TRANSFER_SRGB (2) or KTX2_KHR_DF_TRANSFER_LINEAR (1), depending on the encoder's decode profile. See DFD - Linear vs. sRGB Determination.
Supercompression global data (seek table)
XUBC7 stores a seek table in the KTX2 supercompressionGlobalData block, using the same ktx2_slice_offset_len_desc_std layout as UASTC HDR 6x6i and XUASTC LDR. The transcoder needs it to locate the variable-length compressed slice for each image. It is an array with one 12-byte descriptor per KTX2 "image", where:
image_count = max(header.layer_count, 1) * header.face_count * header.level_count
The array is indexed with the same image_index formula given in the UASTC HDR 6x6i section:
image_index =
(level_index * max(header.layer_count, 1) * header.face_count) +
(layer_index * header.face_count) +
face_index;
Each descriptor is (all fields little-endian):
struct ktx2_slice_offset_len_desc_std // 12 bytes
{
uint32_t m_slice_byte_offset; // offset of this image's compressed data, RELATIVE to the start of the
// KTX2 mipmap level's data (NOT the absolute file offset)
uint32_t m_slice_byte_length; // compressed byte length of this image's XUBC7 slice
uint32_t m_profile; // codec-specific profile/version word (see below)
};
To get the absolute file offset of an image's compressed data for image (level L, layer, face):
absolute_offset = levelIndex[L].byteOffset + seek_table[image_index].m_slice_byte_offset
Here levelIndex[] is the standard KTX2 level index array (see the KTX2 specification). It's stored immediately after the KTX2 header, and holds one 24-byte entry per mipmap level:
struct levelIndexEntry // 24 bytes, one per mipmap level
{
uint64_t byteOffset; // absolute file offset of this level's data
uint64_t byteLength; // total byte length of this level's data
uint64_t uncompressedByteLength; // 0 for XUBC7
};
All fields are little-endian. The array is ordered largest mip first, so levelIndex[0] is the base (full-resolution) level. Note that the level data itself is physically stored in the file smallest mip first, so byteOffset values decrease as the array index increases. For concrete examples, see the LevelN.byteOffset values in the ktx info dumps near the end of this page.
The m_profile word for XUBC7. The encoder packs two 8-bit fields into the low 16 bits; the high 16 bits are reserved and written as 0:
bits 0..7 : slice marker / profile-version byte -- a verbatim copy of the FIRST byte of this slice's
compressed payload:
0xB7 = regular blob-container slice (alpha comes from xbc7_header.m_flags)
0xB8 = tiny-mip slice, no alpha
0xB9 = tiny-mip slice, has alpha
bits 8..15 : XUBC7 codec variant index ("version"), currently hardcoded to 0x01
(there is only one variant today; this exists so future stream
revisions can be signaled without a container change)
bits 16..31 : reserved (0)
i.e. the encoder computes m_profile = payload_byte0 | (0x01 << 8). Because the low byte is copied straight from the first payload byte, it doubles as a per-mip marker/alpha indicator (see Alpha above): a descriptor whose low byte is 0xB9 is a tiny mip with alpha, 0xB8 a tiny mip without alpha, and 0xB7 a regular slice whose alpha is stored in its xbc7_header.m_flags.
For comparison, the other two formats that share this struct pack m_profile differently:
| Format | m_profile low 16 bits |
|---|---|
| XUBC7 | payload_byte0 (marker 0xB7/0xB8/0xB9) in bits 0-7, codec variant 0x01 in bits 8-15 |
| XUASTC LDR | entropy profile index (arithmetic / hybrid / Zstd) in bits 0-7, codec variant 0x01 in bits 8-15 |
| UASTC HDR 6x6i | full little-endian 16-bit marker = payload_byte0 | (payload_byte1 << 8) |
Transcoder behavior. The v2.50 transcoder reads only m_slice_byte_offset and m_slice_byte_length from each descriptor and currently ignores m_profile entirely; every profile / variant / alpha decision for XUBC7 is re-derived from the slice payload itself at decode time. m_profile is written for forward compatibility and for external introspection tools (so a tool can report a slice's marker and codec variant without having to decode the payload).
Entropy coding / Zstd
At the KTX2 container level the supercompression scheme is KTX2_SS_XUBC7 (6), not KTX2_SS_ZSTANDARD (2). The mipmap level data is therefore NOT Zstd-decompressed as a single unit (unlike UASTC LDR 4x4). Instead, each XUBC7 slice is a self-describing container whose internal sub-blocks ("blobs") are individually Zstd-compressed, or stored raw when Zstd wouldn't shrink them (see the payload structure below). As a consequence, the per-level uncompressedByteLength in the level index is always 0 for XUBC7, and no mipPadding bytes are written.
Slice payload structure
Each addressed slice (one KTX2 "image", located via the seek table above) is one of three payload forms, distinguished by its first byte — which is also the byte copied into the seek table's m_profile low 8 bits:
1. Blob-container slice (marker 0xB7) — the normal form. It is a tagged "blob stream": a small directory of independently-stored byte blobs, each either Zstd-compressed or stored raw (whichever is smaller — so noisy streams such as AC sign bits automatically stay raw). Serialized layout (little-endian; sizes are LEB128 varints, 7 bits/byte, high bit = continue):
[uint8 0xB7] begin marker (BLOB_STREAM_MAGIC_BEGIN)
[uint8 num_blobs] only non-empty blobs are stored
repeated num_blobs times:
[uint8 id_and_flag] low 7 bits = blob id (< 128); high bit: 1 = Zstd-compressed, 0 = stored raw
if raw: [varint size] // size > 0; 'size' bytes follow
if compressed: [varint uncompressed_size][varint stored_size] // both > 0, stored_size < uncompressed_size
[blob data]
[uint8 0x6A] end marker (BLOB_STREAM_MAGIC_END); must land exactly on the final byte
The header blob (blob id cBlobHeader = 0) is a 7-byte xbc7_header:
#pragma pack(push, 1)
struct xbc7_header
{
uint16_t m_width_in_texels; // LE, exact image width (1..16384)
uint16_t m_height_in_texels; // LE, exact image height (1..16384)
uint8_t m_dct_q; // global DCT quantizer, 1..100
uint8_t m_flags; // bit 0 = XBC7_FLAG_HAS_ALPHA (1); any other bit set => rejected as unknown
uint8_t m_num_stripes; // encoder stripe count (>= 1)
};
#pragma pack(pop)
The remaining blobs carry the per-block command / config / partition / endpoint / weight streams that reconstruct each BC7 block. Those are internal to the XUBC7 codec and outside the scope of this container document — see the XUBC7 File Format page for a high-level description of the full slice structure and blob catalog.
2 & 3. Tiny-mip slice (markers 0xB8 / 0xB9) — the smallest mip levels, where the blob-container overhead isn't worthwhile, are stored uncompressed with the alpha bit carried in the marker:
[uint8 marker] 0xB8 = no alpha, 0xB9 = has alpha
[uint8 num_blocks_x] (> 0)
[uint8 num_blocks_y] (> 0)
[16 bytes per block] standard packed BC7 blocks, raster order (exactly num_blocks_x*num_blocks_y*16 bytes)
A tiny-mip slice stores no exact texel dimensions and no DCT quantizer; its logical size is block-aligned (num_blocks_x*4 by num_blocks_y*4), and the whole payload length is validated to be exactly 3 + num_blocks_x*num_blocks_y*16 bytes.
DFD - Linear vs. sRGB Determination
For LDR formats, the DFD's transfer function field will be set to either LINEAR or sRGB, which indicates which ASTC LDR decoding profile was used during encoding. For HDR, it'll always be LINEAR (i.e. no transfer function).
uint32_t KTX2_KHR_DF_TRANSFER_LINEAR = 1;
uint32_t KTX2_KHR_DF_TRANSFER_SRGB = 2;
DFD - REC 2020/BT2100 Color Gamut Determination
The C++ compressor API has numerous low-level settings (in struct basis_compressor_params) controlling compression and KTX2 file format generation. If the m_astc_hdr_6x6_options.m_rec2020_bt2100_color_gamut bool flag was set to true, the DFD's color primary field will be set to BC2020 (4). This is done even if the output format isn't ASTC HDR 6x6 or UASTC HDR 6x6i. (We'll be improving how this is handled in the future.)
This corresponds to the cFlagREC2020 in the simplified C++ API basis_compress(), or the BU_COMP_FLAGS_REC2020 compressor flag in the C API.
enum ktx2_df_color_primaries
{
KTX2_DF_PRIMARIES_UNSPECIFIED = 0,
KTX2_DF_PRIMARIES_BT709 = 1,
KTX2_DF_PRIMARIES_SRGB = 1,
KTX2_DF_PRIMARIES_BT601_EBU = 2,
KTX2_DF_PRIMARIES_BT601_SMPTE = 3,
KTX2_DF_PRIMARIES_BT2020 = 4,
KTX2_DF_PRIMARIES_CIEXYZ = 5,
KTX2_DF_PRIMARIES_ACES = 6,
KTX2_DF_PRIMARIES_ACESCC = 7,
KTX2_DF_PRIMARIES_NTSC1953 = 8,
KTX2_DF_PRIMARIES_PAL525 = 9,
KTX2_DF_PRIMARIES_DISPLAYP3 = 10,
KTX2_DF_PRIMARIES_ADOBERGB = 11
};
Important note about the REC 2020/BT2100 flag: The ASTC HDR 6x6 and UASTC HDR 6x6i's encoder uses different formulas to convert the input HDR linear RGB pixels to ICtCp/Delta E ITP space if the input is known to be REC 2020/BT 2100 vs. the default REC 709. It's important this flag is set correctly, otherwise the encoder will be internally computing distortions in the wrong color space.
DFD - Alpha Channel Determination
For ETC1S: The texture is assumed to have alpha if the DFD's length is 60 bytes (2 samples), otherwise it's assumed to be RGB only.
For UASTC LDR 4x4, ASTC LDR 4x4-12x12, XUASTC LDR 4x4-12x12, or XUBC7, the DFD's channelType field (at uint32_t offset 7 relative to the beginning of the DFD or the initial DFD totalSize field, bit offset 24, 4-bits length) is interpreted to determine if the texture has alpha:
uint32_t KTX2_DF_CHANNEL_UASTC_RGBA = 3U;
uint32_t KTX2_DF_CHANNEL_UASTC_RRRG = 5U;
If channelType is either of these values, it's assumed the texture has an alpha channel. See the Khronos Data Format Specification v1.4.0, "Section 5.6.14 KHR_DF_MODEL_UASTC" for the table of UASTC channel names. (For XUBC7, this DFD field is advisory for external callers; the authoritative alpha flag is carried per slice inside each slice's payload, and the format decoder tracks that has_alpha flag per decoded slice.)
Our HDR formats (UASTC HDR 4x4, ASTC HDR 6x6, and UASTC HDR 6x6i) don't support alpha channels.
Key Value Fields
HDR Range (ktx_map_range)
For HDR files we always write the KTXmapRange structure, which can be used by a pixel shader to reconstruct the actual float range used in the original source .EXR/.HDR/.DDS file(s):
// always little endian
struct ktx_map_range
{
float m_scale; // scale to multiply the sampled values by, default=1.0
float m_offset; // offset to add after multiplication, always 0.0
};
This information is necessary because our compressor will automatically rescale input HDR images that use extreme dynamic ranges beyond values representable using ASTC HDR (which is slightly less than MAX_HALF):
// If the input has values higher than this, they need to be linearly scaled so all values are between [0,ASTC_HDR_MAX_VAL], and the linear scaling inverted in the shader.
const float ASTC_HDR_MAX_VAL = 65216.0f; // actually MAX_QLOG12_VAL
The compressor prints a warning (currently to stdout) when this rescaling occurs. Note rescaling only ever occurs on images where any texels have RGB channel values larger than ASTC_HDR_MAX_VAL.
Compressor Version
The key KTXwriter will be set to the string Basis Universal 2.50. The format for our library will always be "Basis Universal X.YY", where X is the major version, and YY is the minor version.
HDR Upconversion Nit Multiplier
The LDRUpconversionMultiplier key indicates the HDR texture was automatically upconverted from a 24-bpp SDR/LDR asset. Upconversion is performed by converting the input to linear light (undoing the sRGB transfer function), and then multiplying the normalized linear [0.0, 1.0] intensity range by typically 80–100 nits (where ~100 nits corresponds to SDR reference white).
The value is written as an ASCII decimal number, typically "100" unless changed by the user at compression time. This maps SDR white to a physically meaningful absolute luminance level.
This value can be used in a shader to convert sampled HDR values back to a normalized [0,1] linear range by dividing by the multiplier. The resulting normalized value can then be converted from linear to sRGB, as needed.
This scaling is required because the ASTC HDR 6x6 / UASTC HDR 6x6i compressor converts input data to ICtCp and expects the input to be in linear absolute luminance (nits), not normalized relative intensity.
HDR Upconversion sRGB to Linear Flag
The LDRUpconversionSRGBToLinear key indicates whether the input LDR/SDR asset was converted from sRGB to linear light during upconversion. The value written will always be "1", or the key won't be written at all.
Using the basisu command line tool to dump basic KTX2 header info
Our command line tool can examine our generated .KTX2 files. Example on bik158.ktx2:
C:\dev\bin>wasmtime --wasm threads=yes --wasi threads=yes --dir=. basisu_mt.wasm -info bik158.ktx2
Basis Universal LDR/HDR GPU Texture Supercompression System v2.00.0 (WASI Threaded)
Copyright (C) 2019-2026 Binomial LLC, All rights reserved
No SSE, Multithreading: 1, Zstandard support: 1, OpenCL: 0
Input file "bik158.ktx2", KTX2: 1
Resolution: 960x960
Block size: 6x6
Mipmap Levels: 1
Texture Array Size (layers): 0
Total Faces: 1 (2D)
Is Texture Video: 0
Supercompression Format: XUASTC_LDR_6x6
Supercompression Scheme: BASISLZ
Has Alpha: 0
KTX2 header vk_format: 0x0 (decimal 0)
Data Format Descriptor (DFD):
DFD length in bytes: 44
DFD color model: 169
DFD color primaries: 1 (BT709)
DFD transfer func: 2 (SRGB)
DFD flags: 0
DFD samples: 1
DFD chan0: RGB
DFD hex values:
0x2C,0x0,0x0,0x0,
0x0,0x0,0x0,0x0,
0x2,0x0,0x28,0x0,
0xA9,0x1,0x2,0x0,
0x5,0x5,0x0,0x0,
0x0,0x0,0x0,0x0,
0x0,0x0,0x0,0x0,
0x0,0x0,0x7F,0x0,
0x0,0x0,0x0,0x0,
0x0,0x0,0x0,0x0,
0xFF,0xFF,0xFF,0xFF
Total key values: 1
0. Key: "KTXwriter", Value length in bytes: 22 Value Bytes: 0x42,0x61,0x73,0x69,0x73,0x20,0x55,0x6E,0x69,0x76,0x65,0x72,0x73,0x61,0x6C,0x20,0x32,0x2E,0x30,0x30,0x0,0x0
Levels:
0. Offset: 192, Length: 149083, Uncompressed Length: 0
Image level info:
--- Level Index: 0, Layer Index: 0, Face Index: 0
Orig width/height: 960x960
Width/height: 960x960
Block width/height: 6x6
Num blocks: 160x160, Total blocks: 25600
Alpha flag: false, I-frame flag: false
Success
Using the KTX command line tool to validate KTX2 files
This tool was built from the KTX-Software repo as of 2/24/2026. Here's an example on a UASTC HDR 6x6i file generated using Basis Universal v2.1:
C:\dev\ktx2_2\KTX-Software-main\build\Debug>ktx info c:\dev\bu_2_13\basis_universal\bin\uastc_hdr_6x6i\Desk.ktx2
Validation successful
Header
identifier: «KTX 20»\r\n\x1A\n
vkFormat: VK_FORMAT_UNDEFINED
typeSize: 1
pixelWidth: 644
pixelHeight: 874
pixelDepth: 0
layerCount: 0
faceCount: 1
levelCount: 1
supercompressionScheme: KTX_SS_UASTC_HDR_6x6_INTERMEDIATE
dataFormatDescriptor.byteOffset: 0x68
dataFormatDescriptor.byteLength: 44
keyValueData.byteOffset: 0x94
keyValueData.byteLength: 60
supercompressionGlobalData.byteOffset: 0xd0
supercompressionGlobalData.byteLength: 12
Level Index
Level0.byteOffset: 0xdc
Level0.byteLength: 205424
Level0.uncompressedByteLength: 0
Data Format Descriptor
DFD total bytes: 44
Vendor ID: KHR_DF_VENDORID_KHRONOS
Descriptor type: KHR_DF_KHR_DESCRIPTORTYPE_BASICFORMAT
Version: KHR_DF_VERSIONNUMBER_1_3
Descriptor block size: 40
Flags: 0x0 (KHR_DF_FLAG_ALPHA_STRAIGHT)
Transfer: KHR_DF_TRANSFER_LINEAR
Primaries: KHR_DF_PRIMARIES_BT709
Model: KHR_DF_MODEL_UASTC_HDR_6x6
Dimensions: 6, 6, 1, 1
Plane bytes: 16, 0, 0, 0, 0, 0, 0, 0
Sample 0:
Qualifiers: 0x80 (KHR_DF_SAMPLE_DATATYPE_FLOAT)
Channel Type: 0x0 (KHR_DF_CHANNEL_UASTC_HDR_6x6_RGB)
Length: 128 bits Offset: 0
Position: 0, 0, 0, 0
Lower: 0x00000000
Upper: 0x3f800000
Key/Value Data
KTXmapRange:
scale: 1.000000
offset: 0.000000
KTXwriter: Basis Universal 2.10
UASTC HDR6X6 Intermediate Supercompression Global Data
rgbSliceByteLength: 205424
rgbSliceByteOffset: 0
rgbSliceType: 0xabcd
Another example, this time a mipmapped texture array with 2 slices, UASTC HDR 6x6i:
C:\dev\bu_2_28\basis_universal\example>c:\dev\ktx2_2\KTX-Software-main\build\Debug\ktx info test_uastc_hdr_6x6i_array_custom_mips.ktx2
Validation successful
Header
identifier: «KTX 20»\r\n\x1A\n
vkFormat: VK_FORMAT_UNDEFINED
typeSize: 1
pixelWidth: 384
pixelHeight: 256
pixelDepth: 0
layerCount: 2
faceCount: 1
levelCount: 8
supercompressionScheme: KTX_SS_UASTC_HDR_6x6_INTERMEDIATE
dataFormatDescriptor.byteOffset: 0x110
dataFormatDescriptor.byteLength: 44
keyValueData.byteOffset: 0x13c
keyValueData.byteLength: 60
supercompressionGlobalData.byteOffset: 0x178
supercompressionGlobalData.byteLength: 192
Level Index
Level0.byteOffset: 0xb2b
Level0.byteLength: 869
Level0.uncompressedByteLength: 0
Level1.byteOffset: 0x7d2
Level1.byteLength: 857
Level1.uncompressedByteLength: 0
Level2.byteOffset: 0x597
Level2.byteLength: 571
Level2.uncompressedByteLength: 0
Level3.byteOffset: 0x352
Level3.byteLength: 581
Level3.uncompressedByteLength: 0
Level4.byteOffset: 0x290
Level4.byteLength: 194
Level4.uncompressedByteLength: 0
Level5.byteOffset: 0x270
Level5.byteLength: 32
Level5.uncompressedByteLength: 0
Level6.byteOffset: 0x254
Level6.byteLength: 28
Level6.uncompressedByteLength: 0
Level7.byteOffset: 0x238
Level7.byteLength: 28
Level7.uncompressedByteLength: 0
Data Format Descriptor
DFD total bytes: 44
Vendor ID: KHR_DF_VENDORID_KHRONOS
Descriptor type: KHR_DF_KHR_DESCRIPTORTYPE_BASICFORMAT
Version: KHR_DF_VERSIONNUMBER_1_3
Descriptor block size: 40
Flags: 0x0 (KHR_DF_FLAG_ALPHA_STRAIGHT)
Transfer: KHR_DF_TRANSFER_LINEAR
Primaries: KHR_DF_PRIMARIES_BT709
Model: KHR_DF_MODEL_UASTC_HDR_6x6
Dimensions: 6, 6, 1, 1
Plane bytes: 16, 0, 0, 0, 0, 0, 0, 0
Sample 0:
Qualifiers: 0x80 (KHR_DF_SAMPLE_DATATYPE_FLOAT)
Channel Type: 0x0 (KHR_DF_CHANNEL_UASTC_HDR_6x6_RGB)
Length: 128 bits Offset: 0
Position: 0, 0, 0, 0
Lower: 0x00000000
Upper: 0x3f800000
Key/Value Data
KTXmapRange:
scale: 1.000000
offset: 0.000000
KTXwriter: Basis Universal 2.10
UASTC HDR6X6 Intermediate Supercompression Global Data
rgbSliceByteLength: 440
rgbSliceByteOffset: 0
rgbSliceType: 0xabcd
rgbSliceByteLength: 429
rgbSliceByteOffset: 0x1b8
rgbSliceType: 0xabcd
rgbSliceByteLength: 434
rgbSliceByteOffset: 0
rgbSliceType: 0xabcd
rgbSliceByteLength: 423
rgbSliceByteOffset: 0x1b2
rgbSliceType: 0xabcd
rgbSliceByteLength: 291
rgbSliceByteOffset: 0
rgbSliceType: 0xabcd
rgbSliceByteLength: 280
rgbSliceByteOffset: 0x123
rgbSliceType: 0xabcd
rgbSliceByteLength: 296
rgbSliceByteOffset: 0
rgbSliceType: 0xabcd
rgbSliceByteLength: 285
rgbSliceByteOffset: 0x128
rgbSliceType: 0xabcd
rgbSliceByteLength: 105
rgbSliceByteOffset: 0
rgbSliceType: 0xabcd
rgbSliceByteLength: 89
rgbSliceByteOffset: 0x69
rgbSliceType: 0xabcd
rgbSliceByteLength: 16
rgbSliceByteOffset: 0
rgbSliceType: 0xabcd
rgbSliceByteLength: 16
rgbSliceByteOffset: 0x10
rgbSliceType: 0xabcd
rgbSliceByteLength: 14
rgbSliceByteOffset: 0
rgbSliceType: 0xabcd
rgbSliceByteLength: 14
rgbSliceByteOffset: 0xe
rgbSliceType: 0xabcd
rgbSliceByteLength: 14
rgbSliceByteOffset: 0
rgbSliceType: 0xabcd
rgbSliceByteLength: 14
rgbSliceByteOffset: 0xe
rgbSliceType: 0xabcd