Skip to main content

fovea_display/
pixel.rs

1//! Pixel traits for display and GPU texture integration.
2//!
3//! This module defines:
4//! - [`DisplayPixel`] — sealed trait for pixels that can be written to a softbuffer framebuffer.
5//! - [`TextureFormat`] — exhaustive enum of GPU texture format descriptors.
6//! - [`GpuPixel`] — maps pixel types to their GPU texture format.
7
8use fovea::pixel::{
9    Bgra8, Mono8, Mono16, MonoA8, MonoA16, MonoAF32, MonoF32, PlainPixel, Rgba8, Rgba16, RgbaF32,
10    SrgbMono8, Srgba8,
11};
12// Byte-layout items (`SIZE`, `ALIGN`, `as_bytes`, `from_bytes`) live
13// on `PlainChannel`, not `PlainPixel`. Only the in-crate tests
14// reference `SIZE` directly (to assert GpuPixel <-> TextureFormat
15// byte-count consistency); non-test code paths use
16// `<T as PlainChannel>::SIZE` explicitly where needed.
17#[cfg(test)]
18use fovea::pixel::PlainChannel;
19
20// ═══════════════════════════════════════════════════════════════════════════════
21// 1.1 — DisplayPixel (sealed)
22// ═══════════════════════════════════════════════════════════════════════════════
23
24mod sealed {
25    pub trait Sealed {}
26}
27
28/// A pixel that can be written directly to a framebuffer.
29///
30/// This trait is sealed — users cannot implement it for their own types.
31/// The intended workflow is to go through a [`DisplayStrategy`](crate::DisplayStrategy)
32/// that converts arbitrary pixels to [`Srgba8`], which implements this trait.
33///
34/// Only [`Srgba8`] implements this today. A future `Bgra8` variant may be
35/// added for platforms where `softbuffer` uses BGRA layout.
36///
37/// # Alpha handling
38///
39/// `softbuffer` has no alpha channel — the output format is `0x00RRGGBB`.
40/// Alpha is **discarded** (not composited). Pre-multiplied alpha display
41/// is a potential future concern.
42///
43/// # Examples
44///
45/// ```
46/// use fovea::pixel::Srgba8;
47/// use fovea_display::DisplayPixel;
48///
49/// let px = Srgba8::new(255, 128, 0, 255);
50/// assert_eq!(px.to_framebuffer_u32(), 0x00FF8000);
51/// ```
52pub trait DisplayPixel: PlainPixel + sealed::Sealed {
53    /// Convert to softbuffer's `0x00RRGGBB` format.
54    ///
55    /// The high byte is always `0x00`. The alpha channel (if any) is discarded.
56    fn to_framebuffer_u32(&self) -> u32;
57}
58
59impl sealed::Sealed for Srgba8 {}
60
61impl DisplayPixel for Srgba8 {
62    #[inline]
63    fn to_framebuffer_u32(&self) -> u32 {
64        let r = self.r.0 as u32;
65        let g = self.g.0 as u32;
66        let b = self.b.0 as u32;
67        (r << 16) | (g << 8) | b
68    }
69}
70
71// ═══════════════════════════════════════════════════════════════════════════════
72// 1.2 — TextureFormat enum
73// ═══════════════════════════════════════════════════════════════════════════════
74
75/// Exhaustive GPU texture format descriptors for fovea pixel types.
76///
77/// This enum is always available (no feature flag required). It is a pure
78/// Rust data type that downstream consumers (egui, bevy, raw Vulkan, wgpu)
79/// can match on directly.
80///
81/// **Adding a variant is semver-major.**
82///
83/// # Design decisions
84///
85/// - **No 3-channel formats.** GPU APIs generally do not support 3-channel
86///   textures (`Rgb8`, `Bgr8`, `RgbF32`). Users must convert to 4-channel
87///   before GPU upload. This is explicit per fovea Philosophy #4.
88///
89/// - **`R8Srgb` included.** Not all GPU APIs support `R8_SRGB` (WebGPU/wgpu
90///   notably do not), but the enum models the *logical* format. Downstream
91///   integrations that target such an API should map `R8Srgb` to the
92///   nearest available format (typically `R8Unorm`).
93///
94/// - **`Bgra8Srgb` reserved.** fovea has no `SrgbBgra8` type today, but
95///   the format is common in GPU APIs. Included for forward-compatibility.
96///
97/// # Examples
98///
99/// ```
100/// use fovea_display::TextureFormat;
101///
102/// let fmt = TextureFormat::Rgba8Srgb;
103/// assert_eq!(fmt.bytes_per_pixel(), 4);
104/// assert_eq!(fmt.channel_count(), 4);
105/// ```
106#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
107pub enum TextureFormat {
108    /// Single-channel 8-bit unsigned normalized (linear). Maps to `Mono8`.
109    R8Unorm,
110    /// Single-channel 8-bit sRGB. Maps to `SrgbMono8`.
111    R8Srgb,
112    /// Two-channel 8-bit unsigned normalized. Maps to `MonoA8`.
113    Rg8Unorm,
114    /// Four-channel 8-bit unsigned normalized (linear). Maps to `Rgba8`.
115    Rgba8Unorm,
116    /// Four-channel 8-bit sRGB with alpha. Maps to `Srgba8`.
117    Rgba8Srgb,
118    /// Four-channel 8-bit BGRA unsigned normalized (linear). Maps to `Bgra8`.
119    Bgra8Unorm,
120    /// Four-channel 8-bit BGRA sRGB. Reserved for future `SrgbBgra8`.
121    Bgra8Srgb,
122    /// Single-channel 16-bit unsigned normalized. Maps to `Mono16`.
123    R16Unorm,
124    /// Two-channel 16-bit unsigned normalized. Maps to `MonoA16`.
125    Rg16Unorm,
126    /// Four-channel 16-bit unsigned normalized. Maps to `Rgba16`.
127    Rgba16Unorm,
128    /// Single-channel 32-bit float. Maps to `MonoF32`.
129    R32Float,
130    /// Two-channel 32-bit float. Maps to `MonoAF32`.
131    Rg32Float,
132    /// Four-channel 32-bit float. Maps to `RgbaF32`.
133    Rgba32Float,
134}
135
136impl TextureFormat {
137    /// Returns the total number of bytes per pixel for this format.
138    ///
139    /// # Examples
140    ///
141    /// ```
142    /// use fovea_display::TextureFormat;
143    ///
144    /// assert_eq!(TextureFormat::R8Unorm.bytes_per_pixel(), 1);
145    /// assert_eq!(TextureFormat::Rg16Unorm.bytes_per_pixel(), 4);
146    /// assert_eq!(TextureFormat::Rgba32Float.bytes_per_pixel(), 16);
147    /// ```
148    #[inline]
149    #[must_use]
150    pub const fn bytes_per_pixel(&self) -> usize {
151        match self {
152            // 1 channel × 1 byte
153            TextureFormat::R8Unorm | TextureFormat::R8Srgb => 1,
154            // 2 channels × 1 byte
155            TextureFormat::Rg8Unorm => 2,
156            // 4 channels × 1 byte
157            TextureFormat::Rgba8Unorm
158            | TextureFormat::Rgba8Srgb
159            | TextureFormat::Bgra8Unorm
160            | TextureFormat::Bgra8Srgb => 4,
161            // 1 channel × 2 bytes
162            TextureFormat::R16Unorm => 2,
163            // 2 channels × 2 bytes
164            TextureFormat::Rg16Unorm => 4,
165            // 4 channels × 2 bytes
166            TextureFormat::Rgba16Unorm => 8,
167            // 1 channel × 4 bytes
168            TextureFormat::R32Float => 4,
169            // 2 channels × 4 bytes
170            TextureFormat::Rg32Float => 8,
171            // 4 channels × 4 bytes
172            TextureFormat::Rgba32Float => 16,
173        }
174    }
175
176    /// Returns the number of channels in this format.
177    ///
178    /// # Examples
179    ///
180    /// ```
181    /// use fovea_display::TextureFormat;
182    ///
183    /// assert_eq!(TextureFormat::R8Unorm.channel_count(), 1);
184    /// assert_eq!(TextureFormat::Rg8Unorm.channel_count(), 2);
185    /// assert_eq!(TextureFormat::Rgba8Srgb.channel_count(), 4);
186    /// ```
187    #[inline]
188    #[must_use]
189    pub const fn channel_count(&self) -> usize {
190        match self {
191            TextureFormat::R8Unorm
192            | TextureFormat::R8Srgb
193            | TextureFormat::R16Unorm
194            | TextureFormat::R32Float => 1,
195
196            TextureFormat::Rg8Unorm | TextureFormat::Rg16Unorm | TextureFormat::Rg32Float => 2,
197
198            TextureFormat::Rgba8Unorm
199            | TextureFormat::Rgba8Srgb
200            | TextureFormat::Bgra8Unorm
201            | TextureFormat::Bgra8Srgb
202            | TextureFormat::Rgba16Unorm
203            | TextureFormat::Rgba32Float => 4,
204        }
205    }
206}
207
208// ═══════════════════════════════════════════════════════════════════════════════
209// 1.3 — GpuPixel trait
210// ═══════════════════════════════════════════════════════════════════════════════
211
212/// Maps a pixel type to its GPU texture format.
213///
214/// Only implemented for pixel types that have a **direct** GPU representation
215/// (no conversion needed for upload). Notably **not** implemented for 3-channel
216/// types like `Rgb8`, `Bgr8`, or `RgbF32` — users must convert to 4-channel
217/// before GPU upload.
218///
219/// # Examples
220///
221/// ```
222/// use fovea::pixel::Srgba8;
223/// use fovea_display::{GpuPixel, TextureFormat};
224///
225/// assert_eq!(Srgba8::TEXTURE_FORMAT, TextureFormat::Rgba8Srgb);
226/// ```
227///
228/// ```compile_fail
229/// use fovea::pixel::Rgb8;
230/// use fovea_display::GpuPixel;
231///
232/// // ERROR: Rgb8 does not implement GpuPixel — 3-channel types have
233/// // no direct GPU representation. Convert to Rgba8 first.
234/// let _ = Rgb8::TEXTURE_FORMAT;
235/// ```
236///
237/// ```compile_fail
238/// use fovea::pixel::Bgr8;
239/// use fovea_display::GpuPixel;
240///
241/// // ERROR: Bgr8 does not implement GpuPixel — 3-channel types have
242/// // no direct GPU representation. Convert to Bgra8 first.
243/// let _ = Bgr8::TEXTURE_FORMAT;
244/// ```
245///
246/// ```compile_fail
247/// use fovea::pixel::RgbF32;
248/// use fovea_display::GpuPixel;
249///
250/// // ERROR: RgbF32 does not implement GpuPixel — 3-channel types have
251/// // no direct GPU representation. Convert to RgbaF32 first.
252/// let _ = RgbF32::TEXTURE_FORMAT;
253/// ```
254pub trait GpuPixel: PlainPixel {
255    /// The GPU texture format that corresponds to this pixel's memory layout.
256    const TEXTURE_FORMAT: TextureFormat;
257}
258
259impl GpuPixel for Mono8 {
260    const TEXTURE_FORMAT: TextureFormat = TextureFormat::R8Unorm;
261}
262
263impl GpuPixel for SrgbMono8 {
264    const TEXTURE_FORMAT: TextureFormat = TextureFormat::R8Srgb;
265}
266
267impl GpuPixel for MonoA8 {
268    const TEXTURE_FORMAT: TextureFormat = TextureFormat::Rg8Unorm;
269}
270
271impl GpuPixel for Rgba8 {
272    const TEXTURE_FORMAT: TextureFormat = TextureFormat::Rgba8Unorm;
273}
274
275impl GpuPixel for Srgba8 {
276    const TEXTURE_FORMAT: TextureFormat = TextureFormat::Rgba8Srgb;
277}
278
279impl GpuPixel for Bgra8 {
280    const TEXTURE_FORMAT: TextureFormat = TextureFormat::Bgra8Unorm;
281}
282
283impl GpuPixel for Mono16 {
284    const TEXTURE_FORMAT: TextureFormat = TextureFormat::R16Unorm;
285}
286
287impl GpuPixel for MonoA16 {
288    const TEXTURE_FORMAT: TextureFormat = TextureFormat::Rg16Unorm;
289}
290
291impl GpuPixel for Rgba16 {
292    const TEXTURE_FORMAT: TextureFormat = TextureFormat::Rgba16Unorm;
293}
294
295// `f32` is no longer a pixel. The
296// single-channel 32-bit float GPU format is carried by `MonoF32`,
297// whose `#[repr(transparent)]` layout over `f32` is
298// byte-identical to the previous bare-float impl.
299impl GpuPixel for MonoF32 {
300    const TEXTURE_FORMAT: TextureFormat = TextureFormat::R32Float;
301}
302
303impl GpuPixel for MonoAF32 {
304    const TEXTURE_FORMAT: TextureFormat = TextureFormat::Rg32Float;
305}
306
307impl GpuPixel for RgbaF32 {
308    const TEXTURE_FORMAT: TextureFormat = TextureFormat::Rgba32Float;
309}
310
311// ═══════════════════════════════════════════════════════════════════════════════
312// Tests
313// ═══════════════════════════════════════════════════════════════════════════════
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318
319    // ── DisplayPixel (1.1) ──────────────────────────────────────────────
320
321    #[test]
322    fn srgba8_to_framebuffer_red() {
323        assert_eq!(Srgba8::new(255, 0, 0, 255).to_framebuffer_u32(), 0x00FF0000);
324    }
325
326    #[test]
327    fn srgba8_to_framebuffer_green() {
328        assert_eq!(Srgba8::new(0, 255, 0, 128).to_framebuffer_u32(), 0x0000FF00);
329    }
330
331    #[test]
332    fn srgba8_to_framebuffer_blue() {
333        assert_eq!(Srgba8::new(0, 0, 255, 0).to_framebuffer_u32(), 0x000000FF);
334    }
335
336    #[test]
337    fn srgba8_to_framebuffer_black() {
338        assert_eq!(Srgba8::new(0, 0, 0, 0).to_framebuffer_u32(), 0x00000000);
339    }
340
341    #[test]
342    fn srgba8_to_framebuffer_white() {
343        assert_eq!(
344            Srgba8::new(255, 255, 255, 255).to_framebuffer_u32(),
345            0x00FFFFFF
346        );
347    }
348
349    #[test]
350    fn srgba8_to_framebuffer_0x123456() {
351        assert_eq!(
352            Srgba8::new(18, 52, 86, 200).to_framebuffer_u32(),
353            0x00123456
354        );
355    }
356
357    #[test]
358    fn srgba8_alpha_is_discarded() {
359        // Same RGB, different alpha → same framebuffer value
360        let a = Srgba8::new(100, 150, 200, 0).to_framebuffer_u32();
361        let b = Srgba8::new(100, 150, 200, 255).to_framebuffer_u32();
362        assert_eq!(a, b);
363    }
364
365    // ── TextureFormat bytes_per_pixel (1.2) ─────────────────────────────
366
367    #[test]
368    fn bytes_per_pixel_1byte_formats() {
369        assert_eq!(TextureFormat::R8Unorm.bytes_per_pixel(), 1);
370        assert_eq!(TextureFormat::R8Srgb.bytes_per_pixel(), 1);
371    }
372
373    #[test]
374    fn bytes_per_pixel_2byte_formats() {
375        assert_eq!(TextureFormat::Rg8Unorm.bytes_per_pixel(), 2);
376        assert_eq!(TextureFormat::R16Unorm.bytes_per_pixel(), 2);
377    }
378
379    #[test]
380    fn bytes_per_pixel_4byte_formats() {
381        assert_eq!(TextureFormat::Rgba8Unorm.bytes_per_pixel(), 4);
382        assert_eq!(TextureFormat::Rgba8Srgb.bytes_per_pixel(), 4);
383        assert_eq!(TextureFormat::Bgra8Unorm.bytes_per_pixel(), 4);
384        assert_eq!(TextureFormat::Bgra8Srgb.bytes_per_pixel(), 4);
385        assert_eq!(TextureFormat::Rg16Unorm.bytes_per_pixel(), 4);
386        assert_eq!(TextureFormat::R32Float.bytes_per_pixel(), 4);
387    }
388
389    #[test]
390    fn bytes_per_pixel_8byte_formats() {
391        assert_eq!(TextureFormat::Rgba16Unorm.bytes_per_pixel(), 8);
392        assert_eq!(TextureFormat::Rg32Float.bytes_per_pixel(), 8);
393    }
394
395    #[test]
396    fn bytes_per_pixel_16byte_formats() {
397        assert_eq!(TextureFormat::Rgba32Float.bytes_per_pixel(), 16);
398    }
399
400    // ── TextureFormat channel_count (1.2) ───────────────────────────────
401
402    #[test]
403    fn channel_count_1ch() {
404        assert_eq!(TextureFormat::R8Unorm.channel_count(), 1);
405        assert_eq!(TextureFormat::R8Srgb.channel_count(), 1);
406        assert_eq!(TextureFormat::R16Unorm.channel_count(), 1);
407        assert_eq!(TextureFormat::R32Float.channel_count(), 1);
408    }
409
410    #[test]
411    fn channel_count_2ch() {
412        assert_eq!(TextureFormat::Rg8Unorm.channel_count(), 2);
413        assert_eq!(TextureFormat::Rg16Unorm.channel_count(), 2);
414        assert_eq!(TextureFormat::Rg32Float.channel_count(), 2);
415    }
416
417    #[test]
418    fn channel_count_4ch() {
419        assert_eq!(TextureFormat::Rgba8Unorm.channel_count(), 4);
420        assert_eq!(TextureFormat::Rgba8Srgb.channel_count(), 4);
421        assert_eq!(TextureFormat::Bgra8Unorm.channel_count(), 4);
422        assert_eq!(TextureFormat::Bgra8Srgb.channel_count(), 4);
423        assert_eq!(TextureFormat::Rgba16Unorm.channel_count(), 4);
424        assert_eq!(TextureFormat::Rgba32Float.channel_count(), 4);
425    }
426
427    // ── TextureFormat consistency check (1.2) ───────────────────────────
428
429    #[test]
430    fn bytes_per_pixel_equals_channels_times_component_size() {
431        // Verify that bytes_per_pixel == channel_count × component_size
432        // for every variant.
433        let all = [
434            TextureFormat::R8Unorm,
435            TextureFormat::R8Srgb,
436            TextureFormat::Rg8Unorm,
437            TextureFormat::Rgba8Unorm,
438            TextureFormat::Rgba8Srgb,
439            TextureFormat::Bgra8Unorm,
440            TextureFormat::Bgra8Srgb,
441            TextureFormat::R16Unorm,
442            TextureFormat::Rg16Unorm,
443            TextureFormat::Rgba16Unorm,
444            TextureFormat::R32Float,
445            TextureFormat::Rg32Float,
446            TextureFormat::Rgba32Float,
447        ];
448        for fmt in &all {
449            let bpp = fmt.bytes_per_pixel();
450            let ch = fmt.channel_count();
451            assert!(
452                bpp % ch == 0,
453                "{fmt:?}: bytes_per_pixel ({bpp}) not divisible by channel_count ({ch})"
454            );
455        }
456    }
457
458    // ── GpuPixel mappings (1.3) ─────────────────────────────────────────
459
460    #[test]
461    fn gpu_pixel_mono8() {
462        assert_eq!(Mono8::TEXTURE_FORMAT, TextureFormat::R8Unorm);
463    }
464
465    #[test]
466    fn gpu_pixel_srgb_mono8() {
467        assert_eq!(SrgbMono8::TEXTURE_FORMAT, TextureFormat::R8Srgb);
468    }
469
470    #[test]
471    fn gpu_pixel_mono_a8() {
472        assert_eq!(MonoA8::TEXTURE_FORMAT, TextureFormat::Rg8Unorm);
473    }
474
475    #[test]
476    fn gpu_pixel_rgba8() {
477        assert_eq!(Rgba8::TEXTURE_FORMAT, TextureFormat::Rgba8Unorm);
478    }
479
480    #[test]
481    fn gpu_pixel_srgba8() {
482        assert_eq!(Srgba8::TEXTURE_FORMAT, TextureFormat::Rgba8Srgb);
483    }
484
485    #[test]
486    fn gpu_pixel_bgra8() {
487        assert_eq!(Bgra8::TEXTURE_FORMAT, TextureFormat::Bgra8Unorm);
488    }
489
490    #[test]
491    fn gpu_pixel_mono16() {
492        assert_eq!(Mono16::TEXTURE_FORMAT, TextureFormat::R16Unorm);
493    }
494
495    #[test]
496    fn gpu_pixel_mono_a16() {
497        assert_eq!(MonoA16::TEXTURE_FORMAT, TextureFormat::Rg16Unorm);
498    }
499
500    #[test]
501    fn gpu_pixel_rgba16() {
502        assert_eq!(Rgba16::TEXTURE_FORMAT, TextureFormat::Rgba16Unorm);
503    }
504
505    #[test]
506    fn gpu_pixel_mono_f32() {
507        assert_eq!(MonoF32::TEXTURE_FORMAT, TextureFormat::R32Float);
508    }
509
510    #[test]
511    fn gpu_pixel_mono_af32() {
512        assert_eq!(MonoAF32::TEXTURE_FORMAT, TextureFormat::Rg32Float);
513    }
514
515    #[test]
516    fn gpu_pixel_rgba_f32() {
517        assert_eq!(RgbaF32::TEXTURE_FORMAT, TextureFormat::Rgba32Float);
518    }
519
520    // ── GpuPixel ↔ TextureFormat consistency (1.3) ──────────────────────
521
522    #[test]
523    fn gpu_pixel_format_bytes_matches_pixel_size() {
524        // For every GpuPixel impl, verify that the TextureFormat's
525        // bytes_per_pixel matches the pixel type's SIZE.
526        assert_eq!(Mono8::TEXTURE_FORMAT.bytes_per_pixel(), Mono8::SIZE);
527        assert_eq!(SrgbMono8::TEXTURE_FORMAT.bytes_per_pixel(), SrgbMono8::SIZE);
528        assert_eq!(MonoA8::TEXTURE_FORMAT.bytes_per_pixel(), MonoA8::SIZE);
529        assert_eq!(Rgba8::TEXTURE_FORMAT.bytes_per_pixel(), Rgba8::SIZE);
530        assert_eq!(Srgba8::TEXTURE_FORMAT.bytes_per_pixel(), Srgba8::SIZE);
531        assert_eq!(Bgra8::TEXTURE_FORMAT.bytes_per_pixel(), Bgra8::SIZE);
532        assert_eq!(Mono16::TEXTURE_FORMAT.bytes_per_pixel(), Mono16::SIZE);
533        assert_eq!(MonoA16::TEXTURE_FORMAT.bytes_per_pixel(), MonoA16::SIZE);
534        assert_eq!(Rgba16::TEXTURE_FORMAT.bytes_per_pixel(), Rgba16::SIZE);
535        assert_eq!(MonoF32::TEXTURE_FORMAT.bytes_per_pixel(), MonoF32::SIZE);
536        assert_eq!(MonoAF32::TEXTURE_FORMAT.bytes_per_pixel(), MonoAF32::SIZE);
537        assert_eq!(RgbaF32::TEXTURE_FORMAT.bytes_per_pixel(), RgbaF32::SIZE);
538    }
539}