Skip to main content

frame_alloc/implementations/wrappers/
regioned.rs

1use crate::strategies::cpu_id::NoCpuId;
2use crate::util::cache::CachePadded;
3use crate::{AllocError, CpuId, InitError, PageSize, PhysRange, PhysicalAllocator, RegionInit};
4use core::marker::PhantomData;
5use core::mem::{ManuallyDrop, MaybeUninit};
6use core::num::NonZeroUsize;
7use core::ptr;
8use core::sync::atomic::AtomicUsize;
9use core::sync::atomic::Ordering::Relaxed;
10
11/// Move `[A; N]` into `[CachePadded<A>; N]` in a `const` context.
12///
13/// `pub(crate)` only so the white-box drop test can reach it; not part of the
14/// public API.
15pub(crate) const fn pad_cells<A, const N: usize>(items: [A; N]) -> [CachePadded<A>; N] {
16    let src = ManuallyDrop::new(items);
17    let src_ptr = &src as *const ManuallyDrop<[A; N]> as *const A;
18    let mut cells: [MaybeUninit<CachePadded<A>>; N] = [const { MaybeUninit::uninit() }; N];
19    let mut i = 0;
20    while i < N {
21        // SAFETY: `src_ptr.add(i)` is in bounds for `i < N` and is read exactly
22        // once across the loop.
23        let a = unsafe { ptr::read(src_ptr.add(i)) };
24        cells[i] = MaybeUninit::new(CachePadded::new(a));
25        i += 1;
26    }
27    // SAFETY: every cell was initialised above, and `[MaybeUninit<T>; N]` shares
28    // layout with `[T; N]`. `cells` is `MaybeUninit`, so its elements are never
29    // dropped — no element is dropped twice.
30    unsafe {
31        ptr::read(&cells as *const [MaybeUninit<CachePadded<A>>; N] as *const [CachePadded<A>; N])
32    }
33}
34
35/// A wrapper that routes over **`REGIONS` disjoint physical spans**, one
36/// unmodified inner allocator `A` per span.
37///
38/// All regions are expected to share the same backend configuration — base
39/// frame, order count, and `max_page`.
40///
41/// **Allocation** starts on the calling CPU's home region
42/// ([`CpuId::current_cpu`] modulo `REGIONS`, or region 0 under the default
43/// [`NoCpuId`]) and work-steals round-robin through the other regions on local
44/// [`OutOfMemory`](AllocError::OutOfMemory).
45///
46/// **Deallocation** and [`add_usable`](Self::add_usable) route by physical
47/// address to the owning region. An address owned by no region is *dropped*
48/// (debug builds panic).
49pub struct RegionedAllocator<const REGIONS: usize, A, S = NoCpuId> {
50    /// One independent inner allocator per disjoint span, each on its own cache line.
51    regions: [CachePadded<A>; REGIONS],
52    /// `[base, end)` physical span per region, used to route a physical address
53    /// back to its owning region. Written once by `init_at`, read-only after;
54    /// `(0, 0)` marks an uninitialised region.
55    bounds: [(AtomicUsize, AtomicUsize); REGIONS],
56    /// Base frame size; mirrors each inner `A`'s own base frame.
57    base_frame: PageSize,
58    _selector: PhantomData<fn() -> S>,
59}
60
61impl<const REGIONS: usize, A, S> RegionedAllocator<REGIONS, A, S> {
62    /// Create a new, uninitialised regioned allocator from `REGIONS` pre-built
63    /// inner allocators.
64    ///
65    /// `base_frame` must match the base frame size of every inner allocator.
66    pub const fn new(base_frame: PageSize, regions: [A; REGIONS]) -> Self {
67        assert!(REGIONS > 0, "REGIONS must be > 0");
68        Self {
69            regions: pad_cells(regions),
70            bounds: [const { (AtomicUsize::new(0), AtomicUsize::new(0)) }; REGIONS],
71            base_frame,
72            _selector: PhantomData,
73        }
74    }
75
76    /// Fallible initialisation of region `idx` over the disjoint physical span
77    /// `[phys_base, phys_base + span_len)`, freeing only `usable`. Delegates to
78    /// the backend's [`RegionInit::try_init`] and records the span for address
79    /// routing.
80    ///
81    /// Call at most once *successfully* per region index, single-threaded and
82    /// before publishing the allocator to concurrent users. On `Err` the region
83    /// is left untouched - a corrected retry is permitted.
84    ///
85    /// # Errors
86    ///
87    /// * [`InitError::Misaligned`] if `phys_base` is not base-frame aligned;
88    /// * [`InitError::InvalidSpan`] if `span_len` is zero, not a base-frame
89    ///   multiple, or overflows the address space;
90    /// * [`InitError::AlreadyInitialized`] if region `idx` was already initialised;
91    /// * [`InitError::OverlapsRegion`] if the span overlaps an initialised region;
92    /// * any error returned by the backend's [`RegionInit::try_init`].
93    ///
94    /// # Panics
95    ///
96    /// If `idx >= REGIONS` (a structural bug, like any out-of-bounds index).
97    ///
98    /// # Safety
99    ///
100    /// The `(phys_base, span_len, usable)` arguments must satisfy the backend's
101    /// [`RegionInit::try_init`] safety contract, including the single-threaded
102    /// pre-publication requirement. The spans of distinct regions must be
103    /// disjoint, and no two `try_init_at` calls may race with each other.
104    pub unsafe fn try_init_at(
105        &self,
106        idx: usize,
107        phys_base: usize,
108        span_len: usize,
109        usable: &[PhysRange],
110    ) -> Result<(), InitError>
111    where
112        A: RegionInit,
113    {
114        assert!(
115            idx < REGIONS,
116            "region index {idx} out of range (REGIONS={REGIONS})"
117        );
118        if !phys_base.is_multiple_of(self.base_frame.bytes()) {
119            return Err(InitError::Misaligned {
120                required: self.base_frame.bytes(),
121            });
122        }
123        if span_len == 0 || !span_len.is_multiple_of(self.base_frame.bytes()) {
124            return Err(InitError::InvalidSpan);
125        }
126        let span_end = phys_base
127            .checked_add(span_len)
128            .ok_or(InitError::InvalidSpan)?;
129
130        if self.bounds[idx].0.load(Relaxed) != 0 || self.bounds[idx].1.load(Relaxed) != 0 {
131            return Err(InitError::AlreadyInitialized);
132        }
133        for j in 0..REGIONS {
134            if j == idx {
135                continue;
136            }
137            let lo = self.bounds[j].0.load(Relaxed);
138            let hi = self.bounds[j].1.load(Relaxed);
139            if hi != 0 && phys_base < hi && span_end > lo {
140                return Err(InitError::OverlapsRegion { other: j });
141            }
142        }
143
144        // SAFETY: the caller upholds the backend's `try_init` contract for this
145        // span. On `Err` the backend guarantees it is untouched, and we skip the
146        // bounds stores below, so the region stays uninitialised and retryable.
147        unsafe { self.regions[idx].try_init(phys_base, span_len, usable) }?;
148
149        self.bounds[idx].0.store(phys_base, Relaxed);
150        self.bounds[idx].1.store(span_end, Relaxed);
151        Ok(())
152    }
153
154    /// As [`try_init_at`](Self::try_init_at) but panic on any [`InitError`].
155    ///
156    /// Call once per region index, single-threaded, before publishing the
157    /// allocator to concurrent users. (Same as [`try_init_at`](Self::try_init_at)).
158    ///
159    /// # Safety
160    ///
161    /// `idx < REGIONS`. The `(phys_base, span_len, usable)` arguments must satisfy
162    /// the backend's [`RegionInit::try_init`] contract, including the
163    /// single-threaded pre-publication requirement. The spans of distinct regions
164    /// must be disjoint, and no two `init_at`/`try_init_at` calls may race with
165    /// each other.
166    ///
167    /// # Panics
168    ///
169    /// If `idx >= REGIONS`, or if initialisation returns an [`InitError`].
170    pub unsafe fn init_at(
171        &self,
172        idx: usize,
173        phys_base: usize,
174        span_len: usize,
175        usable: &[PhysRange],
176    ) where
177        A: RegionInit,
178    {
179        // SAFETY: forwarded under the caller's trait-level guarantees.
180        match unsafe { self.try_init_at(idx, phys_base, span_len, usable) } {
181            Ok(()) => {}
182            Err(e) => panic!("RegionedAllocator::init_at failed: {e:?}"),
183        }
184    }
185
186    /// Transition a reserved in-span range to free, routed to the owning region.
187    /// The repeatable post-init counterpart of [`RegionInit::add_usable`]
188    ///
189    /// # Safety
190    ///
191    /// `[base, base + len)` must lie within a single initialised region's span,
192    /// be currently reserved (not already free), exclusively owned, and not
193    /// aliased while registered.
194    pub unsafe fn add_usable(&self, base: usize, len: usize)
195    where
196        A: RegionInit,
197    {
198        let Some(owner) = self.region_of(base) else {
199            debug_assert!(false, "add_usable: base {base:#x} not owned by any region");
200            return;
201        };
202        debug_assert!(
203            base.checked_add(len)
204                .is_some_and(|end| end <= self.bounds[owner].1.load(Relaxed)),
205            "add_usable range escapes its owning region"
206        );
207        // SAFETY: `[base, base + len)` lies in `owner`'s span; the backend's
208        // `add_usable` contract is upheld.
209        unsafe { self.regions[owner].add_usable(base, len) };
210    }
211
212    /// Allocate strictly within region `idx`
213    ///
214    /// # Panics
215    ///
216    /// If `idx >= REGIONS`.
217    pub fn alloc_in_region(
218        &self,
219        idx: usize,
220        ps: PageSize,
221        count: NonZeroUsize,
222    ) -> Result<usize, AllocError>
223    where
224        A: PhysicalAllocator,
225    {
226        assert!(
227            idx < REGIONS,
228            "region index {idx} out of range (REGIONS={REGIONS})"
229        );
230        self.regions[idx].allocate_physical(ps, count)
231    }
232
233    /// Allocate from the first region in `chain` that can satisfy the request,
234    /// trying them in the given order
235    ///
236    /// # Panics
237    ///
238    /// If any index in `chain` is `>= REGIONS`.
239    pub fn alloc_in_chain(
240        &self,
241        chain: &[usize],
242        ps: PageSize,
243        count: NonZeroUsize,
244    ) -> Result<usize, AllocError>
245    where
246        A: PhysicalAllocator,
247    {
248        for &idx in chain {
249            assert!(
250                idx < REGIONS,
251                "region index {idx} in chain out of range (REGIONS={REGIONS})"
252            );
253            match self.regions[idx].allocate_physical(ps, count) {
254                Ok(phys) => return Ok(phys),
255                Err(AllocError::OutOfMemory) => continue,
256                Err(e) => return Err(e),
257            }
258        }
259        Err(AllocError::OutOfMemory)
260    }
261
262    /// Index of the region whose span contains `phys`, or `None` if no
263    /// initialised region owns it.
264    #[inline]
265    fn region_of(&self, phys: usize) -> Option<usize> {
266        for i in 0..REGIONS {
267            let lo = self.bounds[i].0.load(Relaxed);
268            let hi = self.bounds[i].1.load(Relaxed);
269            if phys >= lo && phys < hi {
270                return Some(i);
271            }
272        }
273        None
274    }
275
276    /// The inner per-region allocators, for diagnostics.
277    #[cfg(any(feature = "stats", test))]
278    pub(crate) fn regions(&self) -> impl ExactSizeIterator<Item = &A> {
279        self.regions.iter().map(|r| &**r)
280    }
281
282    /// The recorded `(base, end)` routing span of region `idx`; `(0, 0)` marks an
283    /// uninitialised region. (Test only).
284    #[cfg(test)]
285    pub(crate) fn region_bounds(&self, idx: usize) -> (usize, usize) {
286        (
287            self.bounds[idx].0.load(Relaxed),
288            self.bounds[idx].1.load(Relaxed),
289        )
290    }
291}
292
293unsafe impl<const REGIONS: usize, A, S> PhysicalAllocator for RegionedAllocator<REGIONS, A, S>
294where
295    A: PhysicalAllocator,
296    S: CpuId,
297{
298    fn allocate_physical(&self, ps: PageSize, count: NonZeroUsize) -> Result<usize, AllocError> {
299        let start = S::current_cpu() % REGIONS;
300        for offset in 0..REGIONS {
301            let i = (start + offset) % REGIONS;
302            match self.regions[i].allocate_physical(ps, count) {
303                Ok(phys) => return Ok(phys),
304                Err(AllocError::OutOfMemory) => continue,
305                Err(e) => return Err(e),
306            }
307        }
308        Err(AllocError::OutOfMemory)
309    }
310
311    unsafe fn deallocate_physical(&self, ps: PageSize, count: NonZeroUsize, phys: usize) {
312        match self.region_of(phys) {
313            // SAFETY: `phys` lies in `owner`'s span; the caller upholds the
314            // per-frame contract of the inner allocator.
315            Some(owner) => unsafe { self.regions[owner].deallocate_physical(ps, count, phys) },
316            None => debug_assert!(
317                false,
318                "deallocate_physical: phys {phys:#x} not owned by any region"
319            ),
320        }
321    }
322}
323
324#[cfg(any(feature = "stats", test))]
325impl<const REGIONS: usize, A: crate::AllocatorStats, S> crate::AllocatorStats
326    for RegionedAllocator<REGIONS, A, S>
327{
328    fn total_bytes(&self) -> usize {
329        self.regions().map(|r| r.total_bytes()).sum()
330    }
331
332    fn free_bytes(&self) -> usize {
333        self.regions().map(|r| r.free_bytes()).sum()
334    }
335
336    fn largest_free_bytes(&self) -> usize {
337        self.regions()
338            .map(|r| r.largest_free_bytes())
339            .max()
340            .unwrap_or(0)
341    }
342}