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}