diff --git a/src/avhw.rs b/src/avhw.rs index 22c2ef7..1fb1f0e 100644 --- a/src/avhw.rs +++ b/src/avhw.rs @@ -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 { + fn new_inner(hw_dev: &AvHwDevCtx, w: u32, h: u32, sw_fmt: ff::format::Pixel) -> Result { 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 { // 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 { 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::::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) } diff --git a/src/state.rs b/src/state.rs index 1e83f40..ed4ea94 100644 --- a/src/state.rs +++ b/src/state.rs @@ -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 State { }); 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 State { 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 State { 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 Dispatch for State { qhandle: &QueueHandle>, ) { use wayland_client::protocol::wl_registry::Event as RegistryEvent; + tracing::debug!("Dispatch::event fired: {:?}", event); match event { RegistryEvent::Global { @@ -793,7 +799,7 @@ impl Dispatch for State { 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 Dispatch for State { 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); } }