Skip to main content

molframe_core/coords/
storage.rs

1//! Coordinate storage.
2//!
3//! Positions are `f32` triples in one contiguous, 64-byte-aligned buffer per
4//! frame. Three decimal places in ångström is what gets deposited, and `f32`
5//! represents that exactly over any box that will ever be modelled, so the
6//! wider type would double the bandwidth of every geometric kernel to store
7//! digits nobody measured. Kernels that accumulate — centroids, inertia
8//! tensors, sums of squares — widen to `f64` internally, where it matters.
9//!
10//! One buffer per frame, rather than one per chunk, is what makes handing the
11//! whole coordinate set to a numeric array a pointer rather than a copy. Chunks
12//! address ranges of it.
13//!
14//! Alignment is obtained by allocating in lanes of sixteen positions. A lane is
15//! 192 bytes and demands 64-byte alignment, so the allocator hands back an
16//! aligned block and no pointer arithmetic is needed to get one. The cost is at
17//! most fifteen unused positions at the end of a frame.
18
19use bytemuck::{Pod, Zeroable};
20use std::fmt;
21use std::mem::size_of;
22use std::ops::Range;
23use std::sync::Arc;
24
25use crate::{ExecutionContext, MemoryBudgetError, MemoryReservation};
26
27/// Positions per lane. Sixteen triples is 192 bytes — three cache lines exactly,
28/// which is what lets the lane demand 64-byte alignment without padding.
29const LANE: usize = 16;
30
31#[repr(C, align(64))]
32#[derive(Clone, Copy, Pod, Zeroable)]
33struct CoordLane([[f32; 3]; LANE]);
34
35/// How many times the coordinates of a structure have changed.
36///
37/// Everything derived from positions — spatial indices, cached distances, views
38/// handed to a caller — records the generation it was built against. Using one
39/// after the coordinates moved is then a detectable mistake rather than a
40/// silently wrong answer.
41#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
42#[repr(transparent)]
43pub struct CoordinateGeneration(u64);
44
45impl CoordinateGeneration {
46    /// The generation of a structure that has not been edited.
47    pub const INITIAL: Self = Self(0);
48
49    /// Rebuilds a generation from a value previously produced by this type.
50    ///
51    /// The counter is opaque to callers that only need freshness checks, but
52    /// preserving it at an interop boundary is useful when a cached view is
53    /// serialised or handed between language runtimes.  No validation is
54    /// required because every `u64` is a representable generation.
55    #[must_use]
56    pub const fn from_raw(value: u64) -> Self {
57        Self(value)
58    }
59
60    /// Returns the next generation, or `None` when the counter is exhausted.
61    #[must_use]
62    pub const fn next(self) -> Option<Self> {
63        match self.0.checked_add(1) {
64            Some(next) => Some(Self(next)),
65            None => None,
66        }
67    }
68
69    /// The raw counter.
70    #[must_use]
71    pub const fn get(self) -> u64 {
72        self.0
73    }
74}
75
76/// An axis-aligned bounding box.
77///
78/// Every chunk carries one. A query that wants atoms within some distance of a
79/// region can reject a whole chunk by comparing boxes, before reading a single
80/// position out of it.
81#[derive(Clone, Copy, PartialEq, Debug)]
82pub struct Aabb {
83    /// Componentwise minimum.
84    pub min: [f32; 3],
85    /// Componentwise maximum.
86    pub max: [f32; 3],
87}
88
89impl Aabb {
90    /// An empty box, which absorbs any point it is extended by.
91    pub const EMPTY: Self = Self {
92        min: [f32::INFINITY; 3],
93        max: [f32::NEG_INFINITY; 3],
94    };
95
96    /// Returns true when no point has been added.
97    #[must_use]
98    #[inline]
99    pub fn is_empty(&self) -> bool {
100        self.min[0] > self.max[0] || self.min[1] > self.max[1] || self.min[2] > self.max[2]
101    }
102
103    /// Grows the box to contain `point`.
104    ///
105    /// Non-finite components are ignored: a coordinate that is not a number
106    /// says nothing about where the atoms are, and letting it poison the box
107    /// would disable chunk skipping for the whole structure.
108    #[inline]
109    pub fn extend(&mut self, point: [f32; 3]) {
110        extend_axis(&mut self.min[0], &mut self.max[0], point[0]);
111        extend_axis(&mut self.min[1], &mut self.max[1], point[1]);
112        extend_axis(&mut self.min[2], &mut self.max[2], point[2]);
113    }
114
115    /// Grows the box to contain another box.
116    #[inline]
117    pub fn union(&mut self, other: &Self) {
118        if other.is_empty() {
119            return;
120        }
121
122        self.extend(other.min);
123        self.extend(other.max);
124    }
125
126    /// Returns true when the two boxes come within `distance` of one another.
127    ///
128    /// Compares squared separations, so no square root is taken.
129    #[must_use]
130    #[inline]
131    pub fn within(&self, other: &Self, distance: f32) -> bool {
132        if self.is_empty() || other.is_empty() {
133            return false;
134        }
135
136        let maximum_gap_squared = distance * distance;
137
138        let x = axis_gap(self.min[0], self.max[0], other.min[0], other.max[0]);
139        let mut gap_squared = x * x;
140
141        if gap_squared > maximum_gap_squared {
142            return false;
143        }
144
145        let y = axis_gap(self.min[1], self.max[1], other.min[1], other.max[1]);
146        gap_squared += y * y;
147
148        if gap_squared > maximum_gap_squared {
149            return false;
150        }
151
152        let z = axis_gap(self.min[2], self.max[2], other.min[2], other.max[2]);
153        gap_squared += z * z;
154
155        gap_squared <= maximum_gap_squared
156    }
157}
158
159impl Default for Aabb {
160    /// Creates an empty bounding box.
161    fn default() -> Self {
162        Self::EMPTY
163    }
164}
165
166/// The positions of one frame.
167///
168/// # Examples
169///
170/// ```
171/// use molframe_core::CoordinateBlock;
172///
173/// let mut block = CoordinateBlock::with_capacity(3);
174/// block.push([1.0, 2.0, 3.0]);
175/// block.push([4.0, 5.0, 6.0]);
176///
177/// assert_eq!(block.len(), 2);
178/// assert_eq!(block.as_slice()[1], [4.0, 5.0, 6.0]);
179/// ```
180#[derive(Clone, Default)]
181pub struct CoordinateBlock {
182    lanes: Arc<Vec<CoordLane>>,
183    len: u32,
184    reservation: Option<Arc<MemoryReservation>>,
185}
186
187impl CoordinateBlock {
188    /// Creates an empty block.
189    #[must_use]
190    pub fn new() -> Self {
191        Self {
192            lanes: Arc::new(Vec::new()),
193            len: 0,
194            reservation: None,
195        }
196    }
197
198    /// Creates a block with room for `positions` without reallocating.
199    #[must_use]
200    pub fn with_capacity(positions: usize) -> Self {
201        Self {
202            lanes: Arc::new(Vec::with_capacity(lanes_for_positions(positions))),
203            len: 0,
204            reservation: None,
205        }
206    }
207
208    /// The number of positions held.
209    #[must_use]
210    pub const fn len(&self) -> u32 {
211        self.len
212    }
213
214    /// Returns true when the block holds no positions.
215    #[must_use]
216    pub const fn is_empty(&self) -> bool {
217        self.len == 0
218    }
219
220    /// Appends a position.
221    ///
222    /// Copy-on-write detachment is performed at most once for the append, and
223    /// the visible length changes only after the target slot has been written.
224    pub fn push(&mut self, position: [f32; 3]) {
225        let Some((index, next_len)) = append_indices(self.len) else {
226            return;
227        };
228
229        let lanes = Arc::make_mut(&mut self.lanes);
230
231        if write_position(lanes, index, position) {
232            self.len = next_len;
233        }
234    }
235
236    /// All positions, contiguously.
237    ///
238    /// This slice is the whole point of the layout: it is what an array
239    /// interface receives without a copy.
240    #[must_use]
241    #[inline]
242    pub fn as_slice(&self) -> &[[f32; 3]] {
243        let all: &[[f32; 3]] = bytemuck::cast_slice(self.lanes.as_slice());
244        visible_slice(all, self.len)
245    }
246
247    /// All positions, mutably.
248    ///
249    /// Reaching for this outside a scoped edit skips the generation bump that
250    /// invalidates derived state, which is why it is not part of the public
251    /// structure API.
252    #[inline]
253    pub(crate) fn as_mut_slice(&mut self) -> &mut [[f32; 3]] {
254        let lanes = Arc::make_mut(&mut self.lanes);
255        let all: &mut [[f32; 3]] = bytemuck::cast_slice_mut(lanes.as_mut_slice());
256
257        visible_mut_slice(all, self.len)
258    }
259
260    /// Detaches shared coordinates only after charging their complete backing capacity.
261    ///
262    /// # Errors
263    ///
264    /// Returns a budget error without changing this block when the copy cannot fit.
265    pub(crate) fn make_unique_in(
266        &mut self,
267        context: &ExecutionContext,
268    ) -> Result<(), MemoryBudgetError> {
269        if Arc::strong_count(&self.lanes) == 1 {
270            return Ok(());
271        }
272        let bytes = self.allocated_bytes();
273        let mut reservation = context.try_reserve(bytes)?;
274        let mut lanes = Vec::with_capacity(self.lanes.capacity());
275        lanes.extend_from_slice(&self.lanes);
276        let retained = lanes.capacity().saturating_mul(size_of::<CoordLane>());
277        if retained > bytes {
278            reservation.try_grow(retained - bytes)?;
279        } else {
280            reservation.shrink_to(retained);
281        }
282        self.lanes = Arc::new(lanes);
283        self.reservation = Some(Arc::new(reservation));
284        Ok(())
285    }
286
287    /// The positions a chunk covers, or `None` if the range runs past the end.
288    #[must_use]
289    #[inline]
290    pub fn range(&self, range: Range<u32>) -> Option<&[[f32; 3]]> {
291        let range = usize_range(range)?;
292        self.as_slice().get(range)
293    }
294
295    /// The bounding box of a range of positions.
296    #[must_use]
297    pub fn bounds(&self, range: Range<u32>) -> Aabb {
298        let Some(positions) = self.range(range) else {
299            return Aabb::EMPTY;
300        };
301
302        let mut bounds = Aabb::EMPTY;
303
304        for &position in positions {
305            bounds.extend(position);
306        }
307
308        bounds
309    }
310
311    /// Backing allocation bytes, including spare capacity and unused lane tails.
312    #[must_use]
313    pub fn allocated_bytes(&self) -> usize {
314        self.lanes.capacity() * size_of::<CoordLane>()
315    }
316}
317
318impl fmt::Debug for CoordinateBlock {
319    /// Formats block metadata without dumping the potentially large coordinate buffer.
320    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
321        f.debug_struct("CoordinateBlock")
322            .field("positions", &self.len)
323            .finish_non_exhaustive()
324    }
325}
326
327impl FromIterator<[f32; 3]> for CoordinateBlock {
328    /// Collects positions directly into aligned coordinate lanes.
329    ///
330    /// Construction writes directly into the backing `Vec`, avoiding an
331    /// `Arc::make_mut` uniqueness check for every collected position.
332    fn from_iter<T: IntoIterator<Item = [f32; 3]>>(iter: T) -> Self {
333        let mut iter = iter.into_iter();
334        let hinted_positions = iter.size_hint().0;
335
336        let mut lanes = Vec::new();
337        let hinted_lanes = lanes_for_positions(hinted_positions);
338
339        // A size hint is advisory and may come from arbitrary iterator code.
340        // Failure to honour it must not prevent incremental collection.
341        if hinted_lanes != 0 {
342            let _reserve_failed = lanes.try_reserve_exact(hinted_lanes).is_err();
343        }
344
345        let mut len = 0u32;
346
347        for position in &mut iter {
348            let Some((index, next_len)) = append_indices(len) else {
349                break;
350            };
351
352            if !write_position(&mut lanes, index, position) {
353                break;
354            }
355
356            len = next_len;
357        }
358
359        Self {
360            lanes: Arc::new(lanes),
361            len,
362            reservation: None,
363        }
364    }
365}
366
367/// Extends one bounding-box axis when `value` is finite.
368#[inline]
369fn extend_axis(minimum: &mut f32, maximum: &mut f32, value: f32) {
370    if !value.is_finite() {
371        return;
372    }
373
374    *minimum = minimum.min(value);
375    *maximum = maximum.max(value);
376}
377
378/// Returns the non-overlapping gap between two intervals on one axis.
379#[inline]
380fn axis_gap(own_min: f32, own_max: f32, other_min: f32, other_max: f32) -> f32 {
381    (other_min - own_max).max(own_min - other_max).max(0.0)
382}
383
384/// Converts a position count into the number of aligned lanes required.
385#[inline]
386fn lanes_for_positions(positions: usize) -> usize {
387    positions.div_ceil(LANE)
388}
389
390/// Computes the backing-buffer index and next visible length for an append.
391///
392/// Returns `None` when the block has exhausted its `u32` position space or the
393/// current index cannot be represented by the platform.
394#[inline]
395fn append_indices(len: u32) -> Option<(usize, u32)> {
396    let next_len = len.checked_add(1)?;
397    let index = usize::try_from(len).ok()?;
398
399    Some((index, next_len))
400}
401
402/// Writes one position into its lane, growing the lane vector when necessary.
403///
404/// Returns `false` only when the supplied logical index is inconsistent with
405/// the current contiguous lane layout.
406#[inline]
407fn write_position(lanes: &mut Vec<CoordLane>, index: usize, position: [f32; 3]) -> bool {
408    let (lane_index, slot) = lane_position(index);
409
410    if lane_index > lanes.len() {
411        return false;
412    }
413
414    if lane_index == lanes.len() {
415        let mut lane = CoordLane([[0.0; 3]; LANE]);
416
417        let Some(target) = lane.0.get_mut(slot) else {
418            return false;
419        };
420
421        *target = position;
422        lanes.push(lane);
423        return true;
424    }
425
426    let Some(target) = lanes
427        .get_mut(lane_index)
428        .and_then(|lane| lane.0.get_mut(slot))
429    else {
430        return false;
431    };
432
433    *target = position;
434    true
435}
436
437/// Maps a logical position to its lane and in-lane slot.
438#[inline]
439const fn lane_position(position: usize) -> (usize, usize) {
440    (position / LANE, position % LANE)
441}
442
443/// Restricts a backing slice to the number of logically visible values.
444///
445/// An impossible length is treated as an empty view rather than causing an
446/// out-of-bounds panic.
447#[inline]
448fn visible_slice<T>(values: &[T], len: u32) -> &[T] {
449    let Ok(len) = usize::try_from(len) else {
450        return &[];
451    };
452
453    match values.get(..len) {
454        Some(values) => values,
455        None => &[],
456    }
457}
458
459/// Restricts a mutable backing slice to the number of logically visible values.
460///
461/// An impossible length is treated as an empty view rather than exposing
462/// storage outside the block's logical extent.
463#[inline]
464fn visible_mut_slice<T>(values: &mut [T], len: u32) -> &mut [T] {
465    let Ok(len) = usize::try_from(len) else {
466        return &mut [];
467    };
468
469    match values.get_mut(..len) {
470        Some(values) => values,
471        None => &mut [],
472    }
473}
474
475/// Converts an atom-position range into platform-native slice indices.
476#[inline]
477fn usize_range(range: Range<u32>) -> Option<Range<usize>> {
478    let start = usize::try_from(range.start).ok()?;
479    let end = usize::try_from(range.end).ok()?;
480
481    Some(start..end)
482}
483
484#[cfg(test)]
485#[path = "storage_tests.rs"]
486mod tests;