Skip to main content

ktx2/dfd/
generate.rs

1use alloc::vec::Vec;
2use core::num::NonZeroU8;
3
4use crate::dfd::{Basic, ChannelTypeQualifiers, DataFormatFlags, SampleInformation};
5use crate::{ColorModel, ColorPrimaries, Format, TransferFunction};
6
7// RGBSDA channel IDs (from the Khronos Data Format Specification).
8const CHANNEL_R: u8 = 0;
9const CHANNEL_G: u8 = 1;
10const CHANNEL_B: u8 = 2;
11const CHANNEL_STENCIL: u8 = 13;
12const CHANNEL_DEPTH: u8 = 14;
13const CHANNEL_ALPHA: u8 = 15;
14
15// YUVSDA channel IDs (same numeric namespace, different color model).
16const CHANNEL_Y: u8 = 0;
17const CHANNEL_U: u8 = 1;
18const CHANNEL_V: u8 = 2;
19
20// BCn compressed channel IDs.
21// Channel 0 is COLOR for BC1A–BC3, BC4, BC6H, BC7;
22const BC_COLOR: u8 = 0;
23// Single bit alpha channel for BC1.
24const BC1A_ALPHA: u8 = 1;
25const BC5_RED: u8 = 0;
26const BC5_GREEN: u8 = 1;
27const BC_ALPHA: u8 = 15;
28
29// ETC2/EAC compressed channel IDs.
30const ETC2_RED: u8 = 0;
31const ETC2_GREEN: u8 = 1;
32const ETC2_COLOR: u8 = 2;
33const ETC2_ALPHA: u8 = 15;
34
35// ASTC compressed channel IDs.
36const ASTC_DATA: u8 = 0;
37
38// PVRTC compressed channel IDs.
39const PVRTC_COLOR: u8 = 0;
40
41// E5B9G9R9 shared-exponent format constants.
42const RGB9E5_MANTISSA_BITS: u8 = 9;
43const RGB9E5_EXPONENT_BITS: u8 = 5;
44const RGB9E5_EXPONENT_OFFSET: u16 = 27;
45const RGB9E5_EXPONENT_BIAS: u32 = 15;
46const RGB9E5_EXPONENT_MAX: u32 = (1 << RGB9E5_EXPONENT_BITS) - 1;
47// The upper bound the KTX reference validator expects for the 9-bit mantissa,
48// derived from the shared-exponent encoding where the full-range mantissa
49// maps through the exponent bias.
50const RGB9E5_MANTISSA_UPPER: u32 = 8448;
51
52// DFD sample positions are in 1/256 of the texel block dimension.
53const SAMPLE_POS_ORIGIN: [u8; 4] = [0, 0, 0, 0];
54// 0.5 in 1/256 texel-block coordinates; for a 2x1 block, y=128 means the
55// center of the single pixel row.
56const HALF_TEXEL: u8 = 128;
57
58/// The numeric interpretation of sample data.
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum Datatype {
61    /// Unsigned normalized integer, mapped to `[0.0, 1.0]`.
62    Unorm,
63    /// Signed normalized integer, mapped to `[-1.0, 1.0]`.
64    Snorm,
65    /// Unsigned integer, not normalized.
66    Uint,
67    /// Signed integer, not normalized.
68    Sint,
69    /// Signed floating-point.
70    Sfloat,
71    /// Unsigned floating-point.
72    Ufloat,
73    /// Signed fixed-point with 5 fractional bits.
74    Sfixed5,
75}
76
77/// The order of chroma samples in a 4:2:2 subsampled format.
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum ChromaSubsamplingSampleOrder {
80    /// GBGR memory order (e.g. `G8B8G8R8_422_UNORM`).
81    Gbgr,
82    /// BGRG memory order (e.g. `B8G8R8G8_422_UNORM`).
83    Bgrg,
84}
85
86/// The transfer function inherent to a format, if any.
87///
88/// Formats with an sRGB counterpart have strict transfer function requirements:
89/// the UNORM variant must NOT use sRGB transfer (use the SRGB variant instead),
90/// and the SRGB variant MUST use sRGB transfer.
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub(super) enum FormatInherentTransferFunction {
93    /// No sRGB relationship. Transfer function defaults to linear and may be
94    /// overridden freely.
95    Linear,
96    /// This is the UNORM variant of a format that also has a dedicated SRGB
97    /// variant. The transfer function must NOT be sRGB — use the SRGB variant
98    /// of the format instead.
99    LinearWithSrgbCounterpart,
100    /// This IS the sRGB variant. The transfer function MUST be sRGB.
101    Srgb,
102}
103
104/// Error type for DFD generation and validation.
105#[derive(Debug)]
106#[non_exhaustive]
107pub enum BuildError {
108    /// The [`Format`] is not recognized by the DFD generation table.
109    UnsupportedFormat,
110    /// Premultiplied alpha was requested for a depth-stencil format, which has
111    /// no alpha channel.
112    DepthStencilPremultipliedAlpha,
113    /// A transfer function override was specified for a depth-stencil format.
114    /// Depth-stencil formats always use linear transfer.
115    DepthStencilTransferFunction,
116    /// A color primaries override was specified for a depth-stencil format.
117    /// Depth-stencil formats always use BT.709 primaries.
118    DepthStencilColorPrimaries,
119    /// A color model override was specified for a depth-stencil format.
120    /// Depth-stencil formats always use RGBSDA.
121    DepthStencilColorModel,
122    /// A color model override was specified for a compressed format. Compressed
123    /// formats must use their intrinsic color model.
124    CompressedColorModel,
125    /// The transfer function was set to sRGB, but this format has a dedicated
126    /// sRGB variant. Use the sRGB variant of the format instead.
127    SrgbTransferNotAllowed,
128    /// The transfer function was overridden to a non-sRGB value, but this
129    /// format is an sRGB variant and must use sRGB transfer.
130    SrgbTransferRequired,
131}
132
133impl core::fmt::Display for BuildError {
134    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
135        let str = match self {
136            Self::UnsupportedFormat => "format is not recognized by the DFD generation table",
137            Self::DepthStencilPremultipliedAlpha => {
138                "premultiplied alpha is not supported for depth-stencil formats (no alpha channel)"
139            }
140            Self::DepthStencilTransferFunction => {
141                "transfer function override is not supported for depth-stencil formats (always linear)"
142            }
143            Self::DepthStencilColorPrimaries => {
144                "color primaries override is not supported for depth-stencil formats (always BT.709)"
145            }
146            Self::DepthStencilColorModel => {
147                "color model override is not supported for depth-stencil formats (always RGBSDA)"
148            }
149            Self::CompressedColorModel => {
150                "color model override is not supported for compressed formats (must use intrinsic model)"
151            }
152            Self::SrgbTransferNotAllowed => {
153                "sRGB transfer function is not allowed for this format; use the dedicated sRGB variant instead"
154            }
155            Self::SrgbTransferRequired => "this format is an sRGB variant and must use sRGB transfer function",
156        };
157
158        f.pad(str)
159    }
160}
161
162#[cfg(feature = "std")]
163impl std::error::Error for BuildError {}
164
165/// Describes how to build a DFD for a given format.
166#[derive(Debug, Clone, Copy, PartialEq, Eq)]
167pub(super) enum Builder {
168    /// Standard uncompressed or simple format.
169    Standard {
170        /// How sample values are interpreted numerically.
171        datatype: Datatype,
172        /// Whether the format uses sRGB transfer.
173        srgb: FormatInherentTransferFunction,
174        /// Number of bytes per texel.
175        bytes_per_texel: u8,
176        /// Size in bytes of the underlying data type for endian conversion.
177        ///
178        /// - **Component formats** (R8G8B8A8, R16G16, etc.): individual
179        ///   component size in bytes.
180        /// - **nPACK16 formats** (R10X6G10X6_2PACK16, etc.): 16-bit word
181        ///   size (2), not the total texel size.
182        /// - **Single-word pack formats** (R4G4_PACK8, R5G6B5_PACK16,
183        ///   A8B8G8R8_PACK32): bytes_per_texel (the entire pack unit).
184        type_size: u8,
185        /// Bit width of each channel, in ascending bit-offset order.
186        bit_count: &'static [u8],
187        /// Bit offset of each channel within the texel, in ascending order.
188        bit_offset: &'static [u8],
189        /// RGBSDA channel ID for each sample, in ascending bit-offset order.
190        channel_ids: &'static [u8],
191    },
192    /// Combined depth-stencil format with mixed datatypes per channel.
193    DepthStencil {
194        /// Bit width of the depth channel (16, 24, or 32).
195        depth_bits: u8,
196        /// Numeric interpretation of the depth channel.
197        depth_datatype: Datatype,
198    },
199    /// Shared-exponent format (`E5B9G9R9_UFLOAT_PACK32`).
200    Rgb9e5,
201    /// Compressed block format (BCn, ETC2, EAC, ASTC, PVRTC).
202    Compressed {
203        /// Compressed-format color model (e.g. `BC1A`, `ETC2`, `ASTC`).
204        color_model: ColorModel,
205        /// Whether the format uses sRGB transfer.
206        srgb: FormatInherentTransferFunction,
207        /// Texel block dimensions `[width, height, depth]`.
208        block_dimensions: [u8; 3],
209        /// Bytes per compressed block.
210        bytes_per_block: u8,
211        /// Numeric interpretation of the compressed data.
212        datatype: Datatype,
213        /// Channel types for each sample. One entry = single sample covering the
214        /// whole block. Two entries = two 64-bit halves (first at bit offset 0,
215        /// second at bit offset 64).
216        channel_types: &'static [u8],
217    },
218    /// 4:2:2 horizontally subsampled format.
219    Subsampled422 {
220        /// The order of chroma samples in memory.
221        sample_order: ChromaSubsamplingSampleOrder,
222        /// Significant bit width per channel (8, 10, 12, or 16).
223        bit_width: u8,
224    },
225}
226
227impl Builder {
228    /// Returns the `type_size` value for the KTX2 header.
229    ///
230    /// This is the size in bytes of the underlying data type, used for
231    /// endian conversion on big-endian targets.
232    pub fn type_size(&self) -> u32 {
233        let type8 = match *self {
234            Builder::Standard { type_size, .. } => type_size,
235            Builder::Compressed { .. } => 1,
236            // All three combined depth-stencil formats are single-plane
237            // packed in KTX2; type_size = the depth component's native size.
238            Builder::DepthStencil { depth_bits, .. } => match depth_bits {
239                16 => 2,
240                24 => 4, // X8_D24 packed into 32 bits
241                32 => 4,
242                _ => unreachable!("unsupported depth bit width: {depth_bits}"),
243            },
244            Builder::Rgb9e5 => 4,                                            // PACK32
245            Builder::Subsampled422 { bit_width, .. } => (bit_width + 7) / 8, // Round up to whole bytes.
246        };
247        type8 as u32
248    }
249
250    /// Returns the [`Builder`] for a given [`Format`], or `None` if the
251    /// format is unknown.
252    ///
253    /// All `Standard` entries have `bit_offset`, `bit_count`, and `channel_ids`
254    /// in ascending bit-offset order, matching the DFD spec's sample ordering
255    /// requirement.
256    ///
257    /// NOTE: If new variants are added to [`Format`] in `enums.rs`, a
258    /// corresponding match arm must be added here.
259    pub fn from_format(format: Format) -> Option<Builder> {
260        use ColorModel as Cm;
261        use Datatype as Dt;
262        use Format as F;
263        use FormatInherentTransferFunction::{Linear, LinearWithSrgbCounterpart as Counterpart, Srgb};
264
265        const R: u8 = CHANNEL_R;
266        const G: u8 = CHANNEL_G;
267        const B: u8 = CHANNEL_B;
268        const A: u8 = CHANNEL_ALPHA;
269        const D: u8 = CHANNEL_DEPTH;
270        const S: u8 = CHANNEL_STENCIL;
271
272        fn s(
273            datatype: Datatype,
274            srgb: FormatInherentTransferFunction,
275            bytes_per_texel: u8,
276            type_size: u8,
277            bit_count: &'static [u8],
278            bit_offset: &'static [u8],
279            channel_ids: &'static [u8],
280        ) -> Builder {
281            Builder::Standard {
282                datatype,
283                srgb,
284                bytes_per_texel,
285                type_size,
286                bit_count,
287                bit_offset,
288                channel_ids,
289            }
290        }
291
292        fn ds(depth_bits: u8, depth_datatype: Datatype) -> Builder {
293            Builder::DepthStencil {
294                depth_bits,
295                depth_datatype,
296            }
297        }
298
299        fn c422(sample_order: ChromaSubsamplingSampleOrder, bit_width: u8) -> Builder {
300            Builder::Subsampled422 {
301                sample_order,
302                bit_width,
303            }
304        }
305
306        #[allow(clippy::too_many_arguments)]
307        fn c(
308            color_model: ColorModel,
309            srgb: FormatInherentTransferFunction,
310            block_dimensions: [u8; 3],
311            bytes_per_block: u8,
312            datatype: Datatype,
313            channel_types: &'static [u8],
314        ) -> Builder {
315            Builder::Compressed {
316                color_model,
317                srgb,
318                block_dimensions,
319                bytes_per_block,
320                datatype,
321                channel_types,
322            }
323        }
324
325        // All Standard entries below list (bit_count, bit_offset, channel_ids)
326        // in ascending bit-offset order so that build_basic() can emit samples
327        // directly without a post-hoc sort.
328        //
329        // NOTE: If new arms are added here, the corresponding variant must exist
330        // in [`Format`] in `enums.rs`.
331        #[rustfmt::skip]
332        let res = match format {
333            // ---- Packed 8-bit (MSB→LSB: R4, G4) ----
334            F::R4G4_UNORM_PACK8                   => s(Dt::Unorm,   Linear,      1,  1, &[4, 4],           &[0, 4],            &[G, R]),
335
336            // ---- Packed 16-bit ----
337            // MSB→LSB: R4, G4, B4, A4 → ascending: A@0, B@4, G@8, R@12
338            F::R4G4B4A4_UNORM_PACK16              => s(Dt::Unorm,   Linear,      2,  2, &[4, 4, 4, 4],     &[0, 4, 8, 12],     &[A, B, G, R]),
339            F::B4G4R4A4_UNORM_PACK16              => s(Dt::Unorm,   Linear,      2,  2, &[4, 4, 4, 4],     &[0, 4, 8, 12],     &[A, R, G, B]),
340            F::R5G6B5_UNORM_PACK16                => s(Dt::Unorm,   Linear,      2,  2, &[5, 6, 5],        &[0, 5, 11],        &[B, G, R]),
341            F::B5G6R5_UNORM_PACK16                => s(Dt::Unorm,   Linear,      2,  2, &[5, 6, 5],        &[0, 5, 11],        &[R, G, B]),
342            F::R5G5B5A1_UNORM_PACK16              => s(Dt::Unorm,   Linear,      2,  2, &[1, 5, 5, 5],     &[0, 1, 6, 11],     &[A, B, G, R]),
343            F::B5G5R5A1_UNORM_PACK16              => s(Dt::Unorm,   Linear,      2,  2, &[1, 5, 5, 5],     &[0, 1, 6, 11],     &[A, R, G, B]),
344            F::A1R5G5B5_UNORM_PACK16              => s(Dt::Unorm,   Linear,      2,  2, &[5, 5, 5, 1],     &[0, 5, 10, 15],    &[B, G, R, A]),
345
346            // ---- R8 ----
347            F::R8_UNORM                           => s(Dt::Unorm,   Counterpart, 1,  1, &[8],              &[0],               &[R]),
348            F::R8_SNORM                           => s(Dt::Snorm,   Linear,      1,  1, &[8],              &[0],               &[R]),
349            F::R8_UINT                            => s(Dt::Uint,    Linear,      1,  1, &[8],              &[0],               &[R]),
350            F::R8_SINT                            => s(Dt::Sint,    Linear,      1,  1, &[8],              &[0],               &[R]),
351            F::R8_SRGB                            => s(Dt::Unorm,   Srgb,        1,  1, &[8],              &[0],               &[R]),
352
353            // ---- R8G8 ----
354            F::R8G8_UNORM                         => s(Dt::Unorm,   Counterpart, 2,  1, &[8, 8],           &[0, 8],            &[R, G]),
355            F::R8G8_SNORM                         => s(Dt::Snorm,   Linear,      2,  1, &[8, 8],           &[0, 8],            &[R, G]),
356            F::R8G8_UINT                          => s(Dt::Uint,    Linear,      2,  1, &[8, 8],           &[0, 8],            &[R, G]),
357            F::R8G8_SINT                          => s(Dt::Sint,    Linear,      2,  1, &[8, 8],           &[0, 8],            &[R, G]),
358            F::R8G8_SRGB                          => s(Dt::Unorm,   Srgb,        2,  1, &[8, 8],           &[0, 8],            &[R, G]),
359
360            // ---- R8G8B8 ----
361            F::R8G8B8_UNORM                       => s(Dt::Unorm,   Counterpart, 3,  1, &[8, 8, 8],        &[0, 8, 16],        &[R, G, B]),
362            F::R8G8B8_SNORM                       => s(Dt::Snorm,   Linear,      3,  1, &[8, 8, 8],        &[0, 8, 16],        &[R, G, B]),
363            F::R8G8B8_UINT                        => s(Dt::Uint,    Linear,      3,  1, &[8, 8, 8],        &[0, 8, 16],        &[R, G, B]),
364            F::R8G8B8_SINT                        => s(Dt::Sint,    Linear,      3,  1, &[8, 8, 8],        &[0, 8, 16],        &[R, G, B]),
365            F::R8G8B8_SRGB                        => s(Dt::Unorm,   Srgb,        3,  1, &[8, 8, 8],        &[0, 8, 16],        &[R, G, B]),
366
367            // ---- B8G8R8 ----
368            F::B8G8R8_UNORM                       => s(Dt::Unorm,   Counterpart, 3,  1, &[8, 8, 8],        &[0, 8, 16],        &[B, G, R]),
369            F::B8G8R8_SNORM                       => s(Dt::Snorm,   Linear,      3,  1, &[8, 8, 8],        &[0, 8, 16],        &[B, G, R]),
370            F::B8G8R8_UINT                        => s(Dt::Uint,    Linear,      3,  1, &[8, 8, 8],        &[0, 8, 16],        &[B, G, R]),
371            F::B8G8R8_SINT                        => s(Dt::Sint,    Linear,      3,  1, &[8, 8, 8],        &[0, 8, 16],        &[B, G, R]),
372            F::B8G8R8_SRGB                        => s(Dt::Unorm,   Srgb,        3,  1, &[8, 8, 8],        &[0, 8, 16],        &[B, G, R]),
373
374            // ---- R8G8B8A8 ----
375            F::R8G8B8A8_UNORM                     => s(Dt::Unorm,   Counterpart, 4,  1, &[8, 8, 8, 8],     &[0, 8, 16, 24],    &[R, G, B, A]),
376            F::R8G8B8A8_SNORM                     => s(Dt::Snorm,   Linear,      4,  1, &[8, 8, 8, 8],     &[0, 8, 16, 24],    &[R, G, B, A]),
377            F::R8G8B8A8_UINT                      => s(Dt::Uint,    Linear,      4,  1, &[8, 8, 8, 8],     &[0, 8, 16, 24],    &[R, G, B, A]),
378            F::R8G8B8A8_SINT                      => s(Dt::Sint,    Linear,      4,  1, &[8, 8, 8, 8],     &[0, 8, 16, 24],    &[R, G, B, A]),
379            F::R8G8B8A8_SRGB                      => s(Dt::Unorm,   Srgb,        4,  1, &[8, 8, 8, 8],     &[0, 8, 16, 24],    &[R, G, B, A]),
380
381            // ---- B8G8R8A8 ----
382            F::B8G8R8A8_UNORM                     => s(Dt::Unorm,   Counterpart, 4,  1, &[8, 8, 8, 8],     &[0, 8, 16, 24],    &[B, G, R, A]),
383            F::B8G8R8A8_SNORM                     => s(Dt::Snorm,   Linear,      4,  1, &[8, 8, 8, 8],     &[0, 8, 16, 24],    &[B, G, R, A]),
384            F::B8G8R8A8_UINT                      => s(Dt::Uint,    Linear,      4,  1, &[8, 8, 8, 8],     &[0, 8, 16, 24],    &[B, G, R, A]),
385            F::B8G8R8A8_SINT                      => s(Dt::Sint,    Linear,      4,  1, &[8, 8, 8, 8],     &[0, 8, 16, 24],    &[B, G, R, A]),
386            F::B8G8R8A8_SRGB                      => s(Dt::Unorm,   Srgb,        4,  1, &[8, 8, 8, 8],     &[0, 8, 16, 24],    &[B, G, R, A]),
387
388            // ---- A8B8G8R8 packed 32-bit (MSB→LSB: A8, B8, G8, R8) ----
389            F::A8B8G8R8_UNORM_PACK32              => s(Dt::Unorm,   Counterpart, 4,  4, &[8, 8, 8, 8],     &[0, 8, 16, 24],    &[R, G, B, A]),
390            F::A8B8G8R8_SNORM_PACK32              => s(Dt::Snorm,   Linear,      4,  4, &[8, 8, 8, 8],     &[0, 8, 16, 24],    &[R, G, B, A]),
391            F::A8B8G8R8_UINT_PACK32               => s(Dt::Uint,    Linear,      4,  4, &[8, 8, 8, 8],     &[0, 8, 16, 24],    &[R, G, B, A]),
392            F::A8B8G8R8_SINT_PACK32               => s(Dt::Sint,    Linear,      4,  4, &[8, 8, 8, 8],     &[0, 8, 16, 24],    &[R, G, B, A]),
393            F::A8B8G8R8_SRGB_PACK32               => s(Dt::Unorm,   Srgb,        4,  4, &[8, 8, 8, 8],     &[0, 8, 16, 24],    &[R, G, B, A]),
394
395            // ---- A2R10G10B10 packed 32-bit (MSB→LSB: A2, R10, G10, B10) ----
396            F::A2R10G10B10_UNORM_PACK32           => s(Dt::Unorm,   Linear,      4,  4, &[10, 10, 10, 2],  &[0, 10, 20, 30],   &[B, G, R, A]),
397            F::A2R10G10B10_SNORM_PACK32           => s(Dt::Snorm,   Linear,      4,  4, &[10, 10, 10, 2],  &[0, 10, 20, 30],   &[B, G, R, A]),
398            F::A2R10G10B10_UINT_PACK32            => s(Dt::Uint,    Linear,      4,  4, &[10, 10, 10, 2],  &[0, 10, 20, 30],   &[B, G, R, A]),
399            F::A2R10G10B10_SINT_PACK32            => s(Dt::Sint,    Linear,      4,  4, &[10, 10, 10, 2],  &[0, 10, 20, 30],   &[B, G, R, A]),
400
401            // ---- A2B10G10R10 packed 32-bit (MSB→LSB: A2, B10, G10, R10) ----
402            F::A2B10G10R10_UNORM_PACK32           => s(Dt::Unorm,   Linear,      4,  4, &[10, 10, 10, 2],  &[0, 10, 20, 30],   &[R, G, B, A]),
403            F::A2B10G10R10_SNORM_PACK32           => s(Dt::Snorm,   Linear,      4,  4, &[10, 10, 10, 2],  &[0, 10, 20, 30],   &[R, G, B, A]),
404            F::A2B10G10R10_UINT_PACK32            => s(Dt::Uint,    Linear,      4,  4, &[10, 10, 10, 2],  &[0, 10, 20, 30],   &[R, G, B, A]),
405            F::A2B10G10R10_SINT_PACK32            => s(Dt::Sint,    Linear,      4,  4, &[10, 10, 10, 2],  &[0, 10, 20, 30],   &[R, G, B, A]),
406
407            // ---- R16 ----
408            F::R16_UNORM                          => s(Dt::Unorm,   Linear,      2,  2, &[16],             &[0],               &[R]),
409            F::R16_SNORM                          => s(Dt::Snorm,   Linear,      2,  2, &[16],             &[0],               &[R]),
410            F::R16_UINT                           => s(Dt::Uint,    Linear,      2,  2, &[16],             &[0],               &[R]),
411            F::R16_SINT                           => s(Dt::Sint,    Linear,      2,  2, &[16],             &[0],               &[R]),
412            F::R16_SFLOAT                         => s(Dt::Sfloat,  Linear,      2,  2, &[16],             &[0],               &[R]),
413
414            // ---- R16G16 ----
415            F::R16G16_UNORM                       => s(Dt::Unorm,   Linear,      4,  2, &[16, 16],         &[0, 16],           &[R, G]),
416            F::R16G16_SNORM                       => s(Dt::Snorm,   Linear,      4,  2, &[16, 16],         &[0, 16],           &[R, G]),
417            F::R16G16_UINT                        => s(Dt::Uint,    Linear,      4,  2, &[16, 16],         &[0, 16],           &[R, G]),
418            F::R16G16_SINT                        => s(Dt::Sint,    Linear,      4,  2, &[16, 16],         &[0, 16],           &[R, G]),
419            F::R16G16_SFLOAT                      => s(Dt::Sfloat,  Linear,      4,  2, &[16, 16],         &[0, 16],           &[R, G]),
420
421            // ---- R16G16B16 ----
422            F::R16G16B16_UNORM                    => s(Dt::Unorm,   Linear,      6,  2, &[16, 16, 16],     &[0, 16, 32],       &[R, G, B]),
423            F::R16G16B16_SNORM                    => s(Dt::Snorm,   Linear,      6,  2, &[16, 16, 16],     &[0, 16, 32],       &[R, G, B]),
424            F::R16G16B16_UINT                     => s(Dt::Uint,    Linear,      6,  2, &[16, 16, 16],     &[0, 16, 32],       &[R, G, B]),
425            F::R16G16B16_SINT                     => s(Dt::Sint,    Linear,      6,  2, &[16, 16, 16],     &[0, 16, 32],       &[R, G, B]),
426            F::R16G16B16_SFLOAT                   => s(Dt::Sfloat,  Linear,      6,  2, &[16, 16, 16],     &[0, 16, 32],       &[R, G, B]),
427
428            // ---- R16G16B16A16 ----
429            F::R16G16B16A16_UNORM                 => s(Dt::Unorm,   Linear,      8,  2, &[16, 16, 16, 16], &[0, 16, 32, 48],   &[R, G, B, A]),
430            F::R16G16B16A16_SNORM                 => s(Dt::Snorm,   Linear,      8,  2, &[16, 16, 16, 16], &[0, 16, 32, 48],   &[R, G, B, A]),
431            F::R16G16B16A16_UINT                  => s(Dt::Uint,    Linear,      8,  2, &[16, 16, 16, 16], &[0, 16, 32, 48],   &[R, G, B, A]),
432            F::R16G16B16A16_SINT                  => s(Dt::Sint,    Linear,      8,  2, &[16, 16, 16, 16], &[0, 16, 32, 48],   &[R, G, B, A]),
433            F::R16G16B16A16_SFLOAT                => s(Dt::Sfloat,  Linear,      8,  2, &[16, 16, 16, 16], &[0, 16, 32, 48],   &[R, G, B, A]),
434
435            // ---- R32 ----
436            F::R32_UINT                           => s(Dt::Uint,    Linear,      4,  4, &[32],             &[0],               &[R]),
437            F::R32_SINT                           => s(Dt::Sint,    Linear,      4,  4, &[32],             &[0],               &[R]),
438            F::R32_SFLOAT                         => s(Dt::Sfloat,  Linear,      4,  4, &[32],             &[0],               &[R]),
439
440            // ---- R32G32 ----
441            F::R32G32_UINT                        => s(Dt::Uint,    Linear,      8,  4, &[32, 32],         &[0, 32],           &[R, G]),
442            F::R32G32_SINT                        => s(Dt::Sint,    Linear,      8,  4, &[32, 32],         &[0, 32],           &[R, G]),
443            F::R32G32_SFLOAT                      => s(Dt::Sfloat,  Linear,      8,  4, &[32, 32],         &[0, 32],           &[R, G]),
444
445            // ---- R32G32B32 ----
446            F::R32G32B32_UINT                     => s(Dt::Uint,    Linear,      12, 4, &[32, 32, 32],     &[0, 32, 64],       &[R, G, B]),
447            F::R32G32B32_SINT                     => s(Dt::Sint,    Linear,      12, 4, &[32, 32, 32],     &[0, 32, 64],       &[R, G, B]),
448            F::R32G32B32_SFLOAT                   => s(Dt::Sfloat,  Linear,      12, 4, &[32, 32, 32],     &[0, 32, 64],       &[R, G, B]),
449
450            // ---- R32G32B32A32 ----
451            F::R32G32B32A32_UINT                  => s(Dt::Uint,    Linear,      16, 4, &[32, 32, 32, 32], &[0, 32, 64, 96],   &[R, G, B, A]),
452            F::R32G32B32A32_SINT                  => s(Dt::Sint,    Linear,      16, 4, &[32, 32, 32, 32], &[0, 32, 64, 96],   &[R, G, B, A]),
453            F::R32G32B32A32_SFLOAT                => s(Dt::Sfloat,  Linear,      16, 4, &[32, 32, 32, 32], &[0, 32, 64, 96],   &[R, G, B, A]),
454
455            // ---- R64 ----
456            F::R64_UINT                           => s(Dt::Uint,    Linear,      8,  8, &[64],             &[0],               &[R]),
457            F::R64_SINT                           => s(Dt::Sint,    Linear,      8,  8, &[64],             &[0],               &[R]),
458            F::R64_SFLOAT                         => s(Dt::Sfloat,  Linear,      8,  8, &[64],             &[0],               &[R]),
459
460            // ---- R64G64 ----
461            F::R64G64_UINT                        => s(Dt::Uint,    Linear,      16, 8, &[64, 64],         &[0, 64],           &[R, G]),
462            F::R64G64_SINT                        => s(Dt::Sint,    Linear,      16, 8, &[64, 64],         &[0, 64],           &[R, G]),
463            F::R64G64_SFLOAT                      => s(Dt::Sfloat,  Linear,      16, 8, &[64, 64],         &[0, 64],           &[R, G]),
464
465            // ---- R64G64B64 ----
466            F::R64G64B64_UINT                     => s(Dt::Uint,    Linear,      24, 8, &[64, 64, 64],     &[0, 64, 128],      &[R, G, B]),
467            F::R64G64B64_SINT                     => s(Dt::Sint,    Linear,      24, 8, &[64, 64, 64],     &[0, 64, 128],      &[R, G, B]),
468            F::R64G64B64_SFLOAT                   => s(Dt::Sfloat,  Linear,      24, 8, &[64, 64, 64],     &[0, 64, 128],      &[R, G, B]),
469
470            // ---- R64G64B64A64 ----
471            F::R64G64B64A64_UINT                  => s(Dt::Uint,    Linear,      32, 8, &[64, 64, 64, 64], &[0, 64, 128, 192], &[R, G, B, A]),
472            F::R64G64B64A64_SINT                  => s(Dt::Sint,    Linear,      32, 8, &[64, 64, 64, 64], &[0, 64, 128, 192], &[R, G, B, A]),
473            F::R64G64B64A64_SFLOAT                => s(Dt::Sfloat,  Linear,      32, 8, &[64, 64, 64, 64], &[0, 64, 128, 192], &[R, G, B, A]),
474
475            // ---- Packed float (MSB→LSB: B10, G11, R11) ----
476            F::B10G11R11_UFLOAT_PACK32            => s(Dt::Ufloat,  Linear,      4,  4, &[11, 11, 10],     &[0, 11, 22],       &[R, G, B]),
477
478            // ---- Depth/stencil ----
479            F::D16_UNORM                          => s(Dt::Unorm,   Linear,      2,  2, &[16],             &[0],               &[D]),
480            F::X8_D24_UNORM_PACK32                => s(Dt::Unorm,   Linear,      4,  4, &[24],             &[0],               &[D]),
481            F::D32_SFLOAT                         => s(Dt::Sfloat,  Linear,      4,  4, &[32],             &[0],               &[D]),
482            F::S8_UINT                            => s(Dt::Uint,    Linear,      1,  1, &[8],              &[0],               &[S]),
483
484            // ---- Extended packed 16-bit ----
485            F::A4R4G4B4_UNORM_PACK16              => s(Dt::Unorm,   Linear,      2,  2, &[4, 4, 4, 4],     &[0, 4, 8, 12],     &[B, G, R, A]),
486            F::A4B4G4R4_UNORM_PACK16              => s(Dt::Unorm,   Linear,      2,  2, &[4, 4, 4, 4],     &[0, 4, 8, 12],     &[R, G, B, A]),
487            F::A1B5G5R5_UNORM_PACK16              => s(Dt::Unorm,   Linear,      2,  2, &[5, 5, 5, 1],     &[0, 5, 10, 15],    &[R, G, B, A]),
488
489            // ---- Extended Fs ----
490            F::R16G16_SFIXED5                     => s(Dt::Sfixed5, Linear,      4,  2, &[16, 16],         &[0, 16],           &[R, G]),
491            F::A8_UNORM                           => s(Dt::Unorm,   Linear,      1,  1, &[8],              &[0],               &[A]),
492
493            // ---- R10X6 unorm/uint ----
494            // nPACK16 Fs are MSB-justified: the N-bit value sits in the
495            // high bits of each 16-bit word, with (16-N) padding bits at the
496            // bottom. So R10X6 has bit_offset = 16 - 10 = 6 within each word.
497            F::R10X6_UNORM_PACK16                 => s(Dt::Unorm,   Linear,      2,  2, &[10],             &[6],               &[R]),
498            F::R10X6G10X6_UNORM_2PACK16           => s(Dt::Unorm,   Linear,      4,  2, &[10, 10],         &[6, 22],           &[R, G]),
499            F::R10X6G10X6B10X6A10X6_UNORM_4PACK16 => s(Dt::Unorm,   Linear,      8,  2, &[10, 10, 10, 10], &[6, 22, 38, 54],   &[R, G, B, A]),
500            F::R10X6_UINT_PACK16                  => s(Dt::Uint,    Linear,      2,  2, &[10],             &[6],               &[R]),
501            F::R10X6G10X6_UINT_2PACK16            => s(Dt::Uint,    Linear,      4,  2, &[10, 10],         &[6, 22],           &[R, G]),
502            F::R10X6G10X6B10X6A10X6_UINT_4PACK16  => s(Dt::Uint,    Linear,      8,  2, &[10, 10, 10, 10], &[6, 22, 38, 54],   &[R, G, B, A]),
503
504            // ---- R12X4 unorm/uint (MSB-justified, offset = 16 - 12 = 4) ----
505            F::R12X4_UNORM_PACK16                 => s(Dt::Unorm,   Linear,      2,  2, &[12],             &[4],               &[R]),
506            F::R12X4G12X4_UNORM_2PACK16           => s(Dt::Unorm,   Linear,      4,  2, &[12, 12],         &[4, 20],           &[R, G]),
507            F::R12X4G12X4B12X4A12X4_UNORM_4PACK16 => s(Dt::Unorm,   Linear,      8,  2, &[12, 12, 12, 12], &[4, 20, 36, 52],   &[R, G, B, A]),
508            F::R12X4_UINT_PACK16                  => s(Dt::Uint,    Linear,      2,  2, &[12],             &[4],               &[R]),
509            F::R12X4G12X4_UINT_2PACK16            => s(Dt::Uint,    Linear,      4,  2, &[12, 12],         &[4, 20],           &[R, G]),
510            F::R12X4G12X4B12X4A12X4_UINT_4PACK16  => s(Dt::Uint,    Linear,      8,  2, &[12, 12, 12, 12], &[4, 20, 36, 52],   &[R, G, B, A]),
511
512            // ---- R14X2 unorm/uint (MSB-justified, offset = 16 - 14 = 2) ----
513            F::R14X2_UNORM_PACK16                 => s(Dt::Unorm,   Linear,      2,  2, &[14],             &[2],               &[R]),
514            F::R14X2G14X2_UNORM_2PACK16           => s(Dt::Unorm,   Linear,      4,  2, &[14, 14],         &[2, 18],           &[R, G]),
515            F::R14X2G14X2B14X2A14X2_UNORM_4PACK16 => s(Dt::Unorm,   Linear,      8,  2, &[14, 14, 14, 14], &[2, 18, 34, 50],   &[R, G, B, A]),
516            F::R14X2_UINT_PACK16                  => s(Dt::Uint,    Linear,      2,  2, &[14],             &[2],               &[R]),
517            F::R14X2G14X2_UINT_2PACK16            => s(Dt::Uint,    Linear,      4,  2, &[14, 14],         &[2, 18],           &[R, G]),
518            F::R14X2G14X2B14X2A14X2_UINT_4PACK16  => s(Dt::Uint,    Linear,      8,  2, &[14, 14, 14, 14], &[2, 18, 34, 50],   &[R, G, B, A]),
519
520            // ---- Shared-exponent ----
521            F::E5B9G9R9_UFLOAT_PACK32 => Builder::Rgb9e5,
522
523            // ---- Combined depth-stencil ----
524            //                           ds(bits, datatype)
525            F::D16_UNORM_S8_UINT  => ds(16, Dt::Unorm),
526            F::D24_UNORM_S8_UINT  => ds(24, Dt::Unorm),
527            F::D32_SFLOAT_S8_UINT => ds(32, Dt::Sfloat),
528
529            // ---- 4:2:2 subsampled ----
530            //                                           c422(order, bits)
531            F::G8B8G8R8_422_UNORM                     => c422(ChromaSubsamplingSampleOrder::Gbgr,  8),
532            F::B8G8R8G8_422_UNORM                     => c422(ChromaSubsamplingSampleOrder::Bgrg, 8),
533            F::G10X6B10X6G10X6R10X6_422_UNORM_4PACK16 => c422(ChromaSubsamplingSampleOrder::Gbgr,  10),
534            F::B10X6G10X6R10X6G10X6_422_UNORM_4PACK16 => c422(ChromaSubsamplingSampleOrder::Bgrg, 10),
535            F::G12X4B12X4G12X4R12X4_422_UNORM_4PACK16 => c422(ChromaSubsamplingSampleOrder::Gbgr,  12),
536            F::B12X4G12X4R12X4G12X4_422_UNORM_4PACK16 => c422(ChromaSubsamplingSampleOrder::Bgrg, 12),
537            F::G16B16G16R16_422_UNORM                 => c422(ChromaSubsamplingSampleOrder::Gbgr,  16),
538            F::B16G16R16G16_422_UNORM                 => c422(ChromaSubsamplingSampleOrder::Bgrg, 16),
539
540            // ---- Compressed: BCn ----
541            //                              c(model,      srgb,        dim,       bpb,  datatype,   channels)
542            F::BC1_RGB_UNORM_BLOCK       => c(Cm::BC1A,   Counterpart, [4,  4,  1], 8,  Dt::Unorm,  &[BC_COLOR]),
543            F::BC1_RGB_SRGB_BLOCK        => c(Cm::BC1A,   Srgb,        [4,  4,  1], 8,  Dt::Unorm,  &[BC_COLOR]),
544            F::BC1_RGBA_UNORM_BLOCK      => c(Cm::BC1A,   Counterpart, [4,  4,  1], 8,  Dt::Unorm,  &[BC1A_ALPHA]),
545            F::BC1_RGBA_SRGB_BLOCK       => c(Cm::BC1A,   Srgb,        [4,  4,  1], 8,  Dt::Unorm,  &[BC1A_ALPHA]),
546            F::BC2_UNORM_BLOCK           => c(Cm::BC2,    Counterpart, [4,  4,  1], 16, Dt::Unorm,  &[BC_ALPHA, BC_COLOR]),
547            F::BC2_SRGB_BLOCK            => c(Cm::BC2,    Srgb,        [4,  4,  1], 16, Dt::Unorm,  &[BC_ALPHA, BC_COLOR]),
548            F::BC3_UNORM_BLOCK           => c(Cm::BC3,    Counterpart, [4,  4,  1], 16, Dt::Unorm,  &[BC_ALPHA, BC_COLOR]),
549            F::BC3_SRGB_BLOCK            => c(Cm::BC3,    Srgb,        [4,  4,  1], 16, Dt::Unorm,  &[BC_ALPHA, BC_COLOR]),
550            F::BC4_UNORM_BLOCK           => c(Cm::BC4,    Linear,      [4,  4,  1], 8,  Dt::Unorm,  &[BC_COLOR]),
551            F::BC4_SNORM_BLOCK           => c(Cm::BC4,    Linear,      [4,  4,  1], 8,  Dt::Snorm,  &[BC_COLOR]),
552            F::BC5_UNORM_BLOCK           => c(Cm::BC5,    Linear,      [4,  4,  1], 16, Dt::Unorm,  &[BC5_RED, BC5_GREEN]),
553            F::BC5_SNORM_BLOCK           => c(Cm::BC5,    Linear,      [4,  4,  1], 16, Dt::Snorm,  &[BC5_RED, BC5_GREEN]),
554            F::BC6H_UFLOAT_BLOCK         => c(Cm::BC6H,   Linear,      [4,  4,  1], 16, Dt::Ufloat, &[BC_COLOR]),
555            F::BC6H_SFLOAT_BLOCK         => c(Cm::BC6H,   Linear,      [4,  4,  1], 16, Dt::Sfloat, &[BC_COLOR]),
556            F::BC7_UNORM_BLOCK           => c(Cm::BC7,    Counterpart, [4,  4,  1], 16, Dt::Unorm,  &[BC_COLOR]),
557            F::BC7_SRGB_BLOCK            => c(Cm::BC7,    Srgb,        [4,  4,  1], 16, Dt::Unorm,  &[BC_COLOR]),
558
559            // ---- Compressed: ETC2 / EAC ----
560            F::ETC2_R8G8B8_UNORM_BLOCK   => c(Cm::ETC2,   Counterpart, [4,  4,  1], 8,  Dt::Unorm,  &[ETC2_COLOR]),
561            F::ETC2_R8G8B8_SRGB_BLOCK    => c(Cm::ETC2,   Srgb,        [4,  4,  1], 8,  Dt::Unorm,  &[ETC2_COLOR]),
562            F::ETC2_R8G8B8A1_UNORM_BLOCK => c(Cm::ETC2,   Counterpart, [4,  4,  1], 8,  Dt::Unorm,  &[ETC2_COLOR, ETC2_ALPHA]),
563            F::ETC2_R8G8B8A1_SRGB_BLOCK  => c(Cm::ETC2,   Srgb,        [4,  4,  1], 8,  Dt::Unorm,  &[ETC2_COLOR, ETC2_ALPHA]),
564            F::ETC2_R8G8B8A8_UNORM_BLOCK => c(Cm::ETC2,   Counterpart, [4,  4,  1], 16, Dt::Unorm,  &[ETC2_ALPHA, ETC2_COLOR]),
565            F::ETC2_R8G8B8A8_SRGB_BLOCK  => c(Cm::ETC2,   Srgb,        [4,  4,  1], 16, Dt::Unorm,  &[ETC2_ALPHA, ETC2_COLOR]),
566            F::EAC_R11_UNORM_BLOCK       => c(Cm::ETC2,   Linear,      [4,  4,  1], 8,  Dt::Unorm,  &[ETC2_RED]),
567            F::EAC_R11_SNORM_BLOCK       => c(Cm::ETC2,   Linear,      [4,  4,  1], 8,  Dt::Snorm,  &[ETC2_RED]),
568            F::EAC_R11G11_UNORM_BLOCK    => c(Cm::ETC2,   Linear,      [4,  4,  1], 16, Dt::Unorm,  &[ETC2_RED, ETC2_GREEN]),
569            F::EAC_R11G11_SNORM_BLOCK    => c(Cm::ETC2,   Linear,      [4,  4,  1], 16, Dt::Snorm,  &[ETC2_RED, ETC2_GREEN]),
570
571            // ---- Compressed: ASTC 2D ----
572            F::ASTC_4x4_UNORM_BLOCK      => c(Cm::ASTC,   Counterpart, [4,  4,  1], 16, Dt::Unorm,  &[ASTC_DATA]),
573            F::ASTC_4x4_SRGB_BLOCK       => c(Cm::ASTC,   Srgb,        [4,  4,  1], 16, Dt::Unorm,  &[ASTC_DATA]),
574            F::ASTC_4x4_SFLOAT_BLOCK     => c(Cm::ASTC,   Linear,      [4,  4,  1], 16, Dt::Sfloat, &[ASTC_DATA]),
575            F::ASTC_5x4_UNORM_BLOCK      => c(Cm::ASTC,   Counterpart, [5,  4,  1], 16, Dt::Unorm,  &[ASTC_DATA]),
576            F::ASTC_5x4_SRGB_BLOCK       => c(Cm::ASTC,   Srgb,        [5,  4,  1], 16, Dt::Unorm,  &[ASTC_DATA]),
577            F::ASTC_5x4_SFLOAT_BLOCK     => c(Cm::ASTC,   Linear,      [5,  4,  1], 16, Dt::Sfloat, &[ASTC_DATA]),
578            F::ASTC_5x5_UNORM_BLOCK      => c(Cm::ASTC,   Counterpart, [5,  5,  1], 16, Dt::Unorm,  &[ASTC_DATA]),
579            F::ASTC_5x5_SRGB_BLOCK       => c(Cm::ASTC,   Srgb,        [5,  5,  1], 16, Dt::Unorm,  &[ASTC_DATA]),
580            F::ASTC_5x5_SFLOAT_BLOCK     => c(Cm::ASTC,   Linear,      [5,  5,  1], 16, Dt::Sfloat, &[ASTC_DATA]),
581            F::ASTC_6x5_UNORM_BLOCK      => c(Cm::ASTC,   Counterpart, [6,  5,  1], 16, Dt::Unorm,  &[ASTC_DATA]),
582            F::ASTC_6x5_SRGB_BLOCK       => c(Cm::ASTC,   Srgb,        [6,  5,  1], 16, Dt::Unorm,  &[ASTC_DATA]),
583            F::ASTC_6x5_SFLOAT_BLOCK     => c(Cm::ASTC,   Linear,      [6,  5,  1], 16, Dt::Sfloat, &[ASTC_DATA]),
584            F::ASTC_6x6_UNORM_BLOCK      => c(Cm::ASTC,   Counterpart, [6,  6,  1], 16, Dt::Unorm,  &[ASTC_DATA]),
585            F::ASTC_6x6_SRGB_BLOCK       => c(Cm::ASTC,   Srgb,        [6,  6,  1], 16, Dt::Unorm,  &[ASTC_DATA]),
586            F::ASTC_6x6_SFLOAT_BLOCK     => c(Cm::ASTC,   Linear,      [6,  6,  1], 16, Dt::Sfloat, &[ASTC_DATA]),
587            F::ASTC_8x5_UNORM_BLOCK      => c(Cm::ASTC,   Counterpart, [8,  5,  1], 16, Dt::Unorm,  &[ASTC_DATA]),
588            F::ASTC_8x5_SRGB_BLOCK       => c(Cm::ASTC,   Srgb,        [8,  5,  1], 16, Dt::Unorm,  &[ASTC_DATA]),
589            F::ASTC_8x5_SFLOAT_BLOCK     => c(Cm::ASTC,   Linear,      [8,  5,  1], 16, Dt::Sfloat, &[ASTC_DATA]),
590            F::ASTC_8x6_UNORM_BLOCK      => c(Cm::ASTC,   Counterpart, [8,  6,  1], 16, Dt::Unorm,  &[ASTC_DATA]),
591            F::ASTC_8x6_SRGB_BLOCK       => c(Cm::ASTC,   Srgb,        [8,  6,  1], 16, Dt::Unorm,  &[ASTC_DATA]),
592            F::ASTC_8x6_SFLOAT_BLOCK     => c(Cm::ASTC,   Linear,      [8,  6,  1], 16, Dt::Sfloat, &[ASTC_DATA]),
593            F::ASTC_8x8_UNORM_BLOCK      => c(Cm::ASTC,   Counterpart, [8,  8,  1], 16, Dt::Unorm,  &[ASTC_DATA]),
594            F::ASTC_8x8_SRGB_BLOCK       => c(Cm::ASTC,   Srgb,        [8,  8,  1], 16, Dt::Unorm,  &[ASTC_DATA]),
595            F::ASTC_8x8_SFLOAT_BLOCK     => c(Cm::ASTC,   Linear,      [8,  8,  1], 16, Dt::Sfloat, &[ASTC_DATA]),
596            F::ASTC_10x5_UNORM_BLOCK     => c(Cm::ASTC,   Counterpart, [10, 5,  1], 16, Dt::Unorm,  &[ASTC_DATA]),
597            F::ASTC_10x5_SRGB_BLOCK      => c(Cm::ASTC,   Srgb,        [10, 5,  1], 16, Dt::Unorm,  &[ASTC_DATA]),
598            F::ASTC_10x5_SFLOAT_BLOCK    => c(Cm::ASTC,   Linear,      [10, 5,  1], 16, Dt::Sfloat, &[ASTC_DATA]),
599            F::ASTC_10x6_UNORM_BLOCK     => c(Cm::ASTC,   Counterpart, [10, 6,  1], 16, Dt::Unorm,  &[ASTC_DATA]),
600            F::ASTC_10x6_SRGB_BLOCK      => c(Cm::ASTC,   Srgb,        [10, 6,  1], 16, Dt::Unorm,  &[ASTC_DATA]),
601            F::ASTC_10x6_SFLOAT_BLOCK    => c(Cm::ASTC,   Linear,      [10, 6,  1], 16, Dt::Sfloat, &[ASTC_DATA]),
602            F::ASTC_10x8_UNORM_BLOCK     => c(Cm::ASTC,   Counterpart, [10, 8,  1], 16, Dt::Unorm,  &[ASTC_DATA]),
603            F::ASTC_10x8_SRGB_BLOCK      => c(Cm::ASTC,   Srgb,        [10, 8,  1], 16, Dt::Unorm,  &[ASTC_DATA]),
604            F::ASTC_10x8_SFLOAT_BLOCK    => c(Cm::ASTC,   Linear,      [10, 8,  1], 16, Dt::Sfloat, &[ASTC_DATA]),
605            F::ASTC_10x10_UNORM_BLOCK    => c(Cm::ASTC,   Counterpart, [10, 10, 1], 16, Dt::Unorm,  &[ASTC_DATA]),
606            F::ASTC_10x10_SRGB_BLOCK     => c(Cm::ASTC,   Srgb,        [10, 10, 1], 16, Dt::Unorm,  &[ASTC_DATA]),
607            F::ASTC_10x10_SFLOAT_BLOCK   => c(Cm::ASTC,   Linear,      [10, 10, 1], 16, Dt::Sfloat, &[ASTC_DATA]),
608            F::ASTC_12x10_UNORM_BLOCK    => c(Cm::ASTC,   Counterpart, [12, 10, 1], 16, Dt::Unorm,  &[ASTC_DATA]),
609            F::ASTC_12x10_SRGB_BLOCK     => c(Cm::ASTC,   Srgb,        [12, 10, 1], 16, Dt::Unorm,  &[ASTC_DATA]),
610            F::ASTC_12x10_SFLOAT_BLOCK   => c(Cm::ASTC,   Linear,      [12, 10, 1], 16, Dt::Sfloat, &[ASTC_DATA]),
611            F::ASTC_12x12_UNORM_BLOCK    => c(Cm::ASTC,   Counterpart, [12, 12, 1], 16, Dt::Unorm,  &[ASTC_DATA]),
612            F::ASTC_12x12_SRGB_BLOCK     => c(Cm::ASTC,   Srgb,        [12, 12, 1], 16, Dt::Unorm,  &[ASTC_DATA]),
613            F::ASTC_12x12_SFLOAT_BLOCK   => c(Cm::ASTC,   Linear,      [12, 12, 1], 16, Dt::Sfloat, &[ASTC_DATA]),
614
615            // ---- Compressed: ASTC 3D ----
616            F::ASTC_3x3x3_UNORM_BLOCK    => c(Cm::ASTC,   Counterpart, [3,  3,  3], 16, Dt::Unorm,  &[ASTC_DATA]),
617            F::ASTC_3x3x3_SRGB_BLOCK     => c(Cm::ASTC,   Srgb,        [3,  3,  3], 16, Dt::Unorm,  &[ASTC_DATA]),
618            F::ASTC_3x3x3_SFLOAT_BLOCK   => c(Cm::ASTC,   Linear,      [3,  3,  3], 16, Dt::Sfloat, &[ASTC_DATA]),
619            F::ASTC_4x3x3_UNORM_BLOCK    => c(Cm::ASTC,   Counterpart, [4,  3,  3], 16, Dt::Unorm,  &[ASTC_DATA]),
620            F::ASTC_4x3x3_SRGB_BLOCK     => c(Cm::ASTC,   Srgb,        [4,  3,  3], 16, Dt::Unorm,  &[ASTC_DATA]),
621            F::ASTC_4x3x3_SFLOAT_BLOCK   => c(Cm::ASTC,   Linear,      [4,  3,  3], 16, Dt::Sfloat, &[ASTC_DATA]),
622            F::ASTC_4x4x3_UNORM_BLOCK    => c(Cm::ASTC,   Counterpart, [4,  4,  3], 16, Dt::Unorm,  &[ASTC_DATA]),
623            F::ASTC_4x4x3_SRGB_BLOCK     => c(Cm::ASTC,   Srgb,        [4,  4,  3], 16, Dt::Unorm,  &[ASTC_DATA]),
624            F::ASTC_4x4x3_SFLOAT_BLOCK   => c(Cm::ASTC,   Linear,      [4,  4,  3], 16, Dt::Sfloat, &[ASTC_DATA]),
625            F::ASTC_4x4x4_UNORM_BLOCK    => c(Cm::ASTC,   Counterpart, [4,  4,  4], 16, Dt::Unorm,  &[ASTC_DATA]),
626            F::ASTC_4x4x4_SRGB_BLOCK     => c(Cm::ASTC,   Srgb,        [4,  4,  4], 16, Dt::Unorm,  &[ASTC_DATA]),
627            F::ASTC_4x4x4_SFLOAT_BLOCK   => c(Cm::ASTC,   Linear,      [4,  4,  4], 16, Dt::Sfloat, &[ASTC_DATA]),
628            F::ASTC_5x4x4_UNORM_BLOCK    => c(Cm::ASTC,   Counterpart, [5,  4,  4], 16, Dt::Unorm,  &[ASTC_DATA]),
629            F::ASTC_5x4x4_SRGB_BLOCK     => c(Cm::ASTC,   Srgb,        [5,  4,  4], 16, Dt::Unorm,  &[ASTC_DATA]),
630            F::ASTC_5x4x4_SFLOAT_BLOCK   => c(Cm::ASTC,   Linear,      [5,  4,  4], 16, Dt::Sfloat, &[ASTC_DATA]),
631            F::ASTC_5x5x4_UNORM_BLOCK    => c(Cm::ASTC,   Counterpart, [5,  5,  4], 16, Dt::Unorm,  &[ASTC_DATA]),
632            F::ASTC_5x5x4_SRGB_BLOCK     => c(Cm::ASTC,   Srgb,        [5,  5,  4], 16, Dt::Unorm,  &[ASTC_DATA]),
633            F::ASTC_5x5x4_SFLOAT_BLOCK   => c(Cm::ASTC,   Linear,      [5,  5,  4], 16, Dt::Sfloat, &[ASTC_DATA]),
634            F::ASTC_5x5x5_UNORM_BLOCK    => c(Cm::ASTC,   Counterpart, [5,  5,  5], 16, Dt::Unorm,  &[ASTC_DATA]),
635            F::ASTC_5x5x5_SRGB_BLOCK     => c(Cm::ASTC,   Srgb,        [5,  5,  5], 16, Dt::Unorm,  &[ASTC_DATA]),
636            F::ASTC_5x5x5_SFLOAT_BLOCK   => c(Cm::ASTC,   Linear,      [5,  5,  5], 16, Dt::Sfloat, &[ASTC_DATA]),
637            F::ASTC_6x5x5_UNORM_BLOCK    => c(Cm::ASTC,   Counterpart, [6,  5,  5], 16, Dt::Unorm,  &[ASTC_DATA]),
638            F::ASTC_6x5x5_SRGB_BLOCK     => c(Cm::ASTC,   Srgb,        [6,  5,  5], 16, Dt::Unorm,  &[ASTC_DATA]),
639            F::ASTC_6x5x5_SFLOAT_BLOCK   => c(Cm::ASTC,   Linear,      [6,  5,  5], 16, Dt::Sfloat, &[ASTC_DATA]),
640            F::ASTC_6x6x5_UNORM_BLOCK    => c(Cm::ASTC,   Counterpart, [6,  6,  5], 16, Dt::Unorm,  &[ASTC_DATA]),
641            F::ASTC_6x6x5_SRGB_BLOCK     => c(Cm::ASTC,   Srgb,        [6,  6,  5], 16, Dt::Unorm,  &[ASTC_DATA]),
642            F::ASTC_6x6x5_SFLOAT_BLOCK   => c(Cm::ASTC,   Linear,      [6,  6,  5], 16, Dt::Sfloat, &[ASTC_DATA]),
643            F::ASTC_6x6x6_UNORM_BLOCK    => c(Cm::ASTC,   Counterpart, [6,  6,  6], 16, Dt::Unorm,  &[ASTC_DATA]),
644            F::ASTC_6x6x6_SRGB_BLOCK     => c(Cm::ASTC,   Srgb,        [6,  6,  6], 16, Dt::Unorm,  &[ASTC_DATA]),
645            F::ASTC_6x6x6_SFLOAT_BLOCK   => c(Cm::ASTC,   Linear,      [6,  6,  6], 16, Dt::Sfloat, &[ASTC_DATA]),
646
647            // ---- Compressed: PVRTC ----
648            F::PVRTC1_2BPP_UNORM_BLOCK   => c(Cm::PVRTC,  Counterpart, [8,  4,  1], 8,  Dt::Unorm,  &[PVRTC_COLOR]),
649            F::PVRTC1_2BPP_SRGB_BLOCK    => c(Cm::PVRTC,  Srgb,        [8,  4,  1], 8,  Dt::Unorm,  &[PVRTC_COLOR]),
650            F::PVRTC1_4BPP_UNORM_BLOCK   => c(Cm::PVRTC,  Counterpart, [4,  4,  1], 8,  Dt::Unorm,  &[PVRTC_COLOR]),
651            F::PVRTC1_4BPP_SRGB_BLOCK    => c(Cm::PVRTC,  Srgb,        [4,  4,  1], 8,  Dt::Unorm,  &[PVRTC_COLOR]),
652            F::PVRTC2_2BPP_UNORM_BLOCK   => c(Cm::PVRTC2, Counterpart, [8,  4,  1], 8,  Dt::Unorm,  &[PVRTC_COLOR]),
653            F::PVRTC2_2BPP_SRGB_BLOCK    => c(Cm::PVRTC2, Srgb,        [8,  4,  1], 8,  Dt::Unorm,  &[PVRTC_COLOR]),
654            F::PVRTC2_4BPP_UNORM_BLOCK   => c(Cm::PVRTC2, Counterpart, [4,  4,  1], 8,  Dt::Unorm,  &[PVRTC_COLOR]),
655            F::PVRTC2_4BPP_SRGB_BLOCK    => c(Cm::PVRTC2, Srgb,        [4,  4,  1], 8,  Dt::Unorm,  &[PVRTC_COLOR]),
656
657            // Unknown format value
658            _ => return None,
659        };
660        Some(res)
661    }
662
663    /// Builds a [`Basic`] DFD block from this scheme.
664    ///
665    /// Optional parameters override the defaults derived from the scheme:
666    /// - `alpha_premultiplied`: sets [`DataFormatFlags::ALPHA_PREMULTIPLIED`] (default: straight alpha).
667    /// - `transfer_function`: overrides the transfer function (default: [`TransferFunction::Linear`]
668    ///   or [`TransferFunction::SRGB`] based on the scheme's [`FormatInherentTransferFunction`]).
669    /// - `color_primaries`: overrides color primaries (default: [`ColorPrimaries::BT709`]).
670    /// - `color_model`: overrides color model (default: [`ColorModel::RGBSDA`]).
671    ///
672    /// # Errors
673    ///
674    /// Returns [`BuildError`] if the caller's overrides conflict with the format's
675    /// constraints:
676    /// - Depth-stencil formats reject all overrides (fixed layout).
677    /// - Compressed formats reject `color_model` overrides (must use intrinsic model).
678    /// - Formats with an sRGB counterpart must not use sRGB transfer; sRGB variants
679    ///   must use sRGB transfer.
680    pub fn build(
681        &self,
682        alpha_premultiplied: bool,
683        transfer_function: Option<TransferFunction>,
684        color_primaries: Option<ColorPrimaries>,
685        color_model: Option<ColorModel>,
686    ) -> Result<Basic, BuildError> {
687        match *self {
688            Builder::Standard {
689                datatype,
690                srgb,
691                bytes_per_texel,
692                bit_count,
693                bit_offset,
694                channel_ids,
695                ..
696            } => {
697                let color_model = color_model.unwrap_or(ColorModel::RGBSDA);
698                let color_primaries = color_primaries.unwrap_or(ColorPrimaries::BT709);
699                let transfer_function = resolve_transfer_function(srgb, transfer_function)?;
700                let flags = if alpha_premultiplied {
701                    DataFormatFlags::ALPHA_PREMULTIPLIED
702                } else {
703                    DataFormatFlags::STRAIGHT_ALPHA
704                };
705
706                // channel_ids, bit_offset, and bit_count are all in ascending
707                // bit-offset order (guaranteed by the describe() table), so
708                // samples can be emitted directly without sorting.
709                let mut sample_information = Vec::with_capacity(channel_ids.len());
710                for i in 0..channel_ids.len() {
711                    let (lower, upper) = lower_upper(datatype, bit_count[i]);
712                    sample_information.push(SampleInformation {
713                        bit_offset: bit_offset[i] as u16,
714                        bit_length: NonZeroU8::new(bit_count[i]).unwrap(),
715                        channel_type: channel_ids[i],
716                        channel_type_qualifiers: sample_qualifiers(datatype, srgb, channel_ids[i]),
717                        sample_positions: SAMPLE_POS_ORIGIN,
718                        lower,
719                        upper,
720                    });
721                }
722
723                let mut bytes_planes = [0u8; 8];
724                bytes_planes[0] = bytes_per_texel;
725
726                Ok(Basic {
727                    color_model: Some(color_model),
728                    color_primaries: Some(color_primaries),
729                    transfer_function: Some(transfer_function),
730                    flags,
731                    texel_block_dimensions: [NonZeroU8::new(1).unwrap(); 4],
732                    bytes_planes,
733                    sample_information,
734                })
735            }
736
737            Builder::DepthStencil {
738                depth_bits,
739                depth_datatype,
740            } => {
741                if alpha_premultiplied {
742                    return Err(BuildError::DepthStencilPremultipliedAlpha);
743                }
744                if transfer_function.is_some() {
745                    return Err(BuildError::DepthStencilTransferFunction);
746                }
747                if color_primaries.is_some() {
748                    return Err(BuildError::DepthStencilColorPrimaries);
749                }
750                if color_model.is_some() {
751                    return Err(BuildError::DepthStencilColorModel);
752                }
753
754                // All depth-stencil formats are single-plane packed.
755                // D16_UNORM_S8_UINT: 4 bytes (D16 @ 0, S8 @ 16)
756                // D24_UNORM_S8_UINT: 4 bytes (S8 @ 0, D24 @ 8)
757                // D32_SFLOAT_S8_UINT: 8 bytes (D32 @ 0, S8 @ 32)
758                let bytes_plane0: u8 = match depth_bits {
759                    16 => 4,
760                    24 => 4,
761                    32 => 8,
762                    _ => unreachable!("unsupported depth bit width: {depth_bits}"),
763                };
764
765                let mut bytes_planes = [0u8; 8];
766                bytes_planes[0] = bytes_plane0;
767
768                let depth_quals = qualifiers(depth_datatype);
769                let (depth_lower, depth_upper) = lower_upper(depth_datatype, depth_bits);
770                let (stencil_lower, stencil_upper) = lower_upper(Datatype::Uint, 8);
771
772                // D24_UNORM_S8_UINT stores stencil in the low byte (S8 @ 0,
773                // D24 @ 8) matching X8_D24_UNORM_PACK32 layout. The other two
774                // store depth first.
775                let (depth_offset, stencil_offset) = match depth_bits {
776                    16 => (0u16, 16u16),
777                    24 => (8, 0),
778                    32 => (0, 32),
779                    _ => unreachable!("unsupported depth bit width: {depth_bits}"),
780                };
781
782                // Samples are sorted by bit_offset.
783                let mut sample_information = Vec::from([
784                    SampleInformation {
785                        bit_offset: depth_offset,
786                        bit_length: NonZeroU8::new(depth_bits).unwrap(),
787                        channel_type: CHANNEL_DEPTH,
788                        channel_type_qualifiers: depth_quals,
789                        sample_positions: SAMPLE_POS_ORIGIN,
790                        lower: depth_lower,
791                        upper: depth_upper,
792                    },
793                    SampleInformation {
794                        bit_offset: stencil_offset,
795                        bit_length: NonZeroU8::new(8).unwrap(),
796                        channel_type: CHANNEL_STENCIL,
797                        channel_type_qualifiers: ChannelTypeQualifiers::empty(),
798                        sample_positions: SAMPLE_POS_ORIGIN,
799                        lower: stencil_lower,
800                        upper: stencil_upper,
801                    },
802                ]);
803
804                sample_information.sort_unstable_by_key(|s| s.bit_offset);
805
806                Ok(Basic {
807                    color_model: Some(ColorModel::RGBSDA),
808                    color_primaries: Some(ColorPrimaries::BT709),
809                    transfer_function: Some(TransferFunction::Linear),
810                    flags: DataFormatFlags::empty(),
811                    texel_block_dimensions: [NonZeroU8::new(1).unwrap(); 4],
812                    bytes_planes,
813                    sample_information,
814                })
815            }
816
817            Builder::Rgb9e5 => {
818                let color_model = color_model.unwrap_or(ColorModel::RGBSDA);
819                let color_primaries = color_primaries.unwrap_or(ColorPrimaries::BT709);
820                let transfer_function = transfer_function.unwrap_or(TransferFunction::Linear);
821                let flags = if alpha_premultiplied {
822                    DataFormatFlags::ALPHA_PREMULTIPLIED
823                } else {
824                    DataFormatFlags::STRAIGHT_ALPHA
825                };
826
827                let mut bytes_planes = [0u8; 8];
828                bytes_planes[0] = 4;
829
830                // Layout (LSB→MSB): R9 @ 0, G9 @ 9, B9 @ 18, E5 @ 27.
831                //
832                // Samples are interleaved per-channel: R_base, R_exp, G_base,
833                // G_exp, B_base, B_exp — NOT all bases then all exponents.
834                const CHANNELS: [u8; 3] = [CHANNEL_R, CHANNEL_G, CHANNEL_B];
835                const BASE_OFFSETS: [u16; 3] = [0, 9, 18];
836
837                let mut sample_information = Vec::with_capacity(6);
838                for i in 0..3 {
839                    sample_information.push(SampleInformation {
840                        bit_offset: BASE_OFFSETS[i],
841                        bit_length: NonZeroU8::new(RGB9E5_MANTISSA_BITS).unwrap(),
842                        channel_type: CHANNELS[i],
843                        channel_type_qualifiers: ChannelTypeQualifiers::empty(),
844                        sample_positions: SAMPLE_POS_ORIGIN,
845                        lower: 0,
846                        upper: RGB9E5_MANTISSA_UPPER,
847                    });
848                    sample_information.push(SampleInformation {
849                        bit_offset: RGB9E5_EXPONENT_OFFSET,
850                        bit_length: NonZeroU8::new(RGB9E5_EXPONENT_BITS).unwrap(),
851                        channel_type: CHANNELS[i],
852                        channel_type_qualifiers: ChannelTypeQualifiers::EXPONENT,
853                        sample_positions: SAMPLE_POS_ORIGIN,
854                        lower: RGB9E5_EXPONENT_BIAS,
855                        upper: RGB9E5_EXPONENT_MAX,
856                    });
857                }
858
859                Ok(Basic {
860                    color_model: Some(color_model),
861                    color_primaries: Some(color_primaries),
862                    transfer_function: Some(transfer_function),
863                    flags,
864                    texel_block_dimensions: [NonZeroU8::new(1).unwrap(); 4],
865                    bytes_planes,
866                    sample_information,
867                })
868            }
869
870            Builder::Compressed {
871                color_model: model,
872                srgb,
873                block_dimensions,
874                bytes_per_block,
875                datatype,
876                channel_types,
877            } => {
878                if color_model.is_some() {
879                    return Err(BuildError::CompressedColorModel);
880                }
881                let color_primaries = color_primaries.unwrap_or(ColorPrimaries::BT709);
882                let transfer_function = resolve_transfer_function(srgb, transfer_function)?;
883                let flags = if alpha_premultiplied {
884                    DataFormatFlags::ALPHA_PREMULTIPLIED
885                } else {
886                    DataFormatFlags::STRAIGHT_ALPHA
887                };
888
889                let (lower, upper) = compressed_lower_upper(datatype);
890
891                // Single-sample formats: one sample covering the whole block.
892                // Dual-sample formats with 16-byte blocks: two 64-bit halves.
893                // Dual-sample formats with 8-byte blocks: both samples cover the
894                // same 64-bit block (e.g. ETC2 punchthrough alpha).
895                let sample_bits = if channel_types.len() == 1 {
896                    bytes_per_block * 8
897                } else {
898                    64
899                };
900                let sample_stride: u16 = if channel_types.len() > 1 && bytes_per_block > 8 {
901                    64
902                } else {
903                    0
904                };
905
906                let mut sample_information = Vec::with_capacity(channel_types.len());
907                for (i, &ch) in channel_types.iter().enumerate() {
908                    sample_information.push(SampleInformation {
909                        bit_offset: (i as u16) * sample_stride,
910                        bit_length: NonZeroU8::new(sample_bits).unwrap(),
911                        channel_type: ch,
912                        channel_type_qualifiers: sample_qualifiers(datatype, srgb, ch),
913                        sample_positions: SAMPLE_POS_ORIGIN,
914                        lower,
915                        upper,
916                    });
917                }
918
919                let mut bytes_planes = [0u8; 8];
920                bytes_planes[0] = bytes_per_block;
921
922                Ok(Basic {
923                    color_model: Some(model),
924                    color_primaries: Some(color_primaries),
925                    transfer_function: Some(transfer_function),
926                    flags,
927                    texel_block_dimensions: [
928                        NonZeroU8::new(block_dimensions[0]).unwrap(),
929                        NonZeroU8::new(block_dimensions[1]).unwrap(),
930                        NonZeroU8::new(block_dimensions[2]).unwrap(),
931                        NonZeroU8::new(1).unwrap(),
932                    ],
933                    bytes_planes,
934                    sample_information,
935                })
936            }
937
938            Builder::Subsampled422 {
939                sample_order,
940                bit_width,
941            } => {
942                let color_model = color_model.unwrap_or(ColorModel::YUVSDA);
943                let color_primaries = color_primaries.unwrap_or(ColorPrimaries::BT709);
944                let transfer_function = transfer_function.unwrap_or(TransferFunction::Linear);
945                let flags = if alpha_premultiplied {
946                    DataFormatFlags::ALPHA_PREMULTIPLIED
947                } else {
948                    DataFormatFlags::STRAIGHT_ALPHA
949                };
950
951                let (lower, upper) = lower_upper(Datatype::Unorm, bit_width);
952
953                // Each channel occupies a word whose size is the next power-of-two
954                // byte boundary (8-bit → 8-bit word, 10/12/16-bit → 16-bit word).
955                let word_bits = (bit_width as u16).next_power_of_two().max(8);
956                let pad_bits = word_bits - bit_width as u16;
957                let bytes_per_block = (word_bits * 4 / 8) as u8;
958
959                // Memory order and texel positions:
960                //   GBGR: [Y₀, U, Y₁, V]  — Y at positions 0 and 1, U and V co-sited at 0
961                //   BGRG: [U, Y₀, V, Y₁]  — Y at positions 0 and 1, U and V co-sited at 0
962                let layout: [(u8, u8); 4] = if sample_order == ChromaSubsamplingSampleOrder::Gbgr {
963                    [(CHANNEL_Y, 0), (CHANNEL_U, 0), (CHANNEL_Y, 1), (CHANNEL_V, 0)]
964                } else {
965                    [(CHANNEL_U, 0), (CHANNEL_Y, 0), (CHANNEL_V, 0), (CHANNEL_Y, 1)]
966                };
967
968                let sample_information = layout
969                    .iter()
970                    .enumerate()
971                    .map(|(i, &(channel, pos_x))| SampleInformation {
972                        bit_offset: (i as u16) * word_bits + pad_bits,
973                        bit_length: NonZeroU8::new(bit_width).unwrap(),
974                        channel_type: channel,
975                        channel_type_qualifiers: ChannelTypeQualifiers::empty(),
976                        sample_positions: [pos_x, HALF_TEXEL, 0, 0],
977                        lower,
978                        upper,
979                    })
980                    .collect();
981
982                let mut bytes_planes = [0u8; 8];
983                bytes_planes[0] = bytes_per_block;
984
985                Ok(Basic {
986                    color_model: Some(color_model),
987                    color_primaries: Some(color_primaries),
988                    transfer_function: Some(transfer_function),
989                    flags,
990                    texel_block_dimensions: [
991                        NonZeroU8::new(2).unwrap(),
992                        NonZeroU8::new(1).unwrap(),
993                        NonZeroU8::new(1).unwrap(),
994                        NonZeroU8::new(1).unwrap(),
995                    ],
996                    bytes_planes,
997                    sample_information,
998                })
999            }
1000        }
1001    }
1002}
1003
1004/// Returns the [`ChannelTypeQualifiers`] for the given datatype.
1005fn qualifiers(datatype: Datatype) -> ChannelTypeQualifiers {
1006    match datatype {
1007        Datatype::Unorm => ChannelTypeQualifiers::empty(),
1008        Datatype::Snorm => ChannelTypeQualifiers::SIGNED,
1009        Datatype::Uint => ChannelTypeQualifiers::empty(),
1010        Datatype::Sint => ChannelTypeQualifiers::SIGNED,
1011        Datatype::Sfloat => ChannelTypeQualifiers::SIGNED | ChannelTypeQualifiers::FLOAT,
1012        Datatype::Ufloat => ChannelTypeQualifiers::FLOAT,
1013        Datatype::Sfixed5 => ChannelTypeQualifiers::SIGNED,
1014    }
1015}
1016
1017/// Returns the [`ChannelTypeQualifiers`] for a specific sample, accounting for
1018/// the sRGB alpha exception.
1019///
1020/// In sRGB formats, the alpha channel (ID 15) is always stored linearly — only
1021/// the color channels use the sRGB OETF. The DFD signals this with the LINEAR
1022/// qualifier on the alpha sample.
1023fn sample_qualifiers(
1024    datatype: Datatype,
1025    srgb: FormatInherentTransferFunction,
1026    channel_id: u8,
1027) -> ChannelTypeQualifiers {
1028    let mut quals = qualifiers(datatype);
1029    if srgb == FormatInherentTransferFunction::Srgb && channel_id == CHANNEL_ALPHA {
1030        quals |= ChannelTypeQualifiers::LINEAR;
1031    }
1032    quals
1033}
1034
1035/// Returns the (lower, upper) sample bounds for the given datatype and bit width.
1036fn lower_upper(datatype: Datatype, bits: u8) -> (u32, u32) {
1037    match datatype {
1038        Datatype::Unorm => (0, (1u32 << bits) - 1),
1039        Datatype::Snorm => {
1040            let max = (1u32 << (bits - 1)) - 1;
1041            let min = (-(max as i32)) as u32;
1042            (min, max)
1043        }
1044        // Integer (non-normalized) formats use 0/1 bounds per the DFD spec,
1045        // meaning "the entire representable range". The bit width is irrelevant.
1046        Datatype::Uint => (0, 1),
1047        Datatype::Sint => ((-1i32) as u32, 1),
1048        Datatype::Sfloat => ((-1.0f32).to_bits(), (1.0f32).to_bits()),
1049        Datatype::Ufloat => (0, (1.0f32).to_bits()),
1050        // R16G16_SFIXED5: 16-bit signed with 5 fractional bits.
1051        // Range is -2^10 / 2^5 = -32 to +2^10 / 2^5 = 32.
1052        Datatype::Sfixed5 => ((-32i32) as u32, 32),
1053    }
1054}
1055
1056/// Returns the (lower, upper) sample bounds for compressed format datatypes.
1057///
1058/// Unlike [`lower_upper`], there is no meaningful per-channel bit width for
1059/// compressed formats. The bounds represent the conceptual numeric range.
1060fn compressed_lower_upper(datatype: Datatype) -> (u32, u32) {
1061    match datatype {
1062        Datatype::Unorm => (0, 0xFFFFFFFF),
1063        // Compressed snorm uses full i32 range (not the per-component
1064        // symmetric range used by uncompressed formats).
1065        Datatype::Snorm => (i32::MIN as u32, i32::MAX as u32),
1066        Datatype::Sfloat => ((-1.0f32).to_bits(), (1.0f32).to_bits()),
1067        Datatype::Ufloat => (0, (1.0f32).to_bits()),
1068        _ => unreachable!("unsupported compressed datatype"),
1069    }
1070}
1071
1072/// Validates the transfer function against the [`SrgbTransfer`] constraint.
1073///
1074/// Returns the resolved transfer function, or an error if the caller's
1075/// override conflicts with the sRGB variant rules.
1076#[rustfmt::skip]
1077fn resolve_transfer_function(
1078    srgb: FormatInherentTransferFunction,
1079    requested: Option<TransferFunction>,
1080) -> Result<TransferFunction, BuildError> {
1081    match (srgb, requested) {
1082        // Srgb variant: must be SRGB, reject non-SRGB overrides.
1083        (FormatInherentTransferFunction::Srgb, None) =>                         Ok(TransferFunction::SRGB),
1084        (FormatInherentTransferFunction::Srgb, Some(TransferFunction::SRGB)) => Ok(TransferFunction::SRGB),
1085        (FormatInherentTransferFunction::Srgb, Some(_)) =>                      Err(BuildError::SrgbTransferRequired),
1086        // HasSrgbCounterpart: must NOT be SRGB.
1087        (FormatInherentTransferFunction::LinearWithSrgbCounterpart, None) =>                         Ok(TransferFunction::Linear),
1088        (FormatInherentTransferFunction::LinearWithSrgbCounterpart, Some(TransferFunction::SRGB)) => Err(BuildError::SrgbTransferNotAllowed),
1089        (FormatInherentTransferFunction::LinearWithSrgbCounterpart, Some(tf)) =>   Ok(tf),
1090        // Linear: no restriction.
1091        (FormatInherentTransferFunction::Linear, None) =>                       Ok(TransferFunction::Linear),
1092        (FormatInherentTransferFunction::Linear, Some(tf)) => Ok(tf),
1093    }
1094}