diff --git a/crates/ass-renderer/src/backends/software.rs b/crates/ass-renderer/src/backends/software.rs index a95349d..95b4a94 100644 --- a/crates/ass-renderer/src/backends/software.rs +++ b/crates/ass-renderer/src/backends/software.rs @@ -90,7 +90,7 @@ fn draw_raster_layer(pixmap: &mut PixmapMut<'_>, data: &crate::pipeline::RasterD } /// Draw vector layer data onto the given pixmap. -fn draw_vector_layer(pixmap: &mut PixmapMut<'_>, data: &crate::pipeline::VectorData) -> Result<(), RenderError> { +fn draw_vector_layer(pixmap: &mut PixmapMut<'_>, data: &crate::pipeline::VectorData, arena: &bumpalo::Bump) -> Result<(), RenderError> { log::trace!( "DRAWING RENDER: draw_vector_layer called with color {:?} effects={:?}", data.color, data.effects.len() @@ -188,7 +188,7 @@ fn draw_vector_layer(pixmap: &mut PixmapMut<'_>, data: &crate::pipeline::VectorD temp.stroke_path(path, &stroke_paint, &sk_stroke, temp_transform, None); } - apply_gaussian_blur(&mut temp, radius); + apply_gaussian_blur(&mut temp, radius, arena); let composite_paint = tiny_skia::PixmapPaint { blend_mode: tiny_skia::BlendMode::SourceOver, @@ -221,6 +221,7 @@ fn draw_text_layer( cache: &mut RenderCache, glyph_renderer: &mut crate::pipeline::shaping::GlyphRenderer, font_db: &Arc, + arena: &bumpalo::Bump, data: &crate::pipeline::TextData, ) -> Result<(), RenderError> { use crate::pipeline::shaping::shape_text_with_style; @@ -711,7 +712,7 @@ fn draw_text_layer( } } - apply_be_blur(&mut edge_temp, eb_radius as u32); + apply_be_blur(&mut edge_temp, eb_radius as u32, arena); let blend_transform = render_base.pre_translate( -(blur_size as f32) - *width, @@ -1002,7 +1003,7 @@ fn draw_text_layer( render_all_layers!(temp_pixmap, temp_base_transform, None::<&tiny_skia::Mask>, expand, expand + scaled_baseline); // Apply blur to the entire composited result - apply_gaussian_blur(&mut temp_pixmap, radius); + apply_gaussian_blur(&mut temp_pixmap, radius, arena); // Composite blurred temp pixmap back to self.pixmap at the correct position let composite_transform = Transform::from_translate( @@ -1036,7 +1037,10 @@ fn draw_text_layer( /// /// The `sigma` parameter is a gaussian sigma value. We build a 1-D kernel at /// radius ceil(3*sigma) and apply separable horizontal + vertical passes. -fn apply_gaussian_blur(pixmap: &mut Pixmap, radius: f32) { +/// +/// `arena` is a per-frame bumpalo arena; scratch allocations are freed when the +/// arena resets at the start of the next frame. +fn apply_gaussian_blur(pixmap: &mut Pixmap, radius: f32, arena: &bumpalo::Bump) { if radius <= 0.0 { return; } @@ -1052,19 +1056,19 @@ fn apply_gaussian_blur(pixmap: &mut Pixmap, radius: f32) { return; } - // Build normalised 1-D gaussian kernel. + // Build normalised 1-D gaussian kernel — arena allocated. let kernel_size = kernel_radius * 2 + 1; - let mut kernel = vec![0.0f32; kernel_size]; + let mut kernel = bumpalo::collections::Vec::with_capacity_in(kernel_size, arena); let two_sigma_sq = 2.0 * sigma * sigma; let mut sum = 0.0f32; for i in 0..kernel_size { let x = i as f32 - kernel_radius as f32; let w = (-x * x / two_sigma_sq).exp(); - kernel[i] = w; + kernel.push(w); sum += w; } // Normalise so weights sum to 1. - for w in &mut kernel { + for w in kernel.iter_mut() { *w /= sum; } @@ -1072,7 +1076,10 @@ fn apply_gaussian_blur(pixmap: &mut Pixmap, radius: f32) { let height = pixmap.height() as usize; let data = pixmap.data_mut(); - let mut temp = vec![0u8; data.len()]; + // Scratch buffer for the horizontal pass — arena allocated. + let mut temp = bumpalo::collections::Vec::from_iter_in( + core::iter::repeat(0u8).take(data.len()), arena + ); // Horizontal pass: data → temp for y in 0..height { @@ -1132,7 +1139,10 @@ fn apply_gaussian_blur(pixmap: &mut Pixmap, radius: f32) { /// Applies a 3x3 weighted kernel `[[1,2,1],[2,4,2],[1,2,1]]` (total weight 16) /// the specified number of `passes` times. This is the ASS spec edge blur, which /// is distinct from both gaussian blur and box blur. -fn apply_be_blur(pixmap: &mut Pixmap, passes: u32) { +/// +/// `arena` is a per-frame bumpalo arena; scratch allocations are freed when the +/// arena resets at the start of the next frame. +fn apply_be_blur(pixmap: &mut Pixmap, passes: u32, arena: &bumpalo::Bump) { if passes == 0 { return; } @@ -1147,7 +1157,10 @@ fn apply_be_blur(pixmap: &mut Pixmap, passes: u32) { // Total weight = 16 let kernel: [[u32; 3]; 3] = [[1, 2, 1], [2, 4, 2], [1, 2, 1]]; - let mut temp = vec![0u8; width * height * 4]; + // Scratch buffer — arena allocated. + let mut temp = bumpalo::collections::Vec::from_iter_in( + core::iter::repeat(0u8).take(width * height * 4), arena + ); for _ in 0..passes { let data = pixmap.data(); @@ -1190,14 +1203,14 @@ fn apply_be_blur(pixmap: &mut Pixmap, passes: u32) { /// Apply optimized gaussian blur using SIMD when available #[cfg(feature = "simd")] #[allow(dead_code)] // Placeholder for future SIMD optimization -fn apply_gaussian_blur_simd(pixmap: &mut Pixmap, radius: f32) { +fn apply_gaussian_blur_simd(pixmap: &mut Pixmap, radius: f32, arena: &bumpalo::Bump) { if radius <= 0.0 { return; } // Use SIMD instructions for faster blur // This is a placeholder - real SIMD implementation would use intrinsics - apply_gaussian_blur(pixmap, radius); + apply_gaussian_blur(pixmap, radius, arena); } impl RenderBackend for SoftwareBackend { @@ -1245,13 +1258,14 @@ impl RenderBackend for SoftwareBackend { let font_db: &Arc = &self.font_db; let cache: &mut RenderCache = &mut self.cache; let glyph_renderer: &mut crate::pipeline::shaping::GlyphRenderer = &mut self.glyph_renderer; + let arena: &bumpalo::Bump = self.arena.get_mut().unwrap(); let mut pixmap = PixmapMut::from_bytes(frame_buf.as_mut(), w, h) .ok_or(RenderError::InvalidDimensions)?; for layer in layers { match layer { IntermediateLayer::Raster(data) => draw_raster_layer(&mut pixmap, data)?, - IntermediateLayer::Vector(data) => draw_vector_layer(&mut pixmap, data)?, - IntermediateLayer::Text(data) => draw_text_layer(&mut pixmap, cache, glyph_renderer, font_db, data)?, + IntermediateLayer::Vector(data) => draw_vector_layer(&mut pixmap, data, arena)?, + IntermediateLayer::Text(data) => draw_text_layer(&mut pixmap, cache, glyph_renderer, font_db, arena, data)?, } } } @@ -1316,6 +1330,7 @@ impl RenderBackend for SoftwareBackend { let font_db: &Arc = &self.font_db; let cache: &mut RenderCache = &mut self.cache; let glyph_renderer: &mut crate::pipeline::shaping::GlyphRenderer = &mut self.glyph_renderer; + let arena: &bumpalo::Bump = self.arena.get_mut().unwrap(); let mut pixmap = PixmapMut::from_bytes(frame_buf.as_mut(), w, h) .ok_or(RenderError::InvalidDimensions)?; @@ -1330,8 +1345,8 @@ impl RenderBackend for SoftwareBackend { if layer.intersects_region(region) { match layer { IntermediateLayer::Raster(data) => draw_raster_layer(&mut pixmap, data)?, - IntermediateLayer::Vector(data) => draw_vector_layer(&mut pixmap, data)?, - IntermediateLayer::Text(data) => draw_text_layer(&mut pixmap, cache, glyph_renderer, font_db, data)?, + IntermediateLayer::Vector(data) => draw_vector_layer(&mut pixmap, data, arena)?, + IntermediateLayer::Text(data) => draw_text_layer(&mut pixmap, cache, glyph_renderer, font_db, arena, data)?, } } }