Skip to main content

edgefirst_tensor/
pbo.rs

1// SPDX-FileCopyrightText: Copyright 2025 Au-Zone Technologies
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::{
5    error::{Error, Result},
6    BufferIdentity, TensorMap, TensorMapTrait, TensorMemory, TensorTrait,
7};
8use log::trace;
9use num_traits::Num;
10use std::{
11    ffi::c_void,
12    fmt,
13    marker::PhantomData,
14    ops::{Deref, DerefMut},
15    ptr::NonNull,
16    sync::{
17        atomic::{AtomicBool, Ordering},
18        Arc, Mutex,
19    },
20};
21
22/// Raw mapped pointer from a PBO. CPU-accessible while the buffer is mapped.
23/// The pointer is only valid between map and unmap calls.
24pub struct PboMapping {
25    pub ptr: *mut u8,
26    pub size: usize,
27}
28
29// SAFETY: PboMapping is only created by PboOps::map_buffer which runs on the
30// GL thread, but the resulting pointer is used on the caller's thread. This is
31// safe because glMapBufferRange returns a CPU-visible pointer that can be
32// accessed from any thread while the buffer remains mapped.
33unsafe impl Send for PboMapping {}
34
35/// Trait for PBO GL operations, implemented by the image crate.
36///
37/// All methods are blocking — they send commands to the GL thread
38/// and wait for completion. Implementations must ensure GL context
39/// is current on the thread that executes the actual GL calls.
40///
41/// # Safety
42///
43/// Implementations must ensure:
44/// - `map_buffer` returns a valid, aligned pointer to `size` bytes of
45///   CPU-accessible memory that remains valid until `unmap_buffer` is called.
46/// - `unmap_buffer` invalidates the pointer and releases the mapping.
47/// - `delete_buffer` frees the GL buffer resources.
48pub unsafe trait PboOps: Send + Sync {
49    /// Map the PBO for CPU read/write access.
50    /// The returned PboMapping is valid until `unmap_buffer` is called.
51    fn map_buffer(&self, buffer_id: u32, size: usize) -> Result<PboMapping>;
52
53    /// Unmap a previously mapped PBO. Must be called before GL operations
54    /// on this buffer (GLES 3.0 requirement).
55    fn unmap_buffer(&self, buffer_id: u32) -> Result<()>;
56
57    /// Delete the PBO. Fire-and-forget — no reply needed.
58    /// Called from PboTensor's Drop impl.
59    fn delete_buffer(&self, buffer_id: u32);
60}
61
62/// Opaque handle to a PBO's GL resources.
63struct PboHandle {
64    ops: Arc<dyn PboOps>,
65    buffer_id: u32,
66    size: usize,
67    mapped: AtomicBool,
68}
69
70impl Drop for PboHandle {
71    fn drop(&mut self) {
72        self.ops.delete_buffer(self.buffer_id);
73    }
74}
75
76/// A tensor backed by an OpenGL Pixel Buffer Object.
77pub struct PboTensor<T>
78where
79    T: Num + Clone + fmt::Debug + Send + Sync,
80{
81    pub name: String,
82    pub shape: Vec<usize>,
83    handle: Arc<PboHandle>,
84    identity: BufferIdentity,
85    /// Byte offset of this tensor's window into the shared GL buffer. Non-zero
86    /// only for sub-views (`view`/`batch`), which share the `Arc<PboHandle>` and
87    /// `BufferIdentity` and address a sub-region by this offset — mirrors
88    /// `DmaTensor::mmap_offset` / `IoSurfaceTensor::view_offset`.
89    pub(crate) view_offset: usize,
90    _marker: PhantomData<T>,
91}
92
93impl<T> fmt::Debug for PboTensor<T>
94where
95    T: Num + Clone + fmt::Debug + Send + Sync,
96{
97    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98        f.debug_struct("PboTensor")
99            .field("name", &self.name)
100            .field("shape", &self.shape)
101            .field("buffer_id", &self.handle.buffer_id)
102            .field("size", &self.handle.size)
103            .finish()
104    }
105}
106
107unsafe impl<T> Send for PboTensor<T> where T: Num + Clone + fmt::Debug + Send + Sync {}
108unsafe impl<T> Sync for PboTensor<T> where T: Num + Clone + fmt::Debug + Send + Sync {}
109
110impl<T> PboTensor<T>
111where
112    T: Num + Clone + fmt::Debug + Send + Sync,
113{
114    /// Create a new PBO tensor from an already-allocated GL buffer.
115    ///
116    /// Called by the image crate after creating the PBO on the GL thread.
117    /// Users should not call this directly — use `ImageProcessor::create_image()`.
118    ///
119    /// # Errors
120    ///
121    /// Returns `Error::ShapeMismatch` if `size` does not equal
122    /// `shape.iter().product::<usize>() * std::mem::size_of::<T>()`.
123    /// Returns `Error::InvalidSize` if `size` is zero.
124    pub fn from_pbo(
125        buffer_id: u32,
126        size: usize,
127        shape: &[usize],
128        name: Option<&str>,
129        ops: Arc<dyn PboOps>,
130    ) -> Result<Self> {
131        if size == 0 {
132            return Err(Error::InvalidSize(0));
133        }
134        let expected = shape.iter().product::<usize>() * std::mem::size_of::<T>();
135        // Allow `size >= expected`: PBOs allocated with a 64-byte-aligned row
136        // stride may be larger than the shape product.  Reject only if the
137        // allocation is strictly smaller than the logical content.
138        if size < expected {
139            return Err(Error::ShapeMismatch(format!(
140                "PBO size {size} is smaller than shape {shape:?} * sizeof({}) = {expected}",
141                std::any::type_name::<T>(),
142            )));
143        }
144        let name = name.unwrap_or("pbo_tensor").to_owned();
145        Ok(Self {
146            name,
147            shape: shape.to_vec(),
148            handle: Arc::new(PboHandle {
149                ops,
150                buffer_id,
151                size,
152                mapped: AtomicBool::new(false),
153            }),
154            identity: BufferIdentity::new(),
155            view_offset: 0,
156            _marker: PhantomData,
157        })
158    }
159
160    /// Returns the GL buffer ID for this PBO.
161    pub fn buffer_id(&self) -> u32 {
162        self.handle.buffer_id
163    }
164
165    /// Returns true if the PBO is currently mapped for CPU access.
166    pub fn is_mapped(&self) -> bool {
167        self.handle.mapped.load(Ordering::Acquire)
168    }
169}
170
171impl<T> TensorTrait<T> for PboTensor<T>
172where
173    T: Num + Clone + fmt::Debug + Send + Sync,
174{
175    fn new(_shape: &[usize], _name: Option<&str>) -> Result<Self> {
176        Err(Error::NotImplemented(
177            "PboTensor cannot be created directly — use ImageProcessor::create_image()".to_owned(),
178        ))
179    }
180
181    #[cfg(unix)]
182    fn from_fd(_fd: std::os::fd::OwnedFd, _shape: &[usize], _name: Option<&str>) -> Result<Self> {
183        Err(Error::NotImplemented(
184            "PboTensor does not support from_fd".to_owned(),
185        ))
186    }
187
188    #[cfg(unix)]
189    fn clone_fd(&self) -> Result<std::os::fd::OwnedFd> {
190        Err(Error::NotImplemented(
191            "PboTensor does not support clone_fd".to_owned(),
192        ))
193    }
194
195    fn memory(&self) -> TensorMemory {
196        TensorMemory::Pbo
197    }
198
199    fn name(&self) -> String {
200        self.name.clone()
201    }
202
203    fn shape(&self) -> &[usize] {
204        &self.shape
205    }
206
207    fn reshape(&mut self, shape: &[usize]) -> Result<()> {
208        if shape.is_empty() {
209            return Err(Error::InvalidSize(0));
210        }
211        let new_size = shape.iter().product::<usize>() * std::mem::size_of::<T>();
212        if new_size != self.handle.size {
213            return Err(Error::ShapeMismatch(format!(
214                "Cannot reshape incompatible shape: {:?} to {:?}",
215                self.shape, shape
216            )));
217        }
218        self.shape = shape.to_vec();
219        self.view_offset = 0;
220        Ok(())
221    }
222
223    fn capacity_bytes(&self) -> usize {
224        self.handle.size
225    }
226
227    /// Capacity-based reconfigure (mirrors Mem/Shm/DMA/IOSurface): allow any
228    /// shape whose byte size fits the PBO allocation, so an oversized reusable
229    /// pool can be `configure_image`d to a smaller image. Without this PBO fell
230    /// back to the strict-`reshape` default and rejected pool reuse.
231    fn set_logical_shape(&mut self, shape: &[usize]) -> Result<()> {
232        if shape.is_empty() {
233            return Err(Error::InvalidSize(0));
234        }
235        let needed = shape.iter().product::<usize>() * std::mem::size_of::<T>();
236        if needed > self.handle.size {
237            return Err(Error::InsufficientCapacity {
238                needed,
239                capacity: self.handle.size,
240            });
241        }
242        self.shape = shape.to_vec();
243        Ok(())
244    }
245
246    fn map(&self) -> Result<TensorMap<T>> {
247        self.map_internal(None)
248    }
249
250    fn buffer_identity(&self) -> &BufferIdentity {
251        &self.identity
252    }
253
254    /// Zero-copy sub-region view sharing this PBO's GL buffer (via the
255    /// `Arc<PboHandle>`) and [`BufferIdentity`], positioned at `offset_bytes`
256    /// from this tensor's own window with logical `shape`. The GL backend keys
257    /// the import on the shared identity and addresses the window via
258    /// `glViewport` / the staged copy; a CPU map adds `view_offset` to the
259    /// mapped base. Mirrors [`DmaTensor::view`](crate::TensorTrait::view).
260    ///
261    /// # Errors
262    ///
263    /// - [`Error::InvalidOperation`] if `offset_bytes` is mis-aligned for `T`.
264    /// - [`Error::InsufficientCapacity`] if the window exceeds the allocation.
265    fn view(&self, offset_bytes: usize, shape: &[usize]) -> Result<Self> {
266        if !offset_bytes.is_multiple_of(std::mem::align_of::<T>()) {
267            return Err(Error::InvalidOperation(format!(
268                "PboTensor::view: offset {offset_bytes} not aligned to align_of::<T>()={}",
269                std::mem::align_of::<T>()
270            )));
271        }
272        let abs_offset = self
273            .view_offset
274            .checked_add(offset_bytes)
275            .ok_or(Error::InvalidSize(offset_bytes))?;
276        let logical = shape.iter().product::<usize>() * std::mem::size_of::<T>();
277        let needed = abs_offset
278            .checked_add(logical)
279            .ok_or(Error::InvalidSize(logical))?;
280        if needed > self.handle.size {
281            return Err(Error::InsufficientCapacity {
282                needed,
283                capacity: self.handle.size,
284            });
285        }
286        Ok(Self {
287            name: self.name.clone(),
288            shape: shape.to_vec(),
289            handle: Arc::clone(&self.handle),
290            identity: self.identity.clone(),
291            view_offset: abs_offset,
292            _marker: PhantomData,
293        })
294    }
295}
296
297impl<T> PboTensor<T>
298where
299    T: Num + Clone + fmt::Debug + Send + Sync,
300{
301    /// Map the PBO so `as_slice()` exposes the full padded buffer (`byte_size`
302    /// bytes) rather than the shape-derived logical count. Mirrors
303    /// [`DmaTensor::map_with_byte_size`]: a CPU producer (e.g. the JPEG decoder)
304    /// or a strided convert source iterates rows via `effective_row_stride()`
305    /// without running past the slice. Crate-private; the only caller is
306    /// `Tensor::map()`, which already checks `byte_size <= capacity_bytes()`.
307    pub(crate) fn map_with_byte_size(&self, byte_size: usize) -> Result<TensorMap<T>> {
308        self.map_internal(Some(byte_size))
309    }
310
311    fn map_internal(&self, byte_size_override: Option<usize>) -> Result<TensorMap<T>> {
312        if self.handle.mapped.swap(true, Ordering::AcqRel) {
313            return Err(Error::PboMapped);
314        }
315        // Always map the full GL allocation (`handle.size`); the slice length is
316        // narrowed by `byte_size_override` (or the logical shape) at access time.
317        match self
318            .handle
319            .ops
320            .map_buffer(self.handle.buffer_id, self.handle.size)
321        {
322            Ok(mapping) => {
323                let pbo_ptr = PboPtr(
324                    NonNull::new(mapping.ptr as *mut c_void)
325                        .ok_or(Error::InvalidSize(self.handle.size))?,
326                );
327                Ok(TensorMap::Pbo(PboMap {
328                    ptr: Arc::new(Mutex::new(pbo_ptr)),
329                    shape: self.shape.clone(),
330                    handle: Arc::clone(&self.handle),
331                    byte_size_override,
332                    view_offset: self.view_offset,
333                    _marker: PhantomData,
334                }))
335            }
336            Err(e) => {
337                self.handle.mapped.store(false, Ordering::Release);
338                Err(e)
339            }
340        }
341    }
342}
343
344// -- PboMap --
345
346#[derive(Debug)]
347struct PboPtr(NonNull<c_void>);
348
349impl Deref for PboPtr {
350    type Target = NonNull<c_void>;
351    fn deref(&self) -> &Self::Target {
352        &self.0
353    }
354}
355
356unsafe impl Send for PboPtr {}
357
358pub struct PboMap<T>
359where
360    T: Num + Clone + fmt::Debug,
361{
362    ptr: Arc<Mutex<PboPtr>>,
363    shape: Vec<usize>,
364    handle: Arc<PboHandle>,
365    /// Optional override for `as_slice().len()`. `None` → `shape.product()`
366    /// elements (logical view). `Some(bytes)` → `bytes / sizeof(T)` elements,
367    /// exposing the full padded GL allocation so callers can iterate rows via
368    /// `row_stride` (set by `Tensor::map()` for strided PBO tensors). Mirrors
369    /// `DmaMap::byte_size_override`.
370    byte_size_override: Option<usize>,
371    /// Byte offset of the sub-view window into the mapped GL buffer. `as_slice`
372    /// advances the base pointer by this many bytes before exposing the slice.
373    view_offset: usize,
374    _marker: PhantomData<T>,
375}
376
377impl<T> fmt::Debug for PboMap<T>
378where
379    T: Num + Clone + fmt::Debug,
380{
381    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
382        f.debug_struct("PboMap")
383            .field("shape", &self.shape)
384            .field("buffer_id", &self.handle.buffer_id)
385            .finish()
386    }
387}
388
389impl<T> TensorMapTrait<T> for PboMap<T>
390where
391    T: Num + Clone + fmt::Debug,
392{
393    fn shape(&self) -> &[usize] {
394        &self.shape
395    }
396
397    fn unmap(&mut self) {
398        trace!("Unmapping PboMap buffer_id={}", self.handle.buffer_id);
399        if let Err(e) = self.handle.ops.unmap_buffer(self.handle.buffer_id) {
400            log::warn!("Failed to unmap PBO buffer {}: {e}", self.handle.buffer_id);
401        }
402        self.handle.mapped.store(false, Ordering::Release);
403    }
404
405    fn as_slice(&self) -> &[T] {
406        let ptr = self.ptr.lock().expect("Failed to lock PboMap pointer");
407        let base = unsafe { (ptr.as_ptr() as *const u8).add(self.view_offset) as *const T };
408        unsafe { std::slice::from_raw_parts(base, self.slice_len_elems()) }
409    }
410
411    fn as_mut_slice(&mut self) -> &mut [T] {
412        let ptr = self.ptr.lock().expect("Failed to lock PboMap pointer");
413        let base = unsafe { (ptr.as_ptr() as *mut u8).add(self.view_offset) as *mut T };
414        unsafe { std::slice::from_raw_parts_mut(base, self.slice_len_elems()) }
415    }
416}
417
418impl<T> PboMap<T>
419where
420    T: Num + Clone + fmt::Debug,
421{
422    /// Number of `T` elements exposed by `as_slice()`. Honours
423    /// `byte_size_override` (the full padded GL allocation) when set; otherwise
424    /// the shape-derived logical count. Mirrors `DmaMap::slice_len_elems`.
425    fn slice_len_elems(&self) -> usize {
426        match self.byte_size_override {
427            Some(bytes) => bytes / std::mem::size_of::<T>(),
428            None => self.shape.iter().product(),
429        }
430    }
431}
432
433impl<T> Deref for PboMap<T>
434where
435    T: Num + Clone + fmt::Debug,
436{
437    type Target = [T];
438    fn deref(&self) -> &[T] {
439        self.as_slice()
440    }
441}
442
443impl<T> DerefMut for PboMap<T>
444where
445    T: Num + Clone + fmt::Debug,
446{
447    fn deref_mut(&mut self) -> &mut [T] {
448        self.as_mut_slice()
449    }
450}
451
452impl<T> Drop for PboMap<T>
453where
454    T: Num + Clone + fmt::Debug,
455{
456    fn drop(&mut self) {
457        self.unmap();
458    }
459}
460
461impl<T> Clone for PboTensor<T>
462where
463    T: Num + Clone + fmt::Debug + Send + Sync,
464{
465    fn clone(&self) -> Self {
466        Self {
467            name: self.name.clone(),
468            shape: self.shape.clone(),
469            handle: Arc::clone(&self.handle),
470            identity: self.identity.clone(),
471            view_offset: self.view_offset,
472            _marker: PhantomData,
473        }
474    }
475}
476
477#[cfg(test)]
478mod tests {
479    use super::*;
480
481    /// Mock PboOps that uses a Vec<u8> as backing storage instead of GL.
482    struct MockPboOps {
483        storage: Mutex<Vec<u8>>,
484    }
485
486    impl MockPboOps {
487        fn new(size: usize) -> Arc<Self> {
488            Arc::new(Self {
489                storage: Mutex::new(vec![0u8; size]),
490            })
491        }
492    }
493
494    // SAFETY: MockPboOps returns a valid pointer to a Vec<u8> that remains
495    // valid while the Mutex is held (tests are single-threaded).
496    unsafe impl PboOps for MockPboOps {
497        fn map_buffer(&self, _buffer_id: u32, size: usize) -> Result<PboMapping> {
498            let storage = self.storage.lock().expect("lock");
499            assert_eq!(storage.len(), size);
500            Ok(PboMapping {
501                ptr: storage.as_ptr() as *mut u8,
502                size,
503            })
504        }
505
506        fn unmap_buffer(&self, _buffer_id: u32) -> Result<()> {
507            Ok(())
508        }
509
510        fn delete_buffer(&self, _buffer_id: u32) {}
511    }
512
513    #[test]
514    fn test_pbo_tensor_create_and_metadata() {
515        let ops = MockPboOps::new(24);
516        let tensor = PboTensor::<u8>::from_pbo(42, 24, &[2, 3, 4], Some("test_pbo"), ops).unwrap();
517        assert_eq!(tensor.memory(), TensorMemory::Pbo);
518        assert_eq!(tensor.name(), "test_pbo");
519        assert_eq!(tensor.shape(), &[2, 3, 4]);
520        assert_eq!(tensor.buffer_id(), 42);
521        assert!(!tensor.is_mapped());
522    }
523
524    #[test]
525    fn test_pbo_tensor_map_write_read() {
526        let ops = MockPboOps::new(12);
527        let tensor = PboTensor::<u8>::from_pbo(1, 12, &[3, 4], Some("rw_test"), ops).unwrap();
528        {
529            let mut map = tensor.map().expect("map should succeed");
530            assert_eq!(map.shape(), &[3, 4]);
531            assert!(tensor.is_mapped());
532            map.as_mut_slice().fill(0xAB);
533            assert!(map.as_slice().iter().all(|&b| b == 0xAB));
534        }
535        assert!(!tensor.is_mapped());
536    }
537
538    #[test]
539    fn test_pbo_tensor_double_map_fails() {
540        let ops = MockPboOps::new(8);
541        let tensor = PboTensor::<u8>::from_pbo(2, 8, &[8], None, ops).unwrap();
542        let _map1 = tensor.map().expect("first map should succeed");
543        assert!(tensor.is_mapped());
544        let result = tensor.map();
545        assert!(result.is_err(), "second map while mapped should fail");
546    }
547
548    #[test]
549    fn test_pbo_tensor_reshape() {
550        let ops = MockPboOps::new(24);
551        let mut tensor = PboTensor::<u8>::from_pbo(3, 24, &[2, 3, 4], None, ops).unwrap();
552        tensor
553            .reshape(&[4, 6])
554            .expect("compatible reshape should succeed");
555        assert_eq!(tensor.shape(), &[4, 6]);
556        let result = tensor.reshape(&[100]);
557        assert!(result.is_err(), "incompatible reshape should fail");
558    }
559
560    #[test]
561    fn test_pbo_tensor_set_logical_shape_capacity_based() {
562        // A 24-byte PBO can be reconfigured to any shape that fits (unlike the
563        // strict `reshape`), so an oversized reusable pool can be
564        // `configure_image`d to a smaller image (the native-chroma decode pool).
565        let ops = MockPboOps::new(24);
566        let mut tensor = PboTensor::<u8>::from_pbo(7, 24, &[24], None, ops).unwrap();
567        // Smaller-than-capacity logical shape is accepted (reshape would reject).
568        tensor
569            .set_logical_shape(&[4, 5])
570            .expect("shape within capacity should succeed");
571        assert_eq!(tensor.shape(), &[4, 5]);
572        // Exactly-capacity is fine.
573        tensor.set_logical_shape(&[24]).unwrap();
574        // Over-capacity is rejected.
575        assert!(
576            tensor.set_logical_shape(&[5, 5]).is_err(),
577            "shape exceeding PBO capacity must be rejected"
578        );
579    }
580
581    #[test]
582    fn test_pbo_tensor_buffer_identity() {
583        let ops1 = MockPboOps::new(8);
584        let ops2 = MockPboOps::new(8);
585        let t1 = PboTensor::<u8>::from_pbo(1, 8, &[8], None, ops1).unwrap();
586        let t2 = PboTensor::<u8>::from_pbo(2, 8, &[8], None, ops2).unwrap();
587        assert_ne!(t1.buffer_identity().id(), t2.buffer_identity().id());
588    }
589
590    #[test]
591    fn test_pbo_tensor_new_returns_error() {
592        let result = PboTensor::<u8>::new(&[8], None);
593        assert!(result.is_err(), "PboTensor::new() should fail");
594    }
595
596    #[cfg(unix)]
597    #[test]
598    fn test_pbo_tensor_fd_ops_return_error() {
599        let ops = MockPboOps::new(8);
600        let tensor = PboTensor::<u8>::from_pbo(1, 8, &[8], None, ops).unwrap();
601        assert!(tensor.clone_fd().is_err());
602    }
603
604    #[test]
605    fn test_pbo_tensor_from_pbo_size_mismatch() {
606        let ops = MockPboOps::new(24);
607        let result = PboTensor::<u8>::from_pbo(1, 24, &[2, 3, 5], None, ops);
608        assert!(result.is_err(), "mismatched size/shape should fail");
609    }
610
611    #[test]
612    fn test_pbo_tensor_from_pbo_zero_size() {
613        let ops = MockPboOps::new(0);
614        let result = PboTensor::<u8>::from_pbo(1, 0, &[0], None, ops);
615        assert!(result.is_err(), "zero size should fail");
616    }
617
618    #[test]
619    fn test_pbo_via_tensor_enum() {
620        let ops = MockPboOps::new(12);
621        let pbo = PboTensor::<u8>::from_pbo(10, 12, &[3, 4], Some("enum_test"), ops).unwrap();
622        let tensor = crate::Tensor::wrap(crate::TensorStorage::Pbo(pbo));
623        assert_eq!(tensor.memory(), TensorMemory::Pbo);
624        assert_eq!(tensor.name(), "enum_test");
625        assert_eq!(tensor.shape(), &[3, 4]);
626        let mut map = tensor.map().expect("map via enum");
627        map.as_mut_slice().fill(42);
628        assert!(map.as_slice().iter().all(|&b| b == 42));
629    }
630}