use std::sync::Mutex;
use std::time::Duration;
use drmkit_scene::DamageRect;
use drmkit_sync::SyncFence;
pub type SlotKey = u64;
pub const MAX_DAMAGE: usize = 16;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Present {
None,
Fresh,
Hold,
}
#[derive(Debug)]
#[non_exhaustive]
pub struct Acquired {
pub kind: Present,
pub key: SlotKey,
pub token: u64,
pub fence: Option<SyncFence>,
pub damage: Vec<DamageRect>,
}
#[derive(Default)]
struct Pending {
key: Option<SlotKey>,
fence: Option<SyncFence>,
damage: Vec<DamageRect>,
}
pub struct RingPresenter {
pending: Mutex<Pending>,
scanning_key: Option<SlotKey>,
scanning_token: u64,
next_token: u64,
outstanding: Vec<(u64, SlotKey)>,
fence_deadline: Option<Duration>,
carried_damage: Vec<DamageRect>,
carried_whole_frame: bool,
}
impl RingPresenter {
#[must_use]
pub fn new(fence_deadline: Option<Duration>) -> Self {
Self {
pending: Mutex::new(Pending::default()),
scanning_key: None,
scanning_token: 0,
next_token: 0,
outstanding: Vec::new(),
fence_deadline,
carried_damage: Vec::new(),
carried_whole_frame: false,
}
}
#[must_use]
pub const fn fence_deadline(&self) -> Option<Duration> {
self.fence_deadline
}
pub fn submit(&self, key: SlotKey, fence: Option<SyncFence>, damage: &[DamageRect]) {
let mut pending = self.lock();
pending.key = Some(key);
pending.fence = fence;
pending.damage.clear();
if damage.len() <= MAX_DAMAGE {
pending.damage.extend_from_slice(damage);
}
}
#[must_use]
pub fn has_fresh_frame(&self) -> bool {
self.lock().key.is_some()
}
#[must_use]
pub const fn scanning_key(&self) -> Option<SlotKey> {
self.scanning_key
}
#[must_use]
pub fn is_referenced(&self, key: SlotKey) -> bool {
self.scanning_key == Some(key) || self.outstanding.iter().any(|(_, k)| *k == key)
}
pub fn acquire(&mut self) -> Acquired {
let (mut key, mut fence, mut damage) = {
let mut pending = self.lock();
if pending.key.is_some() {
(
pending.key.take(),
pending.fence.take(),
std::mem::take(&mut pending.damage),
)
} else {
(None, None, Vec::new())
}
};
if let (Some(deadline), true, Some(f)) = (self.fence_deadline, key.is_some(), fence.take())
{
let waited = if deadline.is_zero() {
Duration::ZERO
} else {
deadline.max(Duration::from_millis(1))
};
if f.wait(waited).is_err() {
self.carry(&damage);
damage.clear();
key = None;
}
}
if let Some(key) = key {
self.scanning_key = Some(key);
self.next_token = self.next_token.wrapping_add(1);
if self.next_token == 0 {
self.next_token = 1;
}
self.scanning_token = self.next_token;
self.outstanding.push((self.scanning_token, key));
let mut out = std::mem::take(&mut self.carried_damage);
let dropped_whole_frame = std::mem::take(&mut self.carried_whole_frame);
if dropped_whole_frame || damage.is_empty() || out.len() + damage.len() > MAX_DAMAGE {
out.clear();
} else {
out.extend_from_slice(&damage);
}
return Acquired {
kind: Present::Fresh,
key,
token: self.scanning_token,
fence,
damage: out,
};
}
if let Some(key) = self.scanning_key {
return Acquired {
kind: Present::Hold,
key,
token: self.scanning_token,
fence: None,
damage: Vec::new(),
};
}
Acquired {
kind: Present::None,
key: 0,
token: 0,
fence: None,
damage: Vec::new(),
}
}
pub fn release(&mut self, token: u64) -> Option<SlotKey> {
if token == 0 {
return None;
}
if token == self.scanning_token {
return None;
}
let at = self.outstanding.iter().position(|(t, _)| *t == token)?;
Some(self.outstanding.remove(at).1)
}
pub fn retire_scanning(&mut self) {
self.scanning_token = 0;
}
pub fn reset(&mut self) {
let mut pending = self.lock();
*pending = Pending::default();
drop(pending);
self.scanning_key = None;
self.scanning_token = 0;
self.outstanding.clear();
self.carried_damage.clear();
self.carried_whole_frame = false;
}
fn carry(&mut self, damage: &[DamageRect]) {
if damage.is_empty() || self.carried_damage.len() + damage.len() > MAX_DAMAGE {
self.carried_damage.clear();
self.carried_damage.shrink_to_fit();
self.carried_whole_frame = true;
return;
}
self.carried_damage.extend_from_slice(damage);
}
fn lock(&self) -> std::sync::MutexGuard<'_, Pending> {
self.pending
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn rect(x: i32, y: i32) -> DamageRect {
DamageRect { x, y, w: 8, h: 8 }
}
#[test]
fn a_dropped_whole_frame_forces_the_next_frame_to_whole_frame() {
let mut p = RingPresenter::new(Some(Duration::from_millis(1)));
p.submit(0, None, &[]);
let first = p.acquire();
assert_eq!(first.kind, Present::Fresh);
p.release(first.token);
let (read, _write) = std::io::pipe().expect("a pipe stands in for a fence");
let fence = SyncFence::import(std::os::fd::AsFd::as_fd(&read)).expect("import");
p.submit(1, Some(fence), &[]);
let held = p.acquire();
assert_eq!(held.kind, Present::Hold, "the deadline miss holds");
p.release(held.token);
p.submit(1, None, &[rect(5, 5)]);
let advanced = p.acquire();
assert_eq!(advanced.kind, Present::Fresh);
assert!(
advanced.damage.is_empty(),
"a rect list here leaves everything outside it stale"
);
}
#[test]
fn a_dropped_partial_frame_is_carried_into_the_next_frame_only() {
let mut p = RingPresenter::new(Some(Duration::from_millis(1)));
p.submit(0, None, &[]);
let token = p.acquire().token;
p.release(token);
let (read, _write) = std::io::pipe().expect("a pipe stands in for a fence");
let fence = SyncFence::import(std::os::fd::AsFd::as_fd(&read)).expect("import");
p.submit(1, Some(fence), &[rect(1, 1)]);
let held = p.acquire();
assert_eq!(held.kind, Present::Hold);
p.release(held.token);
p.submit(1, None, &[rect(5, 5)]);
let advanced = p.acquire();
assert_eq!(
advanced.damage,
vec![rect(1, 1), rect(5, 5)],
"the dropped region is still stale, so it repaints with this frame"
);
p.release(advanced.token);
p.submit(0, None, &[rect(5, 5)]);
let after = p.acquire();
assert_eq!(
after.damage,
vec![rect(5, 5)],
"the carry is spent; repeating it would over-report from here on"
);
}
#[test]
fn a_presenter_with_no_frame_yet_presents_nothing() {
let mut p = RingPresenter::new(None);
let out = p.acquire();
assert_eq!(out.kind, Present::None);
assert_eq!(out.token, 0, "the sentinel token, which release ignores");
assert!(!p.has_fresh_frame());
}
#[test]
fn a_submitted_frame_is_presented_once() {
let mut p = RingPresenter::new(None);
p.submit(3, None, &[]);
assert!(p.has_fresh_frame());
let first = p.acquire();
assert_eq!(first.kind, Present::Fresh);
assert_eq!(first.key, 3);
assert_ne!(first.token, 0);
assert!(!p.has_fresh_frame(), "the pending frame was taken");
let second = p.acquire();
assert_eq!(second.kind, Present::Hold);
assert_eq!(second.key, 3);
assert_eq!(
second.token, first.token,
"a hold re-presents under the live token, or the buffer on screen \
would look superseded and be released out from under the display"
);
}
#[test]
fn idle_holds_do_not_accumulate() {
let mut p = RingPresenter::new(None);
p.submit(0, None, &[]);
let token = p.acquire().token;
for _ in 0..100 {
assert_eq!(p.acquire().kind, Present::Hold);
}
assert_eq!(p.outstanding.len(), 1, "one live token, however many holds");
assert_eq!(p.release(token), None, "still on screen");
}
#[test]
fn a_buffer_is_released_when_superseded() {
let mut p = RingPresenter::new(None);
p.submit(0, None, &[]);
let first = p.acquire().token;
assert_eq!(
p.release(first),
None,
"releasing the buffer on screen would race the producer into \
overwriting live scanout"
);
p.submit(1, None, &[]);
let second = p.acquire().token;
assert_ne!(second, first);
assert_eq!(p.release(first), Some(0), "slot 0 is now off screen");
assert_eq!(p.release(first), None, "and is not released twice");
assert_eq!(p.release(second), None, "the new one is still up");
}
#[test]
fn a_retired_presenter_releases_its_scanning_buffer() {
let mut p = RingPresenter::new(None);
p.submit(3, None, &[]);
let live = p.acquire().token;
let hold = p.acquire().token;
assert_eq!(hold, live, "an idle hold shares the live token");
assert_eq!(p.release(live), None, "still on screen");
p.retire_scanning();
assert_eq!(
p.release(hold),
Some(3),
"no newer frame is coming, so the producer would never get it back"
);
assert_eq!(p.release(hold), None, "and not twice");
}
#[test]
fn the_sentinel_token_releases_nothing() {
let mut p = RingPresenter::new(None);
assert_eq!(p.release(0), None);
}
#[test]
fn a_buffer_is_referenced_until_retired() {
let mut p = RingPresenter::new(None);
p.submit(5, None, &[]);
let first = p.acquire().token;
assert!(p.is_referenced(5));
p.submit(6, None, &[]);
p.acquire();
assert!(p.is_referenced(5), "off screen but not yet retired");
assert!(p.is_referenced(6));
assert_eq!(p.release(first), Some(5));
assert!(!p.is_referenced(5), "retired, so a pool may now evict it");
}
#[test]
fn a_second_submit_replaces_an_unacquired_first() {
let mut p = RingPresenter::new(None);
p.submit(1, None, &[]);
p.submit(2, None, &[]);
let out = p.acquire();
assert_eq!(out.key, 2);
assert_eq!(p.acquire().kind, Present::Hold, "only one frame was queued");
}
#[test]
fn damage_replaces_across_submits() {
let mut p = RingPresenter::new(None);
p.submit(0, None, &[rect(0, 0), rect(8, 0)]);
p.submit(0, None, &[rect(16, 0)]);
let out = p.acquire();
assert_eq!(out.damage, vec![rect(16, 0)]);
}
#[test]
fn over_cap_damage_becomes_whole_frame() {
let mut p = RingPresenter::new(None);
let many: Vec<DamageRect> = (0..=MAX_DAMAGE)
.map(|i| rect(i32::try_from(i).expect("small") * 8, 0))
.collect();
p.submit(0, None, &many);
let out = p.acquire();
assert!(
out.damage.is_empty(),
"an over-cap list must degrade to whole-frame, not truncate to \
{MAX_DAMAGE}"
);
}
#[test]
fn a_reset_forgets_the_scanning_buffer() {
let mut p = RingPresenter::new(None);
p.submit(4, None, &[]);
p.acquire();
assert_eq!(p.scanning_key(), Some(4));
p.reset();
assert_eq!(p.scanning_key(), None);
assert!(!p.is_referenced(4));
assert_eq!(
p.acquire().kind,
Present::None,
"after a resume nothing is on screen to hold, so there is nothing \
to present until the producer submits again"
);
}
#[test]
fn a_wrapping_token_skips_the_sentinel() {
let mut p = RingPresenter::new(None);
p.next_token = u64::MAX;
p.submit(0, None, &[]);
let out = p.acquire();
assert_ne!(out.token, 0, "the sentinel must be skipped on wrap");
assert_eq!(out.token, 1);
}
}