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:
@@ -1003,6 +1003,9 @@ async fn serve_session(
|
|||||||
// so a resize would resolve a DIFFERENT slot — on Windows a fresh monitor ADD instead of the
|
// so a resize would resolve a DIFFERENT slot — on Windows a fresh monitor ADD instead of the
|
||||||
// in-place reconfigure, on KWin a differently-named output — defeating the policy's
|
// in-place reconfigure, on KWin a differently-named output — defeating the policy's
|
||||||
// per-resolution identity. Honest downgrade: reject, client scales (H5).
|
// per-resolution identity. Honest downgrade: reject, client scales (H5).
|
||||||
|
// * a monitor MIRROR (a `capture_monitor` pin): a physical head runs at the mode its owner set
|
||||||
|
// and the mirror backend ignores the requested one, so a resize would restart the identical
|
||||||
|
// cast at the identical size (design/per-monitor-portal-capture.md §7.3).
|
||||||
// The SYNTHETIC source stays reconfigurable on purpose (nothing to rebuild — the ack round-trip
|
// The SYNTHETIC source stays reconfigurable on purpose (nothing to rebuild — the ack round-trip
|
||||||
// is the whole effect): it is the compositor-free protocol test source, and the C-ABI roundtrip
|
// is the whole effect): it is the compositor-free protocol test source, and the C-ABI roundtrip
|
||||||
// test + client harnesses exercise the Reconfigure/Reconfigured plumbing through it.
|
// test + client harnesses exercise the Reconfigure/Reconfigured plumbing through it.
|
||||||
@@ -1011,7 +1014,16 @@ async fn serve_session(
|
|||||||
let per_client_mode_identity = crate::vdisplay::policy::prefs()
|
let per_client_mode_identity = crate::vdisplay::policy::prefs()
|
||||||
.configured_effective()
|
.configured_effective()
|
||||||
.is_some_and(|e| e.identity == crate::vdisplay::policy::Identity::PerClientMode);
|
.is_some_and(|e| e.identity == crate::vdisplay::policy::Identity::PerClientMode);
|
||||||
reconfig_allowed(compositor, per_client_mode_identity)
|
// Read once here, like the identity above: this session opened its display under whatever
|
||||||
|
// the pin said at bring-up, so a console change mid-session must not retroactively change
|
||||||
|
// what THIS session answers a Reconfigure with. 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 silently disable resize as a side effect.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
let mirrored = crate::vdisplay::capture_monitor().is_some();
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
let mirrored = false;
|
||||||
|
reconfig_allowed(compositor, per_client_mode_identity, mirrored)
|
||||||
};
|
};
|
||||||
// Negotiated codec (HEVC / H.264 / AV1), derived from the Welcome. `Copy`, so the control task's
|
// Negotiated codec (HEVC / H.264 / AV1), derived from the Welcome. `Copy`, so the control task's
|
||||||
// `async move` captures a copy and it stays usable for the data-plane SessionContext below.
|
// `async move` captures a copy and it stays usable for the data-plane SessionContext below.
|
||||||
|
|||||||
@@ -496,13 +496,20 @@ struct SendStats {
|
|||||||
/// * a **per-client-mode identity** policy: the mode is part of the display-identity slot key, so a
|
/// * 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),
|
/// 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.
|
/// 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.
|
/// Every other compositor (and the synthetic protocol-test source) with the default identity accepts.
|
||||||
pub(super) fn reconfig_allowed(
|
pub(super) fn reconfig_allowed(
|
||||||
compositor: Option<crate::vdisplay::Compositor>,
|
compositor: Option<crate::vdisplay::Compositor>,
|
||||||
per_client_mode: bool,
|
per_client_mode: bool,
|
||||||
|
mirrored: bool,
|
||||||
) -> 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)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
@@ -3524,22 +3531,39 @@ mod tests {
|
|||||||
use crate::vdisplay::Compositor::{Gamescope, Hyprland, Kwin, Mutter, Wlroots};
|
use crate::vdisplay::Compositor::{Gamescope, Hyprland, Kwin, Mutter, Wlroots};
|
||||||
// gamescope ALWAYS rejects — a resize would respawn the nested game (H1/D3), regardless of
|
// gamescope ALWAYS rejects — a resize would respawn the nested game (H1/D3), regardless of
|
||||||
// the identity policy.
|
// the identity policy.
|
||||||
assert!(!reconfig_allowed(Some(Gamescope), false));
|
assert!(!reconfig_allowed(Some(Gamescope), false, false));
|
||||||
assert!(!reconfig_allowed(Some(Gamescope), true));
|
assert!(!reconfig_allowed(Some(Gamescope), true, false));
|
||||||
// A per-client-mode identity policy rejects on every backend — the resize resolves a
|
// A per-client-mode identity policy rejects on every backend — the resize resolves a
|
||||||
// different display-identity slot (H5).
|
// different display-identity slot (H5).
|
||||||
assert!(!reconfig_allowed(Some(Kwin), true));
|
assert!(!reconfig_allowed(Some(Kwin), true, false));
|
||||||
assert!(!reconfig_allowed(Some(Mutter), true));
|
assert!(!reconfig_allowed(Some(Mutter), true, false));
|
||||||
assert!(!reconfig_allowed(None, true));
|
assert!(!reconfig_allowed(None, true, false));
|
||||||
// Every other compositor with the default identity ACCEPTS (recreate / re-arrival / in-place).
|
// Every other compositor with the default identity ACCEPTS (recreate / re-arrival / in-place).
|
||||||
for c in [Kwin, Mutter, Wlroots, Hyprland] {
|
for c in [Kwin, Mutter, Wlroots, Hyprland] {
|
||||||
assert!(
|
assert!(
|
||||||
reconfig_allowed(Some(c), false),
|
reconfig_allowed(Some(c), false, false),
|
||||||
"{c:?} should allow live reconfigure"
|
"{c:?} should allow live reconfigure"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// The synthetic source (no compositor) is the protocol-test path — always reconfigurable.
|
// 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]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user