Skip to main content

oximedia_gpu/
gpu_fence.rs

1//! GPU fence (synchronization primitive) management for `oximedia-gpu`.
2//!
3//! Provides a CPU-side model for GPU fences used to track command completion,
4//! plus a pool that hands out and collects fences for reuse.
5
6#![allow(dead_code)]
7
8use std::time::{Duration, Instant};
9
10/// Lifecycle state of a GPU fence.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum FenceStatus {
13    /// Fence has been created but not yet signalled.
14    Pending,
15    /// GPU has finished executing up to the signalled point.
16    Signalled,
17    /// Fence was reset after being signalled; can be reused.
18    Reset,
19    /// Fence timed out while waiting.
20    TimedOut,
21}
22
23impl FenceStatus {
24    /// Returns `true` when execution up to the fence point has completed.
25    #[must_use]
26    pub fn is_complete(&self) -> bool {
27        matches!(self, Self::Signalled)
28    }
29
30    /// Returns `true` when the fence can be submitted again.
31    #[must_use]
32    pub fn is_reusable(&self) -> bool {
33        matches!(self, Self::Reset | Self::TimedOut)
34    }
35}
36
37/// A logical GPU fence (CPU-side descriptor).
38///
39/// In real GPU code this would wrap a `wgpu::QuerySet` or a Vulkan `VkFence`;
40/// here it tracks status in CPU memory.
41#[derive(Debug, Clone)]
42pub struct GpuFence {
43    /// Unique identifier.
44    pub id: u64,
45    /// Current status.
46    pub status: FenceStatus,
47    /// Optional debug label.
48    pub label: Option<String>,
49    /// Simulated "signal time" used in wait operations.
50    signal_time: Option<Instant>,
51    /// Simulated GPU latency (how long after signal we consider it "done").
52    simulated_latency: Duration,
53}
54
55impl GpuFence {
56    /// Creates a new fence in `Pending` state.
57    #[must_use]
58    pub fn new(id: u64) -> Self {
59        Self {
60            id,
61            status: FenceStatus::Pending,
62            label: None,
63            signal_time: None,
64            simulated_latency: Duration::from_millis(1),
65        }
66    }
67
68    /// Attaches a debug label.
69    #[must_use]
70    pub fn with_label(mut self, label: impl Into<String>) -> Self {
71        self.label = Some(label.into());
72        self
73    }
74
75    /// Signals the fence, marking GPU work as submitted.
76    ///
77    /// In a real implementation this would record the fence into a command
78    /// buffer; here we just record the signal time.
79    pub fn signal(&mut self) {
80        self.signal_time = Some(Instant::now());
81        self.status = FenceStatus::Signalled;
82    }
83
84    /// Returns `true` when the fence has been signalled.
85    #[must_use]
86    pub fn is_signalled(&self) -> bool {
87        self.status.is_complete()
88    }
89
90    /// Resets the fence so it can be reused.
91    pub fn reset(&mut self) {
92        self.status = FenceStatus::Reset;
93        self.signal_time = None;
94    }
95
96    /// Blocks (simulated) until the fence is signalled or `timeout_ms`
97    /// milliseconds elapse.
98    ///
99    /// Returns `true` if the fence was already signalled (no real blocking in
100    /// this CPU-only simulation).
101    #[allow(clippy::cast_precision_loss)]
102    pub fn wait_timeout_ms(&mut self, timeout_ms: u64) -> bool {
103        match self.status {
104            FenceStatus::Signalled => true,
105            FenceStatus::Pending => {
106                if let Some(t) = self.signal_time {
107                    let elapsed = t.elapsed();
108                    if elapsed >= self.simulated_latency {
109                        self.status = FenceStatus::Signalled;
110                        return true;
111                    }
112                }
113                // Check timeout
114                let _ = timeout_ms; // In simulation we never actually sleep
115                self.status = FenceStatus::TimedOut;
116                false
117            }
118            FenceStatus::Reset | FenceStatus::TimedOut => false,
119        }
120    }
121
122    /// Returns the simulated latency of this fence.
123    #[must_use]
124    pub fn simulated_latency(&self) -> Duration {
125        self.simulated_latency
126    }
127}
128
129/// A pool that manages a collection of [`GpuFence`] objects.
130#[derive(Debug, Default)]
131pub struct GpuFencePool {
132    next_id: u64,
133    active: Vec<GpuFence>,
134    free_list: Vec<GpuFence>,
135}
136
137impl GpuFencePool {
138    /// Creates a new, empty fence pool.
139    #[must_use]
140    pub fn new() -> Self {
141        Self::default()
142    }
143
144    /// Creates or recycles a fence and returns it in `Pending` state.
145    pub fn create_fence(&mut self) -> GpuFence {
146        if let Some(mut f) = self.free_list.pop() {
147            f.reset();
148            f.status = FenceStatus::Pending;
149            self.active.push(f.clone());
150            return f;
151        }
152        let id = self.next_id;
153        self.next_id += 1;
154        let fence = GpuFence::new(id);
155        self.active.push(fence.clone());
156        fence
157    }
158
159    /// Returns a fence to the pool.
160    pub fn return_fence(&mut self, fence: GpuFence) {
161        self.active.retain(|f| f.id != fence.id);
162        self.free_list.push(fence);
163    }
164
165    /// Returns the number of active (in-use) fences.
166    #[must_use]
167    pub fn active_count(&self) -> usize {
168        self.active.len()
169    }
170
171    /// Returns the number of completed (signalled) fences among active ones.
172    #[must_use]
173    pub fn completed_count(&self) -> usize {
174        self.active.iter().filter(|f| f.is_signalled()).count()
175    }
176
177    /// Returns the total number of fences ever created by this pool.
178    #[must_use]
179    pub fn total_created(&self) -> u64 {
180        self.next_id
181    }
182
183    /// Returns a reference to all currently active fences.
184    #[must_use]
185    pub fn active_fences(&self) -> &[GpuFence] {
186        &self.active
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    #[test]
195    fn test_fence_status_is_complete_signalled() {
196        assert!(FenceStatus::Signalled.is_complete());
197    }
198
199    #[test]
200    fn test_fence_status_is_complete_pending_false() {
201        assert!(!FenceStatus::Pending.is_complete());
202    }
203
204    #[test]
205    fn test_fence_status_is_reusable_reset() {
206        assert!(FenceStatus::Reset.is_reusable());
207    }
208
209    #[test]
210    fn test_fence_status_is_reusable_signalled_false() {
211        assert!(!FenceStatus::Signalled.is_reusable());
212    }
213
214    #[test]
215    fn test_gpu_fence_new_pending() {
216        let f = GpuFence::new(0);
217        assert_eq!(f.status, FenceStatus::Pending);
218        assert!(!f.is_signalled());
219    }
220
221    #[test]
222    fn test_gpu_fence_signal_sets_status() {
223        let mut f = GpuFence::new(1);
224        f.signal();
225        assert!(f.is_signalled());
226        assert_eq!(f.status, FenceStatus::Signalled);
227    }
228
229    #[test]
230    fn test_gpu_fence_reset_clears_signal() {
231        let mut f = GpuFence::new(2);
232        f.signal();
233        f.reset();
234        assert_eq!(f.status, FenceStatus::Reset);
235        assert!(f.signal_time.is_none());
236    }
237
238    #[test]
239    fn test_gpu_fence_wait_when_signalled_returns_true() {
240        let mut f = GpuFence::new(3);
241        f.signal();
242        assert!(f.wait_timeout_ms(100));
243    }
244
245    #[test]
246    fn test_gpu_fence_wait_timeout_sets_timed_out() {
247        let mut f = GpuFence::new(4);
248        // Pending and no signal_time set → timeout
249        let result = f.wait_timeout_ms(0);
250        assert!(!result);
251        assert_eq!(f.status, FenceStatus::TimedOut);
252    }
253
254    #[test]
255    fn test_gpu_fence_with_label() {
256        let f = GpuFence::new(5).with_label("frame_complete");
257        assert_eq!(f.label.as_deref(), Some("frame_complete"));
258    }
259
260    #[test]
261    fn test_pool_create_fence_pending() {
262        let mut pool = GpuFencePool::new();
263        let f = pool.create_fence();
264        assert_eq!(f.status, FenceStatus::Pending);
265    }
266
267    #[test]
268    fn test_pool_active_count_increments() {
269        let mut pool = GpuFencePool::new();
270        pool.create_fence();
271        pool.create_fence();
272        assert_eq!(pool.active_count(), 2);
273    }
274
275    #[test]
276    fn test_pool_completed_count_after_signal() {
277        let mut pool = GpuFencePool::new();
278        let mut f = pool.create_fence();
279        f.signal();
280        // Update pool's internal copy by re-inserting (pool holds clone)
281        // completed_count inspects pool.active; signal happened on detached copy.
282        // This tests that completed_count works on in-pool signalled fences.
283        pool.active
284            .iter_mut()
285            .find(|x| x.id == f.id)
286            .expect("operation should succeed in test")
287            .signal();
288        assert_eq!(pool.completed_count(), 1);
289    }
290
291    #[test]
292    fn test_pool_return_fence_moves_to_free_list() {
293        let mut pool = GpuFencePool::new();
294        let f = pool.create_fence();
295        pool.return_fence(f);
296        assert_eq!(pool.active_count(), 0);
297    }
298
299    #[test]
300    fn test_pool_total_created_monotonic() {
301        let mut pool = GpuFencePool::new();
302        pool.create_fence();
303        pool.create_fence();
304        assert_eq!(pool.total_created(), 2);
305    }
306}