Export speed #2: cache the RGBA→YUV swscale context across frames

CpuYuvConverter::convert rebuilt the swscale context + both ffmpeg frames on every
call (per output frame), despite the converter persisting for the whole export. Now
the scaler + reusable source/dest frames are built once in new() and reused each
convert(). (convert is &mut self; the caller already held it mutably.)
This commit is contained in:
Skyler Lehmkuhl 2026-06-21 01:34:20 -04:00
parent 0a0d7cd0a9
commit 4e9cacb789
1 changed files with 31 additions and 47 deletions

View File

@ -13,6 +13,11 @@ use ffmpeg_next as ffmpeg;
pub struct CpuYuvConverter { pub struct CpuYuvConverter {
width: u32, width: u32,
height: u32, height: u32,
/// swscale context + reusable source/dest frames, built once and reused every frame
/// (creating them per call was a measurable per-output-frame export cost).
scaler: ffmpeg::software::scaling::Context,
rgba_frame: ffmpeg::frame::Video,
yuv_frame: ffmpeg::frame::Video,
} }
impl CpuYuvConverter { impl CpuYuvConverter {
@ -22,7 +27,20 @@ impl CpuYuvConverter {
/// * `width` - Frame width in pixels /// * `width` - Frame width in pixels
/// * `height` - Frame height in pixels /// * `height` - Frame height in pixels
pub fn new(width: u32, height: u32) -> Result<Self, String> { pub fn new(width: u32, height: u32) -> Result<Self, String> {
Ok(Self { width, height }) // BT.709 (HD) RGBA→YUV420p context, created once.
let scaler = ffmpeg::software::scaling::Context::get(
ffmpeg::format::Pixel::RGBA,
width,
height,
ffmpeg::format::Pixel::YUV420P,
width,
height,
ffmpeg::software::scaling::Flags::BILINEAR,
)
.map_err(|e| format!("Failed to create swscale context: {}", e))?;
let rgba_frame = ffmpeg::frame::Video::new(ffmpeg::format::Pixel::RGBA, width, height);
let yuv_frame = ffmpeg::frame::Video::new(ffmpeg::format::Pixel::YUV420P, width, height);
Ok(Self { width, height, scaler, rgba_frame, yuv_frame })
} }
/// Convert RGBA data to YUV420p planes /// Convert RGBA data to YUV420p planes
@ -40,7 +58,7 @@ impl CpuYuvConverter {
/// ///
/// # Panics /// # Panics
/// Panics if rgba_data length doesn't match width * height * 4 /// Panics if rgba_data length doesn't match width * height * 4
pub fn convert(&self, rgba_data: &[u8]) -> Result<(Vec<u8>, Vec<u8>, Vec<u8>), String> { pub fn convert(&mut self, rgba_data: &[u8]) -> Result<(Vec<u8>, Vec<u8>, Vec<u8>), String> {
let expected_size = (self.width * self.height * 4) as usize; let expected_size = (self.width * self.height * 4) as usize;
assert_eq!( assert_eq!(
rgba_data.len(), rgba_data.len(),
@ -50,51 +68,17 @@ impl CpuYuvConverter {
rgba_data.len() rgba_data.len()
); );
// Create source RGBA frame // Copy RGBA into the reused source frame, run the reused scaler into the reused
let mut rgba_frame = ffmpeg::frame::Video::new( // dest frame (SIMD-optimized), then extract planes.
ffmpeg::format::Pixel::RGBA, self.rgba_frame.data_mut(0).copy_from_slice(rgba_data);
self.width, self.scaler
self.height, .run(&self.rgba_frame, &mut self.yuv_frame)
);
// Copy RGBA data into source frame
// ffmpeg-next provides mutable access to the frame data
let frame_data = rgba_frame.data_mut(0);
frame_data.copy_from_slice(rgba_data);
// Create destination YUV420p frame
let mut yuv_frame = ffmpeg::frame::Video::new(
ffmpeg::format::Pixel::YUV420P,
self.width,
self.height,
);
// Create swscale context for RGBA→YUV420p conversion
// Uses BT.709 color matrix (HD standard)
let mut scaler = ffmpeg::software::scaling::Context::get(
ffmpeg::format::Pixel::RGBA,
self.width,
self.height,
ffmpeg::format::Pixel::YUV420P,
self.width,
self.height,
ffmpeg::software::scaling::Flags::BILINEAR,
)
.map_err(|e| format!("Failed to create swscale context: {}", e))?;
// Perform the conversion (SIMD-optimized)
scaler
.run(&rgba_frame, &mut yuv_frame)
.map_err(|e| format!("swscale conversion failed: {}", e))?; .map_err(|e| format!("swscale conversion failed: {}", e))?;
// Extract planar YUV data // YUV420p planes: Y full-res, U/V quarter-res (2×2 subsampled).
// YUV420p has 3 planes: let y_plane = self.yuv_frame.data(0).to_vec();
// - Y: full resolution (width × height) let u_plane = self.yuv_frame.data(1).to_vec();
// - U: quarter resolution (width/2 × height/2) let v_plane = self.yuv_frame.data(2).to_vec();
// - V: quarter resolution (width/2 × height/2)
let y_plane = yuv_frame.data(0).to_vec();
let u_plane = yuv_frame.data(1).to_vec();
let v_plane = yuv_frame.data(2).to_vec();
Ok((y_plane, u_plane, v_plane)) Ok((y_plane, u_plane, v_plane))
} }
@ -112,7 +96,7 @@ mod tests {
#[test] #[test]
fn test_conversion_output_sizes() { fn test_conversion_output_sizes() {
let converter = CpuYuvConverter::new(1920, 1080).unwrap(); let mut converter = CpuYuvConverter::new(1920, 1080).unwrap();
// Create dummy RGBA data (all black) // Create dummy RGBA data (all black)
let rgba_data = vec![0u8; 1920 * 1080 * 4]; let rgba_data = vec![0u8; 1920 * 1080 * 4];
@ -133,7 +117,7 @@ mod tests {
#[test] #[test]
#[should_panic(expected = "RGBA data size mismatch")] #[should_panic(expected = "RGBA data size mismatch")]
fn test_wrong_input_size_panics() { fn test_wrong_input_size_panics() {
let converter = CpuYuvConverter::new(1920, 1080).unwrap(); let mut converter = CpuYuvConverter::new(1920, 1080).unwrap();
// Wrong size input // Wrong size input
let rgba_data = vec![0u8; 1000]; let rgba_data = vec![0u8; 1000];