rasterrocket_render/simd/
composite.rs1const LANE: usize = 16;
27
28#[inline]
30const fn div255_u16(v: u16) -> u16 {
31 (v + 255) >> 8
32}
33
34pub fn composite_aa_rgb8_opaque(dst: &mut [u8], src: [u8; 3], a_input: u8, shape: &[u8]) {
57 let count = shape.len();
58 debug_assert_eq!(
59 dst.len(),
60 count * 3,
61 "composite_aa_rgb8_opaque: dst length mismatch (got {}, expected {})",
62 dst.len(),
63 count * 3,
64 );
65
66 let a_in = u16::from(a_input);
67 let [sr, sg, sb] = [u16::from(src[0]), u16::from(src[1]), u16::from(src[2])];
68
69 let full_chunks = count / LANE;
70 let remainder = count % LANE;
71
72 for chunk in 0..full_chunks {
77 let px_base = chunk * LANE;
78 let byte_base = px_base * 3;
79
80 let mut a_src_lane = [0u16; LANE];
82 for (k, a) in a_src_lane.iter_mut().enumerate() {
83 *a = div255_u16(a_in * u16::from(shape[px_base + k]));
84 }
85
86 for (k, &a_src) in a_src_lane.iter().enumerate() {
88 let inv = 255 - a_src;
89 let b = byte_base + k * 3;
90 #[expect(clippy::cast_possible_truncation, reason = "div255_u16 result ≤ 255")]
92 {
93 dst[b] = div255_u16(inv * u16::from(dst[b]) + a_src * sr) as u8;
94 dst[b + 1] = div255_u16(inv * u16::from(dst[b + 1]) + a_src * sg) as u8;
95 dst[b + 2] = div255_u16(inv * u16::from(dst[b + 2]) + a_src * sb) as u8;
96 }
97 }
98 }
99
100 let tail_px = full_chunks * LANE;
102 let tail_byte = tail_px * 3;
103 for k in 0..remainder {
104 let a_src = div255_u16(a_in * u16::from(shape[tail_px + k]));
105 let inv = 255 - a_src;
106 let b = tail_byte + k * 3;
107 #[expect(clippy::cast_possible_truncation, reason = "div255_u16 result ≤ 255")]
108 {
109 dst[b] = div255_u16(inv * u16::from(dst[b]) + a_src * sr) as u8;
110 dst[b + 1] = div255_u16(inv * u16::from(dst[b + 1]) + a_src * sg) as u8;
111 dst[b + 2] = div255_u16(inv * u16::from(dst[b + 2]) + a_src * sb) as u8;
112 }
113 }
114}
115
116#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
125 fn opaque_full_coverage_writes_src() {
126 let src = [200u8, 100, 50];
127 let shape = [255u8; 4];
128 let mut dst = vec![10u8; 12]; composite_aa_rgb8_opaque(&mut dst, src, 255, &shape);
131
132 for i in 0..4 {
133 assert_eq!(&dst[i * 3..i * 3 + 3], &[200, 100, 50], "pixel {i}");
134 }
135 }
136
137 #[test]
138 fn opaque_zero_coverage_leaves_dst() {
139 let src = [200u8, 100, 50];
140 let shape = [0u8; 4];
141 let original: Vec<u8> = (0..12).map(|i: u8| i * 10).collect();
142 let mut dst = original.clone();
143
144 composite_aa_rgb8_opaque(&mut dst, src, 255, &shape);
145
146 assert_eq!(dst, original);
147 }
148
149 #[test]
150 fn opaque_half_coverage_blends() {
151 let src = [255u8, 255, 255];
152 let shape = [128u8];
153 let mut dst = vec![0u8; 3]; composite_aa_rgb8_opaque(&mut dst, src, 255, &shape);
156
157 let v = dst[0];
159 assert!((125..=131).contains(&v), "expected ~128, got {v}");
160 }
161
162 #[test]
163 fn opaque_matches_scalar_for_large_span() {
164 let src = [100u8, 150, 200];
166 let a_input = 200u8;
167 let count = 37usize; #[expect(
171 clippy::cast_possible_truncation,
172 reason = "mod-256 result fits in u8 by construction"
173 )]
174 let shape: Vec<u8> = (0..count).map(|i| (i * 7 % 256) as u8).collect();
175 #[expect(
176 clippy::cast_possible_truncation,
177 reason = "mod-256 result fits in u8 by construction"
178 )]
179 let initial: Vec<u8> = (0..count * 3).map(|i| (i * 3 % 256) as u8).collect();
180
181 let mut ref_dst = initial.clone();
183 let a_in = u16::from(a_input);
184 let [sr, sg, sb] = [u16::from(src[0]), u16::from(src[1]), u16::from(src[2])];
185 for (shape_v, ref_chunk) in shape.iter().zip(ref_dst.chunks_exact_mut(3)) {
186 let a_src = div255_u16(a_in * u16::from(*shape_v));
187 let inv = 255 - a_src;
188 #[expect(clippy::cast_possible_truncation, reason = "div255_u16 result ≤ 255")]
189 {
190 ref_chunk[0] = div255_u16(inv * u16::from(ref_chunk[0]) + a_src * sr) as u8;
191 ref_chunk[1] = div255_u16(inv * u16::from(ref_chunk[1]) + a_src * sg) as u8;
192 ref_chunk[2] = div255_u16(inv * u16::from(ref_chunk[2]) + a_src * sb) as u8;
193 }
194 }
195
196 let mut got = initial;
197 composite_aa_rgb8_opaque(&mut got, src, a_input, &shape);
198
199 assert_eq!(got, ref_dst, "chunked path mismatch vs scalar reference");
200 }
201
202 #[test]
203 fn opaque_empty_is_noop() {
204 let mut dst: Vec<u8> = vec![];
205 composite_aa_rgb8_opaque(&mut dst, [1, 2, 3], 255, &[]);
206 }
207}