Prevent producer timestamp jumps from throttling fdsink, and treat startup cadence as diagnostic for the damage-driven source while still requiring a live decodable GOP.
- commit
- 95491259db85e512f703de9ac5012fbf7a538da7
- parent
- c69d7a85368847d84d4391c419bd4a6b461d40d5
- author
- Dami <dami.etoile@gmail.com>
| M | crates/ | 71 | +++++++++++++++++++++--------- |
| M | crates/ | 12 | +++-- |
@@ -42,8 +42,8 @@ const MAX_PENDING_HANDSHAKES: usize = 4;
4242 const MAX_MEDIA_REPLAY_AGE_FRAME_PERIODS: u64 = 2;4343 const GAMESCOPE_STARTUP_SAMPLE_WINDOW: Duration = Duration::from_millis(500);4444 const GAMESCOPE_STARTUP_MIN_OBSERVATION_SPAN: Duration = Duration::from_millis(100);45-const GAMESCOPE_STARTUP_HEALTHY_MIN_FRAMES: u64 = 8;46-const GAMESCOPE_STARTUP_HEALTHY_MIN_FPS: f64 = 45.0;45+const GAMESCOPE_STARTUP_TARGET_MIN_FRAMES: u64 = 8;46+const GAMESCOPE_STARTUP_TARGET_MIN_FPS: f64 = 45.0;4747 const SOURCE_REAP_GRACE_TIMEOUT: Duration = Duration::from_secs(1);4848 4949 @@ -270,6 +270,7 @@ struct StartupCadenceSample {
270270 first_observed_at: Option<Instant>,271271 last_observed_at: Option<Instant>,272272 receiver_open: bool,273+ decodable_gop_ready: bool,273274 }274275 275276 impl StartupCadenceSample {@@ -314,20 +315,25 @@ impl StartupCadenceSample {
314315 self.observation_span() >= GAMESCOPE_STARTUP_MIN_OBSERVATION_SPAN315316 }316317 317- fn is_healthy(self) -> bool {318+ fn is_usable(self) -> bool {319+ self.receiver_open && self.frame_progress() > 0 && self.decodable_gop_ready320+ }321+322+ fn meets_target_cadence(self) -> bool {318323 self.receiver_open319324 && self.has_representative_span()320- && self.frame_progress() >= GAMESCOPE_STARTUP_HEALTHY_MIN_FRAMES321- && self.fps() >= GAMESCOPE_STARTUP_HEALTHY_MIN_FPS325+ && self.frame_progress() >= GAMESCOPE_STARTUP_TARGET_MIN_FRAMES326+ && self.fps() >= GAMESCOPE_STARTUP_TARGET_MIN_FPS322327 }323328 }324329 325330 fn startup_source_needs_restart(sample: StartupCadenceSample) -> bool {326- !sample.is_healthy()331+ !sample.is_usable()327332 }328333 329334 async fn sample_startup_cadence(330335 receiver: &mut tokio::sync::watch::Receiver<Option<EncodedFrame>>,336+ current_gop: &tokio::sync::watch::Receiver<Option<EncodedGop>>,331337 window: Duration,332338 ) -> StartupCadenceSample {333339 let mut sample = StartupCadenceSample {@@ -337,13 +343,15 @@ async fn sample_startup_cadence(
337343 if let Some(frame) = receiver.borrow_and_update().as_ref() {338344 sample.observe(frame);339345 }346+ sample.decodable_gop_ready = current_gop.borrow().is_some();340347 let deadline = tokio::time::Instant::now() + window;341- while !sample.is_healthy() {348+ while !sample.is_usable() || !sample.has_representative_span() {342349 match tokio::time::timeout_at(deadline, receiver.changed()).await {343350 Ok(Ok(())) => {344351 if let Some(frame) = receiver.borrow_and_update().as_ref() {345352 sample.observe(frame);346353 }354+ sample.decodable_gop_ready = current_gop.borrow().is_some();347355 }348356 Ok(Err(_)) => {349357 sample.receiver_open = false;@@ -380,12 +388,18 @@ async fn select_gamescope_startup_source(
380388 session_clock: SessionClock,381389 mut primary: EncodedSource,382390 ) -> Result<EncodedSource> {383- let primary_sample =384- sample_startup_cadence(&mut primary.frames, GAMESCOPE_STARTUP_SAMPLE_WINDOW).await;391+ let primary_sample = sample_startup_cadence(392+ &mut primary.frames,393+ &primary.current_gop,394+ GAMESCOPE_STARTUP_SAMPLE_WINDOW,395+ )396+ .await;385397 info!(386398 frames = primary_sample.frame_progress(),387399 fps = primary_sample.fps(),388400 receiver_open = primary_sample.receiver_open,401+ decodable_gop_ready = primary_sample.decodable_gop_ready,402+ target_cadence = primary_sample.meets_target_cadence(),389403 "sampled primary Gamescope capture startup"390404 );391405 if !startup_source_needs_restart(primary_sample) {@@ -400,22 +414,30 @@ async fn select_gamescope_startup_source(
400414 let mut replacement = spawn_gamescope_pipewire_after_static_preflight(config, session_clock)401415 .await402416 .context("restarting unhealthy Gamescope capture pipeline")?;403- let replacement_sample =404- sample_startup_cadence(&mut replacement.frames, GAMESCOPE_STARTUP_SAMPLE_WINDOW).await;417+ let replacement_sample = sample_startup_cadence(418+ &mut replacement.frames,419+ &replacement.current_gop,420+ GAMESCOPE_STARTUP_SAMPLE_WINDOW,421+ )422+ .await;405423 info!(406424 frames = replacement_sample.frame_progress(),407425 fps = replacement_sample.fps(),408426 receiver_open = replacement_sample.receiver_open,427+ decodable_gop_ready = replacement_sample.decodable_gop_ready,428+ target_cadence = replacement_sample.meets_target_cadence(),409429 "sampled sequential Gamescope capture startup replacement"410430 );411- if !replacement_sample.is_healthy() {431+ if !replacement_sample.is_usable() {412432 let frames = replacement_sample.frame_progress();413433 let fps = replacement_sample.fps();414434 let receiver_open = replacement_sample.receiver_open;435+ let decodable_gop_ready = replacement_sample.decodable_gop_ready;415436 reap_encoded_source(replacement).await;416437 bail!(417438 "replacement Gamescope capture pipeline remained unhealthy: \418- frames={frames}, fps={fps:.2}, receiver_open={receiver_open}"439+ frames={frames}, fps={fps:.2}, receiver_open={receiver_open}, \440+ decodable_gop_ready={decodable_gop_ready}"419441 );420442 }421443 Ok(replacement)@@ -1718,26 +1740,32 @@ mod tests {
17181740 first_observed_at: Some(first),17191741 last_observed_at: Some(first + span),17201742 receiver_open,1743+ decodable_gop_ready: true,17211744 }17221745 }17231746 17241747 1725- fn startup_restart_requires_sustained_healthy_progress() {1748+ fn startup_restart_requires_a_live_decodable_source_not_target_cadence() {17261749 let slow = startup_sample(12.0, 6, true);1727- let healthy = startup_sample(60.0, 8, true);1728- assert!(startup_source_needs_restart(slow));1729- assert!(!startup_source_needs_restart(healthy));1750+ let target = startup_sample(60.0, 8, true);1751+ assert!(!startup_source_needs_restart(slow));1752+ assert!(!slow.meets_target_cadence());1753+ assert!(!startup_source_needs_restart(target));1754+ assert!(target.meets_target_cadence());17301755 assert!(startup_source_needs_restart(startup_sample(60.0, 8, false)));17311756 assert!(startup_source_needs_restart(startup_sample(0.0, 0, true)));1757+ let mut undecodable = startup_sample(60.0, 8, true);1758+ undecodable.decodable_gop_ready = false;1759+ assert!(startup_source_needs_restart(undecodable));17321760 }17331761 17341762 17351763 fn startup_bursts_are_not_sustained_health() {17361764 let burst = startup_sample(10_000.0, 8, true);1737- assert!(burst.fps() > GAMESCOPE_STARTUP_HEALTHY_MIN_FPS);1765+ assert!(burst.fps() > GAMESCOPE_STARTUP_TARGET_MIN_FPS);17381766 assert!(!burst.has_representative_span());1739- assert!(!burst.is_healthy());1740- assert!(startup_source_needs_restart(burst));1767+ assert!(!burst.meets_target_cadence());1768+ assert!(!startup_source_needs_restart(burst));17411769 }17421770 17431771 @@ -1758,7 +1786,8 @@ mod tests {
17581786 payload_bytes: 3,17591787 }));17601788 1761- let sample = sample_startup_cadence(&mut frame_receiver, Duration::ZERO).await;1789+ let sample =1790+ sample_startup_cadence(&mut frame_receiver, &gop_receiver, Duration::ZERO).await;17621791 assert_eq!(sample.frame_progress(), 1);17631792 let current_gop = gop_receiver.borrow().clone().unwrap();17641793 assert_eq!(current_gop.frames.len(), 1);@@ -560,10 +560,12 @@ fn gamescope_pipeline_args(config: &HostConfig, target_object: &str) -> Result<V
560560 "!".to_owned(),561561 "fdsink".to_owned(),562562 "fd=1".to_owned(),563- // Gamescope advertises a variable PipeWire rate and can deliver on564- // every display vblank. Keep the terminal sink clocked so negotiated565- // 60 fps timestamps cannot be written to the daemon in a 144 Hz burst.566- "sync=true".to_owned(),563+ // Gamescope's PipeWire timestamps can advance in larger jumps than564+ // the negotiated frame duration. Clocking this terminal sink makes it565+ // wait on those producer timestamps while the one-frame leaky queue566+ // drops fresh motion. Videorate already caps the live stream at the567+ // configured maximum, so drain encoded access units immediately.568+ "sync=false".to_owned(),567569 ]);568570 Ok(args.into_iter().map(OsString::from).collect())569571 }@@ -1269,7 +1271,7 @@ mod tests {
12691271 assert!(args.ends_with(&[12701272 "fdsink".to_owned(),12711273 "fd=1".to_owned(),1272- "sync=true".to_owned()1274+ "sync=false".to_owned()12731275 ]));12741276 let videorate = args.iter().position(|arg| arg == "videorate").unwrap();12751277 let queue = args.iter().position(|arg| arg == "queue").unwrap();