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. It was `static`-only on the reasoning that `dynamic` never has
94 /// a typed `HostView<T>` to re-view, since its own `TensorDyn::map_bytes`
95 /// builds a byte view straight from `ef_tensor_map`. That stopped being
96 /// true when the dynamic backend learned to map a PBO-backed tensor
97 /// (`dynamic_backend.rs::map_pin_pbo`): a PBO's real bytes live in the GL
98 /// buffer, so that path goes through `PboTensor<T>::map_with` and gets a
99 /// typed guard it must erase.
100 pub(crate) fn into_bytes(self) -> HostView<'a, u8> {
101 let elem = std::mem::size_of::<T>().max(1);
102 let requested_bytes = match self.byte_size_override {
103 Some(bytes) => bytes,
104 None => self.shape.iter().product::<usize>() * elem,
105 };
106 let byte_len = match self.pin.as_ref() {
107 Some(pin) => requested_bytes.min(pin.len()),
108 None => 0,
109 };
110 HostView {
111 pin: self.pin,
112 shape: vec![byte_len],
113 byte_size_override: None,
114 writable: self.writable,
115 _marker: PhantomData,
116 }
117 }
118}
119
120impl<T> TensorMapTrait<T> for HostView<'_, T>
121where
122 T: Num + Clone + fmt::Debug,
123{
124 fn shape(&self) -> &[usize] {
125 &self.shape
126 }
127
128 fn unmap(&mut self) {
129 // Idempotent: the pin's keepalive releases exactly once, when the last
130 // clone drops, so a second unmap is a no-op rather than a double free.
131 self.pin = None;
132 }
133
134 fn is_writable(&self) -> bool {
135 self.writable
136 }
137
138 fn as_slice(&self) -> &[T] {
139 let Some(pin) = self.pin.as_ref() else {
140 // Unmapped. The per-backend maps dereferenced a stale pointer here;
141 // an empty slice is the safe reading of "there is no window".
142 return &[];
143 };
144 let len = self.len_elems();
145 if len == 0 {
146 return &[];
147 }
148 // SAFETY: the pin's keepalive holds the mapping open for at least this
149 // borrow, and `len_elems` is clamped to the mapped extent.
150 unsafe { std::slice::from_raw_parts(pin.as_ptr() as *const T, len) }
151 }
152
153 fn as_mut_slice(&mut self) -> &mut [T] {
154 crate::assert_map_writable(self.writable, "HostView");
155 let len = self.len_elems();
156 let Some(pin) = self.pin.as_ref() else {
157 return &mut [];
158 };
159 if len == 0 {
160 return &mut [];
161 }
162 // SAFETY: as above, plus `&mut self` gives exclusive access to this
163 // window for the borrow.
164 unsafe { std::slice::from_raw_parts_mut(pin.as_mut_ptr() as *mut T, len) }
165 }
166}
167
168/// Slice access by deref, as the per-backend maps offered: `map.fill(0)` and
169/// `map.iter()` keep working without an explicit `as_slice()`.
170impl<T> std::ops::Deref for HostView<'_, T>
171where
172 T: Num + Clone + fmt::Debug,
173{
174 type Target = [T];
175
176 fn deref(&self) -> &[T] {
177 self.as_slice()
178 }
179}
180
181impl<T> std::ops::DerefMut for HostView<'_, T>
182where
183 T: Num + Clone + fmt::Debug,
184{
185 fn deref_mut(&mut self) -> &mut [T] {
186 self.as_mut_slice()
187 }
188}
189
190impl<T> fmt::Debug for HostView<'_, T> {
191 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192 f.debug_struct("HostView")
193 .field("mapped", &self.pin.is_some())
194 .field("shape", &self.shape)
195 .field("byte_size_override", &self.byte_size_override)
196 .field("writable", &self.writable)
197 .finish()
198 }
199}