diff --git a/src/main.rs b/src/main.rs index d5e2316..a2dc21a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,13 +40,14 @@ use log::{debug, error, info, trace, warn}; use signal_hook::consts::{SIGHUP, SIGINT, SIGTERM, SIGUSR1}; use simplelog::{ColorChoice, CombinedLogger, LevelFilter, TermLogger, TerminalMode}; use thiserror::Error; +use transform::{transpose_if_transform_transposed, Rect}; use wayland_client::{ backend::ObjectId, event_created_child, globals::{registry_queue_init, GlobalListContents}, protocol::{ wl_buffer::WlBuffer, - wl_output::{self, Mode, WlOutput}, + wl_output::{self, Mode, Transform, WlOutput}, wl_registry::WlRegistry, }, ConnectError, Connection, Dispatch, EventQueue, Proxy, QueueHandle, WEnum, @@ -84,6 +85,7 @@ use avhw::{AvHwDevCtx, AvHwFrameCtx}; mod audio; mod fifo; +mod transform; #[cfg(target_os = "linux")] mod platform { @@ -352,6 +354,7 @@ struct PartialOutputInfo { refresh: Option, output: WlOutput, has_recvd_done: bool, + transform: Option, } impl PartialOutputInfo { fn complete(&self, fractional_scale: f64) -> Option { @@ -370,6 +373,7 @@ impl PartialOutputInfo { fractional_scale, size_pixels: *size_pixels, output: self.output.clone(), + transform: self.transform.unwrap_or(Transform::Normal), }) } else { None @@ -386,12 +390,17 @@ struct OutputInfo { refresh: Rational, fractional_scale: f64, output: WlOutput, + transform: Transform, } impl OutputInfo { fn logical_to_pixel(&self, logical: i32) -> i32 { (f64::from(logical) * self.fractional_scale).round() as i32 } + + fn size_screen_space(&self) -> (i32, i32) { + transpose_if_transform_transposed(self.size_pixels, self.transform) + } } #[derive(Default)] @@ -441,13 +450,7 @@ struct State { enum EncConstructionStage { None, - EverythingButFormat { - output: OutputInfo, - x: i32, - y: i32, - w: i32, - h: i32, - }, + EverythingButFormat { output: OutputInfo, roi: Rect }, Complete(EncState), } impl EncConstructionStage { @@ -563,7 +566,7 @@ impl Dispatch for State { EncConstructionStage::None => unreachable!( "Oops, somehow created a screencopy frame without initial enc state stuff?" ), - EncConstructionStage::EverythingButFormat { output, x, y, w, h } => { + EncConstructionStage::EverythingButFormat { output, roi } => { state.enc = EncConstructionStage::Complete( match EncState::new( &state.args, @@ -572,9 +575,9 @@ impl Dispatch for State { .expect("Unknown fourcc"), ), output.refresh, + output.transform, (dmabuf_width as i32, dmabuf_height as i32), - (*x, *y), - (*w, *h), + *roi, Arc::clone(&state.sigusr1_flag), state .dri_device @@ -721,6 +724,14 @@ impl Dispatch for State { }); } } + wl_output::Event::Geometry { transform, .. } => match transform { + WEnum::Value(v) => { + state.update_output_info_wl_output(&id, |info| info.transform = Some(v)) + } + WEnum::Unknown(u) => { + eprintln!("Unknown output transform value: {u}") + } + }, wl_output::Event::Done => { state.done_output_info_wl_output(id, qhandle); } @@ -947,6 +958,7 @@ impl State { refresh: None, output, has_recvd_done: false, + transform: None, }, ); } @@ -1118,7 +1130,7 @@ impl State { let enabled_outputs: Vec<_> = self.outputs.iter().flat_map(|(_, o)| o).collect(); - let (output, (x, y), (w, h)) = match (self.args.geometry, self.args.output.as_str()) { + let (output, roi) = match (self.args.geometry, self.args.output.as_str()) { (None, "") => { // default case, capture whole monitor if enabled_outputs.len() != 1 { @@ -1130,12 +1142,12 @@ impl State { } let output = enabled_outputs[0]; - (output, (0, 0), output.size_pixels) + (output, Rect::new((0, 0), output.size_screen_space())) } (None, disp) => { // --output but no --geometry if let Some(&output) = enabled_outputs.iter().find(|i| i.name == disp) { - (output, (0, 0), output.size_pixels) + (output, Rect::new((0, 0), output.size_screen_space())) } else { eprintln!("display {} not found, bailing", disp); self.quit_flag.store(1, Ordering::SeqCst); @@ -1152,11 +1164,13 @@ impl State { }) { ( output, - ( - output.logical_to_pixel(x - output.loc.0), - output.logical_to_pixel(y - output.loc.1), + Rect::new( + ( + output.logical_to_pixel(x - output.loc.0), + output.logical_to_pixel(y - output.loc.1), + ), + (output.logical_to_pixel(w), output.logical_to_pixel(h)), ), - (output.logical_to_pixel(w), output.logical_to_pixel(h)), ) } else { eprintln!( @@ -1181,10 +1195,7 @@ impl State { self.wl_output = Some(output.output.clone()); self.enc = EncConstructionStage::EverythingButFormat { output: output.clone(), - x, - y, - w, - h, + roi, }; self.queue_copy(qhandle); } @@ -1290,9 +1301,9 @@ impl EncState { args: &Args, capture_pixfmt: Pixel, refresh: Rational, + transform: Transform, (capture_w, capture_h): (i32, i32), // pixels - (roi_x, roi_y): (i32, i32), - (roi_w, roi_h): (i32, i32), + roi_screen_coord: Rect, // roi in screen coordinates (0, 0 is screen upper left, which is not necessarily captured frame upper left) sigusr1_flag: Arc, dri_device: &str, ) -> anyhow::Result { @@ -1401,18 +1412,18 @@ impl EncState { .create_frame_ctx(capture_pixfmt, capture_w, capture_h) .with_context(|| format!("Failed to create vaapi frame context for capture surfaces of format {capture_pixfmt:?} {capture_w}x{capture_h}"))?; - let (enc_w, enc_h) = match args.encode_resolution { + let (enc_w_screen_coord, enc_h_screen_coord) = match args.encode_resolution { Some((x, y)) => (x as i32, y as i32), - None => (roi_w, roi_h), + None => (roi_screen_coord.w, roi_screen_coord.h), }; let (video_filter, filter_timebase) = video_filter( &mut frames_rgb, enc_pixfmt, (capture_w, capture_h), - (roi_x, roi_y), - (roi_w, roi_h), - (enc_w, enc_h), + roi_screen_coord, + (enc_w_screen_coord, enc_h_screen_coord), + transform, ); let enc_pixfmt_av = match enc_pixfmt { @@ -1420,7 +1431,7 @@ impl EncState { EncodePixelFormat::Sw(fmt) => fmt, }; let mut frames_yuv = hw_device_ctx - .create_frame_ctx(enc_pixfmt_av, enc_w, enc_h) + .create_frame_ctx(enc_pixfmt_av, enc_w_screen_coord, enc_h_screen_coord) .with_context(|| { format!("Failed to create a vaapi frame context for encode surfaces of format {enc_pixfmt_av:?} {capture_w}x{capture_h}") })?; @@ -1431,7 +1442,7 @@ impl EncState { args, enc_pixfmt, &encoder, - (enc_w, enc_h), + (enc_w_screen_coord, enc_h_screen_coord), refresh, global_header, &mut hw_device_ctx, @@ -1460,7 +1471,7 @@ impl EncState { args, enc_pixfmt, &encoder, - (enc_w, enc_h), + (enc_w_screen_coord, enc_h_screen_coord), refresh, global_header, &mut hw_device_ctx, @@ -1706,9 +1717,9 @@ fn video_filter( inctx: &mut AvHwFrameCtx, pix_fmt: EncodePixelFormat, (capture_width, capture_height): (i32, i32), - (roi_x, roi_y): (i32, i32), - (roi_w, roi_h): (i32, i32), // size (pixels) of the region to capture - (enc_w, enc_h): (i32, i32), // size (pixels) to encode. if not same as roi_{w,h}, the image will be scaled + roi_screen_coord: Rect, // size (pixels) + (enc_w_screen_coord, enc_h_screen_coord): (i32, i32), // size (pixels) to encode. if not same as roi_{w,h}, the image will be scaled. + transform: Transform, ) -> (filter::Graph, Rational) { let mut g = ffmpeg::filter::graph::Graph::new(); g.add( @@ -1762,6 +1773,34 @@ fn video_filter( ) }; + let transpose_filter = match transform { + Transform::_90 => ",transpose_vaapi=dir=clock", + Transform::_180 => ",transpose_vaapi=dir=reversal", + Transform::_270 => ",transpose_vaapi=dir=cclock", + Transform::Flipped => ",transpose_vaapi=dir=hflip", + Transform::Flipped90 => ",transpose_vaapi=dir=cclock_flip", + Transform::Flipped180 => ",transpose_vaapi=dir=vflip", + Transform::Flipped270 => ",transpose_vaapi=dir=clock_flip", + Transform::Normal | _ => "", + }; + + // it seems intel's vaapi driver doesn't support transpose in RGB space, so we have to transpose + // after the format conversion + // which means we have to transform the crop to be in the *pre* transpose space + let Rect { + x: roi_x, + y: roi_y, + w: roi_w, + h: roi_h, + } = roi_screen_coord.screen_to_frame(capture_width, capture_height, transform); + + // sanity check + assert!(roi_x >= 0, "{roi_x} < 0"); + assert!(roi_y >= 0, "{roi_y} < 0"); + + let (enc_w, enc_h) = + transpose_if_transform_transposed((enc_w_screen_coord, enc_h_screen_coord), transform); + // exact=1 should not be necessary, as the input is not chroma-subsampled // however, there is a bug in ffmpeg that makes it required: https://trac.ffmpeg.org/ticket/10669 // it is harmless to add though, so keep it as a workaround @@ -1770,7 +1809,7 @@ fn video_filter( .input("out", 0) .unwrap() .parse(&format!( - "crop={roi_w}:{roi_h}:{roi_x}:{roi_y}:exact=1,scale_vaapi=format={output_real_pixfmt_name}:w={enc_w}:h={enc_h}{}", + "crop={roi_w}:{roi_h}:{roi_x}:{roi_y}:exact=1,scale_vaapi=format={output_real_pixfmt_name}:w={enc_w}:h={enc_h}{transpose_filter}{}", if let EncodePixelFormat::Vaapi(_) = pix_fmt { "" } else { diff --git a/src/transform.rs b/src/transform.rs new file mode 100644 index 0000000..665bbd7 --- /dev/null +++ b/src/transform.rs @@ -0,0 +1,189 @@ +use wayland_client::protocol::wl_output::Transform; + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct Rect { + pub x: i32, + pub y: i32, + pub w: i32, + pub h: i32, +} + +fn transform_is_transposed(transform: Transform) -> bool { + transform_basis(transform).0[0] == 0 +} + +pub fn transpose_if_transform_transposed((w, h): (i32, i32), transform: Transform) -> (i32, i32) { + if transform_is_transposed(transform) { + (h, w) + } else { + (w, h) + } +} + +fn screen_point_to_frame( + capture_w: i32, + capture_h: i32, + transform: Transform, + x: i32, + y: i32, +) -> (i32, i32) { + let screen_origin_frame_coord = match transform { + Transform::Flipped180 | Transform::_90 => (0, capture_h), + Transform::Flipped270 | Transform::_180 => (capture_w, capture_h), + Transform::Flipped | Transform::_270 => (capture_w, 0), + Transform::Flipped90 | Transform::Normal | _ => (0, 0), + }; + + let screen_basis_frame_coord = transform_basis(transform); + + ( + screen_origin_frame_coord.0 + + x * screen_basis_frame_coord.0[0] + + y * screen_basis_frame_coord.1[0], + screen_origin_frame_coord.1 + + x * screen_basis_frame_coord.0[1] + + y * screen_basis_frame_coord.1[1], + ) +} + +// for each (x, y) in screen space, how many steps in frame space +fn transform_basis(transform: Transform) -> ([i32; 2], [i32; 2]) { + match transform { + Transform::_90 => ([0, -1], [1, 0]), + Transform::_180 => ([-1, 0], [0, -1]), + Transform::_270 => ([0, 1], [-1, 0]), + Transform::Flipped => ([-1, 0], [0, 1]), + Transform::Flipped90 => ([0, 1], [1, 0]), + Transform::Flipped180 => ([1, 0], [0, -1]), + Transform::Flipped270 => ([0, -1], [-1, 0]), + Transform::Normal | _ => ([1, 0], [0, 1]), + } +} + +impl Rect { + pub fn new((x, y): (i32, i32), (w, h): (i32, i32)) -> Self { + Rect { x, y, w, h } + } + + pub fn screen_to_frame(&self, capture_w: i32, capture_h: i32, transform: Transform) -> Rect { + let (x1, y1) = screen_point_to_frame(capture_w, capture_h, transform, self.x, self.y); + let (x2, y2) = screen_point_to_frame( + capture_w, + capture_h, + transform, + self.x + self.w, + self.y + self.h, + ); + + Rect { + x: x1.min(x2), + y: y1.min(y2), + w: (x1 - x2).abs(), + h: (y1 - y2).abs(), + } + } +} + +#[cfg(test)] +mod test { + use wayland_client::protocol::wl_output::Transform; + + use crate::transform::transform_is_transposed; + + use super::Rect; + + #[test] + fn screen_to_frame_normal() { + assert_eq!( + Rect { + x: 10, + y: 20, + w: 30, + h: 40 + } + .screen_to_frame(1920, 1080, Transform::Normal), + Rect { + x: 10, + y: 20, + w: 30, + h: 40 + } + ); + } + + #[test] + fn screen_to_frame_90() { + assert_eq!( + Rect { + x: 10, + y: 20, + w: 30, + h: 40 + } + .screen_to_frame(1920, 1080, Transform::_90), + Rect { + x: 20, + y: 1040, + w: 40, + h: 30 + } + ); + + assert_eq!( + Rect { + x: 0, + y: 0, + w: 1200, + h: 1920 + } + .screen_to_frame(1920, 1200, Transform::_90), + Rect { + x: 0, + y: 0, + w: 1920, + h: 1200 + } + ); + + assert_eq!( + Rect { + x: 743, + y: 1359, + w: 312, + h: 264, + } + .screen_to_frame(1920, 1200, Transform::_90), + Rect { + x: 1359, + y: 145, + w: 264, + h: 312 + } + ) + } + + #[test] + fn screen_to_frame_270() { + assert_eq!( + Rect { + x: 274, + y: 962, + w: 639, + h: 412, + } + .screen_to_frame(1920, 1200, Transform::_270), + Rect { + x: 546, + y: 274, + w: 412, + h: 639 + } + ); + } + + #[test] + fn transform_transposed() { + assert!(!transform_is_transposed(Transform::Normal)); + assert!(transform_is_transposed(Transform::_90)); + } +}