` creation failed …").
+ device: &'static str,
+ /// Suffix for the create-failure line — empty on Linux, the driver-install hint on Windows.
+ hint: &'static str,
+}
+
+impl PadSlots
{
+ /// An empty table of [`MAX_PADS`] slots whose lifecycle log lines carry `label` / `device` /
+ /// `hint` (see the field docs).
+ pub fn new(label: &'static str, device: &'static str, hint: &'static str) -> PadSlots
{
+ PadSlots {
+ pads: (0..MAX_PADS).map(|_| None).collect(),
+ gate: PadGate::new(),
+ label,
+ device,
+ hint,
+ }
+ }
+
+ /// The backend tag this table logs with (for the manager's own arrival line).
+ pub fn label(&self) -> &'static str {
+ self.label
+ }
+
+ /// Drop every allocated pad whose `active_mask` bit cleared (the unplug sweep run on each state
+ /// frame), logging each. Returns the swept indices as a bitmask so the caller resets its
+ /// per-index sibling state; an index another manager owns is `None` here, so it is never swept.
+ pub fn sweep(&mut self, active_mask: u16) -> u16 {
+ let mut swept = 0u16;
+ for (i, slot) in self.pads.iter_mut().enumerate() {
+ if slot.is_some() && active_mask & (1 << i) == 0 {
+ tracing::info!(index = i, "controller unplugged ({})", self.label);
+ *slot = None;
+ swept |= 1 << i;
+ }
+ }
+ swept
+ }
+
+ /// Create the pad at `idx` via `open` if the slot is empty and the create gate allows it.
+ /// Returns `true` only on a fresh create (the caller resets its per-index sibling state);
+ /// `open` logs its own success line (it knows the transport detail), failure is logged here.
+ pub fn ensure(&mut self, idx: usize, open: impl FnOnce(u8) -> Result
) -> bool {
+ if idx >= MAX_PADS || self.pads[idx].is_some() || !self.gate.allow(Instant::now()) {
+ return false;
+ }
+ match open(idx as u8) {
+ Ok(p) => {
+ self.pads[idx] = Some(p);
+ self.gate.on_success();
+ true
+ }
+ Err(e) => {
+ tracing::error!(
+ error = %format!("{e:#}"),
+ "virtual {} creation failed — retrying with backoff{}",
+ self.device,
+ self.hint
+ );
+ self.gate.on_failure(Instant::now());
+ false
+ }
+ }
+ }
+
+ /// The live pad at `idx`, if any (out-of-range → `None`).
+ pub fn get(&self, idx: usize) -> Option<&P> {
+ self.pads.get(idx).and_then(|s| s.as_ref())
+ }
+
+ /// The live pad at `idx`, mutably, if any (out-of-range → `None`).
+ pub fn get_mut(&mut self, idx: usize) -> Option<&mut P> {
+ self.pads.get_mut(idx).and_then(|s| s.as_mut())
+ }
+
+ /// Iterate the live pads as `(index, &mut pad)` (the feedback-pump shape).
+ pub fn iter_mut(&mut self) -> impl Iterator- {
+ self.pads
+ .iter_mut()
+ .enumerate()
+ .filter_map(|(i, s)| s.as_mut().map(|p| (i, p)))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use anyhow::bail;
+
+ fn slots() -> PadSlots {
+ PadSlots::new("Test", "test pad", "")
+ }
+
+ #[test]
+ fn ensure_creates_once_and_reports_freshness() {
+ let mut s = slots();
+ // Fresh create → true; the pad is live.
+ assert!(s.ensure(3, |i| Ok(i as u32 * 10)));
+ assert_eq!(s.get(3), Some(&30));
+ // Occupied slot → no re-open (the closure must not run), no reset signal.
+ assert!(!s.ensure(3, |_| panic!("re-opened an occupied slot")));
+ // Out of range → never opens.
+ assert!(!s.ensure(MAX_PADS, |_| panic!("opened out of range")));
+ assert_eq!(s.get(MAX_PADS), None);
+ }
+
+ #[test]
+ fn sweep_drops_only_cleared_bits_and_returns_them_once() {
+ let mut s = slots();
+ assert!(s.ensure(0, |_| Ok(0)));
+ assert!(s.ensure(2, |_| Ok(2)));
+ assert!(s.ensure(5, |_| Ok(5)));
+ // Mask keeps 2, clears 0 and 5; empty slots (1, 3, …) are untouched non-events.
+ let swept = s.sweep(0b0000_0100);
+ assert_eq!(swept, 0b0010_0001);
+ assert_eq!(s.get(0), None);
+ assert_eq!(s.get(2), Some(&2));
+ assert_eq!(s.get(5), None);
+ // A second identical sweep is a no-op: the indices were returned exactly once.
+ assert_eq!(s.sweep(0b0000_0100), 0);
+ }
+
+ #[test]
+ fn create_failure_arms_the_gate_and_success_heals_it() {
+ let mut s = slots();
+ assert!(!s.ensure(1, |_| bail!("transient")));
+ // Backoff in effect: the next attempt is blocked without even calling `open`.
+ assert!(!s.ensure(1, |_| panic!("open during backoff")));
+ // The gate is manager-wide (create failures are systemic), so other indices block too.
+ assert!(!s.ensure(2, |_| panic!("open during backoff")));
+ // …and a sweep-then-recreate of a *different* live pad is equally gated, but the table
+ // itself is intact: nothing was allocated.
+ assert_eq!(s.get(1), None);
+ }
+
+ #[test]
+ fn recreate_after_sweep_resets_freshness() {
+ let mut s = slots();
+ assert!(s.ensure(4, |_| Ok(1)));
+ s.sweep(0);
+ assert_eq!(s.get(4), None);
+ // The slot is free again → a fresh create (true) with a new value.
+ assert!(s.ensure(4, |_| Ok(2)));
+ assert_eq!(s.get(4), Some(&2));
+ }
+
+ #[test]
+ fn iter_mut_yields_live_pads_with_indices() {
+ let mut s = slots();
+ assert!(s.ensure(1, |_| Ok(10)));
+ assert!(s.ensure(6, |_| Ok(60)));
+ let seen: Vec<(usize, u32)> = s.iter_mut().map(|(i, p)| (i, *p)).collect();
+ assert_eq!(seen, vec![(1, 10), (6, 60)]);
+ }
+}