Skip to main content

edgefirst_tensor/
d3d11_layout.rs

1// SPDX-FileCopyrightText: Copyright 2026 Au-Zone Technologies
2// SPDX-License-Identifier: Apache-2.0
3//! `(PixelFormat, DType, width, height) -> D3D11 texture layout`.
4//!
5//! Compiled on every platform so the table is unit-tested on every CI lane;
6//! only the allocator that consumes it is Windows-only. Single source of
7//! truth for which DXGI format and texture geometry back each HAL image
8//! format; the image crate reads `gl_internal_format` when it builds the
9//! `EGL_ANGLE_image_d3d11_texture` attribute list.
10
11use crate::{DType, PixelFormat};
12
13pub const DXGI_FORMAT_R32G32B32A32_FLOAT: u32 = 2;
14pub const DXGI_FORMAT_R16G16B16A16_FLOAT: u32 = 10;
15pub const DXGI_FORMAT_R8G8B8A8_UNORM: u32 = 28;
16pub const DXGI_FORMAT_R8G8_UNORM: u32 = 49;
17pub const DXGI_FORMAT_R8_UNORM: u32 = 61;
18pub const DXGI_FORMAT_B8G8R8A8_UNORM: u32 = 87;
19
20/// Values accepted for `EGL_TEXTURE_INTERNAL_FORMAT_ANGLE` by ANGLE's D3D11
21/// client-buffer validation (`Renderer11::getD3DTextureInfo`).
22pub const GL_RGBA: u32 = 0x1908;
23pub const GL_BGRA_EXT: u32 = 0x80E1;
24pub const GL_RED_EXT: u32 = 0x1903;
25pub const GL_RG_EXT: u32 = 0x8227;
26
27/// One texture's format and geometry for a HAL image. `texture_width` and
28/// `texture_height` are texels and rows, not image pixels: the packed rows
29/// fold several pixels or several planes into one texel, and a semi-planar
30/// row is the image's row *stride* rather than its width.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct D3d11ImageLayout {
33    pub dxgi_format: u32,
34    pub texture_width: usize,
35    pub texture_height: usize,
36    pub bytes_per_texel: usize,
37    pub gl_internal_format: u32,
38}
39
40impl D3d11ImageLayout {
41    /// Bytes in one texture row, with no driver padding: the row a copy into
42    /// or out of the texture moves.
43    ///
44    /// This is the image's own row for every format except a semi-planar one,
45    /// whose texture row is the stride and can exceed the image width (see
46    /// [`image_d3d11_layout`]). The image footprint of such a tensor is the
47    /// storage's business, not this table's, because only the storage still
48    /// knows the image width.
49    pub fn tight_row_bytes(&self) -> usize {
50        self.texture_width * self.bytes_per_texel
51    }
52    /// Bytes in the whole texture, padding-free: `tight_row_bytes` over every
53    /// row, with the same semi-planar caveat.
54    pub fn tight_bytes(&self) -> usize {
55        self.tight_row_bytes() * self.texture_height
56    }
57
58    /// This layout with its rows widened to `texture_width` texels.
59    ///
60    /// The one sanctioned edit of a table entry, and only for a semi-planar
61    /// row: its texture must be as wide as the row pitch the driver hands a
62    /// CPU map, and a pitch is a property of a created texture rather than of
63    /// the format, so the allocator probes for one and widens the entry here
64    /// (see `D3d11TextureTensor::new_image`). A wrapped external texture is
65    /// widened the same way, to the width its host chose.
66    ///
67    /// A width below the entry's own is not a widening and leaves it alone:
68    /// the table's width is the floor every consumer of the layout may assume.
69    pub fn widened_to(&self, texture_width: usize) -> Self {
70        Self {
71            texture_width: texture_width.max(self.texture_width),
72            ..*self
73        }
74    }
75}
76
77/// Every caller has already rejected a zero `width` or `height`, and each
78/// arm's texel width and row count are then non-zero, so this cannot build an
79/// empty texture.
80fn lay(dxgi_format: u32, w: usize, h: usize, bpt: usize, gl: u32) -> Option<D3d11ImageLayout> {
81    Some(D3d11ImageLayout {
82        dxgi_format,
83        texture_width: w,
84        texture_height: h,
85        bytes_per_texel: bpt,
86        gl_internal_format: gl,
87    })
88}
89
90/// The D3D11 texture that backs `format`/`dtype` at `width` x `height`, or
91/// `None` when the format has no zero-copy texture layout (it then falls to
92/// PBO or Mem in the allocation chain).
93pub fn image_d3d11_layout(
94    format: PixelFormat,
95    dtype: DType,
96    width: usize,
97    height: usize,
98) -> Option<D3d11ImageLayout> {
99    use PixelFormat::*;
100    if width == 0 || height == 0 {
101        return None;
102    }
103    // Four contiguous values per texel: three RGB bytes or floats per pixel
104    // and four planar values per texel both need a width divisible by 4.
105    let quarter = |n: usize| {
106        if n.is_multiple_of(4) {
107            Some(n / 4)
108        } else {
109            None
110        }
111    };
112    match (format, dtype) {
113        (Rgba, DType::U8 | DType::I8) => lay(DXGI_FORMAT_R8G8B8A8_UNORM, width, height, 4, GL_RGBA),
114        (Bgra, DType::U8) => lay(DXGI_FORMAT_B8G8R8A8_UNORM, width, height, 4, GL_BGRA_EXT),
115        (Rgb, DType::U8 | DType::I8) => lay(
116            DXGI_FORMAT_R8G8B8A8_UNORM,
117            quarter(width * 3)?,
118            height,
119            4,
120            GL_RGBA,
121        ),
122        (Grey, DType::U8) => lay(DXGI_FORMAT_R8_UNORM, width, height, 1, GL_RED_EXT),
123        // The HAL's combined luma + interleaved chroma plane, the layout the
124        // Path B shader already decodes on Linux and macOS.
125        //
126        // The texture row is the plane's row *stride*, not the image width:
127        // producers write luma and chroma lines at the stride and the shader
128        // wraps chroma addressing at the sampled texture's width, so the two
129        // have to be one number or a chroma line lands on the wrong row. The
130        // even rounding is the floor -- an interleaved chroma pair cannot
131        // straddle a row edge -- and the Windows allocator widens it further
132        // to the driver's row pitch through
133        // [`D3d11ImageLayout::widened_to`].
134        (Nv12 | Nv16 | Nv24, DType::U8) => {
135            let shape = format.allocation_shape(width, height)?;
136            lay(
137                DXGI_FORMAT_R8_UNORM,
138                shape[1].next_multiple_of(2),
139                shape[0],
140                1,
141                GL_RED_EXT,
142            )
143        }
144        (Yuyv, DType::U8) => lay(DXGI_FORMAT_R8G8_UNORM, width, height, 2, GL_RG_EXT),
145        (PlanarRgb | PlanarRgba, DType::F16) => lay(
146            DXGI_FORMAT_R16G16B16A16_FLOAT,
147            quarter(width)?,
148            format.channels() * height,
149            8,
150            GL_RGBA,
151        ),
152        (PlanarRgb | PlanarRgba, DType::F32) => lay(
153            DXGI_FORMAT_R32G32B32A32_FLOAT,
154            quarter(width)?,
155            format.channels() * height,
156            16,
157            GL_RGBA,
158        ),
159        (Rgb, DType::F16) => lay(
160            DXGI_FORMAT_R16G16B16A16_FLOAT,
161            quarter(width * 3)?,
162            height,
163            8,
164            GL_RGBA,
165        ),
166        (Rgb, DType::F32) => lay(
167            DXGI_FORMAT_R32G32B32A32_FLOAT,
168            quarter(width * 3)?,
169            height,
170            16,
171            GL_RGBA,
172        ),
173        (Rgba, DType::F16) => lay(DXGI_FORMAT_R16G16B16A16_FLOAT, width, height, 8, GL_RGBA),
174        (Rgba, DType::F32) => lay(DXGI_FORMAT_R32G32B32A32_FLOAT, width, height, 16, GL_RGBA),
175        _ => None,
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn packed_8bit_rows_are_direct() {
185        let l = image_d3d11_layout(PixelFormat::Rgba, DType::U8, 640, 480).unwrap();
186        assert_eq!(
187            (
188                l.dxgi_format,
189                l.texture_width,
190                l.texture_height,
191                l.bytes_per_texel,
192                l.gl_internal_format
193            ),
194            (DXGI_FORMAT_R8G8B8A8_UNORM, 640, 480, 4, GL_RGBA)
195        );
196        let l = image_d3d11_layout(PixelFormat::Bgra, DType::U8, 640, 480).unwrap();
197        assert_eq!(
198            (l.dxgi_format, l.gl_internal_format),
199            (DXGI_FORMAT_B8G8R8A8_UNORM, GL_BGRA_EXT)
200        );
201        let l = image_d3d11_layout(PixelFormat::Grey, DType::U8, 641, 3).unwrap();
202        assert_eq!(
203            (
204                l.dxgi_format,
205                l.texture_width,
206                l.bytes_per_texel,
207                l.gl_internal_format
208            ),
209            (DXGI_FORMAT_R8_UNORM, 641, 1, GL_RED_EXT)
210        );
211        assert_eq!(l.tight_bytes(), 641 * 3);
212    }
213
214    #[test]
215    fn packed_rgb_u8_packs_three_bytes_per_rgba_texel() {
216        let l = image_d3d11_layout(PixelFormat::Rgb, DType::U8, 640, 480).unwrap();
217        assert_eq!(
218            (
219                l.dxgi_format,
220                l.texture_width,
221                l.texture_height,
222                l.bytes_per_texel
223            ),
224            (DXGI_FORMAT_R8G8B8A8_UNORM, 480, 480, 4)
225        );
226        assert!(
227            image_d3d11_layout(PixelFormat::Rgb, DType::U8, 641, 480).is_none(),
228            "W % 4 != 0 has no packing"
229        );
230    }
231
232    #[test]
233    fn semi_planar_is_the_combined_r8_plane() {
234        let l = image_d3d11_layout(PixelFormat::Nv12, DType::U8, 128, 64).unwrap();
235        assert_eq!(
236            (
237                l.dxgi_format,
238                l.texture_width,
239                l.texture_height,
240                l.gl_internal_format
241            ),
242            (DXGI_FORMAT_R8_UNORM, 128, 96, GL_RED_EXT)
243        );
244        let l = image_d3d11_layout(PixelFormat::Nv16, DType::U8, 128, 64).unwrap();
245        assert_eq!((l.texture_width, l.texture_height), (128, 128));
246        let l = image_d3d11_layout(PixelFormat::Nv24, DType::U8, 128, 64).unwrap();
247        assert_eq!((l.texture_width, l.texture_height), (128, 192));
248        assert_eq!(
249            l.tight_bytes(),
250            PixelFormat::Nv24
251                .allocation_shape(128, 64)
252                .unwrap()
253                .iter()
254                .product::<usize>()
255        );
256    }
257
258    /// An odd image width rounds the texture row up to the next even texel,
259    /// because a chroma pair that straddles the row edge is unaddressable by
260    /// the shader that decodes the combined plane.
261    #[test]
262    fn semi_planar_rounds_an_odd_width_up_to_an_even_texture_row() {
263        for (fmt, rows) in [
264            (PixelFormat::Nv12, 362),
265            (PixelFormat::Nv16, 482),
266            (PixelFormat::Nv24, 723),
267        ] {
268            let l = image_d3d11_layout(fmt, DType::U8, 321, 241).unwrap();
269            assert_eq!(
270                (l.texture_width, l.texture_height),
271                (322, rows),
272                "{fmt:?} 321x241"
273            );
274        }
275        // An even width is untouched, so nothing that already fit changes.
276        let l = image_d3d11_layout(PixelFormat::Nv12, DType::U8, 640, 480).unwrap();
277        assert_eq!((l.texture_width, l.texture_height), (640, 720));
278    }
279
280    /// The allocator widens a semi-planar entry to the driver's row pitch and
281    /// a wrap widens it to the host's texture width, so `widened_to` has to
282    /// widen and never narrow: the table's width is the floor every consumer
283    /// of a layout may assume.
284    #[test]
285    fn widened_to_widens_and_never_narrows() {
286        let l = image_d3d11_layout(PixelFormat::Nv12, DType::U8, 321, 241).unwrap();
287        assert_eq!(l.texture_width, 322, "an odd width already rounds up");
288
289        // Below the entry's own width: not a widening, so it is ignored.
290        assert_eq!(l.widened_to(64).texture_width, 322);
291        assert_eq!(l.widened_to(321).texture_width, 322);
292        // Equal, and above.
293        assert_eq!(l.widened_to(322).texture_width, 322);
294        assert_eq!(l.widened_to(384).texture_width, 384);
295        assert_eq!(
296            l.widened_to(384).tight_row_bytes(),
297            384,
298            "R8 rows are texels"
299        );
300        // Every other field is the entry's.
301        let w = l.widened_to(384);
302        assert_eq!(
303            (
304                w.dxgi_format,
305                w.texture_height,
306                w.bytes_per_texel,
307                w.gl_internal_format
308            ),
309            (
310                l.dxgi_format,
311                l.texture_height,
312                l.bytes_per_texel,
313                l.gl_internal_format
314            )
315        );
316    }
317
318    #[test]
319    fn yuyv_is_rg8_at_full_width() {
320        let l = image_d3d11_layout(PixelFormat::Yuyv, DType::U8, 320, 240).unwrap();
321        assert_eq!(
322            (
323                l.dxgi_format,
324                l.texture_width,
325                l.bytes_per_texel,
326                l.gl_internal_format
327            ),
328            (DXGI_FORMAT_R8G8_UNORM, 320, 2, GL_RG_EXT)
329        );
330    }
331
332    #[test]
333    fn planar_float_packs_four_per_texel() {
334        let l = image_d3d11_layout(PixelFormat::PlanarRgb, DType::F16, 640, 480).unwrap();
335        assert_eq!(
336            (
337                l.dxgi_format,
338                l.texture_width,
339                l.texture_height,
340                l.bytes_per_texel
341            ),
342            (DXGI_FORMAT_R16G16B16A16_FLOAT, 160, 1440, 8)
343        );
344        let l = image_d3d11_layout(PixelFormat::PlanarRgba, DType::F32, 640, 480).unwrap();
345        assert_eq!(
346            (
347                l.dxgi_format,
348                l.texture_width,
349                l.texture_height,
350                l.bytes_per_texel
351            ),
352            (DXGI_FORMAT_R32G32B32A32_FLOAT, 160, 1920, 16)
353        );
354        assert_eq!(l.tight_bytes(), 4 * 480 * 640 * 4);
355        assert!(image_d3d11_layout(PixelFormat::PlanarRgb, DType::F16, 642, 480).is_none());
356    }
357
358    #[test]
359    fn interleaved_float_rows() {
360        let l = image_d3d11_layout(PixelFormat::Rgb, DType::F32, 640, 480).unwrap();
361        assert_eq!(
362            (
363                l.dxgi_format,
364                l.texture_width,
365                l.texture_height,
366                l.bytes_per_texel
367            ),
368            (DXGI_FORMAT_R32G32B32A32_FLOAT, 480, 480, 16)
369        );
370        let l = image_d3d11_layout(PixelFormat::Rgba, DType::F16, 640, 480).unwrap();
371        assert_eq!(
372            (
373                l.dxgi_format,
374                l.texture_width,
375                l.texture_height,
376                l.bytes_per_texel
377            ),
378            (DXGI_FORMAT_R16G16B16A16_FLOAT, 640, 480, 8)
379        );
380    }
381
382    #[test]
383    fn interleaved_float_rgb_f16_packs_three_halves_per_rgba_texel() {
384        assert_eq!(
385            image_d3d11_layout(PixelFormat::Rgb, DType::F16, 640, 480),
386            Some(D3d11ImageLayout {
387                dxgi_format: DXGI_FORMAT_R16G16B16A16_FLOAT,
388                texture_width: 480,
389                texture_height: 480,
390                bytes_per_texel: 8,
391                gl_internal_format: GL_RGBA,
392            })
393        );
394        assert!(image_d3d11_layout(PixelFormat::Rgb, DType::F16, 641, 480).is_none());
395    }
396
397    #[test]
398    fn interleaved_float_rgba_f32_is_one_pixel_per_texel() {
399        assert_eq!(
400            image_d3d11_layout(PixelFormat::Rgba, DType::F32, 640, 480),
401            Some(D3d11ImageLayout {
402                dxgi_format: DXGI_FORMAT_R32G32B32A32_FLOAT,
403                texture_width: 640,
404                texture_height: 480,
405                bytes_per_texel: 16,
406                gl_internal_format: GL_RGBA,
407            })
408        );
409    }
410
411    #[test]
412    fn a_zero_dimension_has_no_layout_for_any_supported_pair() {
413        let supported = [
414            (PixelFormat::Rgba, DType::U8),
415            (PixelFormat::Bgra, DType::U8),
416            (PixelFormat::Rgb, DType::U8),
417            (PixelFormat::Grey, DType::U8),
418            (PixelFormat::Nv12, DType::U8),
419            (PixelFormat::Nv16, DType::U8),
420            (PixelFormat::Nv24, DType::U8),
421            (PixelFormat::Yuyv, DType::U8),
422            (PixelFormat::PlanarRgb, DType::F16),
423            (PixelFormat::PlanarRgba, DType::F32),
424            (PixelFormat::Rgb, DType::F16),
425            (PixelFormat::Rgb, DType::F32),
426            (PixelFormat::Rgba, DType::F16),
427            (PixelFormat::Rgba, DType::F32),
428        ];
429        for (fmt, dtype) in supported {
430            assert!(
431                image_d3d11_layout(fmt, dtype, 64, 64).is_some(),
432                "{fmt:?}/{dtype:?} 64x64 is supported"
433            );
434            for (w, h) in [(0, 64), (64, 0), (0, 0)] {
435                assert!(
436                    image_d3d11_layout(fmt, dtype, w, h).is_none(),
437                    "{fmt:?}/{dtype:?} {w}x{h}"
438                );
439            }
440        }
441    }
442
443    #[test]
444    fn unsupported_pairs_are_none() {
445        assert!(image_d3d11_layout(PixelFormat::Vyuy, DType::U8, 64, 64).is_none());
446        assert!(image_d3d11_layout(PixelFormat::Grey, DType::F16, 64, 64).is_none());
447        assert!(image_d3d11_layout(PixelFormat::Rgba, DType::U8, 0, 64).is_none());
448    }
449}