fix(native): a mirrored monitor has a fixed mode — refuse to resize it

A physical head runs at the mode its owner set, and MirrorDisplay::create
ignores the requested one on purpose. So a mid-stream Reconfigure against
a mirror could only ever tear the cast down and rebuild the identical one
at the identical size — a hitch that buys nothing, and an invitation to
the worse reflex of reconfiguring the display someone is sitting at.

reconfig_allowed grows a third gate beside gamescope and per-client-mode
identity, and the session captures it at bring-up like the others: this
session opened its display under whatever the pin said then, so a console
change mid-session must not retroactively change the answer it gives.

Linux-only, because vdisplay::open only routes to the mirror there — a
pin left in a Windows host's settings streams nothing different and must
not disable resize as a side effect.

design/per-monitor-portal-capture.md §7.3, open item 4.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-27 23:51:41 +02:00
co-authored by Claude Opus 5
parent b08226dbf1
commit de3ef50434
2 changed files with 45 additions and 9 deletions
+32 -8
View File
@@ -496,13 +496,20 @@ struct SendStats {
/// * a **per-client-mode identity** policy: the mode is part of the display-identity slot key, so a
/// resize resolves a DIFFERENT slot (a fresh Windows monitor / a differently-named KWin output),
/// defeating the policy — honest downgrade is to reject and let the client scale.
/// * a **monitor mirror** (`mirrored`): the source is a physical head running at the mode its owner
/// set, and `MirrorDisplay::create` ignores the requested one by design
/// (design/per-monitor-portal-capture.md §7.3). A resize would tear the cast down and re-`create`
/// the *same* head at the *same* size — a visible hitch that changes nothing — or, worse, invite
/// the reflex of reconfiguring the display someone is sitting in front of. Reject; the client
/// scales, exactly as it already does for gamescope.
///
/// Every other compositor (and the synthetic protocol-test source) with the default identity accepts.
pub(super) fn reconfig_allowed(
compositor: Option<crate::vdisplay::Compositor>,
per_client_mode: bool,
mirrored: bool,
) -> bool {
compositor != Some(crate::vdisplay::Compositor::Gamescope) && !per_client_mode
compositor != Some(crate::vdisplay::Compositor::Gamescope) && !per_client_mode && !mirrored
}
#[allow(clippy::too_many_arguments)]
@@ -3524,22 +3531,39 @@ mod tests {
use crate::vdisplay::Compositor::{Gamescope, Hyprland, Kwin, Mutter, Wlroots};
// gamescope ALWAYS rejects — a resize would respawn the nested game (H1/D3), regardless of
// the identity policy.
assert!(!reconfig_allowed(Some(Gamescope), false));
assert!(!reconfig_allowed(Some(Gamescope), true));
assert!(!reconfig_allowed(Some(Gamescope), false, false));
assert!(!reconfig_allowed(Some(Gamescope), true, false));
// A per-client-mode identity policy rejects on every backend — the resize resolves a
// different display-identity slot (H5).
assert!(!reconfig_allowed(Some(Kwin), true));
assert!(!reconfig_allowed(Some(Mutter), true));
assert!(!reconfig_allowed(None, true));
assert!(!reconfig_allowed(Some(Kwin), true, false));
assert!(!reconfig_allowed(Some(Mutter), true, false));
assert!(!reconfig_allowed(None, true, false));
// Every other compositor with the default identity ACCEPTS (recreate / re-arrival / in-place).
for c in [Kwin, Mutter, Wlroots, Hyprland] {
assert!(
reconfig_allowed(Some(c), false),
reconfig_allowed(Some(c), false, false),
"{c:?} should allow live reconfigure"
);
}
// The synthetic source (no compositor) is the protocol-test path — always reconfigurable.
assert!(reconfig_allowed(None, false));
assert!(reconfig_allowed(None, false, false));
}
/// A mirrored physical head has a fixed mode (§7.3): every backend that would otherwise accept
/// a live reconfigure must reject one while the session is streaming someone's real monitor.
#[test]
fn reconfig_allowed_rejects_a_monitor_mirror_on_every_backend() {
use crate::vdisplay::Compositor::{Hyprland, Kwin, Mutter, Wlroots};
for c in [Kwin, Mutter, Wlroots, Hyprland] {
assert!(
reconfig_allowed(Some(c), false, false),
"{c:?} without a pin should still allow live reconfigure"
);
assert!(
!reconfig_allowed(Some(c), false, true),
"{c:?} mirroring a physical head must reject a resize"
);
}
}
#[test]