1use crate::{GpuDevice, GpuError, Result};
15use rayon::prelude::*;
16
17use super::utils;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum BlendMode {
26 Normal,
28 Multiply,
30 Screen,
32 Overlay,
34 HardLight,
37 ColorDodge,
40 ColorBurn,
43 LinearBurn,
45 Exclusion,
47}
48
49impl Default for BlendMode {
50 fn default() -> Self {
51 Self::Normal
52 }
53}
54
55#[derive(Debug, Clone)]
59pub struct BlendLayer<'a> {
60 pub data: &'a [u8],
62 pub width: u32,
64 pub height: u32,
66 pub opacity: f32,
68 pub blend_mode: BlendMode,
70}
71
72impl<'a> BlendLayer<'a> {
73 pub fn new(
80 data: &'a [u8],
81 width: u32,
82 height: u32,
83 opacity: f32,
84 blend_mode: BlendMode,
85 ) -> Result<Self> {
86 if !(0.0..=1.0).contains(&opacity) {
87 return Err(GpuError::Internal(format!(
88 "Layer opacity {opacity} is outside [0,1]"
89 )));
90 }
91 utils::validate_buffer_size(data, width, height, 4)?;
92 Ok(Self {
93 data,
94 width,
95 height,
96 opacity,
97 blend_mode,
98 })
99 }
100}
101
102pub struct LayerCompositor;
108
109impl LayerCompositor {
110 pub fn blend_layers(
127 _device: &GpuDevice,
128 layers: &[BlendLayer<'_>],
129 output: &mut [u8],
130 width: u32,
131 height: u32,
132 ) -> Result<()> {
133 Self::blend_layers_cpu(layers, output, width, height)
134 }
135
136 pub fn blend_layers_cpu(
142 layers: &[BlendLayer<'_>],
143 output: &mut [u8],
144 width: u32,
145 height: u32,
146 ) -> Result<()> {
147 utils::validate_dimensions(width, height)?;
148 let expected = (width * height * 4) as usize;
149 if output.len() < expected {
150 return Err(GpuError::InvalidBufferSize {
151 expected,
152 actual: output.len(),
153 });
154 }
155
156 for (idx, layer) in layers.iter().enumerate() {
158 if layer.width != width || layer.height != height {
159 return Err(GpuError::Internal(format!(
160 "Layer {idx} dimensions {}×{} do not match output {}×{}",
161 layer.width, layer.height, width, height
162 )));
163 }
164 }
165
166 output[..expected].fill(0);
168
169 for layer in layers {
171 Self::composite_layer(layer, output, width, height)?;
172 }
173
174 Ok(())
175 }
176
177 fn composite_layer(
179 layer: &BlendLayer<'_>,
180 acc: &mut [u8],
181 width: u32,
182 height: u32,
183 ) -> Result<()> {
184 let n_pixels = (width * height) as usize;
185 let opacity = layer.opacity;
186 let mode = layer.blend_mode;
187
188 acc.par_chunks_exact_mut(4)
189 .zip(layer.data.par_chunks_exact(4))
190 .take(n_pixels)
191 .for_each(|(dst, src)| {
192 let dr = dst[0] as f32 / 255.0;
194 let dg = dst[1] as f32 / 255.0;
195 let db = dst[2] as f32 / 255.0;
196 let da = dst[3] as f32 / 255.0;
197
198 let sr = src[0] as f32 / 255.0;
199 let sg = src[1] as f32 / 255.0;
200 let sb = src[2] as f32 / 255.0;
201 let sa = (src[3] as f32 / 255.0) * opacity;
202
203 let (br, bg, bb) = apply_blend(mode, sr, sg, sb, dr, dg, db);
205
206 let out_a = sa + da * (1.0 - sa);
208 let (or, og, ob) = if out_a > 1e-6 {
209 (
210 (br * sa + dr * da * (1.0 - sa)) / out_a,
211 (bg * sa + dg * da * (1.0 - sa)) / out_a,
212 (bb * sa + db * da * (1.0 - sa)) / out_a,
213 )
214 } else {
215 (0.0, 0.0, 0.0)
216 };
217
218 dst[0] = (or.clamp(0.0, 1.0) * 255.0).round() as u8;
219 dst[1] = (og.clamp(0.0, 1.0) * 255.0).round() as u8;
220 dst[2] = (ob.clamp(0.0, 1.0) * 255.0).round() as u8;
221 dst[3] = (out_a.clamp(0.0, 1.0) * 255.0).round() as u8;
222 });
223
224 Ok(())
225 }
226}
227
228#[inline(always)]
235fn apply_blend(
236 mode: BlendMode,
237 sr: f32,
238 sg: f32,
239 sb: f32,
240 dr: f32,
241 dg: f32,
242 db: f32,
243) -> (f32, f32, f32) {
244 match mode {
245 BlendMode::Normal => (sr, sg, sb),
246 BlendMode::Multiply => (sr * dr, sg * dg, sb * db),
247 BlendMode::Screen => (screen(sr, dr), screen(sg, dg), screen(sb, db)),
248 BlendMode::Overlay => (overlay(dr, sr), overlay(dg, sg), overlay(db, sb)),
249 BlendMode::HardLight => (hard_light(sr, dr), hard_light(sg, dg), hard_light(sb, db)),
250 BlendMode::ColorDodge => (
251 color_dodge(sr, dr),
252 color_dodge(sg, dg),
253 color_dodge(sb, db),
254 ),
255 BlendMode::ColorBurn => (color_burn(sr, dr), color_burn(sg, dg), color_burn(sb, db)),
256 BlendMode::LinearBurn => (
257 linear_burn(sr, dr),
258 linear_burn(sg, dg),
259 linear_burn(sb, db),
260 ),
261 BlendMode::Exclusion => (exclusion(sr, dr), exclusion(sg, dg), exclusion(sb, db)),
262 }
263}
264
265#[inline(always)]
267fn screen(a: f32, b: f32) -> f32 {
268 1.0 - (1.0 - a) * (1.0 - b)
269}
270
271#[inline(always)]
273fn overlay(base: f32, blend: f32) -> f32 {
274 if base < 0.5 {
275 2.0 * base * blend
276 } else {
277 1.0 - 2.0 * (1.0 - base) * (1.0 - blend)
278 }
279}
280
281#[inline(always)]
284fn hard_light(src: f32, dst: f32) -> f32 {
285 if src < 0.5 {
286 2.0 * src * dst
287 } else {
288 1.0 - 2.0 * (1.0 - src) * (1.0 - dst)
289 }
290}
291
292#[inline(always)]
294fn color_dodge(src: f32, dst: f32) -> f32 {
295 if src >= 1.0 {
296 1.0
297 } else {
298 (dst / (1.0 - src)).min(1.0)
299 }
300}
301
302#[inline(always)]
304fn color_burn(src: f32, dst: f32) -> f32 {
305 if src <= 0.0 {
306 0.0
307 } else {
308 (1.0 - (1.0 - dst) / src).max(0.0)
309 }
310}
311
312#[inline(always)]
314fn linear_burn(src: f32, dst: f32) -> f32 {
315 (dst + src - 1.0).max(0.0)
316}
317
318#[inline(always)]
320fn exclusion(src: f32, dst: f32) -> f32 {
321 dst + src - 2.0 * dst * src
322}
323
324#[cfg(test)]
329mod tests {
330 use super::*;
331
332 fn solid_rgba(w: u32, h: u32, r: u8, g: u8, b: u8, a: u8) -> Vec<u8> {
333 let mut v = vec![0u8; (w * h * 4) as usize];
334 for px in v.chunks_exact_mut(4) {
335 px[0] = r;
336 px[1] = g;
337 px[2] = b;
338 px[3] = a;
339 }
340 v
341 }
342
343 #[test]
346 fn test_screen_blend_identity() {
347 assert!((screen(0.0, 0.7) - 0.7).abs() < 1e-6);
349 assert!((screen(1.0, 0.5) - 1.0).abs() < 1e-6);
351 }
352
353 #[test]
354 fn test_multiply_blend_zero() {
355 let (r, _g, b) = apply_blend(BlendMode::Multiply, 0.0, 0.5, 1.0, 0.5, 0.5, 0.5);
356 assert!((r - 0.0).abs() < 1e-6);
357 assert!((b - 0.5).abs() < 1e-6);
358 }
359
360 #[test]
361 fn test_overlay_midpoint() {
362 let v = overlay(0.5, 0.5);
364 assert!((v - 0.5).abs() < 1e-6, "overlay midpoint: {v}");
365 }
366
367 #[test]
370 fn test_blend_zero_layers_produces_black() {
371 let w = 4u32;
372 let h = 4u32;
373 let mut output = solid_rgba(w, h, 255, 255, 255, 255);
374 let result = LayerCompositor::blend_layers_cpu(&[], &mut output, w, h);
376 assert!(result.is_ok());
377 for &v in &output {
379 assert_eq!(v, 0, "zero layers should produce transparent black");
380 }
381 }
382
383 #[test]
384 fn test_blend_single_fully_opaque_layer() {
385 let w = 4u32;
386 let h = 4u32;
387 let src = solid_rgba(w, h, 200, 100, 50, 255);
388 let layer =
389 BlendLayer::new(&src, w, h, 1.0, BlendMode::Normal).expect("create blend layer");
390 let mut out = vec![0u8; (w * h * 4) as usize];
391 LayerCompositor::blend_layers_cpu(&[layer], &mut out, w, h).expect("blend single layer");
392 for i in 0..(w * h) as usize {
393 assert_eq!(out[i * 4], 200, "red mismatch at pixel {i}");
394 assert_eq!(out[i * 4 + 1], 100, "green mismatch at pixel {i}");
395 assert_eq!(out[i * 4 + 2], 50, "blue mismatch at pixel {i}");
396 assert_eq!(out[i * 4 + 3], 255, "alpha mismatch at pixel {i}");
397 }
398 }
399
400 #[test]
401 fn test_blend_two_layers_normal_over() {
402 let w = 4u32;
403 let h = 4u32;
404 let bg = solid_rgba(w, h, 0, 0, 255, 255); let fg = solid_rgba(w, h, 255, 0, 0, 128); let layers = [
407 BlendLayer::new(&bg, w, h, 1.0, BlendMode::Normal).expect("create bg layer"),
408 BlendLayer::new(&fg, w, h, 1.0, BlendMode::Normal).expect("create fg layer"),
409 ];
410 let mut out = vec![0u8; (w * h * 4) as usize];
411 LayerCompositor::blend_layers_cpu(&layers, &mut out, w, h).expect("blend two layers");
412 for i in 0..(w * h) as usize {
414 assert_eq!(out[i * 4 + 3], 255, "composite alpha should be 255");
415 assert!(out[i * 4] > 0, "red should be present");
417 }
418 }
419
420 #[test]
421 fn test_blend_multiply_two_identical_layers() {
422 let w = 4u32;
423 let h = 4u32;
424 let layer_data = solid_rgba(w, h, 128, 128, 128, 255);
426 let layers = [
427 BlendLayer::new(&layer_data, w, h, 1.0, BlendMode::Normal)
428 .expect("create normal layer"),
429 BlendLayer::new(&layer_data, w, h, 1.0, BlendMode::Multiply)
430 .expect("create multiply layer"),
431 ];
432 let mut out = vec![0u8; (w * h * 4) as usize];
433 LayerCompositor::blend_layers_cpu(&layers, &mut out, w, h).expect("blend multiply layers");
434 for i in 0..(w * h) as usize {
436 let r = out[i * 4];
437 assert!(
438 r >= 60 && r <= 68,
439 "multiply result {r} out of expected range [60,68]"
440 );
441 }
442 }
443
444 #[test]
445 fn test_blend_layer_dimension_mismatch() {
446 let w = 4u32;
447 let h = 4u32;
448 let small = solid_rgba(2, 2, 255, 0, 0, 255);
449 let layer = BlendLayer {
450 data: &small,
451 width: 2,
452 height: 2,
453 opacity: 1.0,
454 blend_mode: BlendMode::Normal,
455 };
456 let mut out = vec![0u8; (w * h * 4) as usize];
457 let result = LayerCompositor::blend_layers_cpu(&[layer], &mut out, w, h);
458 assert!(result.is_err(), "mismatched dimensions should error");
459 }
460
461 #[test]
462 fn test_blend_layer_invalid_opacity() {
463 let data = solid_rgba(4, 4, 0, 0, 0, 255);
464 let result = BlendLayer::new(&data, 4, 4, 1.5, BlendMode::Normal);
465 assert!(result.is_err(), "opacity > 1.0 should error");
466 }
467
468 #[test]
469 fn test_blend_screen_mode() {
470 let w = 4u32;
471 let h = 4u32;
472 let bg = solid_rgba(w, h, 128, 128, 128, 255);
473 let fg = solid_rgba(w, h, 128, 128, 128, 255);
474 let layers = [
475 BlendLayer::new(&bg, w, h, 1.0, BlendMode::Normal).expect("create bg layer"),
476 BlendLayer::new(&fg, w, h, 1.0, BlendMode::Screen).expect("create screen fg layer"),
477 ];
478 let mut out = vec![0u8; (w * h * 4) as usize];
479 LayerCompositor::blend_layers_cpu(&layers, &mut out, w, h).expect("blend screen layers");
480 for i in 0..(w * h) as usize {
482 let r = out[i * 4];
483 assert!(
484 r >= 185 && r <= 197,
485 "screen result {r} out of expected range [185,197]"
486 );
487 }
488 }
489
490 #[test]
491 fn test_blend_overlay_mode() {
492 let w = 4u32;
493 let h = 4u32;
494 let bg = solid_rgba(w, h, 100, 200, 50, 255);
495 let fg = solid_rgba(w, h, 200, 100, 150, 255);
496 let layers = [
497 BlendLayer::new(&bg, w, h, 1.0, BlendMode::Normal).expect("create bg layer"),
498 BlendLayer::new(&fg, w, h, 1.0, BlendMode::Overlay).expect("create overlay fg layer"),
499 ];
500 let mut out = vec![0u8; (w * h * 4) as usize];
501 let result = LayerCompositor::blend_layers_cpu(&layers, &mut out, w, h);
502 assert!(result.is_ok());
503 }
504
505 #[test]
508 fn test_hard_light_with_mid_grey_src() {
509 let v = hard_light(0.5, 0.5);
511 assert!(
512 (v - 0.5).abs() < 1e-5,
513 "hard_light(0.5,0.5) should be 0.5, got {v}"
514 );
515 }
516
517 #[test]
518 fn test_hard_light_black_src_yields_zero() {
519 assert!((hard_light(0.0, 0.8) - 0.0).abs() < 1e-6);
521 }
522
523 #[test]
524 fn test_hard_light_white_src_yields_screen_like_max() {
525 let v = hard_light(1.0, 0.3);
527 assert!(
528 (v - 1.0).abs() < 1e-6,
529 "hard_light(1, x) should be 1, got {v}"
530 );
531 }
532
533 #[test]
534 fn test_color_dodge_white_src_yields_one() {
535 assert!((color_dodge(1.0, 0.5) - 1.0).abs() < 1e-6);
537 }
538
539 #[test]
540 fn test_color_dodge_black_src_yields_dst() {
541 let dst = 0.6;
543 let v = color_dodge(0.0, dst);
544 assert!(
545 (v - dst).abs() < 1e-6,
546 "color_dodge(0, {dst}) should be {dst}, got {v}"
547 );
548 }
549
550 #[test]
551 fn test_color_burn_white_src_yields_dst() {
552 let dst = 0.4;
554 let v = color_burn(1.0, dst);
555 assert!(
556 (v - dst).abs() < 1e-6,
557 "color_burn(1, {dst}) should be {dst}, got {v}"
558 );
559 }
560
561 #[test]
562 fn test_color_burn_black_src_yields_zero() {
563 assert!((color_burn(0.0, 0.8) - 0.0).abs() < 1e-6);
565 }
566
567 #[test]
568 fn test_linear_burn_saturates_to_zero() {
569 assert!((linear_burn(0.3, 0.5) - 0.0).abs() < 1e-6);
571 }
572
573 #[test]
574 fn test_linear_burn_normal_case() {
575 let v = linear_burn(0.9, 0.8);
577 assert!((v - 0.7).abs() < 1e-5, "expected 0.7, got {v}");
578 }
579
580 #[test]
581 fn test_exclusion_with_black_src_yields_dst() {
582 let dst = 0.65;
584 let v = exclusion(0.0, dst);
585 assert!(
586 (v - dst).abs() < 1e-6,
587 "exclusion(0, {dst}) should be {dst}, got {v}"
588 );
589 }
590
591 #[test]
592 fn test_exclusion_midpoint_yields_zero_five() {
593 let v = exclusion(0.5, 0.5);
595 assert!(
596 (v - 0.5).abs() < 1e-5,
597 "exclusion midpoint should be 0.5, got {v}"
598 );
599 }
600
601 #[test]
602 fn test_hard_light_blend_layers_round_trip() {
603 let w = 4u32;
604 let h = 4u32;
605 let bg = solid_rgba(w, h, 64, 64, 64, 255);
606 let fg = solid_rgba(w, h, 64, 64, 64, 255);
607 let layers = [
608 BlendLayer::new(&bg, w, h, 1.0, BlendMode::Normal).expect("create bg layer"),
609 BlendLayer::new(&fg, w, h, 1.0, BlendMode::HardLight).expect("create hard light layer"),
610 ];
611 let mut out = vec![0u8; (w * h * 4) as usize];
612 LayerCompositor::blend_layers_cpu(&layers, &mut out, w, h).expect("blend hard light");
613 for i in 0..(w * h) as usize {
615 let r = out[i * 4];
616 assert!(r < 64, "hard light with dark src should darken; got {r}");
617 }
618 }
619
620 #[test]
621 fn test_color_dodge_blend_layers_brightens() {
622 let w = 4u32;
623 let h = 4u32;
624 let bg = solid_rgba(w, h, 100, 100, 100, 255);
626 let fg = solid_rgba(w, h, 128, 128, 128, 255);
627 let layers = [
628 BlendLayer::new(&bg, w, h, 1.0, BlendMode::Normal).expect("create bg layer"),
629 BlendLayer::new(&fg, w, h, 1.0, BlendMode::ColorDodge).expect("create dodge layer"),
630 ];
631 let mut out = vec![0u8; (w * h * 4) as usize];
632 LayerCompositor::blend_layers_cpu(&layers, &mut out, w, h).expect("blend color dodge");
633 for i in 0..(w * h) as usize {
635 assert!(
636 out[i * 4] >= 100,
637 "color dodge should not darken; got {}",
638 out[i * 4]
639 );
640 }
641 }
642
643 #[test]
644 fn test_exclusion_blend_layers_round_trip() {
645 let w = 4u32;
646 let h = 4u32;
647 let bg = solid_rgba(w, h, 128, 128, 128, 255);
648 let fg = solid_rgba(w, h, 128, 128, 128, 255);
649 let layers = [
650 BlendLayer::new(&bg, w, h, 1.0, BlendMode::Normal).expect("create bg layer"),
651 BlendLayer::new(&fg, w, h, 1.0, BlendMode::Exclusion).expect("create exclusion layer"),
652 ];
653 let mut out = vec![0u8; (w * h * 4) as usize];
654 LayerCompositor::blend_layers_cpu(&layers, &mut out, w, h).expect("blend exclusion");
655 for i in 0..(w * h) as usize {
657 let r = out[i * 4];
658 assert!(
659 r >= 120 && r <= 136,
660 "exclusion(0.5,0.5) should be ~128; got {r}"
661 );
662 }
663 }
664}