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:
@@ -779,8 +779,11 @@ pub fn effective_topology() -> policy::Topology {
|
||||
#[cfg(target_os = "linux")]
|
||||
#[path = "vdisplay/linux/gamescope.rs"]
|
||||
mod gamescope;
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "vdisplay/windows/identity.rs"]
|
||||
// Platform-neutral per-client stable display-id map (Stage 3): Windows seeds the monitor EDID +
|
||||
// ConnectorIndex from the id; KWin names its output from it. `allow(dead_code)` because only Windows
|
||||
// consumes it in non-test code today — the KWin wiring is the next Stage-3 step.
|
||||
#[allow(dead_code)]
|
||||
#[path = "vdisplay/identity.rs"]
|
||||
pub(crate) mod identity;
|
||||
#[cfg(target_os = "linux")]
|
||||
#[path = "vdisplay/linux/kwin.rs"]
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
//! Platform-neutral **per-client → stable display-id map** (design: `design/display-management.md`
|
||||
//! §5.4 — identity). A client that reconnects gets the SAME small stable id every time, so the
|
||||
//! desktop environment can key its per-display config (notably **DPI scaling**) to it and reapply it:
|
||||
//!
|
||||
//! * **Windows** seeds the pf-vdisplay monitor's EDID serial + IddCx `ConnectorIndex` from the id, so
|
||||
//! Windows reapplies the client's saved `PerMonitorSettings` scaling. The id must stay `1..=15`
|
||||
//! (`ConnectorIndex < MaxMonitorsSupported = 16`).
|
||||
//! * **KWin** names the streamed output `Virtual-punktfunk-<id>`; KWin persists per-output scale/mode
|
||||
//! in `kwinoutputconfig.json` matched by name, so a stable per-client name makes KDE reapply that
|
||||
//! client's scaling. (Generalised here from the Windows-only map; the KWin wiring is Stage 3.)
|
||||
//!
|
||||
//! The map key is a composable string ([`identity_key`]): the client cert fingerprint alone
|
||||
//! (`per-client`), or fingerprint + resolution (`per-client-mode` — distinct scaling per resolution).
|
||||
//! Anonymous/TOFU/GameStream sessions have no fingerprint and resolve to id `0` (auto) upstream,
|
||||
//! never reaching this map.
|
||||
//!
|
||||
//! Persisted to `<config>/display-identity.json` (migrated from the legacy Windows
|
||||
//! `pf-vdisplay-identity.json`) so ids — and the client→config association — survive host restarts.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Max stable id. Bounded by the Windows driver's use of the id as the IddCx `ConnectorIndex`
|
||||
/// (`< MaxMonitorsSupported = 16`), so ids run `1..=15` on every platform for a single shared map.
|
||||
const MAX_ID: u32 = 15;
|
||||
|
||||
/// The map filename (migrated from the legacy Windows-only `pf-vdisplay-identity.json`).
|
||||
const FILE: &str = "display-identity.json";
|
||||
const LEGACY_FILE: &str = "pf-vdisplay-identity.json";
|
||||
|
||||
/// Compose the map key for a client. `per_client_mode` appends the resolution so a client keeps a
|
||||
/// distinct id (and thus distinct persisted scaling) per resolution; otherwise the fingerprint alone.
|
||||
pub(crate) fn identity_key(fp: [u8; 32], mode: (u32, u32), per_client_mode: bool) -> String {
|
||||
let hex: String = fp.iter().map(|b| format!("{b:02x}")).collect();
|
||||
if per_client_mode {
|
||||
format!("{hex}@{}x{}", mode.0, mode.1)
|
||||
} else {
|
||||
hex
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default)]
|
||||
struct Store {
|
||||
/// Monotonic most-recently-used counter (the entry with the highest `seen` is the MRU). Persisted so
|
||||
/// the LRU ordering survives host restarts.
|
||||
tick: u64,
|
||||
entries: Vec<Entry>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Entry {
|
||||
/// The composed client key ([`identity_key`]) — the map key. (Serialized as `fp` for
|
||||
/// back-compat with the legacy Windows `pf-vdisplay-identity.json`.)
|
||||
#[serde(rename = "fp")]
|
||||
key: String,
|
||||
/// The client's stable display id (`1..=15`).
|
||||
id: u32,
|
||||
/// MRU stamp (compared against [`Store::tick`]).
|
||||
seen: u64,
|
||||
}
|
||||
|
||||
/// Persistent client-key → stable-id map (see the module docs).
|
||||
pub(crate) struct DisplayIdentityMap {
|
||||
path: PathBuf,
|
||||
store: Store,
|
||||
}
|
||||
|
||||
impl DisplayIdentityMap {
|
||||
/// Load the persisted map (empty on first run / unreadable / parse failure — a fresh map just
|
||||
/// re-derives ids, costing a client one scaling re-set the first time). Migrates the legacy
|
||||
/// Windows `pf-vdisplay-identity.json` if the new file is absent.
|
||||
pub(crate) fn load() -> Self {
|
||||
let dir = crate::gamestream::config_dir();
|
||||
let path = dir.join(FILE);
|
||||
let bytes = std::fs::read(&path)
|
||||
.or_else(|_| std::fs::read(dir.join(LEGACY_FILE)))
|
||||
.ok();
|
||||
let mut store = bytes
|
||||
.and_then(|b| serde_json::from_slice::<Store>(&b).ok())
|
||||
.unwrap_or_default();
|
||||
// SANITIZE a hand-edited / corrupt / cross-version file before trusting it: resolve()'s
|
||||
// found-entry branch returns the stored id verbatim, so an out-of-range id (0 = the "auto"
|
||||
// sentinel, or > MAX_ID) or a duplicate id/key would flow straight into the display identity.
|
||||
// Drop out-of-range ids and dedup by BOTH key and id (keeping the most-recently-seen on a
|
||||
// clash) so no two clients can map to the same id.
|
||||
store.entries.sort_by_key(|e| std::cmp::Reverse(e.seen));
|
||||
let mut seen_key = std::collections::HashSet::new();
|
||||
let mut seen_id = std::collections::HashSet::new();
|
||||
store.entries.retain(|e| {
|
||||
(1..=MAX_ID).contains(&e.id) && seen_key.insert(e.key.clone()) && seen_id.insert(e.id)
|
||||
});
|
||||
Self { path, store }
|
||||
}
|
||||
|
||||
/// The stable id (`1..=15`) for the client `key` ([`identity_key`]): its remembered id, or a
|
||||
/// freshly assigned one (lowest free, else LRU-evict at the cap). Bumps the entry to MRU and persists.
|
||||
pub(crate) fn resolve(&mut self, key: &str) -> u32 {
|
||||
self.store.tick = self.store.tick.wrapping_add(1);
|
||||
let now = self.store.tick;
|
||||
|
||||
if let Some(e) = self.store.entries.iter_mut().find(|e| e.key == key) {
|
||||
e.seen = now;
|
||||
let id = e.id;
|
||||
self.persist();
|
||||
return id;
|
||||
}
|
||||
|
||||
// New client: prefer the lowest free id in 1..=MAX_ID; if all are taken, evict the LRU entry and
|
||||
// reuse its id (the evicted client re-establishes its scaling once on its next connect).
|
||||
let id = (1..=MAX_ID)
|
||||
.find(|i| !self.store.entries.iter().any(|e| e.id == *i))
|
||||
.unwrap_or_else(|| {
|
||||
let lru = self
|
||||
.store
|
||||
.entries
|
||||
.iter()
|
||||
.enumerate()
|
||||
.min_by_key(|(_, e)| e.seen)
|
||||
.map(|(i, _)| i)
|
||||
.expect("entries are non-empty whenever every id 1..=MAX_ID is taken");
|
||||
let evicted = self.store.entries.remove(lru);
|
||||
evicted.id
|
||||
});
|
||||
self.store.entries.push(Entry {
|
||||
key: key.to_string(),
|
||||
id,
|
||||
seen: now,
|
||||
});
|
||||
self.persist();
|
||||
id
|
||||
}
|
||||
|
||||
/// Persist atomically (temp file + rename). Best-effort: a write failure just means a restart may
|
||||
/// re-derive an id (one scaling re-set). Not a credential, so a plain (non-ACL'd) write is fine.
|
||||
fn persist(&self) {
|
||||
let Ok(bytes) = serde_json::to_vec_pretty(&self.store) else {
|
||||
return;
|
||||
};
|
||||
if let Some(dir) = self.path.parent() {
|
||||
let _ = std::fs::create_dir_all(dir);
|
||||
}
|
||||
let tmp = self.path.with_extension("json.tmp");
|
||||
if std::fs::write(&tmp, &bytes).is_ok() {
|
||||
let _ = std::fs::rename(&tmp, &self.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn fp(n: u8) -> [u8; 32] {
|
||||
let mut f = [0u8; 32];
|
||||
f[0] = n;
|
||||
f
|
||||
}
|
||||
|
||||
fn temp_map(tag: &str) -> DisplayIdentityMap {
|
||||
DisplayIdentityMap {
|
||||
path: std::env::temp_dir().join(format!("pf-id-{tag}-{}.json", std::process::id())),
|
||||
store: Store::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stable_across_calls_and_distinct_per_client() {
|
||||
let mut m = temp_map("stable");
|
||||
let a1 = m.resolve(&identity_key(fp(1), (1920, 1080), false));
|
||||
let b = m.resolve(&identity_key(fp(2), (1920, 1080), false));
|
||||
let a2 = m.resolve(&identity_key(fp(1), (1280, 720), false)); // per-client: mode ignored
|
||||
assert_eq!(a1, a2, "same client → same id (per-client ignores mode)");
|
||||
assert_ne!(a1, b, "distinct clients → distinct ids");
|
||||
assert!((1..=MAX_ID).contains(&a1) && (1..=MAX_ID).contains(&b));
|
||||
let _ = std::fs::remove_file(&m.path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn per_client_mode_splits_by_resolution() {
|
||||
let mut m = temp_map("permode");
|
||||
let hd = m.resolve(&identity_key(fp(1), (1920, 1080), true));
|
||||
let uhd = m.resolve(&identity_key(fp(1), (3840, 2160), true));
|
||||
let hd2 = m.resolve(&identity_key(fp(1), (1920, 1080), true));
|
||||
assert_ne!(hd, uhd, "same client, different resolution → different id");
|
||||
assert_eq!(hd, hd2, "same client + resolution → same id");
|
||||
let _ = std::fs::remove_file(&m.path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lru_eviction_reuses_an_id_at_the_cap() {
|
||||
let mut m = temp_map("lru");
|
||||
for n in 1..=15u8 {
|
||||
m.resolve(&identity_key(fp(n), (1920, 1080), false));
|
||||
}
|
||||
let _ = m.resolve(&identity_key(fp(2), (1920, 1080), false)); // touch 2 so 1 is LRU
|
||||
let id16 = m.resolve(&identity_key(fp(16), (1920, 1080), false));
|
||||
assert!((1..=MAX_ID).contains(&id16));
|
||||
assert_eq!(m.store.entries.len(), 15, "cap holds at 15 entries");
|
||||
assert!(m.store.entries.iter().all(|e| (1..=MAX_ID).contains(&e.id)));
|
||||
let _ = std::fs::remove_file(&m.path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_composition() {
|
||||
assert_eq!(identity_key(fp(0xab), (1920, 1080), false).len(), 64); // hex fp only
|
||||
assert!(identity_key(fp(0xab), (1920, 1080), true).ends_with("@1920x1080"));
|
||||
}
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
//! Per-client → stable monitor-id map for pf-vdisplay (Phase 2: per-client display-config persistence).
|
||||
//!
|
||||
//! Windows keys per-monitor config — notably DPI **scaling** (`HKCU\Control Panel\Desktop\PerMonitorSettings`)
|
||||
//! — on the monitor's EDID identity AND its OS device path (whose per-connector discriminator is the IddCx
|
||||
//! `ConnectorIndex` → target UID). The pf-vdisplay driver seeds BOTH the EDID serial and the `ConnectorIndex`
|
||||
//! from a single monitor `id`. So for Windows to REAPPLY a given client's saved scaling on reconnect, that
|
||||
//! client must get the SAME `id` every time. This map assigns each client (keyed by its cert fingerprint) a
|
||||
//! STABLE id and the host passes it as [`AddRequest::preferred_monitor_id`](pf_driver_proto::control::AddRequest).
|
||||
//!
|
||||
//! The id space is bounded to `1..=15` because the driver uses the id as the IddCx `ConnectorIndex`, which
|
||||
//! must stay `< MaxMonitorsSupported` (16). When more than 15 distinct clients are remembered, the
|
||||
//! LEAST-RECENTLY-USED entry is evicted and its id reused (that evicted client simply re-establishes its
|
||||
//! scaling once on its next connect). The map persists to `%ProgramData%\punktfunk\pf-vdisplay-identity.json`
|
||||
//! so ids — and therefore the client→config association — survive host restarts.
|
||||
//!
|
||||
//! Anonymous/TOFU and GameStream sessions have no fingerprint and resolve to id `0` (auto) upstream, never
|
||||
//! reaching this map — they keep the driver's lowest-free slot behavior unchanged.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Max stable id. The driver uses the id as the IddCx `ConnectorIndex`, which must stay
|
||||
/// `< MaxMonitorsSupported` (16) — so ids run `1..=15`.
|
||||
const MAX_ID: u32 = 15;
|
||||
|
||||
#[derive(Serialize, Deserialize, Default)]
|
||||
struct Store {
|
||||
/// Monotonic most-recently-used counter (the entry with the highest `seen` is the MRU). Persisted so
|
||||
/// the LRU ordering survives host restarts.
|
||||
tick: u64,
|
||||
entries: Vec<Entry>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Entry {
|
||||
/// Lower-hex client cert fingerprint (the map key).
|
||||
fp: String,
|
||||
/// The client's stable monitor id (`1..=15`).
|
||||
id: u32,
|
||||
/// MRU stamp (compared against [`Store::tick`]).
|
||||
seen: u64,
|
||||
}
|
||||
|
||||
/// Persistent fingerprint → stable-id map (see the module docs).
|
||||
pub(crate) struct MonitorIdentityMap {
|
||||
path: PathBuf,
|
||||
store: Store,
|
||||
}
|
||||
|
||||
impl MonitorIdentityMap {
|
||||
/// Load the persisted map (empty on first run / unreadable / parse failure — a fresh map just
|
||||
/// re-derives ids, costing a client one scaling re-set the first time).
|
||||
pub(crate) fn load() -> Self {
|
||||
let path = crate::gamestream::config_dir().join("pf-vdisplay-identity.json");
|
||||
let mut store = std::fs::read(&path)
|
||||
.ok()
|
||||
.and_then(|b| serde_json::from_slice::<Store>(&b).ok())
|
||||
.unwrap_or_default();
|
||||
// SANITIZE a hand-edited / corrupt / cross-version file before trusting it: resolve()'s found-entry
|
||||
// branch returns the stored id verbatim, so an out-of-range id (0 = the "auto" sentinel, or
|
||||
// > MAX_ID) or a duplicate id/fp would flow straight into preferred_monitor_id. Drop out-of-range
|
||||
// ids and dedup by BOTH fp and id (keeping the most-recently-seen on a clash) so no two fingerprints
|
||||
// can map to the same id. (The driver also rejects a live-colliding id as a backstop.)
|
||||
store.entries.sort_by_key(|e| std::cmp::Reverse(e.seen));
|
||||
let mut seen_fp = std::collections::HashSet::new();
|
||||
let mut seen_id = std::collections::HashSet::new();
|
||||
store.entries.retain(|e| {
|
||||
(1..=MAX_ID).contains(&e.id) && seen_fp.insert(e.fp.clone()) && seen_id.insert(e.id)
|
||||
});
|
||||
Self { path, store }
|
||||
}
|
||||
|
||||
/// The stable id (`1..=15`) for the client fingerprint `fp`: its remembered id, or a freshly assigned
|
||||
/// one (lowest free, else LRU-evict at the cap). Bumps the entry to MRU and persists.
|
||||
pub(crate) fn resolve(&mut self, fp: [u8; 32]) -> u32 {
|
||||
let key: String = fp.iter().map(|b| format!("{b:02x}")).collect();
|
||||
self.store.tick = self.store.tick.wrapping_add(1);
|
||||
let now = self.store.tick;
|
||||
|
||||
if let Some(e) = self.store.entries.iter_mut().find(|e| e.fp == key) {
|
||||
e.seen = now;
|
||||
let id = e.id;
|
||||
self.persist();
|
||||
return id;
|
||||
}
|
||||
|
||||
// New client: prefer the lowest free id in 1..=MAX_ID; if all are taken, evict the LRU entry and
|
||||
// reuse its id (the evicted client re-establishes its scaling once on its next connect).
|
||||
let id = (1..=MAX_ID)
|
||||
.find(|i| !self.store.entries.iter().any(|e| e.id == *i))
|
||||
.unwrap_or_else(|| {
|
||||
let lru = self
|
||||
.store
|
||||
.entries
|
||||
.iter()
|
||||
.enumerate()
|
||||
.min_by_key(|(_, e)| e.seen)
|
||||
.map(|(i, _)| i)
|
||||
.expect("entries are non-empty whenever every id 1..=MAX_ID is taken");
|
||||
let evicted = self.store.entries.remove(lru);
|
||||
evicted.id
|
||||
});
|
||||
self.store.entries.push(Entry {
|
||||
fp: key,
|
||||
id,
|
||||
seen: now,
|
||||
});
|
||||
self.persist();
|
||||
id
|
||||
}
|
||||
|
||||
/// Persist atomically (temp file + rename). Best-effort: a write failure just means a restart may
|
||||
/// re-derive an id (one scaling re-set). Not a credential, so a plain (non-ACL'd) write is fine.
|
||||
fn persist(&self) {
|
||||
let Ok(bytes) = serde_json::to_vec_pretty(&self.store) else {
|
||||
return;
|
||||
};
|
||||
if let Some(dir) = self.path.parent() {
|
||||
let _ = std::fs::create_dir_all(dir);
|
||||
}
|
||||
let tmp = self.path.with_extension("json.tmp");
|
||||
if std::fs::write(&tmp, &bytes).is_ok() {
|
||||
let _ = std::fs::rename(&tmp, &self.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn fp(n: u8) -> [u8; 32] {
|
||||
let mut f = [0u8; 32];
|
||||
f[0] = n;
|
||||
f
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stable_across_calls_and_distinct_per_client() {
|
||||
let mut m = MonitorIdentityMap {
|
||||
path: std::env::temp_dir().join(format!("pf-id-test-{}.json", std::process::id())),
|
||||
store: Store::default(),
|
||||
};
|
||||
let a1 = m.resolve(fp(1));
|
||||
let b = m.resolve(fp(2));
|
||||
let a2 = m.resolve(fp(1));
|
||||
assert_eq!(a1, a2, "same client → same id");
|
||||
assert_ne!(a1, b, "distinct clients → distinct ids");
|
||||
assert!((1..=MAX_ID).contains(&a1) && (1..=MAX_ID).contains(&b));
|
||||
let _ = std::fs::remove_file(&m.path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lru_eviction_reuses_an_id_at_the_cap() {
|
||||
let mut m = MonitorIdentityMap {
|
||||
path: std::env::temp_dir().join(format!("pf-id-lru-{}.json", std::process::id())),
|
||||
store: Store::default(),
|
||||
};
|
||||
// Fill all 15 ids (clients 1..=15), then touch client 2 so client 1 is the LRU.
|
||||
for n in 1..=15u8 {
|
||||
m.resolve(fp(n));
|
||||
}
|
||||
let _ = m.resolve(fp(2));
|
||||
// A 16th client evicts the LRU (client 1) and reuses its id; ids stay bounded.
|
||||
let id16 = m.resolve(fp(16));
|
||||
assert!((1..=MAX_ID).contains(&id16));
|
||||
assert_eq!(m.store.entries.len(), 15, "cap holds at 15 entries");
|
||||
assert!(m.store.entries.iter().all(|e| (1..=MAX_ID).contains(&e.id)));
|
||||
let _ = std::fs::remove_file(&m.path);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user