Skip to main content

oximedia_gpu/
fence_pool.rs

1#![allow(dead_code)]
2//! GPU fence pool for efficient synchronization primitive reuse.
3//!
4//! This module provides a pooled allocation strategy for GPU fences,
5//! reducing the overhead of creating and destroying fence objects
6//! on every frame submission.
7
8use std::collections::VecDeque;
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::time::{Duration, Instant};
11
12/// Unique identifier for a pooled fence.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub struct FenceId(
15    /// Inner identifier value.
16    pub u64,
17);
18
19/// Current status of a fence in the pool.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum FenceStatus {
22    /// Fence is available for reuse.
23    Available,
24    /// Fence has been submitted and is pending GPU completion.
25    Pending,
26    /// Fence has been signaled by the GPU.
27    Signaled,
28    /// Fence encountered an error.
29    Error,
30}
31
32impl std::fmt::Display for FenceStatus {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        match self {
35            Self::Available => write!(f, "Available"),
36            Self::Pending => write!(f, "Pending"),
37            Self::Signaled => write!(f, "Signaled"),
38            Self::Error => write!(f, "Error"),
39        }
40    }
41}
42
43/// A single fence entry in the pool.
44#[derive(Debug, Clone)]
45pub struct PooledFence {
46    /// Unique identifier.
47    pub id: FenceId,
48    /// Current status of this fence.
49    pub status: FenceStatus,
50    /// Timestamp when the fence was submitted.
51    pub submit_time: Option<Instant>,
52    /// Timestamp when the fence was signaled.
53    pub signal_time: Option<Instant>,
54    /// Number of times this fence has been recycled.
55    pub recycle_count: u64,
56}
57
58impl PooledFence {
59    /// Create a new pooled fence with the given ID.
60    #[must_use]
61    pub fn new(id: FenceId) -> Self {
62        Self {
63            id,
64            status: FenceStatus::Available,
65            submit_time: None,
66            signal_time: None,
67            recycle_count: 0,
68        }
69    }
70
71    /// Return the latency between submit and signal, if both are recorded.
72    #[must_use]
73    pub fn latency(&self) -> Option<Duration> {
74        match (self.submit_time, self.signal_time) {
75            (Some(submit), Some(signal)) => Some(signal.duration_since(submit)),
76            _ => None,
77        }
78    }
79
80    /// Check if the fence is currently available for reuse.
81    #[must_use]
82    pub fn is_available(&self) -> bool {
83        self.status == FenceStatus::Available
84    }
85
86    /// Check if the fence is pending GPU completion.
87    #[must_use]
88    pub fn is_pending(&self) -> bool {
89        self.status == FenceStatus::Pending
90    }
91}
92
93/// Configuration for the fence pool.
94#[derive(Debug, Clone)]
95pub struct FencePoolConfig {
96    /// Initial number of fences to pre-allocate.
97    pub initial_size: usize,
98    /// Maximum number of fences allowed in the pool.
99    pub max_size: usize,
100    /// Timeout for fence wait operations.
101    pub wait_timeout: Duration,
102    /// Whether to auto-grow the pool when exhausted.
103    pub auto_grow: bool,
104    /// Batch size for growing the pool.
105    pub grow_batch_size: usize,
106}
107
108impl Default for FencePoolConfig {
109    fn default() -> Self {
110        Self {
111            initial_size: 16,
112            max_size: 256,
113            wait_timeout: Duration::from_secs(5),
114            auto_grow: true,
115            grow_batch_size: 8,
116        }
117    }
118}
119
120/// Statistics about fence pool usage.
121#[derive(Debug, Clone, Default)]
122pub struct FencePoolStats {
123    /// Total number of fences in the pool.
124    pub total_fences: usize,
125    /// Number of fences currently available.
126    pub available_count: usize,
127    /// Number of fences currently pending.
128    pub pending_count: usize,
129    /// Number of fences currently signaled.
130    pub signaled_count: usize,
131    /// Total number of allocations performed.
132    pub total_allocations: u64,
133    /// Total number of recycles performed.
134    pub total_recycles: u64,
135    /// Number of times the pool had to grow.
136    pub grow_events: u64,
137    /// Average latency of signaled fences in microseconds.
138    pub avg_latency_us: f64,
139}
140
141impl FencePoolStats {
142    /// Return the utilization ratio of the pool (pending / total).
143    #[allow(clippy::cast_precision_loss)]
144    #[must_use]
145    pub fn utilization(&self) -> f64 {
146        if self.total_fences == 0 {
147            return 0.0;
148        }
149        self.pending_count as f64 / self.total_fences as f64
150    }
151}
152
153/// A pool of reusable GPU fence objects.
154///
155/// Manages fence lifecycle to avoid repeated allocation/deallocation overhead.
156pub struct FencePool {
157    /// All fences in the pool.
158    fences: Vec<PooledFence>,
159    /// Queue of available fence indices.
160    available: VecDeque<usize>,
161    /// Configuration for this pool.
162    config: FencePoolConfig,
163    /// Counter for generating unique fence IDs.
164    next_id: AtomicU64,
165    /// Total allocations performed.
166    total_allocations: u64,
167    /// Total recycles performed.
168    total_recycles: u64,
169    /// Number of grow events.
170    grow_events: u64,
171}
172
173impl FencePool {
174    /// Create a new fence pool with default configuration.
175    #[must_use]
176    pub fn new() -> Self {
177        Self::with_config(FencePoolConfig::default())
178    }
179
180    /// Create a new fence pool with the given configuration.
181    #[must_use]
182    pub fn with_config(config: FencePoolConfig) -> Self {
183        let mut pool = Self {
184            fences: Vec::with_capacity(config.initial_size),
185            available: VecDeque::with_capacity(config.initial_size),
186            next_id: AtomicU64::new(0),
187            total_allocations: 0,
188            total_recycles: 0,
189            grow_events: 0,
190            config,
191        };
192        let initial = pool.config.initial_size;
193        pool.grow(initial);
194        pool
195    }
196
197    /// Grow the pool by the specified number of fences.
198    fn grow(&mut self, count: usize) {
199        let max = self.config.max_size;
200        let current = self.fences.len();
201        let actual_count = count.min(max.saturating_sub(current));
202        for _ in 0..actual_count {
203            let id = FenceId(self.next_id.fetch_add(1, Ordering::Relaxed));
204            let fence = PooledFence::new(id);
205            let index = self.fences.len();
206            self.fences.push(fence);
207            self.available.push_back(index);
208        }
209        if actual_count > 0 {
210            self.grow_events += 1;
211        }
212    }
213
214    /// Acquire a fence from the pool. Returns `None` if no fences are available
215    /// and auto-grow is disabled or the pool is at maximum size.
216    pub fn acquire(&mut self) -> Option<FenceId> {
217        if self.available.is_empty() && self.config.auto_grow {
218            let batch = self.config.grow_batch_size;
219            self.grow(batch);
220        }
221        let index = self.available.pop_front()?;
222        let fence = &mut self.fences[index];
223        fence.status = FenceStatus::Pending;
224        fence.submit_time = Some(Instant::now());
225        fence.signal_time = None;
226        self.total_allocations += 1;
227        Some(fence.id)
228    }
229
230    /// Signal that a fence has completed on the GPU.
231    pub fn signal(&mut self, id: FenceId) -> bool {
232        if let Some(fence) = self.fences.iter_mut().find(|f| f.id == id) {
233            fence.status = FenceStatus::Signaled;
234            fence.signal_time = Some(Instant::now());
235            true
236        } else {
237            false
238        }
239    }
240
241    /// Release a fence back to the pool for reuse.
242    pub fn release(&mut self, id: FenceId) -> bool {
243        if let Some((index, fence)) = self.fences.iter_mut().enumerate().find(|(_, f)| f.id == id) {
244            fence.status = FenceStatus::Available;
245            fence.submit_time = None;
246            fence.signal_time = None;
247            fence.recycle_count += 1;
248            self.available.push_back(index);
249            self.total_recycles += 1;
250            true
251        } else {
252            false
253        }
254    }
255
256    /// Get the current status of a fence.
257    pub fn status(&self, id: FenceId) -> Option<FenceStatus> {
258        self.fences.iter().find(|f| f.id == id).map(|f| f.status)
259    }
260
261    /// Return all currently pending fence IDs.
262    pub fn pending_fences(&self) -> Vec<FenceId> {
263        self.fences
264            .iter()
265            .filter(|f| f.status == FenceStatus::Pending)
266            .map(|f| f.id)
267            .collect()
268    }
269
270    /// Signal all pending fences and release them back to the pool.
271    pub fn flush_all(&mut self) {
272        let pending_ids: Vec<FenceId> = self.pending_fences();
273        for id in &pending_ids {
274            self.signal(*id);
275        }
276        let signaled_ids: Vec<FenceId> = self
277            .fences
278            .iter()
279            .filter(|f| f.status == FenceStatus::Signaled)
280            .map(|f| f.id)
281            .collect();
282        for id in signaled_ids {
283            self.release(id);
284        }
285    }
286
287    /// Return the total number of fences in the pool.
288    pub fn total_count(&self) -> usize {
289        self.fences.len()
290    }
291
292    /// Return the number of available fences.
293    pub fn available_count(&self) -> usize {
294        self.available.len()
295    }
296
297    /// Compute pool statistics.
298    #[allow(clippy::cast_precision_loss)]
299    pub fn stats(&self) -> FencePoolStats {
300        let pending_count = self
301            .fences
302            .iter()
303            .filter(|f| f.status == FenceStatus::Pending)
304            .count();
305        let signaled_count = self
306            .fences
307            .iter()
308            .filter(|f| f.status == FenceStatus::Signaled)
309            .count();
310
311        let latencies: Vec<f64> = self
312            .fences
313            .iter()
314            .filter_map(PooledFence::latency)
315            .map(|d| d.as_micros() as f64)
316            .collect();
317        let avg_latency_us = if latencies.is_empty() {
318            0.0
319        } else {
320            latencies.iter().sum::<f64>() / latencies.len() as f64
321        };
322
323        FencePoolStats {
324            total_fences: self.fences.len(),
325            available_count: self.available.len(),
326            pending_count,
327            signaled_count,
328            total_allocations: self.total_allocations,
329            total_recycles: self.total_recycles,
330            grow_events: self.grow_events,
331            avg_latency_us,
332        }
333    }
334
335    /// Reset the pool, marking all fences as available.
336    pub fn reset(&mut self) {
337        self.available.clear();
338        for (i, fence) in self.fences.iter_mut().enumerate() {
339            fence.status = FenceStatus::Available;
340            fence.submit_time = None;
341            fence.signal_time = None;
342            self.available.push_back(i);
343        }
344    }
345
346    /// Return the wait timeout configured for this pool.
347    pub fn wait_timeout(&self) -> Duration {
348        self.config.wait_timeout
349    }
350
351    /// Check whether the pool has fences available without growing.
352    pub fn has_available(&self) -> bool {
353        !self.available.is_empty()
354    }
355}
356
357impl Default for FencePool {
358    fn default() -> Self {
359        Self::new()
360    }
361}
362
363#[cfg(test)]
364mod tests {
365    use super::*;
366
367    #[test]
368    fn test_create_default_pool() {
369        let pool = FencePool::new();
370        assert_eq!(pool.total_count(), 16);
371        assert_eq!(pool.available_count(), 16);
372    }
373
374    #[test]
375    fn test_create_with_config() {
376        let config = FencePoolConfig {
377            initial_size: 4,
378            max_size: 32,
379            auto_grow: true,
380            grow_batch_size: 4,
381            ..Default::default()
382        };
383        let pool = FencePool::with_config(config);
384        assert_eq!(pool.total_count(), 4);
385        assert_eq!(pool.available_count(), 4);
386    }
387
388    #[test]
389    fn test_acquire_fence() {
390        let mut pool = FencePool::new();
391        let id = pool.acquire();
392        assert!(id.is_some());
393        assert_eq!(pool.available_count(), 15);
394    }
395
396    #[test]
397    fn test_signal_fence() {
398        let mut pool = FencePool::new();
399        let id = pool.acquire().expect("fence acquire should succeed");
400        assert!(pool.signal(id));
401        assert_eq!(pool.status(id), Some(FenceStatus::Signaled));
402    }
403
404    #[test]
405    fn test_release_fence() {
406        let mut pool = FencePool::new();
407        let initial_available = pool.available_count();
408        let id = pool.acquire().expect("fence acquire should succeed");
409        assert_eq!(pool.available_count(), initial_available - 1);
410        pool.signal(id);
411        pool.release(id);
412        assert_eq!(pool.available_count(), initial_available);
413        assert_eq!(pool.status(id), Some(FenceStatus::Available));
414    }
415
416    #[test]
417    fn test_pending_fences() {
418        let mut pool = FencePool::new();
419        let id1 = pool.acquire().expect("fence acquire should succeed");
420        let id2 = pool.acquire().expect("fence acquire should succeed");
421        let pending = pool.pending_fences();
422        assert_eq!(pending.len(), 2);
423        assert!(pending.contains(&id1));
424        assert!(pending.contains(&id2));
425    }
426
427    #[test]
428    fn test_flush_all() {
429        let mut pool = FencePool::new();
430        let _id1 = pool.acquire().expect("fence acquire should succeed");
431        let _id2 = pool.acquire().expect("fence acquire should succeed");
432        assert_eq!(pool.pending_fences().len(), 2);
433        pool.flush_all();
434        assert_eq!(pool.pending_fences().len(), 0);
435        assert_eq!(pool.available_count(), 16);
436    }
437
438    #[test]
439    fn test_auto_grow() {
440        let config = FencePoolConfig {
441            initial_size: 2,
442            max_size: 10,
443            auto_grow: true,
444            grow_batch_size: 3,
445            ..Default::default()
446        };
447        let mut pool = FencePool::with_config(config);
448        let _id1 = pool.acquire().expect("fence acquire should succeed");
449        let _id2 = pool.acquire().expect("fence acquire should succeed");
450        // Pool exhausted, should auto-grow
451        let id3 = pool.acquire();
452        assert!(id3.is_some());
453        assert!(pool.total_count() > 2);
454    }
455
456    #[test]
457    fn test_max_size_limit() {
458        let config = FencePoolConfig {
459            initial_size: 2,
460            max_size: 3,
461            auto_grow: true,
462            grow_batch_size: 10,
463            ..Default::default()
464        };
465        let mut pool = FencePool::with_config(config);
466        let _id1 = pool.acquire().expect("fence acquire should succeed");
467        let _id2 = pool.acquire().expect("fence acquire should succeed");
468        let _id3 = pool.acquire();
469        // Should not exceed max
470        assert!(pool.total_count() <= 3);
471    }
472
473    #[test]
474    fn test_no_auto_grow() {
475        let config = FencePoolConfig {
476            initial_size: 1,
477            max_size: 10,
478            auto_grow: false,
479            ..Default::default()
480        };
481        let mut pool = FencePool::with_config(config);
482        let _id = pool.acquire().expect("fence acquire should succeed");
483        let id2 = pool.acquire();
484        assert!(id2.is_none());
485    }
486
487    #[test]
488    fn test_stats() {
489        let mut pool = FencePool::new();
490        let id1 = pool.acquire().expect("fence acquire should succeed");
491        let _id2 = pool.acquire().expect("fence acquire should succeed");
492        pool.signal(id1);
493        let stats = pool.stats();
494        assert_eq!(stats.total_fences, 16);
495        assert_eq!(stats.pending_count, 1);
496        assert_eq!(stats.signaled_count, 1);
497        assert_eq!(stats.total_allocations, 2);
498    }
499
500    #[test]
501    fn test_stats_utilization() {
502        let stats = FencePoolStats {
503            total_fences: 10,
504            pending_count: 5,
505            ..Default::default()
506        };
507        let util = stats.utilization();
508        assert!((util - 0.5).abs() < f64::EPSILON);
509    }
510
511    #[test]
512    fn test_stats_utilization_empty() {
513        let stats = FencePoolStats::default();
514        assert!((stats.utilization() - 0.0).abs() < f64::EPSILON);
515    }
516
517    #[test]
518    fn test_reset_pool() {
519        let mut pool = FencePool::new();
520        let _id1 = pool.acquire().expect("fence acquire should succeed");
521        let _id2 = pool.acquire().expect("fence acquire should succeed");
522        pool.reset();
523        assert_eq!(pool.available_count(), pool.total_count());
524    }
525
526    #[test]
527    fn test_fence_display() {
528        assert_eq!(format!("{}", FenceStatus::Available), "Available");
529        assert_eq!(format!("{}", FenceStatus::Pending), "Pending");
530        assert_eq!(format!("{}", FenceStatus::Signaled), "Signaled");
531        assert_eq!(format!("{}", FenceStatus::Error), "Error");
532    }
533
534    #[test]
535    fn test_pooled_fence_latency() {
536        let mut fence = PooledFence::new(FenceId(0));
537        assert!(fence.latency().is_none());
538        fence.submit_time = Some(Instant::now());
539        assert!(fence.latency().is_none());
540        fence.signal_time = Some(Instant::now());
541        assert!(fence.latency().is_some());
542    }
543
544    #[test]
545    fn test_wait_timeout() {
546        let pool = FencePool::new();
547        assert_eq!(pool.wait_timeout(), Duration::from_secs(5));
548    }
549
550    #[test]
551    fn test_has_available() {
552        let config = FencePoolConfig {
553            initial_size: 1,
554            max_size: 1,
555            auto_grow: false,
556            ..Default::default()
557        };
558        let mut pool = FencePool::with_config(config);
559        assert!(pool.has_available());
560        let _id = pool.acquire().expect("fence acquire should succeed");
561        assert!(!pool.has_available());
562    }
563}