Computer Graphics · Graphics Tooling
Block-Compressed Texture Bit Rates and What the Blocks Cost
How fixed-size blocks turn a texture into a constant byte budget, why the smallest block is a fixed 16 bytes, and where the rate-distortion trade actually lands.
An uncompressed 1024×1024 RGBA texture occupies 4 MiB. A mip chain adds a third again, so the full pyramid is about 5.3 MiB, and a material with six such maps exceeds 30 MiB. Delivering that over a network and holding it resident is a bandwidth problem with no clever fix, which is why GPU texture formats abandon general-purpose compression in favour of a scheme with a hard, computable size: fixed-size blocks, decoded by fixed-function hardware, with no variable-length anything.
The decision that makes the format possible
Block compression is built on one constraint and one concession. The constraint: a GPU must be able to compute the address of any texel from the coordinate alone, so a compressed texture cannot have blocks whose size depends on their content. The concession: compression quality is allowed to be visibly worse than a general compressor, because the texture will be filtered and lit rather than read as an image.
With fixed-size blocks the total size is exactly
$$\text{bytes} = \left\lceil \frac{W}{b_w} \right\rceil \times \left\lceil \frac{H}{b_h} \right\rceil \times B$$
where $b_w \times b_h$ is the block footprint and $B$ is the block’s byte size. For the BC family, $b_w = b_h = 4$ and $B = 8$ or $16$. For the smallest ASTC footprint, the block is also 4×4, but the compressed block for the 8-bit-per-texel profile is 16 bytes. Both families therefore produce 8 bits per texel at a 4×4 block with 16 bytes, or 4 bits per texel with 8 bytes.
The rounding-up is a real cost, not a technicality. A 40×40 texture needs $10 \times 10 = 100$ blocks — exactly right. A 33×33 texture needs $9 \times 9 = 81$ blocks, which cover $36 \times 36$ texels, so about a fifth of the compressed data has no output texel behind it and must be padded. Small textures therefore have measurably worse bytes-per-pixel than the nominal rate, and a texture atlas of many small charts wastes proportionally more.
The BC family as a progression of models
The BC formats are not variants of one algorithm; they are a sequence of increasingly expensive models of what a 4×4 block of texels looks like.
BC1 spends 8 bytes per block: two 16-bit endpoints in RGB565 format and 16 two-bit indices. The decoder reconstructs four colours on the line between the endpoints — the endpoints themselves and two interpolated points — and each texel picks one by index. This is a line through colour space, so the model is “the block’s colours lie near one line”. It handles gradients and flat regions well and fails on blocks containing two unrelated colours, where the line passes through the average of both and every texel is wrong.
BC3 is BC1 for colour plus a second 8-byte block of alpha: two 8-bit alpha endpoints and 16 three-bit indices giving a line through alpha. The alpha part costs the same as the colour part for a quarter as much data, because alpha is one scalar and 8 bytes is what the block budget allows.
BC4 is exactly that alpha block alone, in both signed and unsigned variants, which makes it the natural format for a single-channel data texture. BC5 pairs two BC4 blocks for two channels, which is why it is the standard choice for tangent-space normal maps: red and green hold $x$ and $y$ with eight bits each, and the shader reconstructs $z$.
BC6H handles 16-bit-per-channel float colour and stores the endpoints in a shared-exponent form rather than as fixed-point, which is how it reaches a high dynamic range without a per-texel float. BC7 spends 16 bytes on eight modes, several of which partition the block into two or three subsets with their own endpoint pairs, and some of which carry per-texel index bits for alpha. BC7 is the quality format for 8-bit RGBA and, in practice, the format where most of the encoder complexity lives: choosing the best mode per block is a search over a few thousand candidate partitions.
| Format | Block bytes | Bits/texel | Channels | Typical use |
|---|---|---|---|---|
| BC1 | 8 | 4 | RGB (+1-bit alpha mode) | Base colour, no alpha |
| BC3 | 16 | 8 | RGBA | Base colour with alpha |
| BC4 | 8 | 4 | One scalar | Masks, height, gloss |
| BC5 | 16 | 8 | Two scalars | Normal $xy$ |
| BC6H | 16 | 8 | RGB, half range | HDR environment data |
| BC7 | 16 | 8 | RGBA | High-quality base colour |
The progression is a direct exchange of decode work for fidelity. BC1’s decoder is a handful of multiply-adds; BC7’s is a mode dispatch, a partition table lookup, up to three endpoint pairs, and index bit extraction — on the order of a few dozen operations per block. All of it happens in fixed-function hardware, so the cost is silicon area and the fixed cost is paid at design time rather than per fetch.
ASTC: choosing the footprint instead of the format
ASTC takes the opposite approach. Rather than offering a fixed set of formats, it offers one algorithm parameterized by block footprint, from 4×4 up to 12×12, and the footprint selects the rate. A 4×4 block holding 16 bytes is 8 bits per texel; a 6×6 block at 16 bytes is about 3.6 bits per texel; a 12×12 block at 16 bytes is under 1 bit per texel.
The mechanism is different in kind from BC. A block stores two “endpoint” colour values in a low-precision encoding plus a set of interpolated weights, one per texel, whose bit depth depends on how many bits remain after the endpoints. The weights are then used as an index into a small weight grid that has been upscaled to the block — so the reconstruction is a bilinear-ish interpolation of a coarse weight field rather than a per-texel index into a line. Larger blocks mean a coarser weight grid and more texels sharing each weight, which is precisely why quality degrades with footprint: a 12×12 block has one set of endpoints for 144 texels and cannot represent a sharp edge that falls inside it.
The reason to prefer ASTC where it is available is that the rate is a continuous knob rather than a format choice. A pipeline can set 6×6 for a terrain layer and 4×4 for a hero asset within the same codec and the same decoder. The cost is decoder complexity in fixed-function hardware and, more practically, portability: support varies across GPU generations and mobile parts, so an asset pipeline targeting several platforms usually needs a BC fallback path, which means the conditioning step produces two variants of every texture.
What the encoder is actually optimizing
An encoder’s job is not “compress the block”, it is “choose the block encoding that minimizes the error the consumer will perceive”. Those differ, and the difference is where most of the practical engineering sits.
For BC1, fitting a line to 16 colours in RGB space has a closed-form answer — principal component analysis gives the direction of maximum variance, and projecting onto it gives the endpoints. In practice encoders use cheaper approximations: pick the pair of texels with maximum distance, or use the bounding box diagonal, then refine. The refinement loop matters because the endpoints are stored at 5-6-5 precision, so a candidate endpoint must be quantized before the four reconstructed colours are computed; an encoder that quantizes after fitting gets a line slightly different from the one it optimized.
The error metric is usually mean squared error in RGB, which is not what the eye weighs. Perceptual encoders weight channels and sometimes apply a gamma-aware metric. Both are heuristics, and a block that is optimal under one metric can be visibly worse under another, which is why encoders expose a quality/speed setting rather than claiming an optimum.
For BC7 the search space is combinatorial: eight modes, each with its own partition layout, endpoint precision, and index count. Exhaustive search per block is far too slow for interactive use, so production encoders partition the modes into a fast path and a slow path and let the setting choose. The consequence for a pipeline is that the output depends on the encoder and its settings, so two runs over the same input are not guaranteed to produce the same bytes, which has real consequences for caching — the problem described in content-addressed incremental asset builds.
Mip chains multiply the mistake
Every mip level of a block-compressed texture is itself block-compressed, and each level halves both dimensions. A 4×4-block format at 1024×1024 therefore has 65 536 blocks at level 0 and roughly 87 000 across the whole chain — the sum converging to $\tfrac{4}{3}$ of the level-0 count. Two things follow.
First, the smallest mip levels are dominated by padding. A 1×1 level is one 4×4 block of which 15 texels are unused, so the deepest levels cost a fixed 16 bytes each regardless of the texel count. A full chain runs to level 10 for a 1024 texture, eleven levels in all, and the last few contribute a few hundred bytes of pure padding. That is negligible in absolute terms but explains why very small textures have poor nominal rates.
Second, filtering across block boundaries is not the same as filtering texels. Bilinear reconstruction at a block edge reads texels from two blocks, each decoded with its own endpoints, so the interpolation crosses a discontinuity in the colour model. Hardware handles this by decoding the neighbouring blocks and interpolating the decoded texels, which is correct but expensive, which is why the fetch cost of a compressed texture is higher than four uncompressed reads. The mitigation is that the decoder caches recently decoded blocks; a fetch pattern that jumps between distant parts of a large texture, such as a ray-traced reflection at a grazing angle, defeats that cache and pays the decode cost repeatedly.
The third level of trouble is the mip filter itself. Box-filtering a block-compressed texture must be done on decoded values — the compressed endpoints cannot be averaged. For an encoder producing a mip chain from an uncompressed source, either the chain is generated before compression, which is correct and standard, or the encoder decodes each level and re-encodes, which accumulates error. Neither is free, and a chain generated by naive averaging of already-compressed levels visibly loses contrast at each step.
Choosing a format as a pipeline decision
The decision that shapes everything downstream is whether the texture holds colour or data, because the two want different error models. A block format that subsamples chroma is a good fit for base colour and a poor fit for a roughness field; a format that treats all channels alike, such as BC6H or a dedicated normal format, is the reverse. The channel assignment therefore constrains the format choice, and vice versa — the interaction worked through in channel packing conventions.
Two further constraints bind. Non-power-of-two dimensions are legal but interact badly with tiling and with the rounding arithmetic above; many pipelines pad or resample to powers of two to keep the block grid clean. And the number of distinct formats a runtime must handle is itself a cost — every additional format is a decode path, a fallback decision, and a test surface — which is the argument for the container that carries several encoded variants of the same texture and lets the runtime select one, taken up in KTX2 containers and Basis transcoding.
References
- BC1 to BC7 Texture Compression Formats — Microsoft Direct3D documentation
- ASTC Texture Compression Specification — Khronos Group
- Real-Time Rendering, Fourth Edition — Akenine-Moller et al., CRC Press, 2018
- Texture Compression using Low-Frequency Signal Modulation — Simon Fenney, Graphics Hardware 2003