use alloc::format;
use alloc::vec::Vec;
use cubecl_environment::backtrace::BackTrace;
use cubecl_environment::stream::StreamId;
use cubecl_environment::sync::{Arc, AtomicUsize, Mutex, Ordering};
use super::ServerError;
#[derive(Debug, Clone, Default)]
pub struct DeviceCaptures(Arc<CaptureRecord>);
#[derive(Debug, Clone)]
pub struct CaptureStatus(Arc<CaptureRecord>);
#[derive(Debug)]
pub struct StreamCaptureState {
phase: CapturePhase,
device: CaptureStatus,
entry: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CaptureEnd {
Owned {
owner: StreamId,
},
Abandoned {
owner: StreamId,
},
}
impl CaptureEnd {
pub fn owner(&self) -> StreamId {
match self {
CaptureEnd::Owned { owner } | CaptureEnd::Abandoned { owner } => *owner,
}
}
pub fn is_abandoned(&self) -> bool {
matches!(self, CaptureEnd::Abandoned { .. })
}
pub fn abandoned_error(&self, caller: StreamId, doomed: Option<ServerError>) -> ServerError {
let mut errors = alloc::vec![ServerError::graph_state(format!(
"end_capture: the capture belongs to logical stream {:?}, not to {caller:?}; it is \
discarded rather than left recording on a stream both share",
self.owner(),
))];
errors.extend(doomed);
ServerError::Several {
errors,
backtrace: BackTrace::capture(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CapturePhase {
NoCapture,
Prepare {
owner: StreamId,
},
Capture {
owner: StreamId,
},
}
#[derive(Debug, Default)]
struct CaptureRecord {
active: AtomicUsize,
recording: AtomicUsize,
streams: Mutex<Vec<(usize, CapturePhase)>>,
next_entry: AtomicUsize,
}
impl DeviceCaptures {
pub fn stream(&self) -> StreamCaptureState {
StreamCaptureState {
phase: CapturePhase::NoCapture,
device: self.status(),
entry: self.0.next_entry.fetch_add(1, Ordering::Relaxed),
}
}
pub fn status(&self) -> CaptureStatus {
CaptureStatus(self.0.clone())
}
}
impl CaptureStatus {
#[inline]
pub fn any_active(&self) -> bool {
self.0.active.load(Ordering::Acquire) > 0
}
#[inline]
pub fn any_recording(&self) -> bool {
self.0.recording.load(Ordering::Acquire) > 0
}
pub fn is_capturing(&self, stream: StreamId) -> bool {
self.0
.streams
.lock()
.iter()
.any(|(_, phase)| phase.owner() == Some(stream))
}
}
impl StreamCaptureState {
pub fn is_recording(&self) -> bool {
matches!(self.phase, CapturePhase::Capture { .. })
}
pub fn is_preparing(&self) -> bool {
matches!(self.phase, CapturePhase::Prepare { .. })
}
pub fn is_active(&self) -> bool {
!matches!(self.phase, CapturePhase::NoCapture)
}
pub fn owner(&self) -> Option<StreamId> {
self.phase.owner()
}
pub fn device(&self) -> &CaptureStatus {
&self.device
}
pub fn prepare(&mut self, owner: StreamId) -> Result<(), ServerError> {
match self.phase {
CapturePhase::NoCapture => {
self.move_to(CapturePhase::Prepare { owner });
Ok(())
}
CapturePhase::Prepare { .. } => Err(ServerError::graph_state(
"graph_prepare: a graph capture is already prepared on this stream",
)),
CapturePhase::Capture { .. } => Err(ServerError::graph_state(
"graph_prepare: a graph capture is already recording on this stream",
)),
}
}
pub fn begin(&mut self) -> Result<(), ServerError> {
match self.phase {
CapturePhase::Prepare { owner } => {
self.move_to(CapturePhase::Capture { owner });
Ok(())
}
CapturePhase::NoCapture => Err(ServerError::graph_state(
"begin_capture: call graph_prepare before starting a capture",
)),
CapturePhase::Capture { .. } => Err(ServerError::graph_state(
"begin_capture: a graph capture is already recording on this stream",
)),
}
}
pub fn end(&mut self, caller: StreamId) -> Result<CaptureEnd, ServerError> {
match self.phase {
CapturePhase::Capture { owner } => {
self.move_to(CapturePhase::NoCapture);
Ok(match owner == caller {
true => CaptureEnd::Owned { owner },
false => CaptureEnd::Abandoned { owner },
})
}
CapturePhase::NoCapture | CapturePhase::Prepare { .. } => {
Err(ServerError::graph_state(
"end_capture: no graph capture is recording on this stream",
))
}
}
}
pub fn abort(&mut self) {
self.move_to(CapturePhase::NoCapture);
}
fn move_to(&mut self, phase: CapturePhase) {
self.phase = phase;
self.device.0.publish(self.entry, phase);
}
}
impl Drop for StreamCaptureState {
fn drop(&mut self) {
self.device.0.publish(self.entry, CapturePhase::NoCapture);
}
}
impl CapturePhase {
fn owner(&self) -> Option<StreamId> {
match self {
CapturePhase::NoCapture => None,
CapturePhase::Prepare { owner } | CapturePhase::Capture { owner } => Some(*owner),
}
}
}
impl CaptureRecord {
fn publish(&self, entry: usize, phase: CapturePhase) {
let mut streams = self.streams.lock();
streams.retain(|(id, _)| *id != entry);
if phase != CapturePhase::NoCapture {
streams.push((entry, phase));
}
let recording = streams
.iter()
.filter(|(_, phase)| matches!(phase, CapturePhase::Capture { .. }))
.count();
self.recording.store(recording, Ordering::Release);
self.active.store(streams.len(), Ordering::Release);
}
}
#[cfg(test)]
mod tests {
use super::*;
const OWNER: StreamId = StreamId { value: 7 };
#[test]
fn transitions_follow_the_capture_order() {
let mut state = DeviceCaptures::default().stream();
assert!(state.begin().is_err(), "a capture must be prepared first");
assert!(state.end(OWNER).is_err(), "nothing is recording yet");
assert!(!state.is_active());
state.prepare(OWNER).unwrap();
assert!(state.is_preparing() && state.owner() == Some(OWNER));
assert!(state.prepare(OWNER).is_err(), "one prepare per capture");
assert!(state.end(OWNER).is_err(), "the window never opened");
state.begin().unwrap();
assert!(state.is_recording() && state.owner() == Some(OWNER));
assert!(state.begin().is_err(), "captures may not overlap");
assert!(state.prepare(OWNER).is_err(), "captures may not overlap");
assert_eq!(
state.end(OWNER).unwrap(),
CaptureEnd::Owned { owner: OWNER }
);
assert!(!state.is_active());
}
#[test]
fn the_window_carries_its_owner() {
let mut state = DeviceCaptures::default().stream();
assert_eq!(state.owner(), None);
state.prepare(OWNER).unwrap();
assert_eq!(state.owner(), Some(OWNER));
assert!(state.is_active(), "the window is open from prepare on");
state.begin().unwrap();
assert_eq!(state.owner(), Some(OWNER));
assert_eq!(state.end(OWNER).unwrap().owner(), OWNER);
assert_eq!(state.owner(), None);
assert!(!state.is_active());
}
#[test]
fn only_the_stream_that_opened_a_capture_may_seal_it() {
let neighbour = StreamId { value: 8 };
let mut state = DeviceCaptures::default().stream();
state.prepare(OWNER).unwrap();
state.begin().unwrap();
assert_eq!(
state.end(neighbour).unwrap(),
CaptureEnd::Abandoned { owner: OWNER },
"the window is not theirs to seal"
);
}
#[test]
fn a_capture_no_one_can_close_does_not_wedge_the_stream() {
let neighbour = StreamId { value: 8 };
let mut state = DeviceCaptures::default().stream();
state.prepare(OWNER).unwrap();
state.begin().unwrap();
assert!(state.end(neighbour).unwrap().is_abandoned());
assert!(!state.is_active(), "the slot serves other work again");
state
.prepare(neighbour)
.expect("the stream is re-capturable");
state.begin().unwrap();
assert!(
state.end(OWNER).unwrap().is_abandoned(),
"the window it opened is long gone"
);
}
#[test]
fn an_abandoned_window_reports_whose_it_was_and_what_doomed_it() {
let caller = StreamId { value: 8 };
let outcome = CaptureEnd::Abandoned { owner: OWNER };
let error = outcome.abandoned_error(caller, Some(ServerError::graph_state("doomed")));
let ServerError::Several { errors, .. } = &error else {
panic!("an abandoned window reports several failures at once, got: {error:?}");
};
let reported = alloc::format!("{error}");
assert!(
reported.contains(&alloc::format!("{OWNER:?}"))
&& reported.contains(&alloc::format!("{caller:?}")),
"the report has to name the window's owner and the caller refused it, got: {reported}"
);
assert_eq!(errors.len(), 2, "the doomed reason travels with it");
assert!(
alloc::format!("{}", errors[1]).contains("doomed"),
"the explanation comes first, then what had already sunk it"
);
}
#[test]
fn a_rejected_transition_changes_nothing() {
let mut state = DeviceCaptures::default().stream();
state.prepare(OWNER).unwrap();
assert!(state.prepare(OWNER).is_err());
assert!(state.is_preparing() && state.owner() == Some(OWNER));
state.begin().unwrap();
}
#[test]
fn abort_recovers_a_window_that_never_opened() {
for recorded in [false, true] {
let mut state = DeviceCaptures::default().stream();
state.prepare(OWNER).unwrap();
if recorded {
state.begin().unwrap();
}
state.abort();
assert!(!state.is_active());
state.prepare(OWNER).expect("the stream is re-capturable");
}
}
#[test]
fn the_record_counts_every_recording_stream() {
let device = DeviceCaptures::default();
let status = device.status();
let mut first = device.stream();
let mut second = device.stream();
first.prepare(OWNER).unwrap();
first.begin().unwrap();
second.prepare(OWNER).unwrap();
assert!(
status.any_recording(),
"a second stream of the owner hides nothing"
);
second.begin().unwrap();
first.end(OWNER).unwrap();
assert!(
status.any_recording(),
"one stream ending leaves the other recording"
);
assert!(status.is_capturing(OWNER));
drop(second);
assert!(
!status.any_recording(),
"a dropped stream leaves the record"
);
assert!(!status.is_capturing(OWNER));
assert!(!status.any_active());
}
}