Keep unconfigured host dimensions immutable across Gamescope generations and bind adaptive resolution policy to the active encoder control dimensions. Add regression coverage for sequential native modes and unsupported adaptive tiers.
- commit
- 66f19fc0d689306f10b2b8f7812132659035f617
- parent
- e2328beae680993a38c0f8a63c413114b77e831b
- author
- Dami <dami.etoile@gmail.com>
| M | crates/ | 12 | ---- |
| M | crates/ | 9 | +-- |
| M | crates/ | 37 | +++++++++++- |
| M | crates/ | 93 | ++++++++++++++++++++++++++++-- |
@@ -492,11 +492,6 @@ impl HostConfig {
492492 self.width.zip(self.height)493493 }494494 495- pub fn resolved_dimensions(&self) -> Result<(u32, u32)> {496- self.configured_dimensions()497- .context("video dimensions have not been resolved")498- }499-500495 pub fn test_pattern_dimensions(&self) -> Result<(u32, u32)> {501496 ensure!(502497 self.source == VideoSource::TestPattern,@@ -507,13 +502,6 @@ impl HostConfig {
507502 .unwrap_or((Self::TEST_PATTERN_WIDTH, Self::TEST_PATTERN_HEIGHT)))508503 }509504 510- pub fn apply_resolved_dimensions(&mut self, width: u32, height: u32) -> Result<()> {511- validate_video_dimensions(width, height)?;512- self.width = Some(width);513- self.height = Some(height);514- Ok(())515- }516-517505 pub fn ensure_runtime_directory(&self) -> Result<()> {518506 if self.state_path.exists() {519507 let metadata = fs::symlink_metadata(&self.state_path)@@ -758,7 +758,7 @@ impl Drop for CaptureTaskGuard {
758758 759759 async fn serve_command(args: ServeArgs) -> Result<()> {760760 let configured_service = args.config.is_some();761- let (mut config, loaded_config_revision, _lifecycle) = if let Some(config_path) = &args.config {761+ let (config, loaded_config_revision, _lifecycle) = if let Some(config_path) = &args.config {762762 let (loaded, lifecycle) =763763 config_management::prepare_service(config_path).map_err(anyhow::Error::new)?;764764 (loaded.config, Some(loaded.revision), lifecycle)@@ -797,8 +797,7 @@ async fn serve_command(args: ServeArgs) -> Result<()> {
797797 798798 let preflight_result: Result<(InputBackend, Option<PointerPositionTracker>)> = async {799799 let pointer_surface_dimensions = if config.source == VideoSource::TestPattern {800- let (width, height) = config.test_pattern_dimensions()?;801- config.apply_resolved_dimensions(width, height)?;800+ config.test_pattern_dimensions()?;802801 let status = tokio::process::Command::new(&config.ffmpeg_path)803802 .arg("-version")804803 .stdin(std::process::Stdio::null())@@ -823,10 +822,6 @@ async fn serve_command(args: ServeArgs) -> Result<()> {
823822 encoded_framerate = preflight.video_mode.framerate,824823 "Gamescope PipeWire capture preflight passed"825824 );826- config.apply_resolved_dimensions(827- u32::from(preflight.video_mode.width),828- u32::from(preflight.video_mode.height),829- )?;830825 if config.audio.is_some() {831826 audio::preflight_audio_static(&config).await?;832827 info!("PipeWire Opus audio static preflight passed");@@ -413,6 +413,13 @@ fn motion_resolution_policy(width: u32, height: u32) -> Result<Option<MotionReso
413413 }414414 }415415 416+fn motion_resolution_policy_for_encoder(417+ control: &EncoderControl,418+) -> Result<Option<MotionResolutionPolicy>> {419+ let (width, height) = control.initial_dimensions();420+ motion_resolution_policy(u32::from(width), u32::from(height))421+}422+416423 417424 struct FeedbackIngressCursor {418425 last_report_id: Option<u64>,@@ -3235,8 +3242,12 @@ async fn serve_media_feedback(
32353242 let mut receiver_open = true;32363243 let mut controller = ShadowBitrateController::new(ceiling_kbps, lease.telemetry.snapshot());32373244 let mut resolution_controller = if encoder_actuation_available {3238- let (width, height) = config.resolved_dimensions()?;3239- motion_resolution_policy(width, height)?3245+ lease3246+ .encoder_control3247+ .as_ref()3248+ .map(motion_resolution_policy_for_encoder)3249+ .transpose()?3250+ .flatten()32403251 } else {32413252 None32423253 };@@ -5197,6 +5208,28 @@ mod tests {
51975208 assert!(motion_resolution_policy(1_366, 768).unwrap().is_none());51985209 }51995210 5211+ 5212+ fn feedback_resolution_policy_uses_encoder_generation_dimensions() {5213+ let full_hd = crate::source::EncoderControlTestHarness::with_dimensions(1_920, 1_080);5214+ let policy = motion_resolution_policy_for_encoder(&full_hd.control)5215+ .unwrap()5216+ .unwrap();5217+ assert_eq!(5218+ policy.native,5219+ VideoDimensions {5220+ width: 1_920,5221+ height: 1_080,5222+ }5223+ );5224+5225+ let no_exact_tier = crate::source::EncoderControlTestHarness::with_dimensions(1_366, 768);5226+ assert!(5227+ motion_resolution_policy_for_encoder(&no_exact_tier.control)5228+ .unwrap()5229+ .is_none()5230+ );5231+ }5232+52005233 52015234 fn motion_downshifts_immediately_and_recovers_only_after_settle_and_clean_windows() {52025235 let now = Instant::now();@@ -76,6 +76,8 @@ pub struct EncoderControlStatus {
7676 /// the latest revision. Revisions are process-monotonic and never wrap.7777 7878 pub struct EncoderControl {79+ initial_width: u16,80+ initial_height: u16,7981 next_revision: Arc<AtomicU64>,8082 desired: watch::Sender<EncoderControlDesired>,8183 status: watch::Receiver<EncoderControlStatus>,@@ -86,7 +88,10 @@ pub struct EncoderControl {
8688 expect(dead_code, reason = "constructed only by the opt-in encoder backend")8789 )]8890 impl EncoderControl {89- fn new() -> (91+ fn new(92+ initial_width: u16,93+ initial_height: u16,94+ ) -> (9095 Self,9196 watch::Receiver<EncoderControlDesired>,9297 watch::Sender<EncoderControlStatus>,@@ -95,6 +100,8 @@ impl EncoderControl {
95100 let (status_tx, status) = watch::channel(EncoderControlStatus::default());96101 (97102 Self {103+ initial_width,104+ initial_height,98105 next_revision: Arc::new(AtomicU64::new(0)),99106 desired,100107 status,@@ -104,6 +111,10 @@ impl EncoderControl {
104111 )105112 }106113 114+ pub(crate) fn initial_dimensions(&self) -> (u16, u16) {115+ (self.initial_width, self.initial_height)116+ }117+107118 fn next_revision(&self) -> Result<u64> {108119 self.next_revision109120 .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |revision| {@@ -315,7 +326,11 @@ pub(crate) struct EncoderControlTestHarness {
315326 316327 impl EncoderControlTestHarness {317328 pub(crate) fn new() -> Self {318- let (control, desired, status) = EncoderControl::new();329+ Self::with_dimensions(1_280, 800)330+ }331+332+ pub(crate) fn with_dimensions(width: u16, height: u16) -> Self {333+ let (control, desired, status) = EncoderControl::new(width, height);319334 Self {320335 control,321336 status,@@ -608,7 +623,7 @@ fn spawn_in_process_pipeline(
608623 ) -> Result<EncodedSource> {609624 let (sender, receiver) = watch::channel(None);610625 let (current_gop_sender, current_gop) = watch::channel(None);611- let (control, desired, status) = EncoderControl::new();626+ let (control, desired, status) = EncoderControl::new(initial_width, initial_height);612627 let task = tokio::spawn(async move {613628 tokio::task::spawn_blocking(move || {614629 run_in_process_pipeline(@@ -2130,9 +2145,44 @@ mod tests {
21302145 }21312146 }21322147 2148+ fn gamescope_dump(serial: u64, width: u16, height: u16) -> Vec<u8> {2149+ serde_json::to_vec(&serde_json::json!([{2150+ "type": "PipeWire:Interface:Node",2151+ "info": {2152+ "props": {2153+ "node.name": "gamescope",2154+ "media.class": "Video/Source",2155+ "device.bus-path": "pci-0000:04:00.0",2156+ "object.serial": serial,2157+ },2158+ "params": {2159+ "EnumFormat": [{2160+ "mediaType": "video",2161+ "mediaSubtype": "raw",2162+ "format": "NV12",2163+ "size": { "width": width, "height": height },2164+ }],2165+ },2166+ },2167+ }]))2168+ .unwrap()2169+ }2170+2171+ 2172+ fn encoder_control_retains_initial_session_dimensions() {2173+ let (native, _desired, _status) = EncoderControl::new(2_560, 1_600);2174+ let (other, _desired, _status) = EncoderControl::new(1_366, 768);2175+2176+ assert_eq!(native.initial_dimensions(), (2_560, 1_600));2177+ assert_eq!(other.initial_dimensions(), (1_366, 768));2178+ native.request_resolution(1_280, 800).unwrap();2179+ assert_eq!(native.initial_dimensions(), (2_560, 1_600));2180+ assert_eq!(other.initial_dimensions(), (1_366, 768));2181+ }2182+21332183 21342184 async fn encoder_control_coalesces_latest_state_and_acknowledges_only_configured_idr() {2135- let (control, mut desired, status) = EncoderControl::new();2185+ let (control, mut desired, status) = EncoderControl::new(1_280, 800);21362186 21372187 let superseded_bitrate_revision = control.request_bitrate_kbps(4_000).unwrap();21382188 let bitrate_revision = control.request_bitrate_kbps(8_000).unwrap();@@ -2227,7 +2277,7 @@ mod tests {
22272277 .is_err()22282278 );22292279 2230- let (closed_control, _desired, closed_status) = EncoderControl::new();2280+ let (closed_control, _desired, closed_status) = EncoderControl::new(1_280, 800);22312281 let closed_revision = closed_control.request_force_keyframe().unwrap();22322282 drop(closed_status);22332283 assert!(@@ -2242,7 +2292,7 @@ mod tests {
22422292 22432293 22442294 async fn recovery_waiter_accepts_a_newer_configured_idr_revision() {2245- let (control, _desired, status) = EncoderControl::new();2295+ let (control, _desired, status) = EncoderControl::new(1_280, 800);22462296 let recovery_revision = control.request_force_keyframe().unwrap();22472297 let newer_revision = control.request_force_keyframe().unwrap();22482298 assert!(newer_revision > recovery_revision);@@ -2756,6 +2806,37 @@ mod tests {
27562806 );27572807 }27582808 2809+ 2810+ fn unconfigured_gamescope_mode_follows_each_resolved_session() {2811+ let mut config = gamescope_config();2812+ config.width = None;2813+ config.height = None;2814+2815+ for (serial, width, height) in [2816+ (731, 2_560, 1_600),2817+ (900, 1_280, 800),2818+ (901, 1_920, 1_080),2819+ (902, 1_366, 768),2820+ ] {2821+ let resolved =2822+ resolve_pipewire_node(&gamescope_dump(serial, width, height), &config).unwrap();2823+ assert_eq!(resolved.target_object, serial.to_string());2824+ assert_eq!(2825+ resolved.pointer_surface_dimensions,2826+ PointerSurfaceDimensions::new(width, height).unwrap()2827+ );2828+ assert_eq!(2829+ resolved.video_mode,2830+ CaptureVideoMode {2831+ width,2832+ height,2833+ framerate: 60,2834+ }2835+ );2836+ assert_eq!(config.configured_dimensions(), None);2837+ }2838+ }2839+27592840 27602841 fn resolves_pinned_upstream_gamescope_pipewire_contract() {27612842 // Sanitized from the standard interface emitted by Valve Gamescope