Skip to main content

softgpu_core/
signal.rs

1//! SoftGPU host signals (CPU atomics). No hardware interrupts.
2//!
3//! # Concurrency invariants
4//!
5//! - `value` and `cancelled` are atomics; waiters poll both.
6//! - Destroy / queue teardown sets `cancelled` so waits exit deterministically
7//!   without requiring the process mutex while spinning.
8//! - SoftGPU does not claim HSA memory-model completeness for foreign AQL
9//!   producers; SeqCst is a SoftGPU software policy for host-side waits.
10
11use crate::handle::PackedHandle;
12use std::sync::atomic::{AtomicBool, AtomicI64, Ordering};
13use std::time::{Duration, Instant};
14
15/// HSA signal condition codes.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17#[repr(u32)]
18pub enum SignalCondition {
19    Eq = 0,
20    Ne = 1,
21    Lt = 2,
22    Gte = 3,
23}
24
25impl SignalCondition {
26    pub fn from_u32(value: u32) -> Option<Self> {
27        match value {
28            0 => Some(Self::Eq),
29            1 => Some(Self::Ne),
30            2 => Some(Self::Lt),
31            3 => Some(Self::Gte),
32            _ => None,
33        }
34    }
35
36    pub fn satisfied(self, observed: i64, compare: i64) -> bool {
37        match self {
38            Self::Eq => observed == compare,
39            Self::Ne => observed != compare,
40            Self::Lt => observed < compare,
41            Self::Gte => observed >= compare,
42        }
43    }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum SignalWaitOutcome {
48    Satisfied(i64),
49    TimedOut(i64),
50    Cancelled(i64),
51}
52
53#[derive(Debug)]
54pub struct SoftGpuSignal {
55    pub handle: PackedHandle,
56    value: AtomicI64,
57    cancelled: AtomicBool,
58    /// True when this signal is owned as a queue doorbell (observe stores).
59    pub is_doorbell: bool,
60    pub queue_id: Option<u64>,
61}
62
63impl SoftGpuSignal {
64    pub fn new(handle: PackedHandle, initial: i64) -> Self {
65        Self {
66            handle,
67            value: AtomicI64::new(initial),
68            cancelled: AtomicBool::new(false),
69            is_doorbell: false,
70            queue_id: None,
71        }
72    }
73
74    pub fn new_doorbell(handle: PackedHandle, initial: i64, queue_id: u64) -> Self {
75        Self {
76            handle,
77            value: AtomicI64::new(initial),
78            cancelled: AtomicBool::new(false),
79            is_doorbell: true,
80            queue_id: Some(queue_id),
81        }
82    }
83
84    pub fn load(&self) -> i64 {
85        self.value.load(Ordering::SeqCst)
86    }
87
88    pub fn store(&self, value: i64) {
89        self.value.store(value, Ordering::SeqCst);
90    }
91
92    pub fn cancel(&self) {
93        self.cancelled.store(true, Ordering::SeqCst);
94    }
95
96    pub fn is_cancelled(&self) -> bool {
97        self.cancelled.load(Ordering::SeqCst)
98    }
99
100    pub fn wait(
101        &self,
102        condition: SignalCondition,
103        compare: i64,
104        timeout_hint_ns: u64,
105        _wait_state_hint: u32,
106    ) -> SignalWaitOutcome {
107        let deadline = if timeout_hint_ns == u64::MAX {
108            None
109        } else {
110            Some(Instant::now() + Duration::from_nanos(timeout_hint_ns))
111        };
112        loop {
113            if self.is_cancelled() {
114                return SignalWaitOutcome::Cancelled(self.load());
115            }
116            let observed = self.load();
117            if condition.satisfied(observed, compare) {
118                return SignalWaitOutcome::Satisfied(observed);
119            }
120            if let Some(deadline) = deadline {
121                if Instant::now() >= deadline {
122                    return SignalWaitOutcome::TimedOut(observed);
123                }
124            }
125            std::thread::yield_now();
126        }
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use crate::handle::HandleKind;
134    use std::thread;
135    use std::time::Duration;
136
137    #[test]
138    fn wait_eq_returns_when_satisfied() {
139        let sig = SoftGpuSignal::new(PackedHandle::pack(HandleKind::Signal, 1, 0), 0);
140        sig.store(7);
141        match sig.wait(SignalCondition::Eq, 7, 1_000_000_000, 1) {
142            SignalWaitOutcome::Satisfied(v) => assert_eq!(v, 7),
143            other => panic!("unexpected {other:?}"),
144        }
145    }
146
147    #[test]
148    fn wait_times_out() {
149        let sig = SoftGpuSignal::new(PackedHandle::pack(HandleKind::Signal, 1, 0), 0);
150        match sig.wait(SignalCondition::Eq, 1, 1_000_000, 1) {
151            SignalWaitOutcome::TimedOut(v) => assert_eq!(v, 0),
152            other => panic!("unexpected {other:?}"),
153        }
154    }
155
156    #[test]
157    fn wait_cancelled_during_spin() {
158        let sig = SoftGpuSignal::new(PackedHandle::pack(HandleKind::Signal, 1, 0), 0);
159        let handle = sig.handle;
160        thread::scope(|s| {
161            s.spawn(|| {
162                thread::sleep(Duration::from_millis(5));
163                // Reconstruct via shared reference — signal lives on stack.
164                let _ = handle;
165            });
166            // Cancel from this thread after starting wait in child would need Arc;
167            // exercise cancel-before-wait and cancel-mid-wait with local cancel.
168            let sig_ref = &sig;
169            s.spawn(|| {
170                thread::sleep(Duration::from_millis(2));
171                sig_ref.cancel();
172            });
173            match sig_ref.wait(SignalCondition::Eq, 99, u64::MAX, 1) {
174                SignalWaitOutcome::Cancelled(_) => {}
175                other => panic!("unexpected {other:?}"),
176            }
177        });
178    }
179}