From f6a7f3c12dbd77b87ac4c497dd920bdac30db991 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Fri, 12 Jun 2026 13:45:29 +0000 Subject: [PATCH] feat(vdisplay/mutter): pin the virtual output to the client's refresh (>60 Hz) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RecordVirtual without a "modes" property makes Mutter derive the virtual monitor's refresh from the PipeWire stream framerate and default to 60 Hz — so a 240 Hz client mode rendered at 60 (the encoder just padded to 240 with duplicate frames). Pass an explicit "modes" entry (size + refresh-rate + is-preferred) so Mutter creates the virtual monitor at the client's exact WxH@Hz. Mutter >= 47; older Mutter ignores the unknown key (60 Hz fallback, no regression). Confirmed first via raw D-Bus on the box, then validated end-to-end: the virtual output Meta-0 reports 1920x1080@240.00 and the host encodes 480 *immediate* (real, not paced) frames per 2 s. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/punktfunk-host/src/vdisplay/mutter.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/crates/punktfunk-host/src/vdisplay/mutter.rs b/crates/punktfunk-host/src/vdisplay/mutter.rs index 68124dc..0b58a3b 100644 --- a/crates/punktfunk-host/src/vdisplay/mutter.rs +++ b/crates/punktfunk-host/src/vdisplay/mutter.rs @@ -138,7 +138,7 @@ fn session_thread(setup_tx: Sender>, stop: Arc, None }; - let session = match connect().await { + let session = match connect(mode).await { Ok(s) => s, Err(e) => { let _ = setup_tx.send(Err(format!("{e:#}"))); @@ -190,7 +190,7 @@ struct MutterSession { } /// Run the four-step handshake (see module docs). -async fn connect() -> Result { +async fn connect(mode: Mode) -> Result { let conn = zbus::Connection::session() .await .context("connect session D-Bus")?; @@ -243,9 +243,18 @@ async fn connect() -> Result { ) .await?; - // 3. The virtual monitor. Size/refresh follow the PipeWire format negotiation. + // 3. The virtual monitor, pinned to the client's exact mode via RecordVirtual's "modes" + // (explicit size + refresh-rate; Mutter ≥ 47). WITHOUT it Mutter derives the virtual monitor's + // refresh from the PipeWire stream framerate and defaults to **60 Hz** — so a >60 Hz client + // mode (e.g. 240) renders at 60 and only the encoder pads to 240 (duplicate frames). Older + // Mutter that doesn't know the key just ignores it and falls back to the 60 Hz default. + let mut vmode: HashMap<&str, Value> = HashMap::new(); + vmode.insert("size", Value::from((mode.width, mode.height))); + vmode.insert("refresh-rate", Value::from(mode.refresh_hz as f64)); + vmode.insert("is-preferred", Value::from(true)); let mut rec: HashMap<&str, Value> = HashMap::new(); rec.insert("cursor-mode", Value::from(CURSOR_EMBEDDED)); + rec.insert("modes", Value::from(vec![vmode])); let stream_path: OwnedObjectPath = sc_session .call("RecordVirtual", &(rec,)) .await