fix(vdisplay/mutter): gate >60Hz virtual mode behind an env flag (teardown SIGSEGV)
ci / web (push) Failing after 34s
ci / rust (push) Successful in 55s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 3s
ci / docs-site (push) Failing after 41s
docker / deploy-docs (push) Successful in 17s
apple / swift (push) Successful in 1m20s

Pinning the virtual output to a high client refresh via RecordVirtual "modes" works
mid-stream, but a high-refresh virtual CRTC SIGSEGVs gnome-shell on session TEARDOWN
(observed at 5120x1440@240) — taking down the whole GNOME session, so subsequent connects
fail with RemoteDesktop ServiceUnknown.

Gate it behind PUNKTFUNK_MUTTER_VIRTUAL_REFRESH, default OFF — Mutter then derives the
virtual monitor's refresh from the PipeWire framerate (60Hz, stable). The >60Hz path stays
in-tree for investigation; re-enable once the teardown crash is understood.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-12 13:59:29 +00:00
parent ecb4e6e1d5
commit 015f2ee47b
+27 -10
View File
@@ -243,18 +243,21 @@ async fn connect(mode: Mode) -> Result<MutterSession> {
)
.await?;
// 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));
// 3. The virtual monitor. By DEFAULT we let Mutter derive the refresh from the PipeWire
// framerate (it defaults the virtual monitor to **60 Hz**) — stable. PUNKTFUNK_MUTTER_VIRTUAL_REFRESH=1
// pins the client's exact WxH@Hz via RecordVirtual's "modes" (explicit size + refresh-rate;
// Mutter ≥ 47) for true >60 Hz — BUT a high-refresh virtual CRTC has been observed to SIGSEGV
// gnome-shell on teardown (large modes, e.g. 5120×1440@240), so it is OFF by default until that
// teardown crash is resolved.
let mut rec: HashMap<&str, Value> = HashMap::new();
rec.insert("cursor-mode", Value::from(CURSOR_EMBEDDED));
rec.insert("modes", Value::from(vec![vmode]));
if virtual_refresh_enabled() && mode.refresh_hz > 60 {
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));
rec.insert("modes", Value::from(vec![vmode]));
}
let stream_path: OwnedObjectPath = sc_session
.call("RecordVirtual", &(rec,))
.await
@@ -348,6 +351,20 @@ fn virtual_primary_enabled() -> bool {
.unwrap_or(false)
}
/// Opt-in: pin the virtual output to the client's exact refresh via RecordVirtual "modes" (true
/// above-60 Hz). OFF by default — a high-refresh virtual CRTC currently SIGSEGVs gnome-shell on
/// session teardown (large modes), so don't risk the host's GNOME session until that's fixed.
fn virtual_refresh_enabled() -> bool {
std::env::var("PUNKTFUNK_MUTTER_VIRTUAL_REFRESH")
.map(|v| {
matches!(
v.trim().to_ascii_lowercase().as_str(),
"1" | "true" | "yes" | "on"
)
})
.unwrap_or(false)
}
/// A DisplayConfig proxy on its own session-bus connection (owned, so it stays alive for the
/// session — independent of the RemoteDesktop/ScreenCast connection).
async fn display_config() -> Result<zbus::Proxy<'static>> {