Pure-Rust reader / writer for Microsoft's DirectDraw Surface (DDS) texture container, the format every Direct3D game ships its baked block-compressed art in. Part of the oxideav workspace family of single-format codec crates.
Coverage as of round 4:
DDS_HEADER(124 bytes) + optionalDDS_HEADER_DXT10(20 bytes) parser.- Bit-exact round-trip of every common uncompressed surface layout:
A8R8G8B8, X8R8G8B8, A8B8G8R8 (DXGI
R8G8B8A8_UNORM), R5G6B5, A1R5G5B5, A4R4G4B4, R8G8B8, A8L8, L8, A8. - BC1..BC5 + BC7 decompression to RGBA8 / R8 / RG8 via
decode_bc1,decode_bc2,decode_bc3,decode_bc4_unorm,decode_bc4_snorm,decode_bc5_unorm,decode_bc5_snorm,decode_bc7. BC7 covers all 8 modes (single-, dual- and three-subset partitions, p-bits, channel rotation, secondary alpha index plane). - BC6H decompression — all 14 modes — to RGBA half-float via
decode_bc6h. Modes 0..13 are implemented per the per-mode bit-allocation tables Microsoft mandates for Direct3D 11 hardware (covering 10.5.5.5, 7.6.6.6, 11.5.5.5 / 11.4.4.4 / 11.4.5.4 / 11.4.4.5, 9.5.5.5, 8.6.6.6 / 8.5.6.5 / 8.5.5.6, 6.6.6.6 absolute TWO-subset variants and 10.10 / 11.9 / 12.8 / 16.4 ONE-subset variants). BothBC6H_UF16(unsigned) andBC6H_SF16(signed) finalisation paths are supported. Reserved 5-bit prefixes (10011, 10111, 11011, 11111) decode to zero RGB per spec. - BC1 / BC2 / BC3 / BC4 / BC5 encoders. Round 4 lifts encoding
beyond BC1:
encode_bc1,encode_bc2,encode_bc3,encode_bc4_unorm,encode_bc5_unormall emit valid block- compressed surfaces from RGBA8 / R8 / RG8 input. Each uses a furthest-point endpoint heuristic (no PCA / cluster fit / RDO); bit-exact roundtrip on solid blocks, ~19 dB PSNR-RGB on small natural-image gradients (>25 dB on the 16×16 test), 8-value interpolated alpha throughout BC3 / BC4 / BC5. BC1 honours punchthrough alpha when requested. - BC6H multi-mode encoder. Round 3 shipped mode 10; round 6
closes the encoder gap with a partition + mode picker that sweeps
every BC6H mode per block.
encode_bc6h(and the f32-input convenienceencode_bc6h_from_f32) iterates: (a) mode 10 (1-subset 10.10 absolute) as the SSE reference, (b) modes 11/12/13 (1-subset 10/12/16-bit base + 9/8/4-bit delta), (c) modes 0..9 (2-subset over the 32-entry BC6H partition table). Each candidate uses furthest-point endpoint seeding + iterative LSQ refinement; delta-mode candidates that overflow the per-channel delta range are rejected so the picker falls through to a wider-fitting mode. Bit-exact roundtrip on solid blocks across every mode; ≥35 dB on tight-range gradients (mode 11/12 wins) and 2-subset partition-friendly content (mode 0/2..9 wins). - BC7 multi-mode encoder. Round-3 shipped mode 6 only; round 4 added the three 2-subset modes — mode 1 (6-bit RGB + shared p-bits, opaque), mode 3 (7-bit RGB + per-endpoint p-bits, opaque) and mode 7 (5-bit RGBA + per-endpoint p-bits, translucent). Round 5 added the 3-subset modes 0 and 2. Round 7 closes the encoder coverage with the 1-subset channel-rotation modes 4 and 5: each block sweeps all 4 rotation values × (mode 4: 2 idx_sel choices) × mode 5, letting content with one independent channel use the higher alpha precision via channel-rotation. With 8-mode coverage (0..7) and full Microsoft / Khronos partition-table sweeps the encoder lifts multi-axis natural-image PSNR-RGB past 30 dB and decorrelated-alpha content past 30 dB-RGBA.
- BC6H_SF16 (signed) encoder.
encode_bc6h_sf16andencode_bc6h_sf16_from_f32emit signed-format BC6H blocks (DXGIBC6H_SF16). Mirrors the decoder's signed-magnitude pipeline: signed-magnitude quantise, signed unquantize, signed finalize. Currently emits mode 10 (1-subset, 10-bit signed absolute, 4-bit indices) — sufficient for typical signed-displacement-map content. Multi-mode SF16 (modes 11/12/13 signed-delta + 2-subset signed) is a follow-on round. - Mipmap chain emission for both uncompressed and BC* surfaces.
encode_dds_uncompressedemits a full mipmap chain whenimage.mip_map_count > 1(caller-supplied surfaces written verbatim, otherwise fabricated by 2×2 box-filter downsampling). Round-4encode_dds_block_compressedaccepts pre-encoded per-mip block bytes viaimage.surfacesand writes them with a legacy FourCC header (BC1..BC5) or DX10 extension header (BC6H + BC7 or any format withhas_dxt10_header == true). .ddscontainer demuxer + muxer. Round-3 lift over the round-2 extension-only entry: the framework-sideContainerRegistrynow installs probe + demuxer + muxer + extension table entries viaregister_containers, so CLI tools (such ascli-convert) can open / write.ddsfiles without touching the codec API directly.- Mipmap chain + cubemap faces + DX10 texture arrays. Every
on-disk surface is parsed into
DdsImage::surfacesin Microsoft's mandated order (array slice → face → mip), tagged withmip_level/array_slice/face.DdsImage::planes[0]still mirrors the base level for callers that don't care. - Full DXGI format table — every
DXGI_FORMATvalue Microsoft assigns (1..=132) is enumerated by name inDxgiFormatfor lossless round-trip; HDR-float, integer, depth/stencil, YUV, and palette formats are recognised but produceDdsError::Unsupportedfrom the layout resolver. - Block-compressed pass-through. BC1..BC7 raw block bytes are
surfaced through
DdsImage::surfaces[i].plane.data; BC1..BC5 + BC7 also decompress to RGBA / R / RG via the dedicateddecode_bc*entry points; BC6H decompresses to RGBA half-float for all 14 modes viadecode_bc6h. - Standalone-friendly via the default-on
registryCargo feature. Disable it (default-features = false) to drop theoxideav-coredependency tree entirely; the crate then exposes only the framework-freeparse_dds/encode_dds_uncompressed/decode_bc1..bc7/decode_bc6h/encode_bc1..bc5API plus crate-localDdsImage/DdsPixelFormat/DdsErrortypes built onstd.
Still deferred (followups):
- BC6H_SF16 multi-mode (delta-encoded modes 11/12/13 signed + 2-subset
modes 0..9 signed) — currently
encode_bc6h_sf16emits mode 10 only. - LSQ refinement metric — current pixel-space LSQ is approximate; fitting in unq-space could push 1-2 dB more on multi-axis HDR content.
use oxideav_dds::{parse_dds, encode_dds_uncompressed, DdsImage, DdsPixelFormat, DdsPlane};
// Parse a DDS file.
let bytes: Vec<u8> = std::fs::read("input.dds").unwrap();
let img = parse_dds(&bytes).unwrap();
println!(
"{}x{} {} (mip levels: {})",
img.width, img.height, img.pixel_format.name(), img.mip_map_count,
);
// Build + write a 4x3 A8R8G8B8 surface.
let mut data = vec![0u8; 4 * 3 * 4];
for (i, b) in data.iter_mut().enumerate() {
*b = (i & 0xff) as u8;
}
let img = DdsImage {
width: 4,
height: 3,
pixel_format: DdsPixelFormat::A8R8G8B8,
planes: vec![DdsPlane { stride: 4 * 4, data }],
pts: None,
mip_map_count: 1,
has_dxt10_header: false,
dxgi_format: None,
};
let out: Vec<u8> = encode_dds_uncompressed(&img).unwrap();
std::fs::write("output.dds", out).unwrap();For block-compressed input the same parse_dds returns an image whose
pixel_format is one of the Bc* variants and whose
surfaces[i].plane.data holds the raw 4x4-block byte array. For
BC1..BC5 + BC7 you can call the matching decode_bc* helper to expand
it into RGBA8 / R8 / RG8:
use oxideav_dds::{decode_bc1, decode_bc7, parse_dds, DdsPixelFormat};
let dds = std::fs::read("texture.dds").unwrap();
let img = parse_dds(&dds).unwrap();
let mut rgba = vec![0u8; (img.width * img.height * 4) as usize];
match img.pixel_format {
DdsPixelFormat::Bc1 => {
decode_bc1(&img.surfaces[0].plane.data, img.width, img.height, &mut rgba).unwrap();
}
DdsPixelFormat::Bc7Unorm | DdsPixelFormat::Bc7UnormSrgb => {
decode_bc7(&img.surfaces[0].plane.data, img.width, img.height, &mut rgba).unwrap();
}
_ => { /* see decode_bc2..bc5 helpers */ }
}To encode an RGBA8 surface to BC1 (DXT1) on disk:
use oxideav_dds::encode_bc1;
let rgba: Vec<u8> = vec![0xff; 16 * 16 * 4];
let mut bc1 = vec![0u8; (16 / 4) * (16 / 4) * 8];
encode_bc1(&rgba, 16, 16, /* punchthrough_alpha = */ false, &mut bc1).unwrap();
// `bc1` now holds the raw block bytes; wrap them in a DDS file with
// FOURCC_DXT1 to write a valid texture.For BC2 (DXT3 explicit alpha), BC3 (DXT5 interpolated alpha), BC4 (single-channel) or BC5 (two-channel) the entry points mirror the BC1 encoder:
use oxideav_dds::{encode_bc2, encode_bc3, encode_bc4_unorm, encode_bc5_unorm};
let rgba = vec![0u8; 16 * 16 * 4];
let mut bc3 = vec![0u8; (16 / 4) * (16 / 4) * 16];
encode_bc3(&rgba, 16, 16, &mut bc3).unwrap();For mipmapped or cubemap textures iterate img.surfaces directly:
each entry carries its own mip_level, array_slice, face, and
(width, height).
Every byte of the parser was written from Microsoft's public DDS
programming-guide pages on learn.microsoft.com (the
"DDS file layout for textures", "DDS pixel format", and "Programming
guide for DDS" articles plus the public DXGI format reference). No
DirectXTex, D3DX, NVTT, squish, or other DDS-handling source code was
consulted, paraphrased, or cross-referenced. Binaries (magick,
texconv) are used only as black-box validators when generating
test fixtures, not as a source of constants or layout.
| Feature | Default | Effect |
|---|---|---|
registry |
yes | Pulls in oxideav-core, exposes the Decoder / Encoder trait surface, registers the codec with the framework via register. |
MIT — see LICENSE.