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}