feat(vdisplay/mutter): pin the virtual output to the client's refresh (>60 Hz)

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) <noreply@anthropic.com>
This commit is contained in:
2026-06-12 13:45:29 +00:00
parent fa407700e0
commit f6a7f3c12d
+12 -3
View File
@@ -138,7 +138,7 @@ fn session_thread(setup_tx: Sender<Result<u32, String>>, stop: Arc<AtomicBool>,
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<MutterSession> {
async fn connect(mode: Mode) -> Result<MutterSession> {
let conn = zbus::Connection::session()
.await
.context("connect session D-Bus")?;
@@ -243,9 +243,18 @@ async fn connect() -> Result<MutterSession> {
)
.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