use dispatch2::{DispatchRetained, DispatchSemaphore, DispatchTime};
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};
pub(super) struct FrameInFlight {
semaphore: DispatchRetained<DispatchSemaphore>,
}
impl FrameInFlight {
pub(super) fn new(depth: usize) -> Self {
Self {
semaphore: DispatchSemaphore::new(depth.max(1) as isize),
}
}
pub(super) fn acquire(&self) -> FrameSlot {
let semaphore = self.semaphore.clone();
let _ = semaphore.wait(DispatchTime::FOREVER);
FrameSlot {
semaphore: Some(semaphore),
}
}
#[cfg(test)]
fn has_free_slot(&self) -> bool {
if self.semaphore.wait(DispatchTime::NOW) == 0 {
self.semaphore.signal();
true
} else {
false
}
}
}
pub(super) struct FrameSlot {
semaphore: Option<DispatchRetained<DispatchSemaphore>>,
}
impl FrameSlot {
fn into_gpu_release(mut self) -> DispatchRetained<DispatchSemaphore> {
self.semaphore
.take()
.expect("FrameSlot::into_gpu_release called exactly once")
}
pub(super) fn into_join(
self,
on_last: Box<dyn FnOnce() + Send>,
) -> (std::sync::Arc<FrameJoin>, SubmissionToken) {
let join = std::sync::Arc::new(FrameJoin {
semaphore: self.into_gpu_release(),
remaining: AtomicUsize::new(1),
on_last: Mutex::new(Some(on_last)),
});
(std::sync::Arc::clone(&join), SubmissionToken(join))
}
}
impl Drop for FrameSlot {
fn drop(&mut self) {
if let Some(semaphore) = self.semaphore.take() {
semaphore.signal();
}
}
}
pub(super) struct FrameJoin {
semaphore: DispatchRetained<DispatchSemaphore>,
remaining: AtomicUsize,
on_last: Mutex<Option<Box<dyn FnOnce() + Send>>>,
}
impl FrameJoin {
pub(super) fn add_part(&self) {
self.remaining.fetch_add(1, Ordering::Relaxed);
}
pub(super) fn arrive(&self) {
if self.remaining.fetch_sub(1, Ordering::AcqRel) != 1 {
return;
}
if let Some(on_last) = self.on_last.lock().ok().and_then(|mut slot| slot.take()) {
on_last();
}
self.semaphore.signal();
}
}
pub(super) struct SubmissionToken(std::sync::Arc<FrameJoin>);
impl Drop for SubmissionToken {
fn drop(&mut self) {
self.0.arrive();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn acquire_consumes_seeded_slots() {
let fif = FrameInFlight::new(2);
let _a = fif.acquire();
let _b = fif.acquire();
assert!(!fif.has_free_slot());
}
#[test]
fn drop_releases_on_abandon() {
let fif = FrameInFlight::new(1);
{
let _slot = fif.acquire();
assert!(!fif.has_free_slot(), "slot should be held while alive");
}
assert!(fif.has_free_slot(), "Drop did not release the slot");
}
#[test]
fn gpu_handoff_releases_exactly_once() {
let fif = FrameInFlight::new(1);
let slot = fif.acquire();
let sem = slot.into_gpu_release();
assert!(
!fif.has_free_slot(),
"into_gpu_release double-released via Drop"
);
sem.signal();
let taken = fif.acquire();
assert!(!fif.has_free_slot(), "slot was released more than once");
drop(taken);
}
#[test]
fn the_join_releases_only_after_every_part_arrives() {
let fif = FrameInFlight::new(1);
let ran = std::sync::Arc::new(AtomicUsize::new(0));
let flag = std::sync::Arc::clone(&ran);
let (join, token) = fif.acquire().into_join(Box::new(move || {
flag.fetch_add(1, Ordering::Relaxed);
}));
join.add_part();
join.add_part();
drop(token);
join.arrive();
assert!(!fif.has_free_slot(), "released with a part outstanding");
assert_eq!(ran.load(Ordering::Relaxed), 0);
join.arrive();
assert_eq!(ran.load(Ordering::Relaxed), 1, "completion work ran once");
assert!(fif.has_free_slot(), "the last arrival did not release");
let taken = fif.acquire();
assert!(!fif.has_free_slot(), "slot was released more than once");
drop(taken);
}
#[test]
fn an_abandoned_frame_releases_through_the_token_alone() {
let fif = FrameInFlight::new(1);
let (_join, token) = fif.acquire().into_join(Box::new(|| {}));
assert!(!fif.has_free_slot());
drop(token);
assert!(fif.has_free_slot(), "the token did not release the slot");
}
#[test]
fn zero_depth_clamps_to_one() {
let fif = FrameInFlight::new(0);
assert!(fif.has_free_slot(), "depth 0 should clamp to 1 usable slot");
let taken = fif.acquire();
assert!(!fif.has_free_slot());
drop(taken);
}
}