From 3c4bbb45210e4d2a852fa4dc55b7b2c2614bf42b Mon Sep 17 00:00:00 2001 From: Orual Date: Sat, 11 Apr 2026 09:26:52 -0400 Subject: [PATCH] refactor: zero-copy output via PixmapMut + Bytes, Frame stores Bytes --- crates/ass-renderer/src/backends/mod.rs | 6 +- crates/ass-renderer/src/backends/software.rs | 493 ++++++++++-------- .../ass-renderer/src/debug/libass_renderer.rs | 4 +- crates/ass-renderer/src/renderer/frame.rs | 47 +- 4 files changed, 293 insertions(+), 257 deletions(-) diff --git a/crates/ass-renderer/src/backends/mod.rs b/crates/ass-renderer/src/backends/mod.rs index 377e87c..56cd166 100644 --- a/crates/ass-renderer/src/backends/mod.rs +++ b/crates/ass-renderer/src/backends/mod.rs @@ -7,7 +7,7 @@ use crate::utils::{DirtyRegion, FontDb, RenderError}; #[cfg(feature = "nostd")] use alloc::{boxed::Box, format, sync::Arc, vec::Vec}; #[cfg(not(feature = "nostd"))] -use std::{boxed::Box, sync::Arc, vec::Vec}; +use std::{boxed::Box, sync::Arc}; // ToString is only needed for backends that aren't compiled with minimal features #[cfg(all( @@ -75,7 +75,7 @@ pub trait RenderBackend { &mut self, layers: &[IntermediateLayer], context: &RenderContext, - ) -> Result, RenderError>; + ) -> Result; /// Composite layers incrementally (dirty regions only) fn composite_layers_incremental( @@ -84,7 +84,7 @@ pub trait RenderBackend { dirty_regions: &[DirtyRegion], previous_frame: &[u8], context: &RenderContext, - ) -> Result, RenderError> { + ) -> Result { // Default implementation: full re-render let _ = (dirty_regions, previous_frame); self.composite_layers(layers, context) diff --git a/crates/ass-renderer/src/backends/software.rs b/crates/ass-renderer/src/backends/software.rs index 387f428..a95349d 100644 --- a/crates/ass-renderer/src/backends/software.rs +++ b/crates/ass-renderer/src/backends/software.rs @@ -5,16 +5,20 @@ use alloc::{boxed::Box, format, sync::Arc, vec, vec::Vec}; #[cfg(not(feature = "nostd"))] use std::{boxed::Box, sync::Arc, vec::Vec}; +use bytes::{Bytes, BytesMut}; use crate::backends::{BackendFeature, BackendType, RenderBackend}; use crate::cache::{RenderCache, TextCacheKey}; use crate::pipeline::{IntermediateLayer, Pipeline, SoftwarePipeline}; use crate::renderer::RenderContext; use crate::utils::{DirtyRegion, FontDb, RenderError}; -use tiny_skia::{Pixmap, Transform}; +use tiny_skia::{Pixmap, PixmapMut, Transform}; /// Software rendering backend using tiny-skia pub struct SoftwareBackend { - pixmap: Pixmap, + frame_buf: BytesMut, + frame_width: u32, + frame_height: u32, + arena: std::sync::Mutex, font_db: Arc, glyph_renderer: crate::pipeline::shaping::GlyphRenderer, cache: RenderCache, @@ -26,11 +30,17 @@ impl SoftwareBackend { /// Create a new software backend, sharing the font database from the /// render context rather than building its own. pub fn new(context: &RenderContext) -> Result { - let pixmap = - Pixmap::new(context.width(), context.height()).ok_or(RenderError::InvalidDimensions)?; - + let w = context.width(); + let h = context.height(); + if w == 0 || h == 0 { + return Err(RenderError::InvalidDimensions); + } + let needed = (w * h * 4) as usize; Ok(Self { - pixmap, + frame_buf: BytesMut::zeroed(needed), + frame_width: w, + frame_height: h, + arena: std::sync::Mutex::new(bumpalo::Bump::new()), font_db: context.font_db_arc(), glyph_renderer: crate::pipeline::shaping::GlyphRenderer::new(), cache: RenderCache::with_limits(2000, 1000), @@ -39,207 +49,180 @@ impl SoftwareBackend { }) } - /// Resize the backend pixmap + /// Resize the backend frame buffer pub fn resize(&mut self, width: u32, height: u32) -> Result<(), RenderError> { - self.pixmap = Pixmap::new(width, height).ok_or(RenderError::InvalidDimensions)?; - Ok(()) - } - - fn composite_layer( - &mut self, - layer: &IntermediateLayer, - _context: &RenderContext, - ) -> Result<(), RenderError> { - match layer { - IntermediateLayer::Raster(raster_data) => { - self.draw_raster_layer(raster_data)?; - } - IntermediateLayer::Vector(path_data) => { - self.draw_vector_layer(path_data)?; - } - IntermediateLayer::Text(text_data) => { - self.draw_text_layer(text_data)?; - } - } - Ok(()) - } - - fn draw_raster_layer(&mut self, data: &crate::pipeline::RasterData) -> Result<(), RenderError> { - if data.pixels.len() != (data.width * data.height * 4) as usize { - return Err(RenderError::InvalidBufferSize { - expected: (data.width * data.height * 4) as usize, - actual: data.pixels.len(), - }); + if width == 0 || height == 0 { + return Err(RenderError::InvalidDimensions); } - - let src_pixmap = Pixmap::from_vec( - data.pixels.clone(), - tiny_skia::IntSize::from_wh(data.width, data.height) - .ok_or(RenderError::InvalidDimensions)?, - ) - .ok_or(RenderError::InvalidPixmap)?; - - let transform = Transform::from_translate(data.x as f32, data.y as f32); - - // Use SourceOver blend mode for proper alpha compositing - let paint = tiny_skia::PixmapPaint { - blend_mode: tiny_skia::BlendMode::SourceOver, - ..Default::default() - }; - - self.pixmap - .draw_pixmap(0, 0, src_pixmap.as_ref(), &paint, transform, None); - + let needed = (width * height * 4) as usize; + self.frame_buf = BytesMut::zeroed(needed); + self.frame_width = width; + self.frame_height = height; Ok(()) } - fn draw_vector_layer(&mut self, data: &crate::pipeline::VectorData) -> Result<(), RenderError> { - log::trace!( - "DRAWING RENDER: draw_vector_layer called with color {:?} effects={:?}", - data.color, data.effects.len() - ); - let Some(original_path) = &data.path else { - return Ok(()); - }; +} - // Apply rotation if present - let rotation_z = data.effects.iter().find_map(|e| { - if let crate::pipeline::TextEffect::Rotation { z, .. } = e { - if *z != 0.0 { Some(*z) } else { None } - } else { - None - } +/// Draw raster layer data onto the given pixmap. +fn draw_raster_layer(pixmap: &mut PixmapMut<'_>, data: &crate::pipeline::RasterData) -> Result<(), RenderError> { + if data.pixels.len() != (data.width * data.height * 4) as usize { + return Err(RenderError::InvalidBufferSize { + expected: (data.width * data.height * 4) as usize, + actual: data.pixels.len(), }); + } - let rotated_path; - let path = if let Some(z) = rotation_z { - let bounds = original_path.bounds(); - let cx = (bounds.left() + bounds.right()) / 2.0; - let cy = (bounds.top() + bounds.bottom()) / 2.0; - - let rotation_transform = Transform::from_translate(cx, cy) - .pre_rotate(-z) // ASS \frz is counter-clockwise - .pre_translate(-cx, -cy); + let src_pixmap = Pixmap::from_vec( + data.pixels.clone(), + tiny_skia::IntSize::from_wh(data.width, data.height) + .ok_or(RenderError::InvalidDimensions)?, + ) + .ok_or(RenderError::InvalidPixmap)?; + + let transform = Transform::from_translate(data.x as f32, data.y as f32); + let paint = tiny_skia::PixmapPaint { + blend_mode: tiny_skia::BlendMode::SourceOver, + ..Default::default() + }; + pixmap.draw_pixmap(0, 0, src_pixmap.as_ref(), &paint, transform, None); + Ok(()) +} - rotated_path = original_path.clone().transform(rotation_transform); - match &rotated_path { - Some(p) => p, - None => original_path, - } +/// Draw vector layer data onto the given pixmap. +fn draw_vector_layer(pixmap: &mut PixmapMut<'_>, data: &crate::pipeline::VectorData) -> Result<(), RenderError> { + log::trace!( + "DRAWING RENDER: draw_vector_layer called with color {:?} effects={:?}", + data.color, data.effects.len() + ); + + let Some(original_path) = &data.path else { + return Ok(()); + }; + + // Apply rotation if present + let rotation_z = data.effects.iter().find_map(|e| { + if let crate::pipeline::TextEffect::Rotation { z, .. } = e { + if *z != 0.0 { Some(*z) } else { None } } else { - original_path - }; - - let mut paint = tiny_skia::Paint::default(); - paint.set_color_rgba8(data.color[0], data.color[1], data.color[2], data.color[3]); - paint.anti_alias = true; - paint.blend_mode = tiny_skia::BlendMode::SourceOver; - - // Build clip mask if clip region is specified - let clip_mask = if let Some((x1, y1, x2, y2)) = data.clip { - let width = self.pixmap.width(); - let height = self.pixmap.height(); - if let Some(mut mask) = tiny_skia::Mask::new(width, height) { - let mut clip_path = tiny_skia::PathBuilder::new(); - clip_path.push_rect( - tiny_skia::Rect::from_ltrb(x1, y1, x2, y2) - .unwrap_or(tiny_skia::Rect::from_xywh(0.0, 0.0, 1.0, 1.0).unwrap()), - ); - if let Some(clip) = clip_path.finish() { - mask.fill_path(&clip, tiny_skia::FillRule::Winding, false, Transform::identity()); - Some(mask) - } else { - None - } + None + } + }); + + let rotated_path; + let path = if let Some(z) = rotation_z { + let bounds = original_path.bounds(); + let cx = (bounds.left() + bounds.right()) / 2.0; + let cy = (bounds.top() + bounds.bottom()) / 2.0; + + let rotation_transform = Transform::from_translate(cx, cy) + .pre_rotate(-z) // ASS \frz is counter-clockwise + .pre_translate(-cx, -cy); + + rotated_path = original_path.clone().transform(rotation_transform); + match &rotated_path { + Some(p) => p, + None => original_path, + } + } else { + original_path + }; + + let mut paint = tiny_skia::Paint::default(); + paint.set_color_rgba8(data.color[0], data.color[1], data.color[2], data.color[3]); + paint.anti_alias = true; + paint.blend_mode = tiny_skia::BlendMode::SourceOver; + + // Build clip mask if clip region is specified + let clip_mask = if let Some((x1, y1, x2, y2)) = data.clip { + let width = pixmap.width(); + let height = pixmap.height(); + if let Some(mut mask) = tiny_skia::Mask::new(width, height) { + let mut clip_path = tiny_skia::PathBuilder::new(); + clip_path.push_rect( + tiny_skia::Rect::from_ltrb(x1, y1, x2, y2) + .unwrap_or(tiny_skia::Rect::from_xywh(0.0, 0.0, 1.0, 1.0).unwrap()), + ); + if let Some(clip) = clip_path.finish() { + mask.fill_path(&clip, tiny_skia::FillRule::Winding, false, Transform::identity()); + Some(mask) } else { None } } else { None - }; - - // Check for blur effect - let blur_radius = data.effects.iter().find_map(|e| { - if let crate::pipeline::TextEffect::Blur { radius } = e { - if *radius > 0.0 { Some(*radius) } else { None } - } else { - None - } - }); - - if let Some(radius) = blur_radius { - // Render to temp pixmap, blur, then composite — same approach as draw_text_layer - let bounds = path.bounds(); - let padding = (radius * 3.0).ceil(); - let temp_w = (bounds.width() + padding * 2.0).ceil() as u32; - let temp_h = (bounds.height() + padding * 2.0).ceil() as u32; - - if let Some(mut temp) = Pixmap::new(temp_w.max(1), temp_h.max(1)) { - temp.fill(tiny_skia::Color::TRANSPARENT); + } + } else { + None + }; + + // Check for blur effect + let blur_radius = data.effects.iter().find_map(|e| { + if let crate::pipeline::TextEffect::Blur { radius } = e { + if *radius > 0.0 { Some(*radius) } else { None } + } else { + None + } + }); - // Translate path so it's drawn relative to the temp pixmap origin - let offset_x = bounds.left() - padding; - let offset_y = bounds.top() - padding; - let temp_transform = Transform::from_translate(-offset_x, -offset_y); - - temp.fill_path( - path, - &paint, - tiny_skia::FillRule::Winding, - temp_transform, - None, - ); + if let Some(radius) = blur_radius { + let bounds = path.bounds(); + let padding = (radius * 3.0).ceil(); + let temp_w = (bounds.width() + padding * 2.0).ceil() as u32; + let temp_h = (bounds.height() + padding * 2.0).ceil() as u32; - // Also stroke if present - if let Some(stroke) = &data.stroke { - let mut stroke_paint = tiny_skia::Paint::default(); - stroke_paint.set_color_rgba8(stroke.color[0], stroke.color[1], stroke.color[2], stroke.color[3]); - stroke_paint.anti_alias = true; - let sk_stroke = tiny_skia::Stroke { width: stroke.width, ..Default::default() }; - temp.stroke_path(path, &stroke_paint, &sk_stroke, temp_transform, None); - } + if let Some(mut temp) = Pixmap::new(temp_w.max(1), temp_h.max(1)) { + temp.fill(tiny_skia::Color::TRANSPARENT); - // Apply blur - apply_gaussian_blur(&mut temp, radius); + let offset_x = bounds.left() - padding; + let offset_y = bounds.top() - padding; + let temp_transform = Transform::from_translate(-offset_x, -offset_y); - // Composite blurred result back to main pixmap - let composite_paint = tiny_skia::PixmapPaint { - blend_mode: tiny_skia::BlendMode::SourceOver, - ..Default::default() - }; - let composite_transform = Transform::from_translate(offset_x, offset_y); - self.pixmap.draw_pixmap( - 0, 0, - temp.as_ref(), - &composite_paint, - composite_transform, - clip_mask.as_ref(), - ); - } - } else { - // No blur — draw directly - self.pixmap.fill_path( - path, - &paint, - tiny_skia::FillRule::Winding, - Transform::identity(), - clip_mask.as_ref(), - ); + temp.fill_path(path, &paint, tiny_skia::FillRule::Winding, temp_transform, None); if let Some(stroke) = &data.stroke { let mut stroke_paint = tiny_skia::Paint::default(); stroke_paint.set_color_rgba8(stroke.color[0], stroke.color[1], stroke.color[2], stroke.color[3]); stroke_paint.anti_alias = true; let sk_stroke = tiny_skia::Stroke { width: stroke.width, ..Default::default() }; - self.pixmap.stroke_path(path, &stroke_paint, &sk_stroke, Transform::identity(), None); + temp.stroke_path(path, &stroke_paint, &sk_stroke, temp_transform, None); } - } - Ok(()) + apply_gaussian_blur(&mut temp, radius); + + let composite_paint = tiny_skia::PixmapPaint { + blend_mode: tiny_skia::BlendMode::SourceOver, + ..Default::default() + }; + let composite_transform = Transform::from_translate(offset_x, offset_y); + pixmap.draw_pixmap(0, 0, temp.as_ref(), &composite_paint, composite_transform, clip_mask.as_ref()); + } + } else { + pixmap.fill_path(path, &paint, tiny_skia::FillRule::Winding, Transform::identity(), clip_mask.as_ref()); + + if let Some(stroke) = &data.stroke { + let mut stroke_paint = tiny_skia::Paint::default(); + stroke_paint.set_color_rgba8(stroke.color[0], stroke.color[1], stroke.color[2], stroke.color[3]); + stroke_paint.anti_alias = true; + let sk_stroke = tiny_skia::Stroke { width: stroke.width, ..Default::default() }; + pixmap.stroke_path(path, &stroke_paint, &sk_stroke, Transform::identity(), None); + } } - fn draw_text_layer(&mut self, data: &crate::pipeline::TextData) -> Result<(), RenderError> { + Ok(()) +} + +/// Draw text layer data onto the given pixmap. +/// +/// Takes explicit references to cache, glyph_renderer, and font_db to allow +/// `SoftwareBackend` to hold a separate mutable borrow on its frame buffer. +fn draw_text_layer( + pixmap: &mut PixmapMut<'_>, + cache: &mut RenderCache, + glyph_renderer: &mut crate::pipeline::shaping::GlyphRenderer, + font_db: &Arc, + data: &crate::pipeline::TextData, +) -> Result<(), RenderError> { use crate::pipeline::shaping::shape_text_with_style; // Extract bold/italic from effects @@ -271,7 +254,7 @@ impl SoftwareBackend { italic, }; - let shaped = if let Some(cached) = self.cache.get_shaped_text(&cache_key) { + let shaped = if let Some(cached) = cache.get_shaped_text(&cache_key) { cached } else { let shaped_text = shape_text_with_style( @@ -280,24 +263,24 @@ impl SoftwareBackend { data.font_size, bold, italic, - &self.font_db, + font_db, )?; - self.cache.store_shaped_text(cache_key, shaped_text) + cache.store_shaped_text(cache_key, shaped_text) }; // Find font for rendering — uses the full fallback chain let font_id = crate::pipeline::shaping::find_font( - &self.font_db, + font_db, &data.font_family, bold, italic, )?; // Render glyphs to paths using cached renderer with spacing - let paths = self.glyph_renderer.render_shaped_text( + let paths = glyph_renderer.render_shaped_text( &shaped, font_id, - &self.font_db, + font_db, data.spacing, )?; @@ -445,8 +428,8 @@ impl SoftwareBackend { } = e { // Create a mask for clipping - let width = self.pixmap.width(); - let height = self.pixmap.height(); + let width = pixmap.width(); + let height = pixmap.height(); if let Some(mut mask) = tiny_skia::Mask::new(width, height) { // Fill the mask with the clipping region @@ -1033,7 +1016,7 @@ impl SoftwareBackend { ..Default::default() }; - self.pixmap.draw_pixmap( + pixmap.draw_pixmap( 0, 0, temp_pixmap.as_ref(), @@ -1042,12 +1025,11 @@ impl SoftwareBackend { clip_mask.as_ref(), ); } else { - // No blur: render directly to self.pixmap as before - render_all_layers!(self.pixmap, base_transform, clip_mask.as_ref(), data.x, baseline_y); + // No blur: render directly to the main pixmap + render_all_layers!(pixmap, base_transform, clip_mask.as_ref(), data.x, baseline_y); } Ok(()) - } } /// Apply a gaussian blur to a pixmap (for `\blur` tag). @@ -1231,46 +1213,68 @@ impl RenderBackend for SoftwareBackend { &mut self, layers: &[IntermediateLayer], context: &RenderContext, - ) -> Result, RenderError> { - // Resize pixmap if dimensions changed; otherwise just clear it. - if self.pixmap.width() != context.width() || self.pixmap.height() != context.height() { - self.pixmap = Pixmap::new(context.width(), context.height()) - .ok_or(RenderError::InvalidDimensions)?; - } - self.pixmap.fill(tiny_skia::Color::TRANSPARENT); + ) -> Result { + let w = context.width(); + let h = context.height(); + let needed = (w * h * 4) as usize; - // Composite each layer - for layer in layers { - self.composite_layer(layer, context)?; + // Reset arena for this frame's scratch allocations. + // Scoped so the mutable borrow ends before calling composite_layer. + { + let arena = self.arena.get_mut().unwrap(); + arena.reset(); } - // Return RGBA data - let data = self.pixmap.data().to_vec(); + // Reclaim buffer from previous frame if consumer has dropped it. + // try_reclaim succeeds (zero-cost) when refcount == 1; allocates only if needed. + if !self.frame_buf.try_reclaim(needed) { + self.frame_buf = BytesMut::zeroed(needed); + } else { + self.frame_buf.resize(needed, 0); + } + self.frame_buf.fill(0); + self.frame_width = w; + self.frame_height = h; + + // Create PixmapMut view — full tiny-skia rendering API available. + // We use explicit field borrows to avoid a conflict between the + // &mut frame_buf borrow (for PixmapMut) and the &mut self borrows + // needed by the draw methods. This is safe — different fields. + { + let frame_buf: &mut BytesMut = &mut self.frame_buf; + 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 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)?, + } + } + } if log::log_enabled!(log::Level::Trace) { - // Sample some pixels to check alpha + let data = self.frame_buf.as_ref(); if data.len() >= 4 { let sample1 = &data[0..4]; - let sample2 = if data.len() >= 4000 { - &data[4000..4004] - } else { - &data[0..4] - }; + let sample2 = if data.len() >= 4000 { &data[4000..4004] } else { &data[0..4] }; log::trace!("PIXMAP SAMPLES: [0]={:?}, [1000]={:?}", sample1, sample2); - // Count non-opaque pixels let non_opaque = data.chunks_exact(4).filter(|p| p[3] != 255).count(); let transparent = data.chunks_exact(4).filter(|p| p[3] == 0).count(); log::trace!( "PIXMAP ALPHA STATS: {} non-opaque pixels, {} transparent pixels out of {}", - non_opaque, - transparent, - data.len() / 4 + non_opaque, transparent, data.len() / 4 ); } } - Ok(data) + // Split off rendered data as Bytes — zero-copy pointer arithmetic. + // BytesMut retains its allocation capacity for try_reclaim next frame. + Ok(self.frame_buf.split().freeze()) } fn composite_layers_incremental( @@ -1279,35 +1283,62 @@ impl RenderBackend for SoftwareBackend { dirty_regions: &[DirtyRegion], previous_frame: &[u8], context: &RenderContext, - ) -> Result, RenderError> { - // Resize pixmap if dimensions changed. - if self.pixmap.width() != context.width() || self.pixmap.height() != context.height() { - self.pixmap = Pixmap::new(context.width(), context.height()) - .ok_or(RenderError::InvalidDimensions)?; + ) -> Result { + let w = context.width(); + let h = context.height(); + let needed = (w * h * 4) as usize; + + // Reset arena for this frame's scratch allocations. + { + let arena = self.arena.get_mut().unwrap(); + arena.reset(); + } + + // Reclaim or allocate frame buffer. + if !self.frame_buf.try_reclaim(needed) { + self.frame_buf = BytesMut::zeroed(needed); + } else { + self.frame_buf.resize(needed, 0); } + self.frame_width = w; + self.frame_height = h; - // Copy previous frame - if previous_frame.len() == self.pixmap.data().len() { - self.pixmap.data_mut().copy_from_slice(previous_frame); + // Copy previous frame if dimensions match. + if previous_frame.len() == needed { + self.frame_buf.copy_from_slice(previous_frame); } else { - self.pixmap.fill(tiny_skia::Color::TRANSPARENT); + self.frame_buf.fill(0); } - // Only redraw dirty regions - for region in dirty_regions { - // TODO: Create clip mask for dirty region - // tiny_skia doesn't expose ClipMask publicly - let _ = region; // TODO: Apply clipping + // Same explicit field-borrow pattern as composite_layers. + { + let frame_buf: &mut BytesMut = &mut self.frame_buf; + 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 mut pixmap = PixmapMut::from_bytes(frame_buf.as_mut(), w, h) + .ok_or(RenderError::InvalidDimensions)?; - // Composite layers within this region - for layer in layers { - if layer.intersects_region(region) { - self.composite_layer(layer, context)?; + // Only redraw dirty regions + for region in dirty_regions { + // TODO: Create clip mask for dirty region + // tiny_skia doesn't expose ClipMask publicly + let _ = region; // TODO: Apply clipping + + // Composite layers within this region + for layer in layers { + 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)?, + } + } } } } - Ok(self.pixmap.data().to_vec()) + Ok(self.frame_buf.split().freeze()) } fn supports_feature(&self, feature: BackendFeature) -> bool { diff --git a/crates/ass-renderer/src/debug/libass_renderer.rs b/crates/ass-renderer/src/debug/libass_renderer.rs index 34006cd..c4166f9 100644 --- a/crates/ass-renderer/src/debug/libass_renderer.rs +++ b/crates/ass-renderer/src/debug/libass_renderer.rs @@ -102,7 +102,7 @@ impl LibassRenderer { } } - Ok(Frame::new(buffer, self.width, self.height, time_cs)) + Ok(Frame::new(buffer.into(), self.width, self.height, time_cs)) } /// Render a frame at the given time (in milliseconds) - legacy method @@ -130,7 +130,7 @@ impl LibassRenderer { pub fn render_to_frame(&mut self, time_ms: i64) -> Result { let buffer = self.render_frame_ms(time_ms)?; Ok(Frame::new( - buffer, + buffer.into(), self.width, self.height, (time_ms / 10) as u32, diff --git a/crates/ass-renderer/src/renderer/frame.rs b/crates/ass-renderer/src/renderer/frame.rs index c2b3dd9..292a112 100644 --- a/crates/ass-renderer/src/renderer/frame.rs +++ b/crates/ass-renderer/src/renderer/frame.rs @@ -1,14 +1,11 @@ //! Rendered frame representation -#[cfg(feature = "nostd")] -use alloc::{vec, vec::Vec}; -#[cfg(not(feature = "nostd"))] -use std::vec::Vec; +use bytes::Bytes; /// Rendered frame containing pixel data #[derive(Clone)] pub struct Frame { - buffer: Vec, + buffer: Bytes, width: u32, height: u32, timestamp: u32, @@ -28,7 +25,7 @@ pub enum PixelFormat { impl Frame { /// Create a new frame with the given buffer - pub fn new(buffer: Vec, width: u32, height: u32, timestamp: u32) -> Self { + pub fn new(buffer: Bytes, width: u32, height: u32, timestamp: u32) -> Self { Self { buffer, width, @@ -41,7 +38,7 @@ impl Frame { /// Create a frame from RGBA data pub fn from_rgba(buffer: Vec, width: u32, height: u32) -> Self { Self { - buffer, + buffer: Bytes::from(buffer), width, height, timestamp: 0, @@ -58,7 +55,7 @@ impl Frame { format: PixelFormat, ) -> Self { Self { - buffer, + buffer: Bytes::from(buffer), width, height, timestamp, @@ -70,7 +67,7 @@ impl Frame { pub fn empty(width: u32, height: u32, timestamp: u32) -> Self { let size = (width * height * 4) as usize; Self { - buffer: vec![0; size], + buffer: Bytes::from(vec![0u8; size]), width, height, timestamp, @@ -88,14 +85,16 @@ impl Frame { &self.buffer } - /// Get mutable frame buffer data - pub fn data_mut(&mut self) -> &mut [u8] { - &mut self.buffer + /// Take ownership of the buffer as Bytes + pub fn into_bytes(self) -> Bytes { + self.buffer } - /// Take ownership of the buffer + /// Take ownership of the buffer as Vec + /// + /// Copies if the Bytes has shared ownership; zero-copy if unique. pub fn into_buffer(self) -> Vec { - self.buffer + self.buffer.to_vec() } /// Get frame width @@ -132,24 +131,30 @@ impl Frame { } /// Convert to RGBA format if not already - pub fn to_rgba(mut self) -> Self { + pub fn to_rgba(self) -> Self { match self.format { PixelFormat::Rgba8 => self, PixelFormat::Bgra8 => { - for chunk in self.buffer.chunks_exact_mut(4) { + let mut buf = self.buffer.to_vec(); + for chunk in buf.chunks_exact_mut(4) { chunk.swap(0, 2); } - self.format = PixelFormat::Rgba8; - self + Self { + buffer: Bytes::from(buf), + format: PixelFormat::Rgba8, + ..self + } } PixelFormat::Rgb8 => { let mut rgba = Vec::with_capacity((self.width * self.height * 4) as usize); for chunk in self.buffer.chunks_exact(3) { rgba.extend_from_slice(&[chunk[0], chunk[1], chunk[2], 255]); } - self.buffer = rgba; - self.format = PixelFormat::Rgba8; - self + Self { + buffer: Bytes::from(rgba), + format: PixelFormat::Rgba8, + ..self + } } } } -- 2.51.2