feat(vdisplay): platform-neutral identity map + per-client-mode (Stage 3)

Generalize the Windows-only per-client stable-id map into vdisplay/identity.rs:
- DisplayIdentityMap keyed on a composable string (identity_key: fingerprint,
  or fingerprint+resolution under per-client-mode); LRU at 15, persisted to
  display-identity.json (migrated from the legacy pf-vdisplay-identity.json).
- Windows manager wired to it, picking the key from the identity policy.
- Foundation for KWin per-slot output naming (persistent KDE scaling) — the
  KWin wiring is the next Stage-3 step (needs a KWin box).
- Unit-tested (stable, per-client-mode split, LRU, key composition).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-05 08:40:18 +00:00
parent cb7ddc0411
commit b150d79626
4 changed files with 229 additions and 178 deletions
@@ -172,7 +172,7 @@ pub(crate) struct VirtualDisplayManager {
/// Persistent per-client (cert-fingerprint) → stable monitor-id map. A monitor CREATE resolves the
/// connecting client's id here, so the client keeps the same EDID serial + IddCx ConnectorIndex across
/// reconnects and Windows reapplies its saved per-monitor config (DPI scaling). See [`super::identity`].
identity_map: Mutex<super::identity::MonitorIdentityMap>,
identity_map: Mutex<super::identity::DisplayIdentityMap>,
}
static VDM: OnceLock<VirtualDisplayManager> = OnceLock::new();
@@ -188,7 +188,7 @@ pub(crate) fn init(driver: Box<dyn VdisplayDriver>) -> &'static VirtualDisplayMa
state: Mutex::new(MgrState::Idle),
setup_lock: Mutex::new(()),
idd_session_stop: Mutex::new(None),
identity_map: Mutex::new(super::identity::MonitorIdentityMap::load()),
identity_map: Mutex::new(super::identity::DisplayIdentityMap::load()),
})
}
@@ -527,9 +527,20 @@ impl VirtualDisplayManager {
) -> Result<Monitor> {
// Resolve the connecting client's STABLE per-client monitor id (so Windows reapplies its saved
// per-monitor config — DPI scaling — on reconnect); `None`/anonymous → 0 = the driver
// auto-allocates the lowest-free id (the original slot-based behavior).
// auto-allocates the lowest-free id (the original slot-based behavior). The `identity` policy
// picks the key: per-client (fingerprint) or per-client-mode (fingerprint + resolution).
let per_client_mode = matches!(
crate::vdisplay::policy::prefs()
.configured_effective()
.map(|e| e.identity),
Some(crate::vdisplay::policy::Identity::PerClientMode)
);
let preferred_id = client_fp
.map(|fp| self.identity_map.lock().unwrap().resolve(fp))
.map(|fp| {
let key =
super::identity::identity_key(fp, (mode.width, mode.height), per_client_mode);
self.identity_map.lock().unwrap().resolve(&key)
})
.unwrap_or(0);
// SAFETY: `create_monitor`'s own `# Safety` contract guarantees `dev` is the live control
// handle; we forward it unchanged to `add_monitor`, whose precondition is exactly that.