- commit
- 146879330b40d81715503ef66b3e6217a36c8bdc
- parent
- a42e112045022c56cf890328d72236a753cc9b19
- author
- Dami <dami.etoile@gmail.com>
| M | crates/ | 344 | +---------------------------- |
| A | crates/ | 355 | ++++++++++++++++++++++++++++++ |
@@ -1,5 +1,4 @@
11 use std::borrow::Cow;2-use std::ffi::OsString;32 use std::process::{ExitStatus, Stdio};43 use std::sync::Arc;54 use std::sync::atomic::{AtomicU64, Ordering};@@ -11,7 +10,9 @@ use sigil_protocol::PointerSurfaceDimensions;
1110 use tokio::io::{AsyncRead, AsyncReadExt};1211 use tokio::process::Command;1312 use tokio::sync::watch;14-use tracing::{debug, warn};13+14+use tracing::debug;15+use tracing::warn;1516 1617 use crate::clock::SessionClock;1718 use crate::config::{@@ -19,9 +20,12 @@ use crate::config::{
1920 };2021 2122 mod annexb;23+mod external;2224 mod preflight;2325 24-use annexb::AnnexBAccessUnitParser;26+27+use external::gamescope_pipeline_args;28+use external::{forward_annex_b_stream, spawn_gamescope_pipewire_with_target};2529 use preflight::preflight_gamescope_static;2630 pub(crate) use preflight::{preflight_gstreamer_element, validate_executable};2731 @@ -510,35 +514,6 @@ fn spawn_gamescope_pipewire_with_preflight(
510514 }511515 }512516 513-fn spawn_gamescope_pipewire_with_target(514- config: HostConfig,515- session_clock: SessionClock,516- preflight: GamescopePreflight,517-) -> Result<EncodedSource> {518- let (sender, receiver) = watch::channel(None);519- let (current_gop_sender, current_gop) = watch::channel(None);520- let target_object = preflight.target_object;521- let video_mode = preflight.video_mode;522- let task = tokio::spawn(async move {523- run_gamescope_pipewire(524- config,525- session_clock,526- target_object,527- video_mode,528- sender,529- current_gop_sender,530- )531- .await532- });533- Ok(EncodedSource {534- frames: receiver,535- current_gop,536- task,537- pointer_surface_dimensions: Some(preflight.pointer_surface_dimensions),538- encoder_control: None,539- })540-}541-542517 543518 fn spawn_gamescope_pipewire_in_process_with_target(544519 config: HostConfig,@@ -654,70 +629,6 @@ fn validate_pipewire_target_object(target_object: &str) -> Result<()> {
654629 Ok(())655630 }656631 657-async fn run_gamescope_pipewire(658- config: HostConfig,659- session_clock: SessionClock,660- target_object: String,661- video_mode: CaptureVideoMode,662- sender: watch::Sender<Option<EncodedFrame>>,663- current_gop_sender: watch::Sender<Option<EncodedGop>>,664-) -> Result<()> {665- let pipewire = gamescope_config(&config)?;666- let args = gamescope_pipeline_args(&config, &target_object, video_mode)?;667- let mut command = Command::new(&pipewire.gst_launch_path);668- command669- .args(args)670- .env("LC_ALL", "C")671- // This is honored by the legacy VAAPI plugin and harmless for GstVA.672- // GstVA selection is independently checked against device-path above.673- .env("GST_VAAPI_DRM_DEVICE", &pipewire.vaapi_render_node)674- .stdin(Stdio::null())675- .stdout(Stdio::piped())676- .stderr(Stdio::piped())677- .kill_on_drop(true);678-679- let mut child = command.spawn().with_context(|| {680- format!(681- "starting Gamescope pipeline with {}",682- pipewire.gst_launch_path.display()683- )684- })?;685- let mut stdout = child686- .stdout687- .take()688- .context("GStreamer stdout was not piped")?;689- let stderr = child690- .stderr691- .take()692- .context("GStreamer stderr was not piped")?;693- let stderr_task = tokio::spawn(log_stderr_chunks(stderr, "gstreamer"));694-695- let result = forward_annex_b_stream(696- session_clock,697- &mut stdout,698- &sender,699- ¤t_gop_sender,700- interactive_gop_frames(video_mode.framerate) as usize,701- video_mode.width,702- video_mode.height,703- )704- .await;705- let _ = child.kill().await;706- let exit_status = child.wait().await.ok();707- stderr_task.abort();708-709- match result {710- Ok(()) => Ok(()),711- Err(error) => {712- if let Some(status) = exit_status {713- Err(error).with_context(|| format!("GStreamer pipeline exited with {status}"))714- } else {715- Err(error)716- }717- }718- }719-}720-721632 async fn run_test_pattern(722633 config: HostConfig,723634 session_clock: SessionClock,@@ -796,48 +707,6 @@ async fn run_test_pattern(
796707 }797708 }798709 799-async fn forward_annex_b_stream<R>(800- session_clock: SessionClock,801- stdout: &mut R,802- sender: &watch::Sender<Option<EncodedFrame>>,803- current_gop_sender: &watch::Sender<Option<EncodedGop>>,804- max_gop_frames: usize,805- width: u16,806- height: u16,807-) -> Result<()>808-where809- R: AsyncRead + Unpin,810-{811- let mut publisher = AccessUnitPublisher::new(812- session_clock,813- sender,814- current_gop_sender,815- max_gop_frames,816- width,817- height,818- );819- let mut parser = AnnexBAccessUnitParser::default();820- let mut chunk = [0_u8; 16 * 1024];821-822- loop {823- let count = stdout824- .read(&mut chunk)825- .await826- .context("reading encoded source output")?;827- if count == 0 {828- bail!("encoded source ended unexpectedly");829- }830-831- for access_unit in parser.push(&chunk[..count])? {832- publisher.publish(access_unit)?;833- if publisher.receivers_closed() {834- debug!("encoded source has no receivers");835- return Ok(());836- }837- }838- }839-}840-841710 struct AccessUnitPublisher<'a> {842711 session_clock: SessionClock,843712 sender: &'a watch::Sender<Option<EncodedFrame>>,@@ -1446,107 +1315,6 @@ fn gamescope_config(config: &HostConfig) -> Result<&GamescopePipewireConfig> {
14461315 .context("gamescope_pipewire configuration is missing")14471316 }14481317 1449-fn gamescope_pipeline_args(1450- config: &HostConfig,1451- target_object: &str,1452- video_mode: CaptureVideoMode,1453-) -> Result<Vec<OsString>> {1454- let pipewire = gamescope_config(config)?;1455- ensure!(1456- !target_object.is_empty()1457- && target_object.len() <= 321458- && target_object.bytes().all(|byte| byte.is_ascii_digit()),1459- "resolved PipeWire object.serial is invalid"1460- );1461- let caps = format!(1462- "video/x-raw,format=NV12,width={},height={},framerate={}/1",1463- video_mode.width, video_mode.height, video_mode.framerate1464- );1465- let encoded_caps = "video/x-h264,stream-format=byte-stream,alignment=au";1466- let mut args = vec![1467- "--quiet".to_owned(),1468- "pipewiresrc".to_owned(),1469- "do-timestamp=true".to_owned(),1470- // Keep Gamescope's negotiated producer pool bounded at its preferred1471- // four buffers, but copy each delivered frame into GStreamer's own1472- // buffer before downstream conversion/encoding can retain it. This1473- // returns producer buffers promptly instead of starving Gamescope at1474- // vblank. `use-bufferpool=false` is the supported replacement for the1475- // deprecated `always-copy=true` spelling.1476- "min-buffers=1".to_owned(),1477- "max-buffers=4".to_owned(),1478- "use-bufferpool=false".to_owned(),1479- format!("target-object={target_object}"),1480- "!".to_owned(),1481- "queue".to_owned(),1482- "max-size-buffers=1".to_owned(),1483- "max-size-bytes=0".to_owned(),1484- "max-size-time=0".to_owned(),1485- "leaky=downstream".to_owned(),1486- "!".to_owned(),1487- "videorate".to_owned(),1488- // Gamescope is damage-driven. Never manufacture replacement frames:1489- // doing so can turn an irregular live source into stale catch-up work.1490- "drop-only=true".to_owned(),1491- format!("max-rate={}", video_mode.framerate),1492- "!".to_owned(),1493- "videoconvert".to_owned(),1494- "!".to_owned(),1495- "videoscale".to_owned(),1496- "!".to_owned(),1497- caps,1498- "!".to_owned(),1499- pipewire.vaapi_encoder.clone(),1500- ];1501- args.push(format!(1502- "rate-control={}",1503- match pipewire.rate_control {1504- VaapiRateControl::Cbr => "cbr",1505- VaapiRateControl::Cqp => "cqp",1506- }1507- ));1508- match pipewire.rate_control {1509- VaapiRateControl::Cbr => args.push(format!(1510- "bitrate={}",1511- pipewire1512- .bitrate_kbps1513- .context("CBR bitrate is missing after validation")?1514- )),1515- VaapiRateControl::Cqp => {1516- let quantizer = pipewire1517- .quantizer1518- .context("CQP quantizer is missing after validation")?;1519- args.push(format!("qpi={quantizer}"));1520- args.push(format!("qpp={quantizer}"));1521- }1522- }1523- args.extend([1524- "target-usage=7".to_owned(),1525- format!(1526- "key-int-max={}",1527- interactive_gop_frames(video_mode.framerate)1528- ),1529- "b-frames=0".to_owned(),1530- "ref-frames=1".to_owned(),1531- "aud=true".to_owned(),1532- "!".to_owned(),1533- "h264parse".to_owned(),1534- "config-interval=-1".to_owned(),1535- "!".to_owned(),1536- encoded_caps.to_owned(),1537- "!".to_owned(),1538- "fdsink".to_owned(),1539- "fd=1".to_owned(),1540- // Gamescope's PipeWire timestamps can advance in larger jumps than1541- // the negotiated frame duration. Clocking this terminal sink makes it1542- // wait on those producer timestamps while the one-frame leaky queue1543- // drops fresh motion. Videorate already caps the live stream at the1544- // configured maximum, so drain encoded access units immediately.1545- "sync=false".to_owned(),1546- ]);1547- Ok(args.into_iter().map(OsString::from).collect())1548-}1549-15501318 fn interactive_gop_frames(framerate: u32) -> u32 {15511319 // A dropped encoded reference requires the client to wait for the next15521320 // independently decodable IDR. Bound that recovery to roughly 500 ms until@@ -1877,7 +1645,7 @@ mod tests {
18771645 }18781646 }18791647 1880- fn configured_video_mode() -> CaptureVideoMode {1648+ pub(super) fn configured_video_mode() -> CaptureVideoMode {18811649 CaptureVideoMode {18821650 width: 1_280,18831651 height: 800,@@ -2351,80 +2119,6 @@ mod tests {
23512119 assert!(current_gop_receiver.borrow().is_none());23522120 }23532121 2354- 2355- fn builds_bounded_low_latency_gamescope_pipeline() {2356- let config = gamescope_config();2357- let args: Vec<String> = gamescope_pipeline_args(&config, "1234", configured_video_mode())2358- .unwrap()2359- .into_iter()2360- .map(|arg| arg.into_string().unwrap())2361- .collect();2362-2363- assert_eq!(args[0], "--quiet");2364- assert!(2365- args.windows(2)2366- .any(|args| { args == ["pipewiresrc".to_owned(), "do-timestamp=true".to_owned()] })2367- );2368- assert!(args.contains(&"target-object=1234".to_owned()));2369- assert!(args.contains(&"min-buffers=1".to_owned()));2370- assert!(args.contains(&"max-buffers=4".to_owned()));2371- assert!(args.contains(&"use-bufferpool=false".to_owned()));2372- assert!(args.contains(&"max-size-buffers=1".to_owned()));2373- assert!(args.contains(&"leaky=downstream".to_owned()));2374- assert!(2375- args.windows(2)2376- .any(|args| { args == ["videorate".to_owned(), "drop-only=true".to_owned()] })2377- );2378- assert!(2379- args.contains(2380- &"video/x-raw,format=NV12,width=1280,height=800,framerate=60/1".to_owned()2381- )2382- );2383- assert!(args.contains(&"vah264enc".to_owned()));2384- assert!(args.contains(&"bitrate=12000".to_owned()));2385- assert!(args.contains(&"rate-control=cbr".to_owned()));2386- assert!(args.contains(&"target-usage=7".to_owned()));2387- assert!(args.contains(&"key-int-max=30".to_owned()));2388- assert!(args.contains(&"b-frames=0".to_owned()));2389- assert!(args.contains(&"aud=true".to_owned()));2390- assert!(args.contains(&"config-interval=-1".to_owned()));2391- assert!(args.contains(&"video/x-h264,stream-format=byte-stream,alignment=au".to_owned()));2392- assert!(args.ends_with(&[2393- "fdsink".to_owned(),2394- "fd=1".to_owned(),2395- "sync=false".to_owned()2396- ]));2397- let videorate = args.iter().position(|arg| arg == "videorate").unwrap();2398- let queue = args.iter().position(|arg| arg == "queue").unwrap();2399- let videoconvert = args.iter().position(|arg| arg == "videoconvert").unwrap();2400- let videoscale = args.iter().position(|arg| arg == "videoscale").unwrap();2401- assert!(queue < videorate && videorate < videoconvert && videoconvert < videoscale);2402- assert!(gamescope_pipeline_args(&config, "gamescope", configured_video_mode()).is_err());2403- }2404-2405- 2406- fn builds_pipeline_for_resolved_non_fixture_mode() {2407- let config = gamescope_config();2408- let mode = CaptureVideoMode {2409- width: 1_920,2410- height: 1_080,2411- framerate: 72,2412- };2413- let args: Vec<String> = gamescope_pipeline_args(&config, "1234", mode)2414- .unwrap()2415- .into_iter()2416- .map(|arg| arg.into_string().unwrap())2417- .collect();2418-2419- assert!(2420- args.contains(2421- &"video/x-raw,format=NV12,width=1920,height=1080,framerate=72/1".to_owned()2422- )2423- );2424- assert!(args.contains(&"max-rate=72".to_owned()));2425- assert!(args.contains(&"key-int-max=36".to_owned()));2426- }2427-24282122 24292123 fn recovery_gop_is_bounded_to_half_a_second() {24302124 assert_eq!(interactive_gop_frames(60), 30);@@ -2432,28 +2126,6 @@ mod tests {
24322126 assert_eq!(interactive_gop_frames(1), 1);24332127 }24342128 2435- 2436- fn builds_explicit_low_power_cqp_pipeline() {2437- let mut config = gamescope_config();2438- let pipewire = config.gamescope_pipewire.as_mut().unwrap();2439- pipewire.vaapi_encoder = "vah264lpenc".into();2440- pipewire.rate_control = VaapiRateControl::Cqp;2441- pipewire.bitrate_kbps = None;2442- pipewire.quantizer = Some(24);2443- config.validate().unwrap();2444-2445- let args: Vec<String> = gamescope_pipeline_args(&config, "1234", configured_video_mode())2446- .unwrap()2447- .into_iter()2448- .map(|arg| arg.into_string().unwrap())2449- .collect();2450- assert!(args.contains(&"vah264lpenc".to_owned()));2451- assert!(args.contains(&"rate-control=cqp".to_owned()));2452- assert!(args.contains(&"qpi=24".to_owned()));2453- assert!(args.contains(&"qpp=24".to_owned()));2454- assert!(!args.iter().any(|arg| arg.starts_with("bitrate=")));2455- }2456-24572129 24582130 fn resolves_exactly_one_pipewire_node_by_properties() {24592131 let config = gamescope_config();@@ -0,0 +1,355 @@
1+use std::ffi::OsString;2+use std::process::Stdio;3+4+use anyhow::{Context, Result, bail, ensure};5+use tokio::io::{AsyncRead, AsyncReadExt};6+use tokio::process::Command;7+use tokio::sync::watch;8+use tracing::debug;9+10+use crate::clock::SessionClock;11+use crate::config::{HostConfig, VaapiRateControl};12+13+use super::annexb::AnnexBAccessUnitParser;14+use super::{15+ AccessUnitPublisher, CaptureVideoMode, EncodedFrame, EncodedGop, EncodedSource,16+ GamescopePreflight, gamescope_config, interactive_gop_frames, log_stderr_chunks,17+};18+19+pub(super) fn spawn_gamescope_pipewire_with_target(20+ config: HostConfig,21+ session_clock: SessionClock,22+ preflight: GamescopePreflight,23+) -> Result<EncodedSource> {24+ let (sender, receiver) = watch::channel(None);25+ let (current_gop_sender, current_gop) = watch::channel(None);26+ let target_object = preflight.target_object;27+ let video_mode = preflight.video_mode;28+ let task = tokio::spawn(async move {29+ run_gamescope_pipewire(30+ config,31+ session_clock,32+ target_object,33+ video_mode,34+ sender,35+ current_gop_sender,36+ )37+ .await38+ });39+ Ok(EncodedSource {40+ frames: receiver,41+ current_gop,42+ task,43+ pointer_surface_dimensions: Some(preflight.pointer_surface_dimensions),44+ encoder_control: None,45+ })46+}47+48+async fn run_gamescope_pipewire(49+ config: HostConfig,50+ session_clock: SessionClock,51+ target_object: String,52+ video_mode: CaptureVideoMode,53+ sender: watch::Sender<Option<EncodedFrame>>,54+ current_gop_sender: watch::Sender<Option<EncodedGop>>,55+) -> Result<()> {56+ let pipewire = gamescope_config(&config)?;57+ let args = gamescope_pipeline_args(&config, &target_object, video_mode)?;58+ let mut command = Command::new(&pipewire.gst_launch_path);59+ command60+ .args(args)61+ .env("LC_ALL", "C")62+ // This is honored by the legacy VAAPI plugin and harmless for GstVA.63+ // GstVA selection is independently checked against device-path above.64+ .env("GST_VAAPI_DRM_DEVICE", &pipewire.vaapi_render_node)65+ .stdin(Stdio::null())66+ .stdout(Stdio::piped())67+ .stderr(Stdio::piped())68+ .kill_on_drop(true);69+70+ let mut child = command.spawn().with_context(|| {71+ format!(72+ "starting Gamescope pipeline with {}",73+ pipewire.gst_launch_path.display()74+ )75+ })?;76+ let mut stdout = child77+ .stdout78+ .take()79+ .context("GStreamer stdout was not piped")?;80+ let stderr = child81+ .stderr82+ .take()83+ .context("GStreamer stderr was not piped")?;84+ let stderr_task = tokio::spawn(log_stderr_chunks(stderr, "gstreamer"));85+86+ let result = forward_annex_b_stream(87+ session_clock,88+ &mut stdout,89+ &sender,90+ ¤t_gop_sender,91+ interactive_gop_frames(video_mode.framerate) as usize,92+ video_mode.width,93+ video_mode.height,94+ )95+ .await;96+ let _ = child.kill().await;97+ let exit_status = child.wait().await.ok();98+ stderr_task.abort();99+100+ match result {101+ Ok(()) => Ok(()),102+ Err(error) => {103+ if let Some(status) = exit_status {104+ Err(error).with_context(|| format!("GStreamer pipeline exited with {status}"))105+ } else {106+ Err(error)107+ }108+ }109+ }110+}111+112+pub(super) async fn forward_annex_b_stream<R>(113+ session_clock: SessionClock,114+ stdout: &mut R,115+ sender: &watch::Sender<Option<EncodedFrame>>,116+ current_gop_sender: &watch::Sender<Option<EncodedGop>>,117+ max_gop_frames: usize,118+ width: u16,119+ height: u16,120+) -> Result<()>121+where122+ R: AsyncRead + Unpin,123+{124+ let mut publisher = AccessUnitPublisher::new(125+ session_clock,126+ sender,127+ current_gop_sender,128+ max_gop_frames,129+ width,130+ height,131+ );132+ let mut parser = AnnexBAccessUnitParser::default();133+ let mut chunk = [0_u8; 16 * 1024];134+135+ loop {136+ let count = stdout137+ .read(&mut chunk)138+ .await139+ .context("reading encoded source output")?;140+ if count == 0 {141+ bail!("encoded source ended unexpectedly");142+ }143+144+ for access_unit in parser.push(&chunk[..count])? {145+ publisher.publish(access_unit)?;146+ if publisher.receivers_closed() {147+ debug!("encoded source has no receivers");148+ return Ok(());149+ }150+ }151+ }152+}153+154+pub(super) fn gamescope_pipeline_args(155+ config: &HostConfig,156+ target_object: &str,157+ video_mode: CaptureVideoMode,158+) -> Result<Vec<OsString>> {159+ let pipewire = gamescope_config(config)?;160+ ensure!(161+ !target_object.is_empty()162+ && target_object.len() <= 32163+ && target_object.bytes().all(|byte| byte.is_ascii_digit()),164+ "resolved PipeWire object.serial is invalid"165+ );166+ let caps = format!(167+ "video/x-raw,format=NV12,width={},height={},framerate={}/1",168+ video_mode.width, video_mode.height, video_mode.framerate169+ );170+ let encoded_caps = "video/x-h264,stream-format=byte-stream,alignment=au";171+ let mut args = vec![172+ "--quiet".to_owned(),173+ "pipewiresrc".to_owned(),174+ "do-timestamp=true".to_owned(),175+ // Keep Gamescope's negotiated producer pool bounded at its preferred176+ // four buffers, but copy each delivered frame into GStreamer's own177+ // buffer before downstream conversion/encoding can retain it. This178+ // returns producer buffers promptly instead of starving Gamescope at179+ // vblank. `use-bufferpool=false` is the supported replacement for the180+ // deprecated `always-copy=true` spelling.181+ "min-buffers=1".to_owned(),182+ "max-buffers=4".to_owned(),183+ "use-bufferpool=false".to_owned(),184+ format!("target-object={target_object}"),185+ "!".to_owned(),186+ "queue".to_owned(),187+ "max-size-buffers=1".to_owned(),188+ "max-size-bytes=0".to_owned(),189+ "max-size-time=0".to_owned(),190+ "leaky=downstream".to_owned(),191+ "!".to_owned(),192+ "videorate".to_owned(),193+ // Gamescope is damage-driven. Never manufacture replacement frames:194+ // doing so can turn an irregular live source into stale catch-up work.195+ "drop-only=true".to_owned(),196+ format!("max-rate={}", video_mode.framerate),197+ "!".to_owned(),198+ "videoconvert".to_owned(),199+ "!".to_owned(),200+ "videoscale".to_owned(),201+ "!".to_owned(),202+ caps,203+ "!".to_owned(),204+ pipewire.vaapi_encoder.clone(),205+ ];206+ args.push(format!(207+ "rate-control={}",208+ match pipewire.rate_control {209+ VaapiRateControl::Cbr => "cbr",210+ VaapiRateControl::Cqp => "cqp",211+ }212+ ));213+ match pipewire.rate_control {214+ VaapiRateControl::Cbr => args.push(format!(215+ "bitrate={}",216+ pipewire217+ .bitrate_kbps218+ .context("CBR bitrate is missing after validation")?219+ )),220+ VaapiRateControl::Cqp => {221+ let quantizer = pipewire222+ .quantizer223+ .context("CQP quantizer is missing after validation")?;224+ args.push(format!("qpi={quantizer}"));225+ args.push(format!("qpp={quantizer}"));226+ }227+ }228+ args.extend([229+ "target-usage=7".to_owned(),230+ format!(231+ "key-int-max={}",232+ interactive_gop_frames(video_mode.framerate)233+ ),234+ "b-frames=0".to_owned(),235+ "ref-frames=1".to_owned(),236+ "aud=true".to_owned(),237+ "!".to_owned(),238+ "h264parse".to_owned(),239+ "config-interval=-1".to_owned(),240+ "!".to_owned(),241+ encoded_caps.to_owned(),242+ "!".to_owned(),243+ "fdsink".to_owned(),244+ "fd=1".to_owned(),245+ // Gamescope's PipeWire timestamps can advance in larger jumps than246+ // the negotiated frame duration. Clocking this terminal sink makes it247+ // wait on those producer timestamps while the one-frame leaky queue248+ // drops fresh motion. Videorate already caps the live stream at the249+ // configured maximum, so drain encoded access units immediately.250+ "sync=false".to_owned(),251+ ]);252+ Ok(args.into_iter().map(OsString::from).collect())253+}254+255+256+mod tests {257+ use super::super::tests::{configured_video_mode, gamescope_config};258+ use super::*;259+260+ 261+ fn builds_bounded_low_latency_gamescope_pipeline() {262+ let config = gamescope_config();263+ let args: Vec<String> = gamescope_pipeline_args(&config, "1234", configured_video_mode())264+ .unwrap()265+ .into_iter()266+ .map(|arg| arg.into_string().unwrap())267+ .collect();268+269+ assert_eq!(args[0], "--quiet");270+ assert!(271+ args.windows(2)272+ .any(|args| { args == ["pipewiresrc".to_owned(), "do-timestamp=true".to_owned()] })273+ );274+ assert!(args.contains(&"target-object=1234".to_owned()));275+ assert!(args.contains(&"min-buffers=1".to_owned()));276+ assert!(args.contains(&"max-buffers=4".to_owned()));277+ assert!(args.contains(&"use-bufferpool=false".to_owned()));278+ assert!(args.contains(&"max-size-buffers=1".to_owned()));279+ assert!(args.contains(&"leaky=downstream".to_owned()));280+ assert!(281+ args.windows(2)282+ .any(|args| { args == ["videorate".to_owned(), "drop-only=true".to_owned()] })283+ );284+ assert!(285+ args.contains(286+ &"video/x-raw,format=NV12,width=1280,height=800,framerate=60/1".to_owned()287+ )288+ );289+ assert!(args.contains(&"vah264enc".to_owned()));290+ assert!(args.contains(&"bitrate=12000".to_owned()));291+ assert!(args.contains(&"rate-control=cbr".to_owned()));292+ assert!(args.contains(&"target-usage=7".to_owned()));293+ assert!(args.contains(&"key-int-max=30".to_owned()));294+ assert!(args.contains(&"b-frames=0".to_owned()));295+ assert!(args.contains(&"aud=true".to_owned()));296+ assert!(args.contains(&"config-interval=-1".to_owned()));297+ assert!(args.contains(&"video/x-h264,stream-format=byte-stream,alignment=au".to_owned()));298+ assert!(args.ends_with(&[299+ "fdsink".to_owned(),300+ "fd=1".to_owned(),301+ "sync=false".to_owned()302+ ]));303+ let videorate = args.iter().position(|arg| arg == "videorate").unwrap();304+ let queue = args.iter().position(|arg| arg == "queue").unwrap();305+ let videoconvert = args.iter().position(|arg| arg == "videoconvert").unwrap();306+ let videoscale = args.iter().position(|arg| arg == "videoscale").unwrap();307+ assert!(queue < videorate && videorate < videoconvert && videoconvert < videoscale);308+ assert!(gamescope_pipeline_args(&config, "gamescope", configured_video_mode()).is_err());309+ }310+311+ 312+ fn builds_pipeline_for_resolved_non_fixture_mode() {313+ let config = gamescope_config();314+ let mode = CaptureVideoMode {315+ width: 1_920,316+ height: 1_080,317+ framerate: 72,318+ };319+ let args: Vec<String> = gamescope_pipeline_args(&config, "1234", mode)320+ .unwrap()321+ .into_iter()322+ .map(|arg| arg.into_string().unwrap())323+ .collect();324+325+ assert!(326+ args.contains(327+ &"video/x-raw,format=NV12,width=1920,height=1080,framerate=72/1".to_owned()328+ )329+ );330+ assert!(args.contains(&"max-rate=72".to_owned()));331+ assert!(args.contains(&"key-int-max=36".to_owned()));332+ }333+334+ 335+ fn builds_explicit_low_power_cqp_pipeline() {336+ let mut config = gamescope_config();337+ let pipewire = config.gamescope_pipewire.as_mut().unwrap();338+ pipewire.vaapi_encoder = "vah264lpenc".into();339+ pipewire.rate_control = VaapiRateControl::Cqp;340+ pipewire.bitrate_kbps = None;341+ pipewire.quantizer = Some(24);342+ config.validate().unwrap();343+344+ let args: Vec<String> = gamescope_pipeline_args(&config, "1234", configured_video_mode())345+ .unwrap()346+ .into_iter()347+ .map(|arg| arg.into_string().unwrap())348+ .collect();349+ assert!(args.contains(&"vah264lpenc".to_owned()));350+ assert!(args.contains(&"rate-control=cqp".to_owned()));351+ assert!(args.contains(&"qpi=24".to_owned()));352+ assert!(args.contains(&"qpp=24".to_owned()));353+ assert!(!args.iter().any(|arg| arg.starts_with("bitrate=")));354+ }355+}