From be1ec2a320f337472020df8a476d5f3723d4277f Mon Sep 17 00:00:00 2001 From: McKenzie Pepper Date: Wed, 29 Jul 2026 23:38:22 -0400 Subject: [PATCH 1/2] feat: use vpp_qsv hardware padding when available MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QSV padding currently falls back to the software pad filter, which forces an hwdownload/hwupload round-trip per frame. Measured on an Intel Arc Pro B50 (640x480 4:3 -> 1440x1080 padded to 1920x1080, h264), that chain runs at 212 fps / 5.2s CPU versus 563 fps / 1.3s CPU for a hardware pad — parity with pad_vaapi, and essentially free relative to plain scaling. ffmpeg's vpp_qsv only gained pad_w/pad_h/pad_x/pad_y/pad_color in patched builds (submission to ffmpeg-devel pending), so plain filter presence cannot gate this. FfmpegInfo now probes AVOptions for selected filters via `ffmpeg -h filter=NAME` (currently just vpp_qsv), and the QSV best_filter substitutes PadQsv only when the pad_w option is advertised; otherwise behaviour is unchanged and the software pad chain is used. Co-Authored-By: Claude Fable 5 --- crates/ffpipeline/src/accel/qsv.rs | 165 +++++++++++++++++++++++++- crates/ffpipeline/src/accel/vaapi.rs | 1 + crates/ffpipeline/src/ffmpeg_info.rs | 109 +++++++++++++++++ crates/ffpipeline/src/filter_chain.rs | 1 + crates/ffpipeline/src/video_filter.rs | 1 + 5 files changed, 276 insertions(+), 1 deletion(-) diff --git a/crates/ffpipeline/src/accel/qsv.rs b/crates/ffpipeline/src/accel/qsv.rs index c92324d..b198995 100644 --- a/crates/ffpipeline/src/accel/qsv.rs +++ b/crates/ffpipeline/src/accel/qsv.rs @@ -9,7 +9,7 @@ use crate::output_settings::VideoFilterOptions; use crate::pipeline::{FrameState, FrameSurface, PixelFormat, SurfaceSet, VideoFormat}; use crate::probe::ProbeResultVideoStream; use crate::video_codec::VideoCodec; -use crate::video_filter::{DeinterlaceFilter, ScaleFilter, VideoFilter, VideoFilterOp}; +use crate::video_filter::{DeinterlaceFilter, PadFilter, ScaleFilter, VideoFilter, VideoFilterOp}; #[derive(Debug, Clone, Serialize)] pub struct Qsv { @@ -42,6 +42,14 @@ impl HwAccel for Qsv { } .into() } + // vpp_qsv only supports padding in patched/newer ffmpeg builds, so + // gate on the pad_w option rather than filter presence; otherwise + // fall through to the software pad (hwdownload round-trip). + VideoFilter::Pad(PadFilter { size, .. }) + if ffmpeg_info.video_filter_has_option(&KnownVideoFilter::VppQsv, "pad_w") => + { + PadQsv { size: *size }.into() + } _ => video_filter.clone(), } } @@ -185,6 +193,37 @@ impl VideoFilterOp for ScaleQsv { } } +#[derive(Debug, Clone)] +pub struct PadQsv { + pub(crate) size: Option, +} + +impl VideoFilterOp for PadQsv { + fn evaluate(&self, _state: &FrameState, _ffmpeg_info: &FfmpegInfo) -> Option { + None + } + + fn apply_to(&self, state: &mut FrameState) { + if let Some(size) = &self.size { + state.size = *size; + state.surface = FrameSurface::Qsv; + } + } + + fn required_surface(&self) -> Option { + Some(FrameSurface::Qsv) + } + + fn as_arg(&self) -> Option { + self.size.as_ref().map(|s| { + format!( + "vpp_qsv=pad_w={}:pad_h={}:pad_x=-1:pad_y=-1:pad_color=black", + s.width, s.height + ) + }) + } +} + #[derive(Debug, Clone)] pub struct FormatQsv { pub(crate) format: PixelFormat, @@ -233,3 +272,127 @@ impl VideoFilterOp for DeinterlaceQsv { Some(format!("deinterlace_qsv=mode={mode}")) } } + +#[cfg(test)] +mod tests { + use std::collections::{HashMap, HashSet}; + + use super::*; + use crate::output_settings::{ScalingMode, VideoFilterOptions}; + use crate::video_filter::PadFilter; + + fn make_qsv() -> Qsv { + Qsv { + capabilities: QsvCapabilities { + supported_decoders: HashMap::new(), + supported_encoders: HashMap::new(), + vpp_pixel_formats: HashSet::new(), + }, + } + } + + fn make_ffmpeg_info(with_pad_option: bool) -> FfmpegInfo { + let mut video_filters = HashSet::new(); + video_filters.insert(KnownVideoFilter::VppQsv.to_string()); + + let mut video_filter_options = HashMap::new(); + let mut options = HashSet::from([String::from("deinterlace"), String::from("denoise")]); + if with_pad_option { + options.extend([ + String::from("pad_w"), + String::from("pad_h"), + String::from("pad_x"), + String::from("pad_y"), + String::from("pad_color"), + ]); + } + video_filter_options.insert(KnownVideoFilter::VppQsv.to_string(), options); + + FfmpegInfo { + hwaccels: HashSet::new(), + video_filters, + preferred_filters: HashMap::new(), + video_filter_options, + } + } + + fn make_frame_state() -> FrameState { + FrameState { + size: FrameSize { + width: 1440, + height: 1080, + }, + is_anamorphic: false, + is_interlaced: false, + sample_aspect_ratio: None, + display_aspect_ratio: None, + surface: FrameSurface::Qsv, + pixel_format: PixelFormat::Nv12, + is_hdr: false, + } + } + + fn pad_1920x1080() -> VideoFilter { + VideoFilter::Pad(PadFilter { + size: Some(FrameSize { + width: 1920, + height: 1080, + }), + scaling_mode: ScalingMode::ScaleAndPad, + }) + } + + #[test] + fn best_filter_selects_pad_qsv_when_vpp_qsv_has_pad_option() { + let qsv = make_qsv(); + let ffmpeg_info = make_ffmpeg_info(true); + let state = make_frame_state(); + let filter_options = VideoFilterOptions::default(); + + let result = qsv.best_filter(&pad_1920x1080(), &ffmpeg_info, &state, &filter_options); + + match result { + VideoFilter::PadQsv(PadQsv { size: Some(size) }) => { + assert_eq!(size.width, 1920); + assert_eq!(size.height, 1080); + } + other => panic!("expected PadQsv, got {other:?}"), + } + } + + #[test] + fn best_filter_falls_back_to_software_pad_without_pad_option() { + let qsv = make_qsv(); + let ffmpeg_info = make_ffmpeg_info(false); + let state = make_frame_state(); + let filter_options = VideoFilterOptions::default(); + + let result = qsv.best_filter(&pad_1920x1080(), &ffmpeg_info, &state, &filter_options); + + assert!( + matches!(result, VideoFilter::Pad(_)), + "expected software Pad fallback, got {result:?}" + ); + } + + #[test] + fn pad_qsv_arg_and_state() { + let pad = PadQsv { + size: Some(FrameSize { + width: 1920, + height: 1080, + }), + }; + + assert_eq!( + pad.as_arg().as_deref(), + Some("vpp_qsv=pad_w=1920:pad_h=1080:pad_x=-1:pad_y=-1:pad_color=black") + ); + + let mut state = make_frame_state(); + pad.apply_to(&mut state); + assert_eq!(state.size.width, 1920); + assert_eq!(state.size.height, 1080); + assert_eq!(state.surface, FrameSurface::Qsv); + } +} diff --git a/crates/ffpipeline/src/accel/vaapi.rs b/crates/ffpipeline/src/accel/vaapi.rs index cf1da60..720364c 100644 --- a/crates/ffpipeline/src/accel/vaapi.rs +++ b/crates/ffpipeline/src/accel/vaapi.rs @@ -556,6 +556,7 @@ mod tests { hwaccels: HashSet::new(), video_filters, preferred_filters: HashMap::new(), + video_filter_options: HashMap::new(), } } diff --git a/crates/ffpipeline/src/ffmpeg_info.rs b/crates/ffpipeline/src/ffmpeg_info.rs index 13f7802..9aa1e88 100644 --- a/crates/ffpipeline/src/ffmpeg_info.rs +++ b/crates/ffpipeline/src/ffmpeg_info.rs @@ -20,6 +20,11 @@ static KNOWN_FILTERS: LazyLock> = LazyLock::new(|| { .collect::>() }); +/// Filters whose AVOptions are probed at load time, so pipeline builders can +/// gate on options that only exist in patched or newer ffmpeg builds (e.g. +/// vpp_qsv pad_w/pad_h). +static OPTION_PROBED_FILTERS: &[KnownVideoFilter] = &[KnownVideoFilter::VppQsv]; + #[derive(Display, EnumIter, IntoStaticStr, Debug, PartialEq)] pub enum KnownHardwareAccel { #[strum(serialize = "cuda")] @@ -96,6 +101,7 @@ pub struct FfmpegInfo { pub(crate) hwaccels: HashSet, pub(crate) video_filters: HashSet, pub(crate) preferred_filters: HashMap, + pub(crate) video_filter_options: HashMap>, } impl FfmpegInfo { @@ -115,10 +121,20 @@ impl FfmpegInfo { } } + let mut video_filter_options: HashMap> = HashMap::new(); + for filter in OPTION_PROBED_FILTERS { + let name = filter.to_string(); + if video_filters.contains(&name) { + let options = Self::load_video_filter_options(path, &name).await?; + video_filter_options.insert(name, options); + } + } + Ok(FfmpegInfo { hwaccels, video_filters, preferred_filters: preferred, + video_filter_options, }) } @@ -131,6 +147,15 @@ impl FfmpegInfo { self.video_filters.contains(&filter.to_string()) } + /// Returns true when the filter is present AND advertises the named + /// AVOption. Only filters in [`OPTION_PROBED_FILTERS`] have their options + /// probed; all other filters always return false. + pub fn video_filter_has_option(&self, filter: &KnownVideoFilter, option: &str) -> bool { + self.video_filter_options + .get(&filter.to_string()) + .is_some_and(|options| options.contains(option)) + } + /// Returns the "best" known filter from the inputted set. "Best" in this case is defined /// as 1. is a filter that the queried ffmpeg contains and 2. has the lowest preference index /// (i.e. index-0 in the preference list has higher priority than index-1) @@ -215,6 +240,42 @@ impl FfmpegInfo { Ok(accels) } + async fn load_video_filter_options( + path: &Path, + filter: &str, + ) -> Result, FFPipelineError> { + let output = Command::new(path) + .args(["-hide_banner", "-h", &format!("filter={filter}")]) + .output() + .await + .map_err(|_| { + FFPipelineError::FfmpegCapabilitiesError(format!("filter options: {filter}")) + })?; + + Ok(Self::parse_filter_options(&String::from_utf8_lossy( + &output.stdout, + ))) + } + + /// Parses `ffmpeg -h filter=NAME` output into the set of AVOption names. + /// Option lines look like ` pad_w ..FV....... description`; + /// the `` in the second column is what distinguishes them from the + /// surrounding header lines. + fn parse_filter_options(help_text: &str) -> HashSet { + let mut options = HashSet::new(); + + for line in help_text.lines() { + let mut parts = line.split_whitespace(); + if let (Some(name), Some(kind)) = (parts.next(), parts.next()) + && kind.starts_with('<') + { + options.insert(name.to_owned()); + } + } + + options + } + async fn load_video_filters( path: &Path, disabled_filters: &[String], @@ -263,6 +324,7 @@ mod tests { hwaccels: HashSet::new(), video_filters, preferred_filters: HashMap::new(), + video_filter_options: HashMap::new(), }; let best_fit = info.find_best_fit( @@ -297,6 +359,7 @@ mod tests { hwaccels: HashSet::new(), video_filters, preferred_filters, + video_filter_options: HashMap::new(), }; let best_fit = info.find_best_fit( @@ -310,6 +373,52 @@ mod tests { assert_eq!(best_fit, Some(&KnownVideoFilter::TonemapVaapi)); } + #[test] + fn test_parse_filter_options() { + let help_text = r"Filter vpp_qsv + Description: Quick Sync Video VPP. + Inputs: + #0: default (video) + Outputs: + #0: default (video) +vpp_qsv AVOptions: + deinterlace ..FV....... deinterlace mode: 0=off, 1=bob, 2=advanced (from 0 to 2) (default 0) + denoise ..FV....... denoise level [0, 100] (from 0 to 100) (default 0) + pad_w ..FV....... set the padded output width (0 = no padding) (from 0 to 32767) (default 0) + pad_h ..FV....... set the padded output height (0 = no padding) (from 0 to 32767) (default 0) + pad_color ..FV....... set the colour of the padded area (default 'black') +"; + let options = FfmpegInfo::parse_filter_options(help_text); + + assert!(options.contains("deinterlace")); + assert!(options.contains("pad_w")); + assert!(options.contains("pad_color")); + assert!(!options.contains("#0:")); + assert!(!options.contains("Description:")); + } + + #[test] + fn test_video_filter_has_option() { + let mut video_filters = HashSet::new(); + video_filters.insert(KnownVideoFilter::VppQsv.to_string()); + + let mut video_filter_options = HashMap::new(); + video_filter_options.insert( + KnownVideoFilter::VppQsv.to_string(), + HashSet::from([String::from("pad_w"), String::from("pad_h")]), + ); + + let info = FfmpegInfo { + video_filters, + video_filter_options, + ..Default::default() + }; + + assert!(info.video_filter_has_option(&KnownVideoFilter::VppQsv, "pad_w")); + assert!(!info.video_filter_has_option(&KnownVideoFilter::VppQsv, "does_not_exist")); + assert!(!info.video_filter_has_option(&KnownVideoFilter::PadVaapi, "pad_w")); + } + #[test] #[cfg(target_os = "windows")] fn test_windows_escape_path() { diff --git a/crates/ffpipeline/src/filter_chain.rs b/crates/ffpipeline/src/filter_chain.rs index 5dfdf71..98651a4 100644 --- a/crates/ffpipeline/src/filter_chain.rs +++ b/crates/ffpipeline/src/filter_chain.rs @@ -765,6 +765,7 @@ mod tests { hwaccels: HashSet::new(), video_filters, preferred_filters: HashMap::new(), + video_filter_options: HashMap::new(), } } diff --git a/crates/ffpipeline/src/video_filter.rs b/crates/ffpipeline/src/video_filter.rs index f925b2e..c4b5d59 100644 --- a/crates/ffpipeline/src/video_filter.rs +++ b/crates/ffpipeline/src/video_filter.rs @@ -89,6 +89,7 @@ pub enum VideoFilter { ScaleRkrga(accel::rkmpp::ScaleRkrga), // QSV hardware filters ScaleQsv(accel::qsv::ScaleQsv), + PadQsv(accel::qsv::PadQsv), FormatQsv(accel::qsv::FormatQsv), DeinterlaceQsv(accel::qsv::DeinterlaceQsv), // Vulkan hardware filters From 20a4d0571b6847b1c147ece84eda8f3bb89b596b Mon Sep 17 00:00:00 2001 From: McKenzie Pepper Date: Thu, 30 Jul 2026 11:45:04 -0400 Subject: [PATCH 2/2] perf: fuse qsv scale+pad into a single vpp_qsv instance Chaining two vpp_qsv instances (scale then pad) drops the final frame at EOF on real hardware (measured on Intel B50: 1439/1440 packets). This reproduces with two plain vpp_qsv scales on stock ffmpeg, so it is a pre-existing ffmpeg flush quirk we must avoid triggering. A single combined instance delivers every frame and uses one VPP session. PadQsv gains an optional fused ScaleQsv; the optimize() pass folds a ScaleQsv into an immediately-following PadQsv (mirroring the existing CUDA fusion), emitting one vpp_qsv=w=..:h=..:pad_w=..:pad_h=..:pad_x=-1:pad_y=-1:pad_color=black. Pad-only chains, scale-only chains, and the software fallback are unchanged. The pad_w capability gate still governs the combined form. Co-Authored-By: Claude Fable 5 --- crates/ffpipeline/src/accel/qsv.rs | 187 ++++++++++++++++++++++++-- crates/ffpipeline/src/filter_chain.rs | 29 +++- 2 files changed, 207 insertions(+), 9 deletions(-) diff --git a/crates/ffpipeline/src/accel/qsv.rs b/crates/ffpipeline/src/accel/qsv.rs index b198995..f59f74e 100644 --- a/crates/ffpipeline/src/accel/qsv.rs +++ b/crates/ffpipeline/src/accel/qsv.rs @@ -48,7 +48,11 @@ impl HwAccel for Qsv { VideoFilter::Pad(PadFilter { size, .. }) if ffmpeg_info.video_filter_has_option(&KnownVideoFilter::VppQsv, "pad_w") => { - PadQsv { size: *size }.into() + PadQsv { + size: *size, + scale: None, + } + .into() } _ => video_filter.clone(), } @@ -196,6 +200,13 @@ impl VideoFilterOp for ScaleQsv { #[derive(Debug, Clone)] pub struct PadQsv { pub(crate) size: Option, + /// When a `ScaleQsv` immediately precedes this pad in the resolved chain, + /// the optimize pass folds the scale into this field so we emit a single + /// combined `vpp_qsv` instance (scale + pad) rather than two chained ones. + /// Chaining two `vpp_qsv` scales drops the final frame at EOF on real + /// hardware (measured on Intel B50); a single instance delivers every frame + /// and uses one VPP session. `None` means pad-only (no fused scale). + pub(crate) scale: Option, } impl VideoFilterOp for PadQsv { @@ -215,12 +226,33 @@ impl VideoFilterOp for PadQsv { } fn as_arg(&self) -> Option { - self.size.as_ref().map(|s| { - format!( - "vpp_qsv=pad_w={}:pad_h={}:pad_x=-1:pad_y=-1:pad_color=black", - s.width, s.height - ) - }) + let pad = self.size.as_ref()?; + + // Fused scale + pad: emit a single vpp_qsv carrying both the scale + // (w/h) and pad (pad_*) options. Keep the trailing setsar=1 that the + // standalone ScaleQsv would have emitted; setsar is a metadata-only + // filter, not a second VPP session, so it doesn't reintroduce the + // EOF-frame-drop quirk. + if let Some(scale) = &self.scale + && let Some(size) = &scale.size + { + let combined = format!( + "vpp_qsv=w={}:h={}:pad_w={}:pad_h={}:pad_x=-1:pad_y=-1:pad_color=black", + size.width, size.height, pad.width, pad.height + ); + + return Some(if scale.input_is_anamorphic { + format!("vpp_qsv=w=iw*sar:h=ih,{combined},setsar=1") + } else { + format!("{combined},setsar=1") + }); + } + + // Pad-only (no fused scale): unchanged single-instance emission. + Some(format!( + "vpp_qsv=pad_w={}:pad_h={}:pad_x=-1:pad_y=-1:pad_color=black", + pad.width, pad.height + )) } } @@ -352,7 +384,10 @@ mod tests { let result = qsv.best_filter(&pad_1920x1080(), &ffmpeg_info, &state, &filter_options); match result { - VideoFilter::PadQsv(PadQsv { size: Some(size) }) => { + VideoFilter::PadQsv(PadQsv { + size: Some(size), + scale: None, + }) => { assert_eq!(size.width, 1920); assert_eq!(size.height, 1080); } @@ -377,11 +412,13 @@ mod tests { #[test] fn pad_qsv_arg_and_state() { + // pad-only (no fused scale): unchanged single-instance emission let pad = PadQsv { size: Some(FrameSize { width: 1920, height: 1080, }), + scale: None, }; assert_eq!( @@ -395,4 +432,138 @@ mod tests { assert_eq!(state.size.height, 1080); assert_eq!(state.surface, FrameSurface::Qsv); } + + #[test] + fn pad_qsv_fused_scale_emits_single_instance() { + // scale 1440x1080 + pad 1920x1080 must collapse to ONE vpp_qsv + let pad = PadQsv { + size: Some(FrameSize { + width: 1920, + height: 1080, + }), + scale: Some(ScaleQsv { + size: Some(FrameSize { + width: 1440, + height: 1080, + }), + input_is_anamorphic: false, + }), + }; + + let arg = pad.as_arg().expect("fused pad should emit an arg"); + assert_eq!( + arg, + "vpp_qsv=w=1440:h=1080:pad_w=1920:pad_h=1080:pad_x=-1:pad_y=-1:pad_color=black,setsar=1" + ); + // exactly one vpp_qsv instance + assert_eq!( + arg.matches("vpp_qsv").count(), + 1, + "expected a single vpp_qsv: {arg}" + ); + } + + #[test] + fn pad_qsv_fused_anamorphic_scale_prescales_then_combines() { + // anamorphic input keeps the sar pre-scale as its own vpp_qsv, but the + // pad still fuses into the second (square-pixel) scale instance. + let pad = PadQsv { + size: Some(FrameSize { + width: 1920, + height: 1080, + }), + scale: Some(ScaleQsv { + size: Some(FrameSize { + width: 1440, + height: 1080, + }), + input_is_anamorphic: true, + }), + }; + + assert_eq!( + pad.as_arg().as_deref(), + Some( + "vpp_qsv=w=iw*sar:h=ih,vpp_qsv=w=1440:h=1080:pad_w=1920:pad_h=1080:pad_x=-1:pad_y=-1:pad_color=black,setsar=1" + ) + ); + } + + #[test] + fn scale_qsv_arg_unchanged() { + // scale-only emission must be byte-identical to before the pad-fusion work + let scale = ScaleQsv { + size: Some(FrameSize { + width: 1440, + height: 1080, + }), + input_is_anamorphic: false, + }; + assert_eq!( + scale.as_arg().as_deref(), + Some("vpp_qsv=w=1440:h=1080,setsar=1") + ); + } + + #[test] + fn optimize_fuses_scale_then_pad_into_single_vpp_qsv() { + use crate::filter_chain::{FilterChain, PipelineFilter}; + use crate::hw_accel::HardwareAccel; + + let accel = HardwareAccel::Qsv(make_qsv()); + let ffmpeg_info = make_ffmpeg_info(true); + let filter_options = VideoFilterOptions::default(); + + // source is 1920x1080 anamorphic-free but at 4:3 content that needs + // letterboxing to 1920x1080; drive it through Scale + Pad video filters. + let initial_state = FrameState { + size: FrameSize { + width: 1440, + height: 1080, + }, + ..make_frame_state() + }; + + let scale: VideoFilter = ScaleFilter { + size: Some(FrameSize { + width: 1440, + height: 1080, + }), + scaling_mode: ScalingMode::ScaleAndPad, + input_is_anamorphic: false, + force_original_aspect_ratio: None, + } + .into(); + + let mut chain = FilterChain::new(vec![ + PipelineFilter::Video(scale), + PipelineFilter::Video(pad_1920x1080()), + ]); + + chain.resolve( + &ffmpeg_info, + &Some(accel), + &filter_options, + &initial_state, + &FrameSurface::Qsv, + &Some(PixelFormat::Nv12), + ); + chain.optimize(); + chain.build("0:a", "0:v", None, None); + + let args = chain.as_arg(); + let filter_complex = &args[1]; + + assert!( + filter_complex.contains( + "vpp_qsv=w=1440:h=1080:pad_w=1920:pad_h=1080:pad_x=-1:pad_y=-1:pad_color=black" + ), + "expected a single combined vpp_qsv scale+pad: {filter_complex}" + ); + assert_eq!( + filter_complex.matches("vpp_qsv").count(), + 1, + "scale+pad must collapse to exactly one vpp_qsv: {filter_complex}" + ); + } } diff --git a/crates/ffpipeline/src/filter_chain.rs b/crates/ffpipeline/src/filter_chain.rs index 98651a4..2007c94 100644 --- a/crates/ffpipeline/src/filter_chain.rs +++ b/crates/ffpipeline/src/filter_chain.rs @@ -507,7 +507,9 @@ impl FilterChain { continue; } - if let Some(fused) = Self::try_fuse_cuda(&self.filters[i], &self.filters[j]) { + let fused = Self::try_fuse_cuda(&self.filters[i], &self.filters[j]) + .or_else(|| Self::try_fuse_qsv(&self.filters[i], &self.filters[j])); + if let Some(fused) = fused { self.filters[i] = fused; self.filters.remove(j); changed = true; @@ -546,6 +548,31 @@ impl FilterChain { } } + /// Fuse a `vpp_qsv` scale immediately followed by a `vpp_qsv` pad into a + /// single combined `vpp_qsv` instance. Chaining two `vpp_qsv` scales drops + /// the final frame at EOF on real hardware (measured on Intel B50), so the + /// scale is folded into the pad which then emits one instance carrying both + /// w/h and pad_* options. + fn try_fuse_qsv(a: &PipelineFilter, b: &PipelineFilter) -> Option { + use VideoFilter::{PadQsv as PadQsvV, ScaleQsv as ScaleQsvV}; + + use crate::accel::qsv::PadQsv; + let (PipelineFilter::Video(va), PipelineFilter::Video(vb)) = (a, b) else { + return None; + }; + match (va, vb) { + (ScaleQsvV(scale), PadQsvV(pad)) + if scale.size.is_some() && pad.size.is_some() && pad.scale.is_none() => + { + Some(PipelineFilter::Video(PadQsvV(PadQsv { + scale: Some(scale.clone()), + ..pad.clone() + }))) + } + _ => None, + } + } + pub(crate) fn build( &mut self, audio_label: &str,