use crate::{GpuDevice, GpuError, Result};
use rayon::prelude::*;
use super::utils;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlendMode {
Normal,
Multiply,
Screen,
Overlay,
HardLight,
ColorDodge,
ColorBurn,
LinearBurn,
Exclusion,
}
impl Default for BlendMode {
fn default() -> Self {
Self::Normal
}
}
#[derive(Debug, Clone)]
pub struct BlendLayer<'a> {
pub data: &'a [u8],
pub width: u32,
pub height: u32,
pub opacity: f32,
pub blend_mode: BlendMode,
}
impl<'a> BlendLayer<'a> {
pub fn new(
data: &'a [u8],
width: u32,
height: u32,
opacity: f32,
blend_mode: BlendMode,
) -> Result<Self> {
if !(0.0..=1.0).contains(&opacity) {
return Err(GpuError::Internal(format!(
"Layer opacity {opacity} is outside [0,1]"
)));
}
utils::validate_buffer_size(data, width, height, 4)?;
Ok(Self {
data,
width,
height,
opacity,
blend_mode,
})
}
}
pub struct LayerCompositor;
impl LayerCompositor {
pub fn blend_layers(
_device: &GpuDevice,
layers: &[BlendLayer<'_>],
output: &mut [u8],
width: u32,
height: u32,
) -> Result<()> {
Self::blend_layers_cpu(layers, output, width, height)
}
pub fn blend_layers_cpu(
layers: &[BlendLayer<'_>],
output: &mut [u8],
width: u32,
height: u32,
) -> Result<()> {
utils::validate_dimensions(width, height)?;
let expected = (width * height * 4) as usize;
if output.len() < expected {
return Err(GpuError::InvalidBufferSize {
expected,
actual: output.len(),
});
}
for (idx, layer) in layers.iter().enumerate() {
if layer.width != width || layer.height != height {
return Err(GpuError::Internal(format!(
"Layer {idx} dimensions {}×{} do not match output {}×{}",
layer.width, layer.height, width, height
)));
}
}
output[..expected].fill(0);
for layer in layers {
Self::composite_layer(layer, output, width, height)?;
}
Ok(())
}
fn composite_layer(
layer: &BlendLayer<'_>,
acc: &mut [u8],
width: u32,
height: u32,
) -> Result<()> {
let n_pixels = (width * height) as usize;
let opacity = layer.opacity;
let mode = layer.blend_mode;
acc.par_chunks_exact_mut(4)
.zip(layer.data.par_chunks_exact(4))
.take(n_pixels)
.for_each(|(dst, src)| {
let dr = dst[0] as f32 / 255.0;
let dg = dst[1] as f32 / 255.0;
let db = dst[2] as f32 / 255.0;
let da = dst[3] as f32 / 255.0;
let sr = src[0] as f32 / 255.0;
let sg = src[1] as f32 / 255.0;
let sb = src[2] as f32 / 255.0;
let sa = (src[3] as f32 / 255.0) * opacity;
let (br, bg, bb) = apply_blend(mode, sr, sg, sb, dr, dg, db);
let out_a = sa + da * (1.0 - sa);
let (or, og, ob) = if out_a > 1e-6 {
(
(br * sa + dr * da * (1.0 - sa)) / out_a,
(bg * sa + dg * da * (1.0 - sa)) / out_a,
(bb * sa + db * da * (1.0 - sa)) / out_a,
)
} else {
(0.0, 0.0, 0.0)
};
dst[0] = (or.clamp(0.0, 1.0) * 255.0).round() as u8;
dst[1] = (og.clamp(0.0, 1.0) * 255.0).round() as u8;
dst[2] = (ob.clamp(0.0, 1.0) * 255.0).round() as u8;
dst[3] = (out_a.clamp(0.0, 1.0) * 255.0).round() as u8;
});
Ok(())
}
}
#[inline(always)]
fn apply_blend(
mode: BlendMode,
sr: f32,
sg: f32,
sb: f32,
dr: f32,
dg: f32,
db: f32,
) -> (f32, f32, f32) {
match mode {
BlendMode::Normal => (sr, sg, sb),
BlendMode::Multiply => (sr * dr, sg * dg, sb * db),
BlendMode::Screen => (screen(sr, dr), screen(sg, dg), screen(sb, db)),
BlendMode::Overlay => (overlay(dr, sr), overlay(dg, sg), overlay(db, sb)),
BlendMode::HardLight => (hard_light(sr, dr), hard_light(sg, dg), hard_light(sb, db)),
BlendMode::ColorDodge => (
color_dodge(sr, dr),
color_dodge(sg, dg),
color_dodge(sb, db),
),
BlendMode::ColorBurn => (color_burn(sr, dr), color_burn(sg, dg), color_burn(sb, db)),
BlendMode::LinearBurn => (
linear_burn(sr, dr),
linear_burn(sg, dg),
linear_burn(sb, db),
),
BlendMode::Exclusion => (exclusion(sr, dr), exclusion(sg, dg), exclusion(sb, db)),
}
}
#[inline(always)]
fn screen(a: f32, b: f32) -> f32 {
1.0 - (1.0 - a) * (1.0 - b)
}
#[inline(always)]
fn overlay(base: f32, blend: f32) -> f32 {
if base < 0.5 {
2.0 * base * blend
} else {
1.0 - 2.0 * (1.0 - base) * (1.0 - blend)
}
}
#[inline(always)]
fn hard_light(src: f32, dst: f32) -> f32 {
if src < 0.5 {
2.0 * src * dst
} else {
1.0 - 2.0 * (1.0 - src) * (1.0 - dst)
}
}
#[inline(always)]
fn color_dodge(src: f32, dst: f32) -> f32 {
if src >= 1.0 {
1.0
} else {
(dst / (1.0 - src)).min(1.0)
}
}
#[inline(always)]
fn color_burn(src: f32, dst: f32) -> f32 {
if src <= 0.0 {
0.0
} else {
(1.0 - (1.0 - dst) / src).max(0.0)
}
}
#[inline(always)]
fn linear_burn(src: f32, dst: f32) -> f32 {
(dst + src - 1.0).max(0.0)
}
#[inline(always)]
fn exclusion(src: f32, dst: f32) -> f32 {
dst + src - 2.0 * dst * src
}
#[cfg(test)]
mod tests {
use super::*;
fn solid_rgba(w: u32, h: u32, r: u8, g: u8, b: u8, a: u8) -> Vec<u8> {
let mut v = vec![0u8; (w * h * 4) as usize];
for px in v.chunks_exact_mut(4) {
px[0] = r;
px[1] = g;
px[2] = b;
px[3] = a;
}
v
}
#[test]
fn test_screen_blend_identity() {
assert!((screen(0.0, 0.7) - 0.7).abs() < 1e-6);
assert!((screen(1.0, 0.5) - 1.0).abs() < 1e-6);
}
#[test]
fn test_multiply_blend_zero() {
let (r, _g, b) = apply_blend(BlendMode::Multiply, 0.0, 0.5, 1.0, 0.5, 0.5, 0.5);
assert!((r - 0.0).abs() < 1e-6);
assert!((b - 0.5).abs() < 1e-6);
}
#[test]
fn test_overlay_midpoint() {
let v = overlay(0.5, 0.5);
assert!((v - 0.5).abs() < 1e-6, "overlay midpoint: {v}");
}
#[test]
fn test_blend_zero_layers_produces_black() {
let w = 4u32;
let h = 4u32;
let mut output = solid_rgba(w, h, 255, 255, 255, 255);
let result = LayerCompositor::blend_layers_cpu(&[], &mut output, w, h);
assert!(result.is_ok());
for &v in &output {
assert_eq!(v, 0, "zero layers should produce transparent black");
}
}
#[test]
fn test_blend_single_fully_opaque_layer() {
let w = 4u32;
let h = 4u32;
let src = solid_rgba(w, h, 200, 100, 50, 255);
let layer =
BlendLayer::new(&src, w, h, 1.0, BlendMode::Normal).expect("create blend layer");
let mut out = vec![0u8; (w * h * 4) as usize];
LayerCompositor::blend_layers_cpu(&[layer], &mut out, w, h).expect("blend single layer");
for i in 0..(w * h) as usize {
assert_eq!(out[i * 4], 200, "red mismatch at pixel {i}");
assert_eq!(out[i * 4 + 1], 100, "green mismatch at pixel {i}");
assert_eq!(out[i * 4 + 2], 50, "blue mismatch at pixel {i}");
assert_eq!(out[i * 4 + 3], 255, "alpha mismatch at pixel {i}");
}
}
#[test]
fn test_blend_two_layers_normal_over() {
let w = 4u32;
let h = 4u32;
let bg = solid_rgba(w, h, 0, 0, 255, 255); let fg = solid_rgba(w, h, 255, 0, 0, 128); let layers = [
BlendLayer::new(&bg, w, h, 1.0, BlendMode::Normal).expect("create bg layer"),
BlendLayer::new(&fg, w, h, 1.0, BlendMode::Normal).expect("create fg layer"),
];
let mut out = vec![0u8; (w * h * 4) as usize];
LayerCompositor::blend_layers_cpu(&layers, &mut out, w, h).expect("blend two layers");
for i in 0..(w * h) as usize {
assert_eq!(out[i * 4 + 3], 255, "composite alpha should be 255");
assert!(out[i * 4] > 0, "red should be present");
}
}
#[test]
fn test_blend_multiply_two_identical_layers() {
let w = 4u32;
let h = 4u32;
let layer_data = solid_rgba(w, h, 128, 128, 128, 255);
let layers = [
BlendLayer::new(&layer_data, w, h, 1.0, BlendMode::Normal)
.expect("create normal layer"),
BlendLayer::new(&layer_data, w, h, 1.0, BlendMode::Multiply)
.expect("create multiply layer"),
];
let mut out = vec![0u8; (w * h * 4) as usize];
LayerCompositor::blend_layers_cpu(&layers, &mut out, w, h).expect("blend multiply layers");
for i in 0..(w * h) as usize {
let r = out[i * 4];
assert!(
r >= 60 && r <= 68,
"multiply result {r} out of expected range [60,68]"
);
}
}
#[test]
fn test_blend_layer_dimension_mismatch() {
let w = 4u32;
let h = 4u32;
let small = solid_rgba(2, 2, 255, 0, 0, 255);
let layer = BlendLayer {
data: &small,
width: 2,
height: 2,
opacity: 1.0,
blend_mode: BlendMode::Normal,
};
let mut out = vec![0u8; (w * h * 4) as usize];
let result = LayerCompositor::blend_layers_cpu(&[layer], &mut out, w, h);
assert!(result.is_err(), "mismatched dimensions should error");
}
#[test]
fn test_blend_layer_invalid_opacity() {
let data = solid_rgba(4, 4, 0, 0, 0, 255);
let result = BlendLayer::new(&data, 4, 4, 1.5, BlendMode::Normal);
assert!(result.is_err(), "opacity > 1.0 should error");
}
#[test]
fn test_blend_screen_mode() {
let w = 4u32;
let h = 4u32;
let bg = solid_rgba(w, h, 128, 128, 128, 255);
let fg = solid_rgba(w, h, 128, 128, 128, 255);
let layers = [
BlendLayer::new(&bg, w, h, 1.0, BlendMode::Normal).expect("create bg layer"),
BlendLayer::new(&fg, w, h, 1.0, BlendMode::Screen).expect("create screen fg layer"),
];
let mut out = vec![0u8; (w * h * 4) as usize];
LayerCompositor::blend_layers_cpu(&layers, &mut out, w, h).expect("blend screen layers");
for i in 0..(w * h) as usize {
let r = out[i * 4];
assert!(
r >= 185 && r <= 197,
"screen result {r} out of expected range [185,197]"
);
}
}
#[test]
fn test_blend_overlay_mode() {
let w = 4u32;
let h = 4u32;
let bg = solid_rgba(w, h, 100, 200, 50, 255);
let fg = solid_rgba(w, h, 200, 100, 150, 255);
let layers = [
BlendLayer::new(&bg, w, h, 1.0, BlendMode::Normal).expect("create bg layer"),
BlendLayer::new(&fg, w, h, 1.0, BlendMode::Overlay).expect("create overlay fg layer"),
];
let mut out = vec![0u8; (w * h * 4) as usize];
let result = LayerCompositor::blend_layers_cpu(&layers, &mut out, w, h);
assert!(result.is_ok());
}
#[test]
fn test_hard_light_with_mid_grey_src() {
let v = hard_light(0.5, 0.5);
assert!(
(v - 0.5).abs() < 1e-5,
"hard_light(0.5,0.5) should be 0.5, got {v}"
);
}
#[test]
fn test_hard_light_black_src_yields_zero() {
assert!((hard_light(0.0, 0.8) - 0.0).abs() < 1e-6);
}
#[test]
fn test_hard_light_white_src_yields_screen_like_max() {
let v = hard_light(1.0, 0.3);
assert!(
(v - 1.0).abs() < 1e-6,
"hard_light(1, x) should be 1, got {v}"
);
}
#[test]
fn test_color_dodge_white_src_yields_one() {
assert!((color_dodge(1.0, 0.5) - 1.0).abs() < 1e-6);
}
#[test]
fn test_color_dodge_black_src_yields_dst() {
let dst = 0.6;
let v = color_dodge(0.0, dst);
assert!(
(v - dst).abs() < 1e-6,
"color_dodge(0, {dst}) should be {dst}, got {v}"
);
}
#[test]
fn test_color_burn_white_src_yields_dst() {
let dst = 0.4;
let v = color_burn(1.0, dst);
assert!(
(v - dst).abs() < 1e-6,
"color_burn(1, {dst}) should be {dst}, got {v}"
);
}
#[test]
fn test_color_burn_black_src_yields_zero() {
assert!((color_burn(0.0, 0.8) - 0.0).abs() < 1e-6);
}
#[test]
fn test_linear_burn_saturates_to_zero() {
assert!((linear_burn(0.3, 0.5) - 0.0).abs() < 1e-6);
}
#[test]
fn test_linear_burn_normal_case() {
let v = linear_burn(0.9, 0.8);
assert!((v - 0.7).abs() < 1e-5, "expected 0.7, got {v}");
}
#[test]
fn test_exclusion_with_black_src_yields_dst() {
let dst = 0.65;
let v = exclusion(0.0, dst);
assert!(
(v - dst).abs() < 1e-6,
"exclusion(0, {dst}) should be {dst}, got {v}"
);
}
#[test]
fn test_exclusion_midpoint_yields_zero_five() {
let v = exclusion(0.5, 0.5);
assert!(
(v - 0.5).abs() < 1e-5,
"exclusion midpoint should be 0.5, got {v}"
);
}
#[test]
fn test_hard_light_blend_layers_round_trip() {
let w = 4u32;
let h = 4u32;
let bg = solid_rgba(w, h, 64, 64, 64, 255);
let fg = solid_rgba(w, h, 64, 64, 64, 255);
let layers = [
BlendLayer::new(&bg, w, h, 1.0, BlendMode::Normal).expect("create bg layer"),
BlendLayer::new(&fg, w, h, 1.0, BlendMode::HardLight).expect("create hard light layer"),
];
let mut out = vec![0u8; (w * h * 4) as usize];
LayerCompositor::blend_layers_cpu(&layers, &mut out, w, h).expect("blend hard light");
for i in 0..(w * h) as usize {
let r = out[i * 4];
assert!(r < 64, "hard light with dark src should darken; got {r}");
}
}
#[test]
fn test_color_dodge_blend_layers_brightens() {
let w = 4u32;
let h = 4u32;
let bg = solid_rgba(w, h, 100, 100, 100, 255);
let fg = solid_rgba(w, h, 128, 128, 128, 255);
let layers = [
BlendLayer::new(&bg, w, h, 1.0, BlendMode::Normal).expect("create bg layer"),
BlendLayer::new(&fg, w, h, 1.0, BlendMode::ColorDodge).expect("create dodge layer"),
];
let mut out = vec![0u8; (w * h * 4) as usize];
LayerCompositor::blend_layers_cpu(&layers, &mut out, w, h).expect("blend color dodge");
for i in 0..(w * h) as usize {
assert!(
out[i * 4] >= 100,
"color dodge should not darken; got {}",
out[i * 4]
);
}
}
#[test]
fn test_exclusion_blend_layers_round_trip() {
let w = 4u32;
let h = 4u32;
let bg = solid_rgba(w, h, 128, 128, 128, 255);
let fg = solid_rgba(w, h, 128, 128, 128, 255);
let layers = [
BlendLayer::new(&bg, w, h, 1.0, BlendMode::Normal).expect("create bg layer"),
BlendLayer::new(&fg, w, h, 1.0, BlendMode::Exclusion).expect("create exclusion layer"),
];
let mut out = vec![0u8; (w * h * 4) as usize];
LayerCompositor::blend_layers_cpu(&layers, &mut out, w, h).expect("blend exclusion");
for i in 0..(w * h) as usize {
let r = out[i * 4];
assert!(
r >= 120 && r <= 136,
"exclusion(0.5,0.5) should be ~128; got {r}"
);
}
}
}