Skip to main content

mediaframe/source/
rgb48.rs

1//! Packed RGB48 source (`AV_PIX_FMT_RGB48{LE,BE}`) — 16 bits per channel,
2//! `u16` element order `R, G, B`. Stride in u16 elements (≥ `3 * width`).
3//!
4//! The marker carries `<const BE: bool = false>`: `Rgb48` (= `Rgb48<false>`)
5//! is the LE source; `Rgb48<true>` is the BE source. The walker
6//! [`rgb48_to::<BE>`] propagates `BE` from [`Rgb48Frame<'_, BE>`] into the
7//! sinker dispatch.
8//!
9//! Outputs (Tier 8 finish):
10//! - `with_rgb`      — narrow each channel `>> 8`, pack as R, G, B.
11//! - `with_rgba`     — same narrow + alpha = `0xFF`.
12//! - `with_rgb_u16`  — native u16 passthrough (R, G, B order preserved).
13//! - `with_rgba_u16` — native u16 passthrough + alpha = `0xFFFF`.
14//! - `with_luma`     — Y′ from R/G/B after narrowing to u8.
15//! - `with_luma_u16` — Y′ computed at u8 precision (matching `with_luma`'s
16//!   output) and zero-extended to u16. Same convention as the 8-bit-source
17//!   family; not native 16-bit luma precision.
18//! - `with_hsv`      — HSV via u8 RGB staging.
19
20use crate::frame::Rgb48Frame;
21
22walker! {
23  packed_be {
24    /// Zero-sized marker for the packed **RGB48** source format
25    /// (`AV_PIX_FMT_RGB48{LE,BE}`). `<const BE: bool>` defaults to `false`
26    /// (LE); the alias `Rgb48` resolves to `Rgb48<false>`.
27    #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
28    marker: Rgb48,
29    frame: Rgb48Frame,
30    row: Rgb48Row,
31    sink: Rgb48Sink,
32    walker: rgb48_to,
33    walker_endian: rgb48_to_endian,
34    buf_field: rgb48,
35    elem_type: u16,
36    row_elems: |w| w * 3,
37    row_doc: "One row of an [`Rgb48`] source — `width * 3` u16 elements \
38              (`R, G, B` per pixel, each channel 16 bits). Endianness is \
39              recorded on the parent [`Rgb48Frame<'_, BE>`] / sinker, not on \
40              the Row itself — the kernel monomorphizes on `BE` at the \
41              sinker dispatch.",
42    walker_doc: "Walks an [`Rgb48Frame<'_, BE>`] row by row into the sink. \
43                 Propagates `<const BE: bool>` from the frame into \
44                 [`Rgb48Sink<BE>`].",
45  }
46}
47
48#[cfg(all(test, feature = "std"))]
49mod tests {
50  use super::*;
51  use crate::{PixelSink, color::Matrix, frame::Rgb48Frame};
52  use core::convert::Infallible;
53
54  struct CountingSink {
55    rows_seen: usize,
56    last_width: usize,
57    last_row_idx: usize,
58  }
59  impl PixelSink for CountingSink {
60    type Input<'r> = Rgb48Row<'r>;
61    type Error = Infallible;
62    fn begin_frame(&mut self, _w: u32, _h: u32) -> Result<(), Infallible> {
63      Ok(())
64    }
65    fn process(&mut self, row: Rgb48Row<'_>) -> Result<(), Infallible> {
66      self.rows_seen += 1;
67      self.last_width = row.rgb48().len();
68      self.last_row_idx = row.row();
69      Ok(())
70    }
71  }
72  impl Rgb48Sink for CountingSink {}
73
74  #[test]
75  fn rgb48_walker_visits_every_row_once() {
76    // width=4, stride=12 (3*4), height=4 → plane needs 48 u16 elements
77    let buf = std::vec![0u16; 12 * 4];
78    let frame = Rgb48Frame::new(&buf, 4, 4, 12);
79    let mut sink = CountingSink {
80      rows_seen: 0,
81      last_width: 0,
82      last_row_idx: 0,
83    };
84    rgb48_to(&frame, true, Matrix::Bt709, &mut sink).unwrap();
85    assert_eq!(sink.rows_seen, 4);
86    assert_eq!(sink.last_width, 12); // width * 3 u16 elements per row
87    assert_eq!(sink.last_row_idx, 3);
88  }
89
90  // Compile-pass regression for the LE-only custom sink spelling. The
91  // generated `$sink<const BE: bool = false>` carries an LE default so
92  // downstream callers can keep writing `impl Rgb48Sink for MySink`
93  // (no `<false>`) and `S: Rgb48Sink` bounds. This mirrors the fix for
94  // codex high-severity finding on `walker_macro.rs:242`.
95  #[test]
96  fn rgb48_sink_le_default_compiles_without_const_arg() {
97    // `impl Rgb48Sink for CountingSink` (above) would already fail to
98    // compile if the LE default regressed; this test additionally pins
99    // the bare-bound form `S: Rgb48Sink` and confirms it monomorphizes
100    // to the LE walker.
101    fn walks_le<S: Rgb48Sink>(frame: &Rgb48Frame<'_>, sink: &mut S) -> Result<(), S::Error> {
102      rgb48_to(frame, true, Matrix::Bt709, sink)
103    }
104
105    let buf = std::vec![0u16; 12 * 4];
106    let frame = Rgb48Frame::new(&buf, 4, 4, 12);
107    let mut sink = CountingSink {
108      rows_seen: 0,
109      last_width: 0,
110      last_row_idx: 0,
111    };
112    walks_le(&frame, &mut sink).unwrap();
113    assert_eq!(sink.rows_seen, 4);
114  }
115
116  // Compile-pass regression for the codex finding (PR #105 review). Switching
117  // from `walker!(packed)` to `walker!(packed_be)` would otherwise change the
118  // public `rgb48_to` signature from one generic param (`S`) to two
119  // (`S, const BE: bool`), which breaks downstream callers using the previous
120  // explicit sink spelling `rgb48_to::<MySink>(...)`. Function-position
121  // const-generic defaults aren't allowed, so the macro emits an LE-only
122  // wrapper preserving the original signature.
123  #[test]
124  fn rgb48_to_explicit_turbofish_one_generic_compiles() {
125    #[allow(clippy::type_complexity)]
126    fn _check<S: Rgb48Sink>() {
127      let _: fn(&crate::frame::Rgb48LeFrame<'_>, bool, Matrix, &mut S) -> Result<(), S::Error> =
128        rgb48_to::<S>;
129    }
130  }
131}