feat(slot): add the latest-frame-wins hand-off between threads

The PipeWire thread publishes into a single slot and the network thread takes
from it, swapping buffers instead of copying so the steady state does not
allocate. An older pending frame is dropped when a newer one arrives.
This commit is contained in:
2026-09-12 10:09:23 +03:00
parent 9855d1e18a
commit b3993be5cd
2 changed files with 188 additions and 0 deletions
+1
View File
@@ -3,3 +3,4 @@
pub mod config; pub mod config;
pub mod hyperhdr; pub mod hyperhdr;
pub mod slot;
+187
View File
@@ -0,0 +1,187 @@
//! "Latest frame wins" hand-off between the PipeWire thread and the network thread.
//!
//! Buffers are swapped rather than copied, so in steady state no allocations happen.
use std::sync::{Condvar, Mutex, MutexGuard};
use std::time::Instant;
/// An RGB24 frame, rows without padding.
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Frame {
pub width: u32,
pub height: u32,
pub rgb: Vec<u8>,
}
#[derive(Debug, Default)]
struct Inner {
frame: Frame,
/// `frame` was published and not taken yet.
pending: bool,
source_gone: bool,
wake: bool,
shutdown: bool,
}
/// What happened since the last [`FrameSlot::wait`].
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct Update {
pub shutdown: bool,
/// The source disappeared. Handle before `new_frame`: such a frame is from a new source.
pub source_gone: bool,
/// The caller's frame was replaced with a newer one.
pub new_frame: bool,
/// Someone called [`FrameSlot::wake`].
pub woken: bool,
}
#[derive(Debug, Default)]
pub struct FrameSlot {
inner: Mutex<Inner>,
cond: Condvar,
}
impl FrameSlot {
pub fn new() -> Self {
Self::default()
}
fn lock(&self) -> MutexGuard<'_, Inner> {
// A panic while holding this lock cannot leave the plain data inconsistent.
self.inner.lock().unwrap_or_else(|e| e.into_inner())
}
/// Publishes `frame` by swapping it into the slot; `frame` receives the previous buffer
/// for reuse. Returns `true` if an untaken frame was replaced.
pub fn publish(&self, frame: &mut Frame) -> bool {
let mut inner = self.lock();
std::mem::swap(&mut inner.frame, frame);
let superseded = std::mem::replace(&mut inner.pending, true);
drop(inner);
self.cond.notify_all();
superseded
}
/// The source is gone; drops a pending frame.
pub fn source_gone(&self) {
let mut inner = self.lock();
inner.pending = false;
inner.source_gone = true;
drop(inner);
self.cond.notify_all();
}
pub fn wake(&self) {
self.lock().wake = true;
self.cond.notify_all();
}
pub fn shutdown(&self) {
self.lock().shutdown = true;
self.cond.notify_all();
}
/// Waits until an event occurs or `deadline` passes. A newer frame is swapped into `frame`.
pub fn wait(&self, frame: &mut Frame, deadline: Option<Instant>) -> Update {
let mut inner = self.lock();
loop {
if inner.shutdown || inner.source_gone || inner.pending || inner.wake {
break;
}
match deadline {
None => inner = self.cond.wait(inner).unwrap_or_else(|e| e.into_inner()),
Some(deadline) => {
let now = Instant::now();
if now >= deadline {
break;
}
inner = self
.cond
.wait_timeout(inner, deadline - now)
.unwrap_or_else(|e| e.into_inner())
.0;
}
}
}
let new_frame = std::mem::take(&mut inner.pending);
if new_frame {
std::mem::swap(&mut inner.frame, frame);
}
Update {
shutdown: inner.shutdown,
source_gone: std::mem::take(&mut inner.source_gone),
new_frame,
woken: std::mem::take(&mut inner.wake),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use std::time::Duration;
fn frame(tag: u8) -> Frame {
Frame {
width: 1,
height: 1,
rgb: vec![tag, tag, tag],
}
}
#[test]
fn latest_wins_and_buffers_rotate() {
let slot = FrameSlot::new();
let mut producer = frame(1);
assert!(!slot.publish(&mut producer));
producer = frame(2);
assert!(slot.publish(&mut producer));
// The producer got the previous buffer back.
assert_eq!(producer.rgb, [1, 1, 1]);
let mut consumer = Frame::default();
let u = slot.wait(&mut consumer, Some(Instant::now()));
assert!(u.new_frame);
assert_eq!(consumer.rgb, [2, 2, 2]);
let u = slot.wait(&mut consumer, Some(Instant::now()));
assert!(!u.new_frame);
assert_eq!(consumer.rgb, [2, 2, 2]);
}
#[test]
fn source_gone_drops_pending_frame() {
let slot = FrameSlot::new();
slot.publish(&mut frame(1));
slot.source_gone();
let mut consumer = Frame::default();
let u = slot.wait(&mut consumer, None);
assert!(u.source_gone && !u.new_frame);
slot.source_gone();
slot.publish(&mut frame(3));
let u = slot.wait(&mut consumer, None);
assert!(u.source_gone && u.new_frame);
assert_eq!(consumer.rgb, [3, 3, 3]);
}
#[test]
fn wait_times_out_and_wakes() {
let slot = Arc::new(FrameSlot::new());
let mut consumer = Frame::default();
let start = Instant::now();
let u = slot.wait(&mut consumer, Some(start + Duration::from_millis(30)));
assert_eq!(u, Update::default());
assert!(start.elapsed() >= Duration::from_millis(30));
let s = slot.clone();
let t = std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(20));
s.shutdown();
});
let u = slot.wait(&mut consumer, None);
assert!(u.shutdown);
t.join().unwrap();
}
}