feat(avhw): integrate transform into VA-API filter graph

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
dailz
2026-04-14 17:02:54 +08:00
parent e89689634d
commit ecd78492ee
2 changed files with 96 additions and 51 deletions

View File

@@ -4,9 +4,11 @@ use std::ptr;
use anyhow::{bail, Result};
use ffmpeg_next as ff;
use ffmpeg_next::ffi as ffi;
use ffmpeg_next::ffi;
use ffmpeg_next::packet::Mut as _;
use crate::transform::Transform;
// ---------------------------------------------------------------------------
// AvHwDevCtx
// ---------------------------------------------------------------------------
@@ -69,12 +71,7 @@ pub struct AvHwFrameCtx {
unsafe impl Send for AvHwFrameCtx {}
impl AvHwFrameCtx {
fn new_inner(
hw_dev: &AvHwDevCtx,
w: u32,
h: u32,
sw_fmt: ff::format::Pixel,
) -> Result<Self> {
fn new_inner(hw_dev: &AvHwDevCtx, w: u32, h: u32, sw_fmt: ff::format::Pixel) -> Result<Self> {
let mut p = unsafe { ffi::av_hwframe_ctx_alloc(hw_dev.as_ptr()) };
if p.is_null() {
bail!("av_hwframe_ctx_alloc returned null");
@@ -159,24 +156,25 @@ impl EncState {
output_path: &Path,
width: u32,
height: u32,
enc_width: u32,
enc_height: u32,
bitrate: u64,
gop_size: u32,
fps: u32,
transform: Transform,
) -> Result<Self> {
// 1. VAAPI device
let hw_device_ctx = AvHwDevCtx::new_vaapi(drm_device)?;
// 2. Frame contexts (capture=XRGB/RGBZ, encode=NV12)
let frames_rgb = AvHwFrameCtx::for_capture(
&hw_device_ctx,
width,
height,
ff::format::Pixel::RGBZ,
)?;
// frames_rgb uses original capture dimensions (matches raw framebuffer)
// frames_yuv uses encoder dimensions (transposed for 90°/270° rotations)
let frames_rgb =
AvHwFrameCtx::for_capture(&hw_device_ctx, width, height, ff::format::Pixel::RGBZ)?;
let frames_yuv = AvHwFrameCtx::for_encode(
&hw_device_ctx,
width,
height,
enc_width,
enc_height,
ff::format::Pixel::NV12,
)?;
@@ -189,8 +187,8 @@ impl EncState {
ctx.encoder().video()?
};
enc.set_width(width);
enc.set_height(height);
enc.set_width(enc_width);
enc.set_height(enc_height);
enc.set_format(ff::format::Pixel::VAAPI);
enc.set_bit_rate(bitrate as usize);
enc.set_gop(gop_size);
@@ -226,14 +224,22 @@ impl EncState {
}
// 4. Open encoder. Video::open() returns Encoder(Video); .0 extracts the Video.
let opened = enc.open().map_err(|e| {
anyhow::anyhow!("Failed to open h264_vaapi encoder: {e}")
})?;
let opened = enc
.open()
.map_err(|e| anyhow::anyhow!("Failed to open h264_vaapi encoder: {e}"))?;
let enc_video = opened.0;
// 5. Filter graph (inline)
let video_filter =
build_filter_graph(&hw_device_ctx, &frames_rgb, width, height, fps)?;
let video_filter = build_filter_graph(
&hw_device_ctx,
&frames_rgb,
width,
height,
enc_width,
enc_height,
fps,
transform,
)?;
// 6. Muxer setup (strict order)
let output_cstr = CString::new(output_path.to_str().unwrap())?;
@@ -329,9 +335,9 @@ impl EncState {
let mut filter_sink = filter_sink_ctx.sink();
// SAFETY: hw_frame is a valid VAAPI hardware frame from capture.
filter_src.add(hw_frame).map_err(|e| {
anyhow::anyhow!("Filter source add failed: {e}")
})?;
filter_src
.add(hw_frame)
.map_err(|e| anyhow::anyhow!("Filter source add failed: {e}"))?;
loop {
let mut filtered = ff::frame::Video::empty();
@@ -352,9 +358,8 @@ impl EncState {
let start_ts = self.starting_timestamp.unwrap();
// SAFETY: avcodec_send_frame sends a valid NV12 VAAPI surface to the encoder.
let ret = unsafe {
ffi::avcodec_send_frame(self.enc_video.as_mut_ptr(), filtered.as_ptr())
};
let ret =
unsafe { ffi::avcodec_send_frame(self.enc_video.as_mut_ptr(), filtered.as_ptr()) };
if ret < 0 {
bail!("avcodec_send_frame failed: error {ret}");
}
@@ -400,9 +405,9 @@ impl EncState {
// Write trailer only if at least one frame was encoded.
if self.frames_written {
self.octx.write_trailer().map_err(|e| {
anyhow::anyhow!("Failed to write trailer: {e}")
})?;
self.octx
.write_trailer()
.map_err(|e| anyhow::anyhow!("Failed to write trailer: {e}"))?;
}
Ok(())
@@ -440,9 +445,8 @@ impl EncState {
}
pkt.set_stream(0);
pkt.write_interleaved(&mut self.octx).map_err(|e| {
anyhow::anyhow!("Failed to write packet: {e}")
})?;
pkt.write_interleaved(&mut self.octx)
.map_err(|e| anyhow::anyhow!("Failed to write packet: {e}"))?;
self.frames_written = true;
}
@@ -459,16 +463,19 @@ fn build_filter_graph(
frames_rgb: &AvHwFrameCtx,
width: u32,
height: u32,
_enc_width: u32,
_enc_height: u32,
fps: u32,
transform: Transform,
) -> Result<ff::filter::Graph> {
let mut graph = ff::filter::Graph::new();
let buffersrc = ff::filter::find("buffer")
.ok_or_else(|| anyhow::anyhow!("filter 'buffer' not found"))?;
let buffersrc =
ff::filter::find("buffer").ok_or_else(|| anyhow::anyhow!("filter 'buffer' not found"))?;
let buffersink = ff::filter::find("buffersink")
.ok_or_else(|| anyhow::anyhow!("filter 'buffersink' not found"))?;
let format_filter = ff::filter::find("format")
.ok_or_else(|| anyhow::anyhow!("filter 'format' not found"))?;
let format_filter =
ff::filter::find("format").ok_or_else(|| anyhow::anyhow!("filter 'format' not found"))?;
let scale_vaapi = ff::filter::find("scale_vaapi")
.ok_or_else(|| anyhow::anyhow!("filter 'scale_vaapi' not found"))?;
@@ -491,7 +498,10 @@ fn build_filter_graph(
(*par).format = Into::<ffi::AVPixelFormat>::into(ff::format::Pixel::VAAPI) as i32;
(*par).width = width as i32;
(*par).height = height as i32;
(*par).time_base = ffi::AVRational { num: 1, den: fps as i32 };
(*par).time_base = ffi::AVRational {
num: 1,
den: fps as i32,
};
(*par).hw_frames_ctx = frames_rgb.ref_clone();
let ret = ffi::av_buffersrc_parameters_set(src_ctx.as_mut_ptr(), par);
ffi::av_freep(par as *mut _ as *mut _);
@@ -503,7 +513,7 @@ fn build_filter_graph(
// format filter: negotiate pixel format to NV12
let mut fmt_ctx = graph.add(&format_filter, "fmt", "pix_fmts=nv12")?;
// scale_vaapi: hardware scaling and colourspace conversion
// scale_vaapi: hardware scaling and colourspace conversion (keeps original dimensions)
let mut scale_ctx = graph.add(&scale_vaapi, "scale", &format!("{width}:{height}"))?;
// SAFETY: scale_vaapi needs hw_device_ctx for VAAPI device access.
unsafe {
@@ -513,14 +523,43 @@ fn build_filter_graph(
// buffersink
let mut sink_ctx = graph.add(&buffersink, "out", "")?;
// Link: src -> format -> scale -> sink
// Build filter chain: src -> format -> scale -> [transpose] -> sink
src_ctx.link(0, &mut fmt_ctx, 0);
fmt_ctx.link(0, &mut scale_ctx, 0);
scale_ctx.link(0, &mut sink_ctx, 0);
graph.validate().map_err(|e| {
anyhow::anyhow!("Filter graph validation failed: {e}")
})?;
match transform {
Transform::Normal90 | Transform::Normal270 => {
let transpose = ff::filter::find("transpose_vaapi")
.ok_or_else(|| anyhow::anyhow!("filter 'transpose_vaapi' not found"))?;
let dir_val = match transform {
Transform::Normal90 => "1",
Transform::Normal270 => "2",
_ => unreachable!(),
};
let mut trans_ctx = graph.add(&transpose, "transpose", &format!("dir={dir_val}"))?;
// SAFETY: transpose_vaapi needs hw_device_ctx for VAAPI device access.
unsafe {
(*trans_ctx.as_mut_ptr()).hw_device_ctx = hw_dev.ref_clone();
}
fmt_ctx.link(0, &mut scale_ctx, 0);
scale_ctx.link(0, &mut trans_ctx, 0);
trans_ctx.link(0, &mut sink_ctx, 0);
}
Transform::Normal180 => {
tracing::warn!(
"Normal180 transform detected; rotation correction deferred to follow-up"
);
fmt_ctx.link(0, &mut scale_ctx, 0);
scale_ctx.link(0, &mut sink_ctx, 0);
}
_ => {
fmt_ctx.link(0, &mut scale_ctx, 0);
scale_ctx.link(0, &mut sink_ctx, 0);
}
}
graph
.validate()
.map_err(|e| anyhow::anyhow!("Filter graph validation failed: {e}"))?;
Ok(graph)
}

View File

@@ -35,7 +35,7 @@ use crate::args::Args;
use crate::avhw::{AvHwDevCtx, EncState};
use crate::cap_wlr_screencopy::CapWlrScreencopy;
use crate::fps_limit::FpsLimit;
use crate::transform::Transform;
use crate::transform::{transpose_if_transform_transposed, Transform};
// ---------------------------------------------------------------------------
// CaptureSource trait
@@ -429,14 +429,19 @@ impl<S: CaptureSource> State<S> {
});
let gop_size = self.args.gop_size.unwrap_or(self.args.fps);
let fps = self.args.fps;
let (enc_w, enc_h) =
transpose_if_transform_transposed(output_info.transform, width as i32, height as i32);
let enc = match EncState::new(
&drm_path,
Path::new(&self.args.output),
width,
height,
enc_w as u32,
enc_h as u32,
bitrate,
gop_size,
fps,
output_info.transform,
) {
Ok(enc) => enc,
Err(e) => {
@@ -473,7 +478,7 @@ impl<S: CaptureSource> State<S> {
match pos {
Some(i) => Some(i),
None => {
let all_probed = outputs.iter().all(|o| o.done_count >= 2);
let all_probed = outputs.iter().all(|o| o.done_count >= 1);
if all_probed {
let available: Vec<&str> =
outputs.iter().filter_map(|o| o.name.as_deref()).collect();
@@ -487,7 +492,7 @@ impl<S: CaptureSource> State<S> {
None
}
}
} else if outputs.iter().all(|o| o.done_count >= 2) {
} else if outputs.iter().all(|o| o.done_count >= 1) {
if outputs.is_empty() {
return false;
}
@@ -635,6 +640,7 @@ impl<S: CaptureSource> Dispatch<WlRegistry, GlobalListContents> for State<S> {
qhandle: &QueueHandle<State<S>>,
) {
use wayland_client::protocol::wl_registry::Event as RegistryEvent;
tracing::debug!("Dispatch<WlRegistry>::event fired: {:?}", event);
match event {
RegistryEvent::Global {
@@ -793,7 +799,7 @@ impl<S: CaptureSource> Dispatch<WlOutput, OutputId> for State<S> {
if let EncConstructionStage::ProbingOutputs { outputs, .. } = &mut state.stage {
if let Some(info) = outputs.get_mut(idx) {
info.done_count += 1;
if info.done_count >= 2 {
if info.done_count >= 1 {
state.try_finalize_output(idx);
}
}
@@ -849,7 +855,7 @@ impl<S: CaptureSource> Dispatch<ZxdgOutputV1, OutputId> for State<S> {
if let EncConstructionStage::ProbingOutputs { outputs, .. } = &mut state.stage {
if let Some(info) = outputs.get_mut(idx) {
info.done_count += 1;
if info.done_count >= 2 {
if info.done_count >= 1 {
state.try_finalize_output(idx);
}
}