A 2048×2048 RGBA8 texture is 16 MB — sampled, cached, streamed. The same texture as BC7 is 4 MB and as ASTC 4×4 even less. GPU-compressed formats decode in the texture unit: they stay compressed in memory, in caches, on the bus — only the sampler expands them.
The landscape
BC1/BC3 (DXT1/5): 4bpp / 8bpp — old, fast, noticeable on gradients
BC4/BC5: single/dual channel — normals, heightmaps
BC6H: HDR, floating point
BC7: high quality RGBA — the desktop standard
ASTC 4x4 … 12x12: scalable 8→0.89bpp — mobile standard, desktop too
ETC2/EAC: GLES3 baseline — mobile only, mostly obsolete on PCLoading a compressed texture
The API is identical — only the format and upload rules differ:
vk::ImageCreateInfo ici{};
ici.format = vk::Format::eBc7UnormBlock; // or eAstc4x4UnormBlock
ici.extent = {w, h, 1}; // pixel extent, not blocks
ici.usage = vk::ImageUsageFlagBits::eSampled
| vk::ImageUsageFlagBits::eTransferDst;
ici.mipLevels = mipCount;
// Upload rules differ from uncompressed:
// vkCmdCopyBufferToImage bufferRowLength/bufferImageHeight are in
// TEXELS but must be multiples of the compressed block extent
// (4x4 for BC, NxM for ASTC); image extent is rounded up to a
// multiple of block size for partial edge blocks.
vk::BufferImageCopy r{};
r.imageExtent = {w, h, 1}; // API takes pixel extent; it aligns itself
r.imageSubresource = {vk::ImageAspectFlagBits::eColor, level, 0, 1};
cmd.copyBufferToImage(staging, img, eTransferDstOptimal, r);Compressed images can’t be color/depth attachments, can’t be
eHostImageCopy-written, and can’t be vkCmdCopyImage’d at
sub-block granularity — only sampled.
KTX2 — the container that does transcoding
KTX2 (Khronos Texture 2) wraps compressed mip-chains and supports UASTC/ETC1S supercompression — one master file transcodes at load time to whatever the target supports:
ktxTexture2* t;
ktxTexture2_CreateFromNamedFile("albedo.ktx2",
KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, &t);
// pick the target format this device supports:
ktx_transcode_fmt_e tf = pickFormat(dev); // BC7 / ASTC / ETC2 / RGBA32
if (ktxTexture2_NeedsTranscoding(t))
ktxTexture2_TranscodeBasis(t, tf, 0);
// t->pData now holds e.g. BC7 blocks → vkCmdCopyBufferToImage
// image format = vk::Format from ktxTexture2_GetVkFormat? map tf → VkFormat
libktx handles the parse; the blocks upload like any compressed
format.
Choosing a format
desktop PC: BC7 (or BC3 for legacy) — universal since Fermi/HD7k
mobile/Android: ASTC 4x4..8x8 (Arm/Adreno/Mali all support)
best quality: ASTC 4x4 ≈ BC7 quality, more block-size options
HDR textures: BC6H
normals: BC5 / ASTC 2-channel (store xy, reconstruct z)
one file all-GPUs: KTX2 UASTC → transcode per-platform at loadCheck support before creating: vkGetPhysicalDeviceFormatProperties2
→ optimalTilingFeatures & eSampledImage on the compressed format —
an unsupported BC7 must fall back to RGBA8 upload.
Practical rules
- Upload extents are in pixels, but
bufferRowLength/imageExtentalign to the compressed block — a 13×17 BC7 image uploads as 16×20. - Compressed
VkImage=VK_IMAGE_USAGE_SAMPLED_BITonly — no color/depth attachment, no storage. - Mips count the same way, but each mip rounds to block size separately — a 4×4 mip at level 6 is still one 16-byte block.
- KTX2’s UASTC→ASTC transcode needs
libktx+basisu; the resulting block data is what you upload — the container is load-time only. formatFeatureFlagson a compressed format still showsSAMPLED— the “compression” is in the format name, not a usage flag.