Skip to main content

edgefirst_tensor/
view.rs

1// SPDX-FileCopyrightText: Copyright 2026 Au-Zone Technologies
2// SPDX-License-Identifier: Apache-2.0
3
4//! One CPU-access window, for every backend.
5//!
6//! Replaces the six per-backend `*Map<T>` types. They differed in how they
7//! *obtained* an address — `mmap`, `IOSurfaceLock`, `glMapBufferRange`,
8//! `AHardwareBuffer_lock` — but once obtained they all carried the same five
9//! things: a base pointer, the shape, an offset, an optional padded extent, and
10//! whether writes are allowed. [`HostPin`] already absorbs the first and third
11//! (its address is offset-adjusted, its keepalive releases whatever the backend
12//! acquired), which leaves this type holding the other three.
13//!
14//! The acquisition difference does not need a type per backend, only a
15//! constructor per backend: persistent ones hand over a real pin, lock-scoped
16//! ones (PBO, AHardwareBuffer) hand over a pin whose keepalive unlocks on drop.
17//! Either way what the caller gets is the same window.
18
19use crate::{pin::HostPin, Num, TensorMapTrait};
20use std::fmt;
21use std::marker::PhantomData;
22
23/// A CPU-accessible window onto a tensor's memory.
24pub struct HostView<'a, T> {
25    /// `None` after [`unmap`](TensorMapTrait::unmap): dropping the pin is what
26    /// releases the backend's mapping or lock, so clearing it *is* the unmap.
27    pin: Option<HostPin<'a>>,
28    shape: Vec<usize>,
29    /// When `Some(bytes)`, expose `bytes / size_of::<T>()` elements instead of
30    /// the shape's product — the padded window a stride-aligned image needs so
31    /// CPU row iteration can honour `row_stride` without running past the end.
32    byte_size_override: Option<usize>,
33    /// Whether `as_mut_slice` is permitted. A read-only map keeps the mapping
34    /// itself readable/writable; this enforces the API contract uniformly, as
35    /// the per-backend maps did.
36    writable: bool,
37    _marker: PhantomData<T>,
38}
39
40impl<'a, T> HostView<'a, T>
41where
42    T: Num + Clone + fmt::Debug,
43{
44    #[allow(dead_code)] // wired when the first backend migrates
45    pub(crate) fn new(
46        pin: HostPin<'a>,
47        shape: Vec<usize>,
48        byte_size_override: Option<usize>,
49        access: crate::CpuAccess,
50    ) -> Self {
51        HostView {
52            pin: Some(pin),
53            shape,
54            byte_size_override,
55            writable: access.writes(),
56            _marker: PhantomData,
57        }
58    }
59
60    /// Elements exposed by `as_slice`.
61    ///
62    /// Clamped to what the pin actually maps, so a `byte_size_override` larger
63    /// than the mapping cannot produce an out-of-bounds slice — the override is
64    /// a request to expose padding, never permission to invent it.
65    fn len_elems(&self) -> usize {
66        let Some(pin) = self.pin.as_ref() else {
67            return 0;
68        };
69        let elem = std::mem::size_of::<T>();
70        if elem == 0 {
71            return 0;
72        }
73        let requested = match self.byte_size_override {
74            Some(bytes) => bytes / elem,
75            None => self.shape.iter().product::<usize>(),
76        };
77        requested.min(pin.len() / elem)
78    }
79
80    /// Re-view this mapping as raw bytes, type-erased.
81    ///
82    /// Keeps the same pin (so the same platform sync bracket still runs on
83    /// [`Drop`]) and the same writability; only the shape bookkeeping changes,
84    /// from "N elements of `T`" to "this many bytes". Computed directly in
85    /// bytes rather than by calling [`len_elems`](Self::len_elems) and
86    /// multiplying back by `size_of::<T>()`, which would round a
87    /// non-multiple-of-`size_of::<T>()` `byte_size_override` down twice.
88    ///
89    /// `pub(crate)`: this is plumbing for `static`'s `TensorDyn::map_bytes`
90    /// (`tensor_dyn/static_backend.rs`), the type-erased byte-level guard a
91    /// C ABI consumer needs (it does not know `T` and cannot call the typed
92    /// `map_with`); it is not meant as a public entry point in its own
93    /// right. `static`-only: `dynamic` never has a typed `HostView<T>` to
94    /// re-view, since its own `TensorDyn::map_bytes` builds a byte view
95    /// straight from `ef_tensor_map` -- including for a PBO, whose storage
96    /// lives inside `libedgefirst_tensor.so` since Stage B and is mapped by
97    /// the library rather than by a `PboTensor<T>` on this side.
98    #[cfg(not(edgefirst_tensor_dynamic))]
99    pub(crate) fn into_bytes(self) -> HostView<'a, u8> {
100        let elem = std::mem::size_of::<T>().max(1);
101        let requested_bytes = match self.byte_size_override {
102            Some(bytes) => bytes,
103            None => self.shape.iter().product::<usize>() * elem,
104        };
105        let byte_len = match self.pin.as_ref() {
106            Some(pin) => requested_bytes.min(pin.len()),
107            None => 0,
108        };
109        HostView {
110            pin: self.pin,
111            shape: vec![byte_len],
112            byte_size_override: None,
113            writable: self.writable,
114            _marker: PhantomData,
115        }
116    }
117}
118
119impl<T> TensorMapTrait<T> for HostView<'_, T>
120where
121    T: Num + Clone + fmt::Debug,
122{
123    fn shape(&self) -> &[usize] {
124        &self.shape
125    }
126
127    fn unmap(&mut self) {
128        // Idempotent: the pin's keepalive releases exactly once, when the last
129        // clone drops, so a second unmap is a no-op rather than a double free.
130        self.pin = None;
131    }
132
133    fn is_writable(&self) -> bool {
134        self.writable
135    }
136
137    fn as_slice(&self) -> &[T] {
138        let Some(pin) = self.pin.as_ref() else {
139            // Unmapped. The per-backend maps dereferenced a stale pointer here;
140            // an empty slice is the safe reading of "there is no window".
141            return &[];
142        };
143        let len = self.len_elems();
144        if len == 0 {
145            return &[];
146        }
147        // SAFETY: the pin's keepalive holds the mapping open for at least this
148        // borrow, and `len_elems` is clamped to the mapped extent.
149        unsafe { std::slice::from_raw_parts(pin.as_ptr() as *const T, len) }
150    }
151
152    fn as_mut_slice(&mut self) -> &mut [T] {
153        crate::assert_map_writable(self.writable, "HostView");
154        let len = self.len_elems();
155        let Some(pin) = self.pin.as_ref() else {
156            return &mut [];
157        };
158        if len == 0 {
159            return &mut [];
160        }
161        // SAFETY: as above, plus `&mut self` gives exclusive access to this
162        // window for the borrow.
163        unsafe { std::slice::from_raw_parts_mut(pin.as_mut_ptr() as *mut T, len) }
164    }
165}
166
167/// Slice access by deref, as the per-backend maps offered: `map.fill(0)` and
168/// `map.iter()` keep working without an explicit `as_slice()`.
169impl<T> std::ops::Deref for HostView<'_, T>
170where
171    T: Num + Clone + fmt::Debug,
172{
173    type Target = [T];
174
175    fn deref(&self) -> &[T] {
176        self.as_slice()
177    }
178}
179
180impl<T> std::ops::DerefMut for HostView<'_, T>
181where
182    T: Num + Clone + fmt::Debug,
183{
184    fn deref_mut(&mut self) -> &mut [T] {
185        self.as_mut_slice()
186    }
187}
188
189impl<T> fmt::Debug for HostView<'_, T> {
190    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
191        f.debug_struct("HostView")
192            .field("mapped", &self.pin.is_some())
193            .field("shape", &self.shape)
194            .field("byte_size_override", &self.byte_size_override)
195            .field("writable", &self.writable)
196            .finish()
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203    use std::sync::Arc;
204
205    /// A view over `pin_len` bytes of a 64-element backing, so even a length
206    /// computed wrongly stays inside real memory.
207    fn view_u32(
208        pin_len: usize,
209        shape: Vec<usize>,
210        byte_size_override: Option<usize>,
211        access: crate::CpuAccess,
212    ) -> HostView<'static, u32> {
213        let backing: Arc<Vec<u32>> = Arc::new((0..64).collect());
214        let ptr = backing.as_ptr() as *mut u8;
215        HostView::new(
216            HostPin::new(backing, ptr, pin_len),
217            shape,
218            byte_size_override,
219            access,
220        )
221    }
222
223    #[test]
224    fn a_byte_override_exposes_that_many_bytes_as_elements() {
225        let v = view_u32(256, vec![1], Some(8), crate::CpuAccess::Read);
226        assert_eq!(v.as_slice(), &[0, 1]);
227    }
228
229    #[test]
230    fn the_exposed_length_is_clamped_to_the_pinned_bytes() {
231        let v = view_u32(16, vec![4], Some(64), crate::CpuAccess::Read);
232        assert_eq!(v.as_slice(), &[0, 1, 2, 3]);
233        let v = view_u32(16, vec![8], None, crate::CpuAccess::Read);
234        assert_eq!(v.as_slice().len(), 4);
235    }
236
237    #[test]
238    fn unmap_empties_the_view() {
239        let mut v = view_u32(256, vec![4], None, crate::CpuAccess::Read);
240        assert_eq!(v.as_slice().len(), 4);
241        v.unmap();
242        assert!(v.as_slice().is_empty());
243        assert!(format!("{v:?}").contains("mapped: false"));
244    }
245
246    #[test]
247    fn debug_names_the_type_and_its_writability() {
248        let v = view_u32(256, vec![4], Some(16), crate::CpuAccess::ReadWrite);
249        let text = format!("{v:?}");
250        assert!(text.contains("HostView"), "{text}");
251        assert!(text.contains("mapped: true"), "{text}");
252        assert!(text.contains("shape: [4]"), "{text}");
253        assert!(text.contains("byte_size_override: Some(16)"), "{text}");
254        assert!(text.contains("writable: true"), "{text}");
255    }
256}