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. 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}