-
Notifications
You must be signed in to change notification settings - Fork 335
DDS Transcoder
Copyright (C) 2025-2026 Binomial LLC. All rights reserved except as granted under the Apache 2.0 LICENSE.
This page documents the transcoder module's .DDS reader/transcoder: basist::dds_transcoder. It covers transcoding from .DDS files only — the encoder library's separate .DDS reading/writing functionality is not covered here.
- Intro
- How Transcoding Works: Three Paths
- Supported .DDS Source Formats
- Texture Topology Support
- Supported Transcode Targets
- Channel Mapping and Precision
- Lossless vs. Lossy
- sRGB Handling
- Alpha Handling
- Transcoder Decode Flags
- C++ API
- Container Parsing Details and Robustness
- Command Line Tool Usage
- Gotchas
- Example Code
- Planned Additions
- Related Pages
Background: .DDS (DirectDraw Surface) is Microsoft's long-standing container file format for GPU textures — it can hold ready-to-upload GPU block-compressed texture data (the BC1-BC7 family, historically known as DXT/S3TC) or plain uncompressed pixels, along with mipmap chains, cubemaps, and texture arrays. GPU block-compressed formats stay compressed in video memory and are decoded by the GPU on the fly while sampling — but each GPU format only works on hardware that supports it. Transcoding means converting texture data from one GPU format to another, so content shipped in one format can run on hardware that wants a different one (e.g. converting a BC7 texture to ASTC for mobile devices).
Basis Universal v2.50's transcoder module can read standard .DDS texture files and transcode them to the other supported LDR GPU texture formats or raster images — using the same API style as the .KTX2 and .basis transcoders. The implementation lives in transcoder/basisu_dds_transcoder.inl, which is compiled into the single-file transcoder (transcoder/basisu_transcoder.cpp). It's self-contained and depends on no encoder code. The file also contains a set of vendor-neutral BC1-BC5 block unpackers (namespace basist::bcu) which the DDS transcoder uses.
The public class is basist::dds_transcoder, declared in transcoder/basisu_transcoder.h. The API deliberately mirrors basist::ktx2_transcoder: the same init() → start_transcoding() → transcode_image_level() lifecycle, the same ktx2_image_level_info struct for per-image queries, the same level/layer/face indexing, and the same "get_layers() returns 0 for non-array textures" convention. If you've integrated the KTX2 transcoder, the DDS transcoder will feel identical.
The DDS transcoder is currently LDR-only. BC6H, the signed BCn variants (BC4/BC5 SNORM), float/half-float formats, and shared-exponent formats are rejected at init() time. (BC6H/HDR DDS support is planned — see Planned Additions.)
Compile-time gating: the container parser and same-format passthrough are always available. The decode→re-encode paths (everything else) require BASISD_SUPPORT_XUASTC (enabled by default) — without it, only passthrough transcodes succeed.
For a given source format and requested target format, transcode_image_level() takes one of three paths:
When the requested target format matches the file's contained format, the block data is copied verbatim — no decoding, no quality loss, and very fast. This applies to BC1, BC3, BC4, BC5, and BC7 sources transcoded to themselves, and to canonical memory-order R8G8B8A8 sources transcoded to cTFRGBA32. (BC2 sources are the exception — see Gotchas.)
Block-compressed formats don't store pixels — they store block parameters: a few representative colors ("endpoints"), a pattern assigning pixels to them, and per-pixel blend weights. BC7 and ASTC are structurally similar enough (BC7 is essentially a simpler ASTC) that these parameters can often be converted directly between the two formats — we call this latent-to-latent transcoding.
When the source is BC7 and the target is ASTC LDR 4x4, the transcoder typically performs a true latent-to-latent transcode: the BC7 block parameters (mode, endpoints, partition, weights) are mapped directly to ASTC block parameters, without texel-wise recompression. This is typically near-lossless, with a fidelity hierarchy by block type:
- Solid color BC7 blocks convert losslessly (to ASTC solid-color/void-extent blocks).
- Single subset blocks convert with the least loss.
- 2 subset blocks incur a little more loss, and 3 subset blocks a little more still.
The transcoder tries multiple ways of converting each block to ASTC, and also exploits ASTC weight grid downsampling. When a BC7 2 or 3 subset block uses a partition pattern that can't be mapped to ASTC at all, the transcoder falls back to unpacking that block to texels and re-packing it to ASTC in real-time (a rare case in practice).
This is the reverse direction of the XUASTC LDR codec's direct ASTC→BC7 transcoding path, and the same machinery the XUBC7 codec uses for its BC7→ASTC target. It makes a BC7 .DDS file a first-class citizen on ASTC-target platforms — not just another double-compression case.
Every other source→target combination decodes the source to 32-bit RGBA texels. What happens next depends on the target:
-
Uncompressed targets: the decoded texels are simply written out — there's no encoding step. So converting, say, a B8G8R8A8 source to
cTFRGBA32is a lossless channel reorder, even though it flows through this same decode path. (Only the 16-bit targets, RGB565/RGBA4444, quantize.) -
Compressed targets: the texels are re-encoded using the transcoder's built-in real-time block encoders:
bc7ffor BC7 and ASTC targets,etc1ffor ETC1, plus the EAC, BC1, BC4, and PVRTC1 packers.
Note that ASTC output from non-BC7 sources is BC7-mediated. Internally, we currently encode to ASTC by running the bc7f real-time BC7 encoder in a special mode that limits the partition patterns to only those that are ASTC compatible, then we transcode its BC7 output directly to ASTC.
DDS files identify their pixel format in one of two ways: classic DX9-style headers use a fourCC code (a four-character tag like DXT1) or a set of channel bit-masks, while modern files add a DX10 extension header carrying an explicit DXGI_FORMAT value. Both styles are supported.
| Source | DX9 fourCC(s) | DXGI formats (DX10 header) | Notes |
|---|---|---|---|
| BC1 | DXT1 |
BC1_UNORM, BC1_UNORM_SRGB
|
Punchthrough alpha supported |
| BC2 |
DXT2, DXT3
|
BC2_UNORM, BC2_UNORM_SRGB
|
Decode-only (see Gotchas) |
| BC3 |
DXT4, DXT5
|
BC3_UNORM, BC3_UNORM_SRGB
|
|
| BC4 |
ATI1, BC4U
|
BC4_UNORM |
Unsigned only |
| BC5 |
ATI2, BC5U
|
BC5_UNORM |
Unsigned only |
| BC7 | (none — BC7 has no DX9 fourCC) |
BC7_UNORM, BC7_UNORM_SRGB
|
DXT2 and DXT4 (the premultiplied-alpha variants) are accepted and decoded byte-identically to DXT3/DXT5 — the premultiplication is treated as a consumer semantic, not decoded specially. (The DX10 header's miscFlags2 alpha-mode field is likewise ignored.)
DX9-style channel-mask formats are accepted only if they exactly match a whitelist of well-known layouts (arbitrary/exotic masks are deliberately rejected rather than best-effort decoded):
- 16-bit: R5G6B5, A1R5G5B5, X1R5G5B5, A4R4G4B4, X4R4G4B4
- 24-bit: R8G8B8 (BGR in memory), B8G8R8 (RGB in memory)
- 32-bit: A8R8G8B8/X8R8G8B8 (BGRA/BGRX in memory), A8B8G8R8/X8B8G8R8 (RGBA/RGBX in memory)
- Luminance/alpha specials: L8, A8L8, A8
DX10 (DXGI) uncompressed formats:
-
32-bit:
R8G8B8A8_UNORM(+_SRGB),B8G8R8A8_UNORM(+_SRGB),B8G8R8X8_UNORM(+_SRGB) -
16-bit:
B5G6R5_UNORM,B5G5R5A1_UNORM,B4G4R4A4_UNORM -
Single/dual channel:
R8_UNORM,R8G8_UNORM,A8_UNORM
The exact physical stored layout is reported by get_dds_format() (a dds_format enum value), which is more specific than the closest-match transcoder_texture_format returned by get_format().
BC6H and all float/half/HDR formats, BC4_SNORM/BC5_SNORM (the signed variants), ASTC-in-DDS, palettized/YUV/bump formats, non-whitelisted channel masks, and any unrecognized fourCC. Rejection happens cleanly at init() time.
Supported: 2D textures, full mipmap chains, cubemaps (+mips), texture arrays (+mips), and cubemap arrays — arrays require the DX10 header (DX9 has no array concept).
Rejected: 1D textures, 3D/volume textures, and partial (incomplete) DX9 cubemaps — all six faces must be present.
Limits: maximum dimension 32768, maximum array size 65536. Mip counts are validated against the maximum possible chain for the texture's dimensions (a lying header is rejected).
Non-multiple-of-4 dimensions work as expected for BCn sources: DDS stores exact texel dimensions with storage rounded up to whole 4x4 blocks, and transcodes to uncompressed targets clip back to the exact dimensions.
Any supported source can be transcoded to any of these targets (the target set is source-independent — e.g. an uncompressed BGRA .DDS can be transcoded to BC7 or ASTC):
- Compressed: ETC1 RGB, ETC2 RGBA, ETC2 EAC R11, ETC2 EAC RG11, BC1 RGB, BC3 RGBA, BC4 R, BC5 RG, BC7 RGBA, ASTC LDR 4x4 RGBA, PVRTC1 4bpp RGB, PVRTC1 4bpp RGBA
- Uncompressed: RGBA32, RGB565, RGBA4444
(For guidance on which GPU texture formats different platforms/devices actually want, Unity's per-platform texture format documentation is a good general reference.)
Two constraints:
- PVRTC1 targets require power-of-2 dimensions (both width and height), and a tight output pitch.
- In builds without
BASISD_SUPPORT_XUASTC, only passthrough targets work.
Use is_transcode_format_supported() to query support (it accounts for the PVRTC1 constraint for the loaded texture's dimensions).
All sources decode internally to 32-bit RGBA texels:
- BC4 decodes into the red channel only (G=B=0, A=255) — it is not replicated to RGB as luminance. A BC4_R target with default channel settings picks red back up correctly, but a target reading green or blue will see 0.
- BC5 decodes into red + green (B=0, A=255).
- L8/A8L8 replicate luminance into all three RGB channels; A8 decodes as (0,0,0,A).
- Sub-byte channels (5/6/4/1-bit) expand to 8 bits by standard bit replication (the GPU/DirectXTex UNORM convention — e.g. 5-bit
v→(v<<3)|(v>>2)). - BC2's explicit 4-bit alpha expands as
a*17. - Absent channels decode opaque: formats without an alpha channel (and the X8/X-variants, which force it) produce A=255.
- BC3's color sub-block is always decoded in 4-color mode per the D3D spec (the punchthrough mode is BC1-only); BC3 alpha comes solely from its interpolated alpha block.
-
Lossless: passthrough copies (same-format), and transcoding any uncompressed source to
cTFRGBA32— that's pure channel reordering/expansion, with nothing quantized. Sub-byte sources (565/1555/4444) expand losslessly to 8 bits via bit replication. - Near-lossless: the BC7→ASTC 4x4 latent path — solid-color blocks convert losslessly, single subset blocks with the least loss, and 2/3 subset blocks with slightly more (see the three paths above).
- Lossy: every decode→re-encode to a compressed target is a real-time re-encode. For compressed sources this is double block-compression — the source's compression loss compounds with the target's. Prefer passthrough or the BC7→ASTC path when quality matters.
(sRGB is the standard color space for photographic/albedo content; GPUs can convert sRGB texture data to linear light automatically while sampling, but only if the texture is tagged as sRGB — which is why file-level sRGB metadata matters.)
is_srgb() reports a best-effort hint that never affects decoded pixel values. It exists so the caller can apply the correct transfer function downstream — most commonly by creating the GPU texture with an sRGB format (so the hardware converts sRGB→linear while sampling/rendering), or, if writing the transcoded data to an output file, by propagating the transfer function into that file's metadata.
- DX10 headers can signal sRGB explicitly:
_UNORM_SRGBDXGI variants report sRGB; plain_UNORMvariants report linear. - DX9 headers carry no sRGB signal, so DX9 sources default to sRGB (photographic/albedo content is the common case). This means the same BC format reports differently by container: a DX9
DXT1file reports sRGB while a DX10BC1_UNORMfile reports linear. - BC4, BC5, R8, R8G8, and A8 are always reported linear (single/dual-channel data has no sRGB concept).
- Note real-world DX10 files frequently mis-tag sRGB content as
_UNORM, so tools that care should offer a manual override.
get_has_alpha() is decided purely from the format — pixels are never scanned:
- Reports alpha: BC1 (conservatively — any block may use punchthrough alpha, so even fully opaque BC1 files report alpha), BC2, BC3, BC7, A8, A8L8, and any uncompressed layout with a nonzero alpha mask.
- Reports opaque: BC4, BC5, L8, R8, R8G8, and the X-variant (X8R8G8B8 etc.) layouts.
Two basisu_decode_flags are honored by the DDS transcode path:
-
cDecodeFlagsHighQuality: selects higher-quality (slower) real-time encoder modes for BC7/ASTC, EAC, and BC1/BC3 targets. -
cDecodeFlagsTranscodeAlphaDataToOpaqueFormats: transcodes the alpha channel instead of RGB — effective for ETC1 and PVRTC1 targets only (it's ignored for ASTC and the other targets).
The other decode flags have no effect on this path.
The lifecycle matches ktx2_transcoder:
-
init(pData, data_size)— parses and validates the entire file (headers, format, every slice offset/size). The data pointer is borrowed and must remain valid for the transcoder's lifetime. Returns false on any malformed or unsupported input. -
start_transcoding()— for DDS this just confirms init succeeded (there are no global tables to unpack, unlike .basis/ETC1S). - Query:
get_width(),get_height(),get_levels(),get_layers()(0 = not an array),get_faces()(6 = cubemap),get_has_alpha(),is_srgb(),get_format()(closest-matchtranscoder_texture_format),get_source_kind(),get_dds_format()(exact physical layout), andget_image_level_info()(fills the samektx2_image_level_infostruct the KTX2 transcoder uses). -
transcode_image_level(...)— transcodes one image. See the parameter reference below.
bool transcode_image_level(
uint32_t level_index, uint32_t layer_index, uint32_t face_index,
void* pOutput_blocks, uint32_t output_blocks_buf_size_in_blocks_or_pixels,
transcoder_texture_format fmt,
uint32_t decode_flags = 0,
uint32_t output_row_pitch_in_blocks_or_pixels = 0,
uint32_t output_rows_in_pixels = 0,
int channel0 = -1, int channel1 = -1);The signature is parameter-for-parameter identical to ktx2_transcoder::transcode_image_level(), except there's no trailing per-thread state pointer (the DDS path doesn't need one).
-
level_index,layer_index,face_index— which stored image to transcode: mip level, array layer, and cubemap face. For non-array textures passlayer_index0 (even thoughget_layers()returns 0), and for non-cubemaps passface_index0. Out-of-range indices return false. -
pOutput_blocks— the destination buffer. NULL returns false. -
output_blocks_buf_size_in_blocks_or_pixels— the destination buffer's capacity, in blocks for compressed targets or in pixels for uncompressed targets — NOT in bytes. (For a compressed target, bytes = blocks × bytes-per-block, e.g. 16 for BC7/BC3/ASTC, 8 for BC1/BC4/ETC1.) A too-small value returns false. -
fmt— the targettranscoder_texture_format. If it equals the file's contained format, the call is a fast passthrough copy. Unsupported targets return false — checkis_transcode_format_supported()first. -
decode_flags— optionalbasisu_decode_flagsbits; onlycDecodeFlagsHighQualityandcDecodeFlagsTranscodeAlphaDataToOpaqueFormatshave any effect on this path (see Transcoder Decode Flags). -
output_row_pitch_in_blocks_or_pixels— the destination row stride, in blocks (compressed targets) or pixels (uncompressed targets). Pass 0 for tightly packed (the image's natural stride:num_blocks_x, orwidthin pixels). For compressed targets the pitch may be larger than the image (padding between rows) but never smaller — a smaller pitch returns false. For uncompressed targets a smaller pitch clips the output to the left columns. PVRTC1 requires a tight pitch (0 or exactlynum_blocks_x). -
output_rows_in_pixels— for uncompressed targets only: the number of output rows. Pass 0 for the image's full height; a smaller value clips vertically. Ignored for compressed targets. -
channel0,channel1— source channel selection for the single/dual-channel targets, ascolor32channel indices (0=R, 1=G, 2=B, 3=A); -1 selects the target's default. Consumed only by BC4_R / EAC_R11 (channel0, default 0=R) and BC5_RG / EAC_RG11 (channel0default 0=R,channel1default 1=G). Note the BC5/RG11channel1default here is green (1), not alpha — this intentionally differs from the .basis/.KTX2 transcoders' channel conventions, and instead follows how BC4/BC5 data actually decodes: a BC4 DDS source decodes into R only (G=B=0, A=255), and a BC5 source into R+G (B=0, A=255). With these defaults, BC4→BC4 and BC5→BC5 round-trip correctly; the KTX2-style alpha convention doesn't fit DDS sources (a BC5 source has nothing in alpha). Pass explicit channel indices if you need a different mapping (e.g.channel1 = 3for the R,A convention).
Returns false on: uninitialized transcoder or NULL output, out-of-range indices, unsupported target, too-small buffer or pitch, PVRTC1 constraint violations, or any internal encode failure — and, in builds without BASISD_SUPPORT_XUASTC, any non-passthrough target.
- A too-small output buffer returns false with nothing written. Every path validates the required extent before any writes — there is no partial-write-then-fail case. (When the output is clipped via a smaller pitch/rows, the buffer requirement is computed from the clipped extent, not the full image.)
-
Uncompressed targets support clipping. A pitch smaller than the image width writes only the left columns; an
output_rows_in_pixelssmaller than the height writes only the top rows — both are supported clips that return true, not errors. Compressed targets are the opposite: the pitch may be larger than the image (row padding) but a smaller pitch returns false, and the rows parameter is ignored. - Nothing is ever zeroed. With an expanded pitch (or extra rows), the padding between/after the written data is left untouched — it keeps whatever the caller's buffer already contained. Pre-clear the buffer if you need deterministic padding.
-
Non-multiple-of-4 dimensions: the source's edge blocks decode with edge-replicated texels (no out-of-bounds reads). Transcoding to an uncompressed target writes only the real
width × heighttexels; transcoding to a compressed target writes whole 4x4 edge blocks whose padded texels carry the replicated edge colors. -
Zero means default, tiny means tiny. Pitch 0 = tightly packed; rows 0 = full image height. But explicit small values are honored: e.g.
output_rows_in_pixels = 1legitimately writes a single pixel row (uncompressed targets). - No caller input triggers an assert — invalid sizes, pitches, and indices all return false (or clip, per the rules above).
Beyond the KTX2-style surface, the DDS transcoder exposes lower-level public methods for callers that want to locate and process the stored texture bytes themselves (each mipmap level, cubemap face, and array layer), instead of going through transcode_image_level():
-
get_slice_desc(slice_desc& out, level, layer, face)— fills a descriptor for one physical image slice:
struct slice_desc
{
uint64_t m_ofs; // byte offset into the init() data (see get_data())
uint32_t m_size; // byte size of this slice's stored data
uint32_t m_width, m_height; // original (unpadded) texel dimensions of this mip
uint32_t m_num_blocks_x, m_num_blocks_y; // 4x4-block grid = ceil(dim/4) (for compressed sources)
uint32_t m_row_pitch; // uncompressed sources: bytes per row; 0/unused for compressed
};-
get_data()/get_data_size()— the borrowed file data passed toinit()(valid for the transcoder's lifetime). A slice's raw stored bytes are[get_data() + desc.m_ofs, get_data() + desc.m_ofs + desc.m_size). Since this is the whole file, the raw 128-byte DDS header (plus the 20-byte DX10 extension header, when present) is also accessible at the start — though there's no parsed-header accessor; the header's interpreted contents are exposed through the query methods above. -
get_total_slices()— the number of physical image slices =(layers ? layers : 1) * faces * levels. On disk (and in slice order), the layout is array layer major, then face, then mip level — matching the standard Microsoft DDS layout.
Together these let you, for example, iterate every mipmap level of every face and hand the raw BC7 block data straight to your GPU upload path — using the transcoder purely as a validating DDS parser.
Transcoding is single-threaded per call; callers can parallelize across levels/layers/faces themselves.
For working example code, see the example_transcoding sample, which uses dds_transcoder alongside the KTX2 and .basis transcoders. The JavaScript/WebAssembly API also exposes the DDS transcoder — see webgl/transcoder/basis_wrappers.cpp.
Details useful when debugging why a particular .DDS file is accepted or rejected. (Microsoft's references for the on-disk structures: DDS_HEADER, DDS_PIXELFORMAT, DDS_HEADER_DXT10.)
- The parser reads every header field by explicit byte offset (endian- and alignment-safe, no struct casting).
dwSizemust be exactly 124 and the pixel format size exactly 32. - Most legacy header flags are advisory and ignored. For uncompressed sources, rows are assumed tightly packed; the header's declared pitch is honored only in one narrow legacy case (DDSD_PITCH set with a DWORD-aligned pitch, matching DirectXTex behavior). For compressed sources,
dwPitchOrLinearSizeis ignored entirely. - A DX10 header
arraySizeof 0 is treated as 1 (some writers store 0 for non-array textures, violating the DX10 spec). - "Bad-tails" legacy files (tail mips stored with less than a full 4x4 block, as produced by texconv's
--bad-tailsoption) are rejected rather than mis-read — every tail mip must occupy a whole block. - All offset/size arithmetic is 64-bit, the total slice count is validated before any allocation, and an up-front file-size check stops a tiny hostile header (e.g. claiming a huge array) from forcing large allocations. Every slice's extent is validated against the file size.
Important: to unpack a .DDS file you must explicitly specify -unpack (or -validate/-info). Unlike .basis/.ktx/.ktx2 inputs, a bare basisu input.dds with no mode flag does not auto-unpack — it's treated as a compression source image and compressed (the encoder accepts uncompressed 32bpp RGBA/BGRA and half/float RGBA .DDS files as inputs — a separate code path from the transcoder documented on this page).
-
basisu -unpack input.dds— transcodes the file to every supported target format, writing .KTX GPU texture files (plus .DDS files for the BC1-7 targets) and round-tripped .PNG images. -
basisu -validate input.dds— runs the same full transcode/round-trip pipeline but writes nothing (validation only). -
basisu -info input.dds— prints the file's header information only. -
-unpack_format_only N— restricts unpacking to a single target format. -
-no_ktx/-no_dds— skip writing the GPU container files (PNGs only);-ktx_only/-dds_only— skip the PNGs (containers only).
In unpack/validate mode, .DDS inputs are detected by file magic ("DDS "), not extension.
-
BC2 (DXT2/DXT3) is decode-only. There is no
cTFBC2transcoder format, soget_format()reportscTFBC3_RGBAas a closest-match hint — but BC2's explicit 4-bit alpha is not byte-compatible with BC3, so BC2 never passes through verbatim; it always decodes and re-encodes (even to BC3).get_dds_format()still reports the exact BC2 layout. - Only true memory-order R8G8B8A8 gets the uncompressed passthrough. Every other uncompressed layout (BGRA, X8, 16-bit, 24-bit) decodes and repacks even when transcoding to its own closest-match format — losslessly to RGBA32, but not via memcpy.
-
get_has_alpha()andis_srgb()are format-level hints — see the sections above. In particular BC1 always reports alpha, and DX9 vs DX10 containers report sRGB differently for the same BC format. -
The BC1 target (
cTFBC1_RGB) is opaque-only: the real-time BC1 encoder emits 4-color (opaque) blocks and doesn't currently generate punchthrough alpha — so transcoding an alpha-bearing source to BC1 produces opaque output (consistent with the .basis/.KTX2 transcoders' BC1 target). BC1 sources with punchthrough alpha are fully supported: BC1→BC1 is a verbatim passthrough, and transcoding to alpha-capable targets (BC7, ETC2, RGBA32, etc.) preserves the punchthrough alpha. - Double block-compression: transcoding a compressed source to a different compressed target compounds two generations of compression loss. Passthrough and BC7→ASTC (latent-to-latent) are the quality-preserving paths.
-
PVRTC1 targets require power-of-2 textures — for non-pow2 textures,
is_transcode_format_supported()reports PVRTC1 as unsupported andtranscode_image_level()fails (returns false). - Volume (3D) textures, 1D textures, and partial cubemaps are rejected at init.
A minimal usage sketch — transcoding mip level 0 of a .DDS file to BC7:
#include "transcoder/basisu_transcoder.h"
#include <vector>
static bool transcode_dds_level0_to_bc7(const void* pData, uint32_t data_size, std::vector<uint8_t>& bc7_blocks)
{
using namespace basist;
// One-time global transcoder init (call once at startup).
basisu_transcoder_init();
// dds_transcoder BORROWS pData (no copy) - the buffer must stay alive for all transcode calls.
dds_transcoder dds;
if (!dds.init(pData, data_size))
return false; // unsupported or corrupt .DDS
if (!dds.start_transcoding())
return false;
// Query mip level 0 (layer 0, face 0).
ktx2_image_level_info info;
if (!dds.get_image_level_info(info, 0, 0, 0))
return false;
// BC7 is a 4x4 block format: 16 bytes per block. For compressed targets, the size
// argument to transcode_image_level() is a BLOCK count, not a byte count.
const uint32_t BC7_BYTES_PER_BLOCK = 16;
bc7_blocks.resize(info.m_total_blocks * BC7_BYTES_PER_BLOCK);
if (!dds.transcode_image_level(0, 0, 0, bc7_blocks.data(), info.m_total_blocks,
transcoder_texture_format::cTFBC7_RGBA))
return false;
return true;
}The same transcode_image_level() call transcodes to any supported target — e.g. cTFASTC_4x4_RGBA or cTFBC1_RGB (block targets), or cTFRGBA32 (then pass a pixel count as the size argument and size the buffer width * height * 4). Use is_transcode_format_supported() to check a target's availability first. For a complete working program (including .KTX2 and .basis transcoding), see the example_transcoding sample.
Planned additions to the DDS transcoder, all extending the latent-to-latent approach (converting block parameters directly, without real-time re-encoding in most cases — the exception being blocks whose partition pattern isn't ASTC compatible, as with the existing BC7→ASTC path):
-
BC6H → ASTC HDR 4x4 transcoding — bringing HDR .DDS support to the transcoder. Once this transcoder exists, our existing real-time BC6H encoder (
bc6hf) can be hooked up to it, giving us real-time encoding to ASTC HDR 4x4 too — which means uncompressed half-float .DDS files could be deployed anywhere (transcoded to BC6H or ASTC HDR 4x4 as the hardware requires). - Typically lossless BC1/BC3 → ASTC LDR 4x4 transcoding — this work is done; it just needs to be ported into the DDS transcoder.
- BC4/BC5 → ASTC LDR transcoding — should be doable with the same approach.
- Transcoder Module Definitions and Classes
- How to Use and Configure the Transcoder
- Transcoder Internals: Analytical Real-time Encoders and Transcoders — the real-time block encoders the decode+repack path uses
- XUBC7 Usage Guide — uses the same BC7→ASTC latent transcoding machinery