use std::sync::{LazyLock, Mutex};
use lru::LruCache;
use crate::raster::{CpuRasterImage, PixelFormat, RasterImage, RasterStorageId, Resolution};
use crate::render_context::{CompositeInput, RenderContext};
const COMPOSITE_FRAMES_CACHE_ENTRIES: usize = 32;
static COMPOSITE_FRAMES_CACHE: LazyLock<
Mutex<LruCache<CompositeFramesCacheKey, CompositeFramesCacheEntry>>,
> = LazyLock::new(|| Mutex::new(LruCache::unbounded()));
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct CompositeFramesCacheKey {
target: Resolution,
inputs: Vec<RasterStorageId>,
}
#[derive(Clone)]
struct CompositeFramesCacheEntry {
_inputs: Vec<RasterImage>,
output: RasterImage,
}
#[cfg(test)]
fn clear_composite_frames_cache_for_tests() {
if let Ok(mut cache) = COMPOSITE_FRAMES_CACHE.lock() {
cache.clear();
}
}
fn composite_frames_cache_key(
frames: &[RasterImage],
target: Resolution,
) -> CompositeFramesCacheKey {
CompositeFramesCacheKey {
target,
inputs: frames.iter().map(RasterImage::storage_id).collect(),
}
}
fn cached_composite_frames(key: &CompositeFramesCacheKey) -> Option<RasterImage> {
COMPOSITE_FRAMES_CACHE
.lock()
.ok()
.and_then(|mut cache| cache.get(key).map(|entry| entry.output.clone()))
}
fn cache_composite_frames(
key: CompositeFramesCacheKey,
inputs: Vec<RasterImage>,
output: RasterImage,
) {
if let Ok(mut cache) = COMPOSITE_FRAMES_CACHE.lock() {
cache.put(
key,
CompositeFramesCacheEntry {
_inputs: inputs,
output,
},
);
while cache.len() > COMPOSITE_FRAMES_CACHE_ENTRIES {
cache.pop_lru();
}
}
}
pub fn composite_at(
dst: &mut [u8],
dst_size: Resolution,
src: &CpuRasterImage,
offset_x: i32,
offset_y: i32,
) {
assert_eq!(
src.format,
PixelFormat::Rgba8,
"composite_at only supports Rgba8 sources",
);
let dst_w = dst_size.width as i32;
let dst_h = dst_size.height as i32;
let src_w = src.width as i32;
let src_h = src.height as i32;
let x_start = offset_x.max(0);
let y_start = offset_y.max(0);
let x_end = (offset_x + src_w).min(dst_w);
let y_end = (offset_y + src_h).min(dst_h);
if x_end <= x_start || y_end <= y_start {
return;
}
let span_w = (x_end - x_start) as usize;
let rows = (y_end - y_start) as usize;
let stride_dst = dst_w as usize * 4;
let stride_src = src_w as usize * 4;
let dst_base = (y_start as usize) * stride_dst + (x_start as usize) * 4;
let src_base =
((y_start - offset_y) as usize) * stride_src + ((x_start - offset_x) as usize) * 4;
let src_pixels = src.pixels.as_ref();
for row in 0..rows {
let dst_row = &mut dst[dst_base + row * stride_dst..][..span_w * 4];
let src_row = &src_pixels[src_base + row * stride_src..][..span_w * 4];
blend_row(dst_row, src_row);
}
}
pub(crate) fn composite_frames_over(
mut frames: Vec<RasterImage>,
target: Resolution,
ctx: &mut dyn RenderContext,
) -> Option<RasterImage> {
match frames.len() {
0 => return None,
1 => {
let frame = frames.pop().expect("len checked");
if frame.width() == target.width && frame.height() == target.height {
return Some(frame);
}
frames.push(frame);
}
_ => {}
}
let cache_key = composite_frames_cache_key(&frames, target);
if let Some(image) = cached_composite_frames(&cache_key) {
return Some(image);
}
let cache_inputs = frames.clone();
if ctx.prefers_gpu() {
let inputs: Vec<CompositeInput<'_>> = frames
.iter()
.map(|image| CompositeInput {
image,
offset_x: 0,
offset_y: 0,
})
.collect();
if let Some(gpu) = ctx.gpu_backend() {
if let Some(image) = gpu.composite(target, &inputs) {
cache_composite_frames(cache_key, cache_inputs, image.clone());
return Some(image);
}
}
}
let mut buffer = vec![0u8; (target.width as usize) * (target.height as usize) * 4];
for frame in frames {
let frame = ctx.readback(frame);
composite_at(&mut buffer, target, &frame, 0, 0);
}
let image = RasterImage::cpu(target.width, target.height, PixelFormat::Rgba8, buffer);
cache_composite_frames(cache_key, cache_inputs, image.clone());
Some(image)
}
#[inline]
fn blend_row(dst: &mut [u8], src: &[u8]) {
debug_assert_eq!(dst.len(), src.len());
debug_assert_eq!(dst.len() % 4, 0);
let dst_chunks = dst.chunks_exact_mut(4);
let src_chunks = src.chunks_exact(4);
for (d, s) in dst_chunks.zip(src_chunks) {
let sa = s[3] as u32;
if sa == 0 {
continue;
}
if sa == 255 {
d.copy_from_slice(s);
continue;
}
let inv_sa = 255 - sa;
let sr = s[0] as u32;
let sg = s[1] as u32;
let sb = s[2] as u32;
let dr = d[0] as u32;
let dg = d[1] as u32;
let db = d[2] as u32;
let da = d[3] as u32;
let out_a_x255 = sa * 255 + da * inv_sa;
let half = out_a_x255 / 2;
let out_r = (sr * sa * 255 + dr * da * inv_sa + half) / out_a_x255;
let out_g = (sg * sa * 255 + dg * da * inv_sa + half) / out_a_x255;
let out_b = (sb * sa * 255 + db * da * inv_sa + half) / out_a_x255;
let out_a = (out_a_x255 + 127) / 255;
d[0] = out_r as u8;
d[1] = out_g as u8;
d[2] = out_b as u8;
d[3] = out_a as u8;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::color::Color;
use crate::geometry::Vec2;
use crate::raster::RasterComponent;
use crate::render_context::{DropShadowInput, GpuPreference, GpuRasterBackend, OutlineInput};
use crate::vector::VectorGraphic;
#[derive(Default)]
struct FakeGpu {
composite_calls: usize,
composite_inputs: Vec<usize>,
composite_succeeds: bool,
}
impl GpuRasterBackend for FakeGpu {
fn composite(
&mut self,
target: Resolution,
inputs: &[CompositeInput<'_>],
) -> Option<RasterImage> {
self.composite_calls += 1;
self.composite_inputs.push(inputs.len());
assert!(inputs
.iter()
.all(|input| input.offset_x == 0 && input.offset_y == 0));
self.composite_succeeds.then(|| {
RasterImage::cpu(
target.width,
target.height,
PixelFormat::Rgba8,
vec![42u8; (target.width as usize) * (target.height as usize) * 4],
)
})
}
fn drop_shadow(&mut self, _input: DropShadowInput<'_>) -> Option<RasterImage> {
None
}
fn outline(&mut self, _input: OutlineInput<'_>) -> Option<RasterImage> {
None
}
fn rasterize(
&mut self,
_graphic: &VectorGraphic,
_target: Resolution,
) -> Option<RasterImage> {
None
}
fn solid_fill(&mut self, _target: Resolution, _color: Color) -> Option<RasterImage> {
None
}
fn temporal_average(
&mut self,
_target: Resolution,
_frames: &[&RasterImage],
_total: u32,
) -> Option<RasterImage> {
None
}
fn readback(&mut self, _image: RasterImage) -> Option<CpuRasterImage> {
None
}
}
struct FakeContext {
gpu: FakeGpu,
readbacks: usize,
}
impl FakeContext {
fn new(composite_succeeds: bool) -> Self {
Self {
gpu: FakeGpu {
composite_succeeds,
..FakeGpu::default()
},
readbacks: 0,
}
}
}
impl RenderContext for FakeContext {
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn gpu_preference(&self) -> GpuPreference {
GpuPreference::PreferGpu
}
fn gpu_backend(&mut self) -> Option<&mut dyn GpuRasterBackend> {
Some(&mut self.gpu)
}
fn render(
&mut self,
_component: &dyn RasterComponent,
_size: Vec2,
_target: Resolution,
) -> RasterImage {
panic!("composite tests do not render components")
}
fn readback(&mut self, image: RasterImage) -> CpuRasterImage {
self.readbacks += 1;
match image {
RasterImage::Cpu(image) => image,
RasterImage::Gpu(_) => panic!("fake context cannot read back GPU surfaces"),
}
}
}
fn image(width: u32, height: u32, pixels: Vec<u8>) -> CpuRasterImage {
assert_eq!(pixels.len(), (width * height * 4) as usize);
CpuRasterImage::new(width, height, PixelFormat::Rgba8, pixels)
}
fn blend_pixel_oracle(d: [u8; 4], s: [u8; 4]) -> [u8; 4] {
let to_f = |v: u8| v as f64 / 255.0;
let from_f = |v: f64| (v * 255.0).round().clamp(0.0, 255.0) as u8;
let (sr, sg, sb, sa) = (to_f(s[0]), to_f(s[1]), to_f(s[2]), to_f(s[3]));
let (dr, dg, db, da) = (to_f(d[0]), to_f(d[1]), to_f(d[2]), to_f(d[3]));
let inv_sa = 1.0 - sa;
let out_a = sa + da * inv_sa;
let (out_r, out_g, out_b) = if out_a > 0.0 {
(
(sr * sa + dr * da * inv_sa) / out_a,
(sg * sa + dg * da * inv_sa) / out_a,
(sb * sa + db * da * inv_sa) / out_a,
)
} else {
(0.0, 0.0, 0.0)
};
[from_f(out_r), from_f(out_g), from_f(out_b), from_f(out_a)]
}
#[test]
fn transparent_source_leaves_dst_unchanged() {
let mut dst = vec![10, 20, 30, 200, 1, 2, 3, 4];
let src = image(2, 1, vec![255, 255, 255, 0, 255, 255, 255, 0]);
let expected = dst.clone();
composite_at(&mut dst, Resolution::new(2, 1), &src, 0, 0);
assert_eq!(dst, expected);
}
#[test]
fn opaque_source_replaces_dst() {
let mut dst = vec![10, 20, 30, 200, 1, 2, 3, 4];
let src = image(2, 1, vec![100, 150, 200, 255, 1, 2, 3, 255]);
composite_at(&mut dst, Resolution::new(2, 1), &src, 0, 0);
assert_eq!(dst, vec![100, 150, 200, 255, 1, 2, 3, 255]);
}
#[test]
fn partial_alpha_matches_f64_oracle_within_one_lsb() {
for sa in [16u8, 64, 128, 200, 240, 254] {
for da in [0u8, 32, 128, 200, 255] {
for sr in [0u8, 64, 200, 255] {
for dr in [0u8, 64, 200, 255] {
let s = [sr, 128, 64, sa];
let d = [dr, 200, 32, da];
let mut dst = d.to_vec();
let src = image(1, 1, s.to_vec());
composite_at(&mut dst, Resolution::new(1, 1), &src, 0, 0);
let expected = blend_pixel_oracle(d, s);
for ch in 0..4 {
let diff = (dst[ch] as i32 - expected[ch] as i32).abs();
assert!(
diff <= 1,
"channel {ch} mismatch >1 LSB: got {} expected {} (s={:?} d={:?})",
dst[ch],
expected[ch],
s,
d,
);
}
}
}
}
}
}
#[test]
fn clips_source_falling_outside_dst() {
let mut dst = vec![0u8; 4];
let src = image(
2,
2,
vec![10, 20, 30, 255, 1, 2, 3, 255, 4, 5, 6, 255, 7, 8, 9, 255],
);
composite_at(&mut dst, Resolution::new(1, 1), &src, -1, -1);
assert_eq!(dst, vec![7, 8, 9, 255]);
}
#[test]
fn fully_clipped_source_is_a_noop() {
let mut dst = vec![42u8; 16];
let src = image(2, 2, vec![255u8; 16]);
let expected = dst.clone();
composite_at(&mut dst, Resolution::new(2, 2), &src, 5, 5);
assert_eq!(dst, expected);
}
#[test]
fn composite_frames_uses_gpu_batch_without_readback() {
clear_composite_frames_cache_for_tests();
let frames = vec![
RasterImage::cpu(1, 1, PixelFormat::Rgba8, vec![255, 0, 0, 255]),
RasterImage::cpu(1, 1, PixelFormat::Rgba8, vec![0, 0, 255, 255]),
];
let mut ctx = FakeContext::new(true);
let image = composite_frames_over(frames, Resolution::new(1, 1), &mut ctx)
.expect("frames produce an output");
let image = image.into_cpu().expect("fake GPU returns CPU image");
assert_eq!(image.pixels.as_ref(), &[42, 42, 42, 42]);
assert_eq!(ctx.readbacks, 0);
assert_eq!(ctx.gpu.composite_calls, 1);
assert_eq!(ctx.gpu.composite_inputs, vec![2]);
clear_composite_frames_cache_for_tests();
}
#[test]
fn composite_frames_reuses_cached_batch_for_same_input_storage() {
clear_composite_frames_cache_for_tests();
let frames = vec![
RasterImage::cpu(1, 1, PixelFormat::Rgba8, vec![255, 0, 0, 255]),
RasterImage::cpu(1, 1, PixelFormat::Rgba8, vec![0, 0, 255, 255]),
];
let mut ctx = FakeContext::new(true);
let _ = composite_frames_over(frames.clone(), Resolution::new(1, 1), &mut ctx)
.expect("first composite produces an output");
let cached = composite_frames_over(frames, Resolution::new(1, 1), &mut ctx)
.expect("cached composite produces an output");
let cached = cached.into_cpu().expect("fake GPU returns CPU image");
assert_eq!(cached.pixels.as_ref(), &[42, 42, 42, 42]);
assert_eq!(ctx.readbacks, 0);
assert_eq!(ctx.gpu.composite_calls, 1);
assert_eq!(ctx.gpu.composite_inputs, vec![2]);
clear_composite_frames_cache_for_tests();
}
#[test]
fn composite_frames_falls_back_to_cpu_when_gpu_declines() {
clear_composite_frames_cache_for_tests();
let frames = vec![
RasterImage::cpu(1, 1, PixelFormat::Rgba8, vec![255, 0, 0, 255]),
RasterImage::cpu(1, 1, PixelFormat::Rgba8, vec![0, 0, 255, 255]),
];
let mut ctx = FakeContext::new(false);
let image = composite_frames_over(frames, Resolution::new(1, 1), &mut ctx)
.expect("frames produce an output");
let image = image.into_cpu().expect("CPU fallback returns CPU image");
assert_eq!(image.pixels.as_ref(), &[0, 0, 255, 255]);
assert_eq!(ctx.readbacks, 2);
assert_eq!(ctx.gpu.composite_calls, 1);
assert_eq!(ctx.gpu.composite_inputs, vec![2]);
clear_composite_frames_cache_for_tests();
}
}