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(feature = "static")]
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}