Skip to main content

rasterrocket_render/pipe/
simple.rs

1//! Simple pipe: `a_input == 255`, `BlendMode::Normal`, no soft mask, no shape.
2//!
3//! Equivalent to `Splash::pipeRunSimple{Mono8,RGB8,XBGR8,BGR8,CMYK8,DeviceN8}`.
4//! Mono1 is excluded: 1-bit packed bitmaps require bit-level addressing that
5//! belongs in the fill/stroke caller, not a generic span function.
6
7use std::cell::RefCell;
8
9use crate::pipe::{self, PipeSrc, PipeState};
10#[cfg(all(target_arch = "x86_64", feature = "simd-avx2"))]
11use crate::simd;
12use crate::types::BlendMode;
13use color::Pixel;
14
15// Per-thread scratch buffer for pattern spans — grow-never-shrink.
16thread_local! {
17    static PAT_BUF: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
18}
19
20/// Write `x1 - x0 + 1` pixels of solid source colour directly into the
21/// destination row, applying the transfer function.  Destination alpha is
22/// set to 255 (fully opaque).
23///
24/// # Preconditions (checked via `debug_assert!` in `render_span`)
25///
26/// - `pipe.no_transparency(false) && pipe.blend_mode == BlendMode::Normal`
27/// - `dst_pixels.len() == count * P::BYTES`
28/// - `P::BYTES > 0` (Mono1 handled by caller)
29pub(crate) fn render_span_simple<P: Pixel>(
30    pipe: &PipeState<'_>,
31    src: &PipeSrc<'_>,
32    dst_pixels: &mut [u8],
33    dst_alpha: Option<&mut [u8]>,
34    x0: i32,
35    x1: i32,
36    y: i32,
37) {
38    debug_assert_eq!(pipe.blend_mode, BlendMode::Normal);
39    debug_assert_eq!(pipe.a_input, 255);
40
41    #[expect(
42        clippy::cast_sign_loss,
43        reason = "x1 >= x0 is a precondition, so x1 - x0 + 1 >= 1 > 0"
44    )]
45    let count = (x1 - x0 + 1) as usize;
46    let ncomps = P::BYTES;
47
48    match src {
49        PipeSrc::Solid(color) => {
50            debug_assert_eq!(color.len(), ncomps, "solid color length must match ncomps");
51            // Apply transfer to the source colour once, then fill.
52            let mut applied = [0u8; 8]; // DeviceN8 is the largest: 8 bytes.
53            debug_assert!(
54                ncomps <= 8,
55                "ncomps {ncomps} > 8; only modes up to DeviceN8 supported"
56            );
57            pipe::apply_transfer_pixel(pipe, color, &mut applied[..ncomps]);
58            let applied = &applied[..ncomps];
59
60            #[cfg(all(target_arch = "x86_64", feature = "simd-avx2"))]
61            {
62                match ncomps {
63                    1 => simd::blend_solid_gray8(dst_pixels, applied[0], count),
64                    3 => simd::blend_solid_rgb8(
65                        dst_pixels,
66                        [applied[0], applied[1], applied[2]],
67                        count,
68                    ),
69                    _ => {
70                        for chunk in dst_pixels.chunks_exact_mut(ncomps) {
71                            chunk.copy_from_slice(applied);
72                        }
73                    }
74                }
75            }
76            #[cfg(not(all(target_arch = "x86_64", feature = "simd-avx2")))]
77            {
78                for chunk in dst_pixels.chunks_exact_mut(ncomps) {
79                    chunk.copy_from_slice(applied);
80                }
81            }
82        }
83        PipeSrc::Pattern(pat) => {
84            PAT_BUF.with(|cell| {
85                let mut buf = cell.borrow_mut();
86                buf.resize(count * ncomps, 0);
87                pat.fill_span(y, x0, x1, &mut buf);
88                for chunk in buf.chunks_exact_mut(ncomps) {
89                    pipe::apply_transfer_in_place(pipe, chunk);
90                }
91                dst_pixels.copy_from_slice(&buf);
92            });
93        }
94    }
95
96    // Overprint: excluded by no_transparency(); this branch is unreachable in
97    // a correct caller.  Panic loudly rather than silently dropping overprint.
98    debug_assert_eq!(
99        pipe.overprint_mask, 0xFFFF_FFFF,
100        "simple pipe reached with overprint_mask {:#010x}; route through render_span_general",
101        pipe.overprint_mask,
102    );
103
104    // Set destination alpha to fully opaque.
105    if let Some(alpha) = dst_alpha {
106        debug_assert_eq!(alpha.len(), count);
107        for a in alpha.iter_mut() {
108            *a = 255;
109        }
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    use crate::state::TransferSet;
117    use color::{Rgb8, TransferLut};
118
119    fn opaque_normal_pipe() -> PipeState<'static> {
120        PipeState {
121            blend_mode: BlendMode::Normal,
122            a_input: 255,
123            overprint_mask: 0xFFFF_FFFF,
124            overprint_additive: false,
125            transfer: TransferSet::identity_rgb(),
126            soft_mask: None,
127            alpha0: None,
128            knockout: false,
129            knockout_opacity: 255,
130            non_isolated_group: false,
131        }
132    }
133
134    #[test]
135    fn solid_fills_all_pixels() {
136        let pipe = opaque_normal_pipe();
137        let color = [100u8, 150, 200];
138        let src = PipeSrc::Solid(&color);
139        let count = 5usize;
140        let mut dst = vec![0u8; count * 3];
141        let mut alpha = vec![0u8; count];
142
143        render_span_simple::<Rgb8>(&pipe, &src, &mut dst, Some(&mut alpha), 0, 4, 0);
144
145        for i in 0..count {
146            assert_eq!(&dst[i * 3..i * 3 + 3], &color, "pixel {i}");
147            assert_eq!(alpha[i], 255, "alpha {i}");
148        }
149    }
150
151    #[test]
152    fn transfer_applied_to_solid() {
153        // Inverting transfer (T[v] = 255 − v) used for all RGB channels; the
154        // identity tables are used for the gray / CMYK / device_n slots that
155        // aren't exercised here.
156        static DN: [[u8; 256]; 8] = [TransferLut::IDENTITY.0; 8];
157        let id = TransferLut::IDENTITY.as_array();
158        let inv = TransferLut::INVERTED.as_array();
159
160        let transfer = TransferSet {
161            rgb: [inv; 3],
162            gray: id,
163            cmyk: [id; 4],
164            device_n: &DN,
165        };
166
167        let pipe = PipeState {
168            blend_mode: BlendMode::Normal,
169            a_input: 255,
170            overprint_mask: 0xFFFF_FFFF,
171            overprint_additive: false,
172            transfer,
173            soft_mask: None,
174            alpha0: None,
175            knockout: false,
176            knockout_opacity: 255,
177            non_isolated_group: false,
178        };
179
180        let color = [100u8, 150, 200];
181        let src = PipeSrc::Solid(&color);
182        let mut dst = vec![0u8; 3];
183        let mut alpha = vec![0u8; 1];
184        render_span_simple::<Rgb8>(&pipe, &src, &mut dst, Some(&mut alpha), 0, 0, 0);
185
186        assert_eq!(dst[0], 255 - 100, "R transfer inverted");
187        assert_eq!(dst[1], 255 - 150, "G transfer inverted");
188        assert_eq!(dst[2], 255 - 200, "B transfer inverted");
189    }
190
191    #[test]
192    fn no_alpha_plane_works() {
193        let pipe = opaque_normal_pipe();
194        let color = [10u8, 20, 30];
195        let src = PipeSrc::Solid(&color);
196        let mut dst = vec![0u8; 3];
197        render_span_simple::<Rgb8>(&pipe, &src, &mut dst, None, 0, 0, 0);
198        assert_eq!(&dst, &[10, 20, 30]);
199    }
200}