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_with(&self, access: crate::CpuAccess) -> Result<TensorMap<T>> {
247        self.map_internal(None, access)
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(
308        &self,
309        byte_size: usize,
310        access: crate::CpuAccess,
311    ) -> Result<TensorMap<T>> {
312        self.map_internal(Some(byte_size), access)
313    }
314
315    fn map_internal(
316        &self,
317        byte_size_override: Option<usize>,
318        access: crate::CpuAccess,
319    ) -> Result<TensorMap<T>> {
320        if self.handle.mapped.swap(true, Ordering::AcqRel) {
321            return Err(Error::PboMapped);
322        }
323        // Always map the full GL allocation (`handle.size`); the slice length is
324        // narrowed by `byte_size_override` (or the logical shape) at access time.
325        match self
326            .handle
327            .ops
328            .map_buffer(self.handle.buffer_id, self.handle.size)
329        {
330            Ok(mapping) => {
331                let pbo_ptr = PboPtr(
332                    NonNull::new(mapping.ptr as *mut c_void)
333                        .ok_or(Error::InvalidSize(self.handle.size))?,
334                );
335                Ok(TensorMap::Pbo(PboMap {
336                    ptr: Arc::new(Mutex::new(pbo_ptr)),
337                    shape: self.shape.clone(),
338                    handle: Arc::clone(&self.handle),
339                    byte_size_override,
340                    view_offset: self.view_offset,
341                    writable: access.writes(),
342                    _marker: PhantomData,
343                }))
344            }
345            Err(e) => {
346                self.handle.mapped.store(false, Ordering::Release);
347                Err(e)
348            }
349        }
350    }
351}
352
353// -- PboMap --
354
355#[derive(Debug)]
356struct PboPtr(NonNull<c_void>);
357
358impl Deref for PboPtr {
359    type Target = NonNull<c_void>;
360    fn deref(&self) -> &Self::Target {
361        &self.0
362    }
363}
364
365unsafe impl Send for PboPtr {}
366
367pub struct PboMap<T>
368where
369    T: Num + Clone + fmt::Debug,
370{
371    ptr: Arc<Mutex<PboPtr>>,
372    shape: Vec<usize>,
373    handle: Arc<PboHandle>,
374    /// Optional override for `as_slice().len()`. `None` → `shape.product()`
375    /// elements (logical view). `Some(bytes)` → `bytes / sizeof(T)` elements,
376    /// exposing the full padded GL allocation so callers can iterate rows via
377    /// `row_stride` (set by `Tensor::map()` for strided PBO tensors). Mirrors
378    /// `DmaMap::byte_size_override`.
379    byte_size_override: Option<usize>,
380    /// Byte offset of the sub-view window into the mapped GL buffer. `as_slice`
381    /// advances the base pointer by this many bytes before exposing the slice.
382    view_offset: usize,
383    /// Whether mutable access is permitted (`map_read()` maps are not).
384    /// The GL mapping itself stays MAP_READ|MAP_WRITE (bit narrowing is a
385    /// follow-up); this enforces the API contract uniformly.
386    writable: bool,
387    _marker: PhantomData<T>,
388}
389
390impl<T> fmt::Debug for PboMap<T>
391where
392    T: Num + Clone + fmt::Debug,
393{
394    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
395        f.debug_struct("PboMap")
396            .field("shape", &self.shape)
397            .field("buffer_id", &self.handle.buffer_id)
398            .finish()
399    }
400}
401
402impl<T> TensorMapTrait<T> for PboMap<T>
403where
404    T: Num + Clone + fmt::Debug,
405{
406    fn shape(&self) -> &[usize] {
407        &self.shape
408    }
409
410    fn unmap(&mut self) {
411        trace!("Unmapping PboMap buffer_id={}", self.handle.buffer_id);
412        if let Err(e) = self.handle.ops.unmap_buffer(self.handle.buffer_id) {
413            log::warn!("Failed to unmap PBO buffer {}: {e}", self.handle.buffer_id);
414        }
415        self.handle.mapped.store(false, Ordering::Release);
416    }
417
418    fn as_slice(&self) -> &[T] {
419        let ptr = self.ptr.lock().expect("Failed to lock PboMap pointer");
420        let base = unsafe { (ptr.as_ptr() as *const u8).add(self.view_offset) as *const T };
421        unsafe { std::slice::from_raw_parts(base, self.slice_len_elems()) }
422    }
423
424    fn as_mut_slice(&mut self) -> &mut [T] {
425        crate::assert_map_writable(self.writable, "Pbo");
426        let ptr = self.ptr.lock().expect("Failed to lock PboMap pointer");
427        let base = unsafe { (ptr.as_ptr() as *mut u8).add(self.view_offset) as *mut T };
428        unsafe { std::slice::from_raw_parts_mut(base, self.slice_len_elems()) }
429    }
430}
431
432impl<T> PboMap<T>
433where
434    T: Num + Clone + fmt::Debug,
435{
436    /// Number of `T` elements exposed by `as_slice()`. Honours
437    /// `byte_size_override` (the full padded GL allocation) when set; otherwise
438    /// the shape-derived logical count. Mirrors `DmaMap::slice_len_elems`.
439    fn slice_len_elems(&self) -> usize {
440        match self.byte_size_override {
441            Some(bytes) => bytes / std::mem::size_of::<T>(),
442            None => self.shape.iter().product(),
443        }
444    }
445}
446
447impl<T> Deref for PboMap<T>
448where
449    T: Num + Clone + fmt::Debug,
450{
451    type Target = [T];
452    fn deref(&self) -> &[T] {
453        self.as_slice()
454    }
455}
456
457impl<T> DerefMut for PboMap<T>
458where
459    T: Num + Clone + fmt::Debug,
460{
461    fn deref_mut(&mut self) -> &mut [T] {
462        self.as_mut_slice()
463    }
464}
465
466impl<T> Drop for PboMap<T>
467where
468    T: Num + Clone + fmt::Debug,
469{
470    fn drop(&mut self) {
471        self.unmap();
472    }
473}
474
475impl<T> Clone for PboTensor<T>
476where
477    T: Num + Clone + fmt::Debug + Send + Sync,
478{
479    fn clone(&self) -> Self {
480        Self {
481            name: self.name.clone(),
482            shape: self.shape.clone(),
483            handle: Arc::clone(&self.handle),
484            identity: self.identity.clone(),
485            view_offset: self.view_offset,
486            _marker: PhantomData,
487        }
488    }
489}
490
491#[cfg(test)]
492mod tests {
493    use super::*;
494
495    /// Mock PboOps that uses a Vec<u8> as backing storage instead of GL.
496    struct MockPboOps {
497        storage: Mutex<Vec<u8>>,
498    }
499
500    impl MockPboOps {
501        fn new(size: usize) -> Arc<Self> {
502            Arc::new(Self {
503                storage: Mutex::new(vec![0u8; size]),
504            })
505        }
506    }
507
508    // SAFETY: MockPboOps returns a valid pointer to a Vec<u8> that remains
509    // valid while the Mutex is held (tests are single-threaded).
510    unsafe impl PboOps for MockPboOps {
511        fn map_buffer(&self, _buffer_id: u32, size: usize) -> Result<PboMapping> {
512            let storage = self.storage.lock().expect("lock");
513            assert_eq!(storage.len(), size);
514            Ok(PboMapping {
515                ptr: storage.as_ptr() as *mut u8,
516                size,
517            })
518        }
519
520        fn unmap_buffer(&self, _buffer_id: u32) -> Result<()> {
521            Ok(())
522        }
523
524        fn delete_buffer(&self, _buffer_id: u32) {}
525    }
526
527    #[test]
528    fn test_pbo_tensor_create_and_metadata() {
529        let ops = MockPboOps::new(24);
530        let tensor = PboTensor::<u8>::from_pbo(42, 24, &[2, 3, 4], Some("test_pbo"), ops).unwrap();
531        assert_eq!(tensor.memory(), TensorMemory::Pbo);
532        assert_eq!(tensor.name(), "test_pbo");
533        assert_eq!(tensor.shape(), &[2, 3, 4]);
534        assert_eq!(tensor.buffer_id(), 42);
535        assert!(!tensor.is_mapped());
536    }
537
538    #[test]
539    fn test_pbo_tensor_map_write_read() {
540        let ops = MockPboOps::new(12);
541        let tensor = PboTensor::<u8>::from_pbo(1, 12, &[3, 4], Some("rw_test"), ops).unwrap();
542        {
543            let mut map = tensor.map().expect("map should succeed");
544            assert_eq!(map.shape(), &[3, 4]);
545            assert!(tensor.is_mapped());
546            map.as_mut_slice().fill(0xAB);
547            assert!(map.as_slice().iter().all(|&b| b == 0xAB));
548        }
549        assert!(!tensor.is_mapped());
550    }
551
552    #[test]
553    fn test_pbo_tensor_double_map_fails() {
554        let ops = MockPboOps::new(8);
555        let tensor = PboTensor::<u8>::from_pbo(2, 8, &[8], None, ops).unwrap();
556        let _map1 = tensor.map().expect("first map should succeed");
557        assert!(tensor.is_mapped());
558        let result = tensor.map();
559        assert!(result.is_err(), "second map while mapped should fail");
560    }
561
562    #[test]
563    fn test_pbo_tensor_reshape() {
564        let ops = MockPboOps::new(24);
565        let mut tensor = PboTensor::<u8>::from_pbo(3, 24, &[2, 3, 4], None, ops).unwrap();
566        tensor
567            .reshape(&[4, 6])
568            .expect("compatible reshape should succeed");
569        assert_eq!(tensor.shape(), &[4, 6]);
570        let result = tensor.reshape(&[100]);
571        assert!(result.is_err(), "incompatible reshape should fail");
572    }
573
574    #[test]
575    fn test_pbo_tensor_set_logical_shape_capacity_based() {
576        // A 24-byte PBO can be reconfigured to any shape that fits (unlike the
577        // strict `reshape`), so an oversized reusable pool can be
578        // `configure_image`d to a smaller image (the native-chroma decode pool).
579        let ops = MockPboOps::new(24);
580        let mut tensor = PboTensor::<u8>::from_pbo(7, 24, &[24], None, ops).unwrap();
581        // Smaller-than-capacity logical shape is accepted (reshape would reject).
582        tensor
583            .set_logical_shape(&[4, 5])
584            .expect("shape within capacity should succeed");
585        assert_eq!(tensor.shape(), &[4, 5]);
586        // Exactly-capacity is fine.
587        tensor.set_logical_shape(&[24]).unwrap();
588        // Over-capacity is rejected.
589        assert!(
590            tensor.set_logical_shape(&[5, 5]).is_err(),
591            "shape exceeding PBO capacity must be rejected"
592        );
593    }
594
595    #[test]
596    fn test_pbo_tensor_buffer_identity() {
597        let ops1 = MockPboOps::new(8);
598        let ops2 = MockPboOps::new(8);
599        let t1 = PboTensor::<u8>::from_pbo(1, 8, &[8], None, ops1).unwrap();
600        let t2 = PboTensor::<u8>::from_pbo(2, 8, &[8], None, ops2).unwrap();
601        assert_ne!(t1.buffer_identity().id(), t2.buffer_identity().id());
602    }
603
604    #[test]
605    fn test_pbo_tensor_new_returns_error() {
606        let result = PboTensor::<u8>::new(&[8], None);
607        assert!(result.is_err(), "PboTensor::new() should fail");
608    }
609
610    #[cfg(unix)]
611    #[test]
612    fn test_pbo_tensor_fd_ops_return_error() {
613        let ops = MockPboOps::new(8);
614        let tensor = PboTensor::<u8>::from_pbo(1, 8, &[8], None, ops).unwrap();
615        assert!(tensor.clone_fd().is_err());
616    }
617
618    #[test]
619    fn test_pbo_tensor_from_pbo_size_mismatch() {
620        let ops = MockPboOps::new(24);
621        let result = PboTensor::<u8>::from_pbo(1, 24, &[2, 3, 5], None, ops);
622        assert!(result.is_err(), "mismatched size/shape should fail");
623    }
624
625    #[test]
626    fn test_pbo_tensor_from_pbo_zero_size() {
627        let ops = MockPboOps::new(0);
628        let result = PboTensor::<u8>::from_pbo(1, 0, &[0], None, ops);
629        assert!(result.is_err(), "zero size should fail");
630    }
631
632    #[test]
633    fn test_pbo_via_tensor_enum() {
634        let ops = MockPboOps::new(12);
635        let pbo = PboTensor::<u8>::from_pbo(10, 12, &[3, 4], Some("enum_test"), ops).unwrap();
636        let tensor = crate::Tensor::wrap(crate::TensorStorage::Pbo(pbo));
637        assert_eq!(tensor.memory(), TensorMemory::Pbo);
638        assert_eq!(tensor.name(), "enum_test");
639        assert_eq!(tensor.shape(), &[3, 4]);
640        let mut map = tensor.map().expect("map via enum");
641        map.as_mut_slice().fill(42);
642        assert!(map.as_slice().iter().all(|&b| b == 42));
643    }
644}