Skip to main content

fovea_display/
texture.rs

1//! Zero-copy texture data source for GPU upload.
2//!
3//! This module defines [`TextureSource`], a trait that combines texture format
4//! metadata with byte-level access to pixel data. It is blanket-implemented
5//! for any [`PlainImage`](fovea::image::PlainImage) image whose pixel type implements
6//! [`GpuPixel`](crate::GpuPixel).
7//!
8//! # Design note: `bytes_per_row` alignment
9//!
10//! GPU APIs often require row alignment (e.g., wgpu requires 256-byte
11//! alignment for `COPY_BYTES_PER_ROW_ALIGNMENT`). This is **not** handled
12//! here — it is the responsibility of the downstream upload code.
13//! [`TextureSource::bytes_per_row`] reports the *logical* bytes per row
14//! (width × pixel size), with no padding.
15
16use fovea::image::PlainImage;
17use fovea::pixel::PlainChannel;
18
19use crate::pixel::{GpuPixel, TextureFormat};
20
21/// Zero-copy texture data source for GPU upload.
22///
23/// Provides format metadata and byte access in a single trait, suitable
24/// for passing directly to GPU texture creation and upload helpers.
25///
26/// This trait is blanket-implemented for any [`PlainImage`] image whose
27/// pixel implements [`GpuPixel`]. You do not need to implement it manually.
28///
29/// # What implements `TextureSource`?
30///
31/// | Type                        | Implements? | Why                                        |
32/// |-----------------------------|-------------|--------------------------------------------|
33/// | `Image<Srgba8>`          | ✅ Yes      | `PlainImage` + `GpuPixel`                   |
34/// | `Image<Mono8>`           | ✅ Yes      | `PlainImage` + `GpuPixel`                   |
35/// | `ImageArray<Rgba8, 4, 4>`  | ✅ Yes      | `PlainImage` + `GpuPixel`                   |
36/// | `Image<Rgb8>`            | ❌ No       | `Rgb8` has no `GpuPixel` impl (3-channel)  |
37/// | `ImageRef<'_, Srgba8>`     | ❌ No       | No `PlainImage` (non-contiguous)            |
38///
39/// # Examples
40///
41/// ```
42/// use fovea::image::{Image, PlainImage};
43/// use fovea::pixel::Srgba8;
44/// use fovea_display::{TextureSource, TextureFormat};
45///
46/// let img = Image::fill(16, 8, Srgba8::new(255, 0, 0, 255));
47/// assert_eq!(img.texture_format(), TextureFormat::Rgba8Srgb);
48/// assert_eq!(img.texture_width(), 16);
49/// assert_eq!(img.texture_height(), 8);
50/// assert_eq!(img.bytes_per_row(), 16 * 4); // 16 pixels × 4 bytes each
51/// assert_eq!(img.texture_bytes().len(), 16 * 8 * 4);
52/// ```
53///
54/// ```compile_fail
55/// use fovea::image::Image;
56/// use fovea::pixel::Rgb8;
57/// use fovea_display::TextureSource;
58///
59/// // ERROR: Rgb8 does not implement GpuPixel — 3-channel types have
60/// // no direct GPU representation. Convert to Rgba8 first.
61/// let img = Image::fill(4, 4, Rgb8::new(0, 0, 0));
62/// let _ = img.texture_format();
63/// ```
64pub trait TextureSource {
65    /// The GPU texture format for this image's pixel type.
66    fn texture_format(&self) -> TextureFormat;
67
68    /// Image width in pixels.
69    fn texture_width(&self) -> u32;
70
71    /// Image height in pixels.
72    fn texture_height(&self) -> u32;
73
74    /// The raw pixel bytes, in row-major order with no padding.
75    ///
76    /// The returned slice has length
77    /// `texture_width() * texture_height() * texture_format().bytes_per_pixel()`.
78    fn texture_bytes(&self) -> &[u8];
79
80    /// Logical bytes per row (width × bytes per pixel), **without** GPU
81    /// alignment padding.
82    ///
83    /// Upload helpers are responsible for adding alignment padding if the
84    /// target GPU API requires it (e.g. wgpu's 256-byte row alignment).
85    fn bytes_per_row(&self) -> u32;
86}
87
88impl<T> TextureSource for T
89where
90    T: PlainImage,
91    T::Pixel: GpuPixel,
92{
93    #[inline]
94    fn texture_format(&self) -> TextureFormat {
95        <T::Pixel as GpuPixel>::TEXTURE_FORMAT
96    }
97
98    #[inline]
99    fn texture_width(&self) -> u32 {
100        self.size().width as u32
101    }
102
103    #[inline]
104    fn texture_height(&self) -> u32 {
105        self.size().height as u32
106    }
107
108    #[inline]
109    fn texture_bytes(&self) -> &[u8] {
110        self.as_bytes()
111    }
112
113    #[inline]
114    fn bytes_per_row(&self) -> u32 {
115        // `SIZE` lives on `PlainChannel` (inherited by `PlainPixel`
116        // via the supertrait relation). The bound on `T::Pixel` is
117        // still `PlainPixel`; we just resolve the constant through the
118        // byte-layout role.
119        (self.size().width * <T::Pixel as PlainChannel>::SIZE) as u32
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use fovea::image::{Image, ImageArray};
127    use fovea::pixel::*;
128
129    // ── Image<Mono8> ─────────────────────────────────────────────────
130
131    #[test]
132    fn image2d_mono8_texture_source() {
133        let img = Image::<Mono8>::zero(10, 5);
134        assert_eq!(img.texture_format(), TextureFormat::R8Unorm);
135        assert_eq!(img.texture_width(), 10);
136        assert_eq!(img.texture_height(), 5);
137        assert_eq!(img.bytes_per_row(), 10);
138        assert_eq!(img.texture_bytes().len(), 10 * 5);
139    }
140
141    // ── Image<Srgba8> ────────────────────────────────────────────────
142
143    #[test]
144    fn image2d_srgba8_texture_source() {
145        let img = Image::fill(16, 8, Srgba8::new(255, 0, 0, 255));
146        assert_eq!(img.texture_format(), TextureFormat::Rgba8Srgb);
147        assert_eq!(img.texture_width(), 16);
148        assert_eq!(img.texture_height(), 8);
149        assert_eq!(img.bytes_per_row(), 16 * 4);
150        assert_eq!(img.texture_bytes().len(), 16 * 8 * 4);
151    }
152
153    // ── Image<Rgba8> ─────────────────────────────────────────────────
154
155    #[test]
156    fn image2d_rgba8_texture_source() {
157        let img = Image::fill(3, 7, Rgba8::new(1, 2, 3, 4));
158        assert_eq!(img.texture_format(), TextureFormat::Rgba8Unorm);
159        assert_eq!(img.texture_width(), 3);
160        assert_eq!(img.texture_height(), 7);
161        assert_eq!(img.bytes_per_row(), 3 * 4);
162        assert_eq!(img.texture_bytes().len(), 3 * 7 * 4);
163    }
164
165    // ── Image<Mono16> ────────────────────────────────────────────────
166
167    #[test]
168    fn image2d_mono16_texture_source() {
169        let img = Image::<Mono16>::zero(100, 50);
170        assert_eq!(img.texture_format(), TextureFormat::R16Unorm);
171        assert_eq!(img.texture_width(), 100);
172        assert_eq!(img.texture_height(), 50);
173        assert_eq!(img.bytes_per_row(), 100 * 2);
174        assert_eq!(img.texture_bytes().len(), 100 * 50 * 2);
175    }
176
177    // ── Image<MonoF32> ──────────────────────────────────────────────
178
179    #[test]
180    fn image2d_f32_texture_source() {
181        // the pixel role for floats is `MonoF32`,
182        // not raw `f32`. `MonoF32` is `#[repr(transparent)]` over
183        // `f32`, so the resulting texture layout is identical
184        // (4-byte R32Float per pixel).
185        let img = Image::<fovea::pixel::MonoF32>::zero(8, 4);
186        assert_eq!(img.texture_format(), TextureFormat::R32Float);
187        assert_eq!(img.texture_width(), 8);
188        assert_eq!(img.texture_height(), 4);
189        assert_eq!(img.bytes_per_row(), 8 * 4);
190        assert_eq!(img.texture_bytes().len(), 8 * 4 * 4);
191    }
192
193    // ── Image<RgbaF32> ──────────────────────────────────────────────
194
195    #[test]
196    fn image2d_rgba_f32_texture_source() {
197        let img = Image::fill(
198            2,
199            3,
200            RgbaF32 {
201                r: 1.0,
202                g: 0.0,
203                b: 0.0,
204                a: 1.0,
205            },
206        );
207        assert_eq!(img.texture_format(), TextureFormat::Rgba32Float);
208        assert_eq!(img.texture_width(), 2);
209        assert_eq!(img.texture_height(), 3);
210        assert_eq!(img.bytes_per_row(), 2 * 16);
211        assert_eq!(img.texture_bytes().len(), 2 * 3 * 16);
212    }
213
214    // ── ImageArray ─────────────────────────────────────────────────────
215
216    #[test]
217    fn image_array_rgba8_texture_source() {
218        let img = ImageArray::<Rgba8, 4, 4>::new([Rgba8::new(0, 0, 0, 0); 16]);
219        assert_eq!(img.texture_format(), TextureFormat::Rgba8Unorm);
220        assert_eq!(img.texture_width(), 4);
221        assert_eq!(img.texture_height(), 4);
222        assert_eq!(img.bytes_per_row(), 4 * 4);
223        assert_eq!(img.texture_bytes().len(), 4 * 4 * 4);
224    }
225
226    // ── bytes_per_row consistency ──────────────────────────────────────
227
228    #[test]
229    fn bytes_per_row_equals_width_times_pixel_size() {
230        let img = Image::fill(17, 3, MonoA16::new(0, 0));
231        assert_eq!(
232            img.bytes_per_row(),
233            img.texture_width() * img.texture_format().bytes_per_pixel() as u32
234        );
235    }
236
237    // ── texture_bytes length consistency ───────────────────────────────
238
239    #[test]
240    fn texture_bytes_len_equals_width_times_height_times_bpp() {
241        let img = Image::fill(13, 7, Bgra8::new(0, 0, 0, 0));
242        let expected = img.texture_width() as usize
243            * img.texture_height() as usize
244            * img.texture_format().bytes_per_pixel();
245        assert_eq!(img.texture_bytes().len(), expected);
246    }
247
248    // ── zero-size image ────────────────────────────────────────────────
249
250    #[test]
251    fn zero_size_image_texture_source() {
252        let img = Image::<Mono8>::zero(0, 0);
253        assert_eq!(img.texture_width(), 0);
254        assert_eq!(img.texture_height(), 0);
255        assert_eq!(img.bytes_per_row(), 0);
256        assert_eq!(img.texture_bytes().len(), 0);
257    }
258
259    // ── pixel byte content verification ────────────────────────────────
260
261    #[test]
262    fn texture_bytes_contain_correct_pixel_data() {
263        let img = Image::fill(2, 1, Srgba8::new(0xAA, 0xBB, 0xCC, 0xDD));
264        let bytes = img.texture_bytes();
265        // Srgba8 is repr(C): r, g, b, a — each as Saturating<u8>
266        assert_eq!(bytes, &[0xAA, 0xBB, 0xCC, 0xDD, 0xAA, 0xBB, 0xCC, 0xDD]);
267    }
268
269    #[test]
270    fn single_pixel_image_texture_bytes() {
271        let img = Image::fill(1, 1, Mono8::new(42));
272        assert_eq!(img.texture_bytes(), &[42]);
273    }
274}