Skip to main content

softgpu_core/
memory.rs

1//! SoftGPU software memory: one host allocator behind regions and AMD pools.
2//!
3//! Provenance: `softgpu-software`. Allocations are process CPU memory, not
4//! R9700 VRAM. Sizes are SoftGPU defaults, not invented hardware limits.
5//!
6//! # Ownership invariants
7//!
8//! - Every live SoftGPU allocation is keyed by base pointer in `live`.
9//! - `Allocation` is `Send` because SoftGPU never shares a mutable allocation
10//!   across threads without going through the process-global runtime mutex;
11//!   the raw pointer is exclusive to SoftGPU until `free`.
12//! - Free of an unknown pointer fails closed (no silent host `free`).
13
14use crate::handle::PackedHandle;
15use std::alloc::{alloc_zeroed, dealloc, Layout};
16use std::collections::HashMap;
17use std::ptr::NonNull;
18use std::sync::atomic::{AtomicU64, Ordering};
19
20/// SoftGPU software pool capacity (host bytes). Not hardware VRAM.
21pub const SOFTGPU_POOL_BYTES: usize = 256 * 1024 * 1024;
22/// SoftGPU max single allocation. Not an R9700 claim.
23pub const SOFTGPU_MAX_ALLOC_BYTES: usize = 64 * 1024 * 1024;
24pub const SOFTGPU_ALLOC_GRANULE: usize = 4096;
25pub const SOFTGPU_ALLOC_ALIGNMENT: usize = 256;
26
27/// HSA `HSA_REGION_SEGMENT_GLOBAL`.
28pub const REGION_SEGMENT_GLOBAL: u32 = 0;
29/// `HSA_REGION_GLOBAL_FLAG_KERNARG | FINE_GRAINED`.
30pub const REGION_FLAGS_FINE_KERNARG: u32 = 1 | 2;
31/// `HSA_REGION_GLOBAL_FLAG_COARSE_GRAINED`.
32pub const REGION_FLAGS_COARSE: u32 = 4;
33
34/// AMD `HSA_AMD_SEGMENT_GLOBAL`.
35pub const AMD_SEGMENT_GLOBAL: u32 = 0;
36/// `KERNARG_INIT | FINE_GRAINED`.
37pub const AMD_POOL_FLAGS_FINE_KERNARG: u32 = 1 | 2;
38/// `COARSE_GRAINED`.
39pub const AMD_POOL_FLAGS_COARSE: u32 = 4;
40pub const AMD_POOL_LOCATION_CPU: u32 = 0;
41pub const AMD_POOL_LOCATION_GPU: u32 = 1;
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum MemoryViewKind {
45    RegionFineKernarg,
46    RegionCoarse,
47    PoolFineHost,
48    PoolCoarseDevice,
49}
50
51#[derive(Debug, Clone)]
52pub struct MemorySpace {
53    pub handle: PackedHandle,
54    pub kind: MemoryViewKind,
55    pub agent_handle: PackedHandle,
56    pub segment: u32,
57    pub global_flags: u32,
58    pub size_bytes: usize,
59    pub alloc_max_size: usize,
60    pub runtime_alloc_allowed: bool,
61    pub granule: usize,
62    pub alignment: usize,
63    pub location: Option<u32>,
64}
65
66impl MemorySpace {
67    pub fn is_pool(&self) -> bool {
68        matches!(
69            self.kind,
70            MemoryViewKind::PoolFineHost | MemoryViewKind::PoolCoarseDevice
71        )
72    }
73
74    pub fn is_region(&self) -> bool {
75        matches!(
76            self.kind,
77            MemoryViewKind::RegionFineKernarg | MemoryViewKind::RegionCoarse
78        )
79    }
80}
81
82/// SoftGPU permission / lifetime metadata for a tracked allocation.
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct AllocationMeta {
85    pub ptr: usize,
86    pub size: usize,
87    pub space: PackedHandle,
88    pub alignment: usize,
89    pub alloc_id: u64,
90    pub host_readable: bool,
91    pub host_writable: bool,
92}
93
94#[derive(Debug)]
95struct Allocation {
96    ptr: NonNull<u8>,
97    layout: Layout,
98    size: usize,
99    space: PackedHandle,
100    alloc_id: u64,
101    host_readable: bool,
102    host_writable: bool,
103}
104
105// SAFETY: SoftGPU owns the allocation exclusively after allocate; the process
106// runtime mutex serializes allocate/free. Pointers are never handed to foreign
107// code with overlapping SoftGPU mutation.
108unsafe impl Send for Allocation {}
109
110/// Process-heap SoftGPU allocator with tracked pointers.
111#[derive(Debug)]
112pub struct SoftGpuAllocator {
113    live: HashMap<usize, Allocation>,
114    bytes_in_use: usize,
115    capacity: usize,
116    next_alloc_id: AtomicU64,
117}
118
119impl Default for SoftGpuAllocator {
120    fn default() -> Self {
121        Self::new(SOFTGPU_POOL_BYTES)
122    }
123}
124
125impl SoftGpuAllocator {
126    pub fn new(capacity: usize) -> Self {
127        Self {
128            live: HashMap::new(),
129            bytes_in_use: 0,
130            capacity,
131            next_alloc_id: AtomicU64::new(1),
132        }
133    }
134
135    pub fn bytes_in_use(&self) -> usize {
136        self.bytes_in_use
137    }
138
139    pub fn capacity(&self) -> usize {
140        self.capacity
141    }
142
143    pub fn live_count(&self) -> usize {
144        self.live.len()
145    }
146
147    pub fn lookup(&self, ptr: *const u8) -> Option<AllocationMeta> {
148        if ptr.is_null() {
149            return None;
150        }
151        self.live.get(&(ptr as usize)).map(|a| AllocationMeta {
152            ptr: a.ptr.as_ptr() as usize,
153            size: a.size,
154            space: a.space,
155            alignment: a.layout.align(),
156            alloc_id: a.alloc_id,
157            host_readable: a.host_readable,
158            host_writable: a.host_writable,
159        })
160    }
161
162    /// Find SoftGPU allocation covering `addr` (for ISA global memory).
163    pub fn find_covering(&self, addr: u64) -> Option<AllocationMeta> {
164        let a = addr as usize;
165        for alloc in self.live.values() {
166            let base = alloc.ptr.as_ptr() as usize;
167            if a >= base && a < base + alloc.size {
168                return Some(AllocationMeta {
169                    ptr: base,
170                    size: alloc.size,
171                    space: alloc.space,
172                    alignment: alloc.layout.align(),
173                    alloc_id: alloc.alloc_id,
174                    host_readable: alloc.host_readable,
175                    host_writable: alloc.host_writable,
176                });
177            }
178        }
179        None
180    }
181
182    /// Read bytes from a SoftGPU-tracked allocation (fail closed if OOB/unknown).
183    pub fn read_bytes_at(&self, addr: u64, out: &mut [u8]) -> Result<(), AllocError> {
184        let meta = self
185            .find_covering(addr)
186            .ok_or(AllocError::InvalidArgument)?;
187        let off = (addr as usize) - meta.ptr;
188        if off + out.len() > meta.size || !meta.host_readable {
189            return Err(AllocError::InvalidArgument);
190        }
191        let base = meta.ptr as *const u8;
192        // SAFETY: allocation is live SoftGPU memory; range checked above.
193        unsafe {
194            std::ptr::copy_nonoverlapping(base.add(off), out.as_mut_ptr(), out.len());
195        }
196        Ok(())
197    }
198
199    /// Write bytes into a SoftGPU-tracked allocation.
200    pub fn write_bytes_at(&mut self, addr: u64, data: &[u8]) -> Result<(), AllocError> {
201        let meta = self
202            .find_covering(addr)
203            .ok_or(AllocError::InvalidArgument)?;
204        let off = (addr as usize) - meta.ptr;
205        if off + data.len() > meta.size || !meta.host_writable {
206            return Err(AllocError::InvalidArgument);
207        }
208        let base = meta.ptr as *mut u8;
209        // SAFETY: allocation is live SoftGPU memory; range checked above.
210        unsafe {
211            std::ptr::copy_nonoverlapping(data.as_ptr(), base.add(off), data.len());
212        }
213        Ok(())
214    }
215
216    pub fn allocate(
217        &mut self,
218        space: PackedHandle,
219        size: usize,
220        granule: usize,
221        alignment: usize,
222        max_alloc: usize,
223    ) -> Result<*mut u8, AllocError> {
224        if size == 0 {
225            return Err(AllocError::InvalidArgument);
226        }
227        if size > max_alloc {
228            return Err(AllocError::InvalidAllocation);
229        }
230        let align = alignment.max(1).next_power_of_two();
231        if !align.is_power_of_two() {
232            return Err(AllocError::InvalidArgument);
233        }
234        let rounded = round_up(size, granule.max(1));
235        if self.bytes_in_use.saturating_add(rounded) > self.capacity {
236            return Err(AllocError::OutOfResources);
237        }
238        let layout =
239            Layout::from_size_align(rounded, align).map_err(|_| AllocError::OutOfResources)?;
240        // SAFETY: layout is non-zero size with power-of-two alignment.
241        let ptr = unsafe { alloc_zeroed(layout) };
242        let Some(nn) = NonNull::new(ptr) else {
243            return Err(AllocError::OutOfResources);
244        };
245        if (nn.as_ptr() as usize) % align != 0 {
246            // SAFETY: free the failed-alignment allocation.
247            unsafe { dealloc(nn.as_ptr(), layout) };
248            return Err(AllocError::OutOfResources);
249        }
250        let alloc_id = self.next_alloc_id.fetch_add(1, Ordering::SeqCst);
251        self.live.insert(
252            nn.as_ptr() as usize,
253            Allocation {
254                ptr: nn,
255                layout,
256                size: rounded,
257                space,
258                alloc_id,
259                host_readable: true,
260                host_writable: true,
261            },
262        );
263        self.bytes_in_use = self.bytes_in_use.saturating_add(rounded);
264        Ok(nn.as_ptr())
265    }
266
267    pub fn free(&mut self, ptr: *mut u8) -> Result<AllocationMeta, AllocError> {
268        if ptr.is_null() {
269            return Err(AllocError::InvalidArgument);
270        }
271        let Some(alloc) = self.live.remove(&(ptr as usize)) else {
272            return Err(AllocError::InvalidArgument);
273        };
274        let meta = AllocationMeta {
275            ptr: alloc.ptr.as_ptr() as usize,
276            size: alloc.size,
277            space: alloc.space,
278            alignment: alloc.layout.align(),
279            alloc_id: alloc.alloc_id,
280            host_readable: alloc.host_readable,
281            host_writable: alloc.host_writable,
282        };
283        self.bytes_in_use = self.bytes_in_use.saturating_sub(alloc.size);
284        // SAFETY: pointer and layout came from this allocator.
285        unsafe { dealloc(alloc.ptr.as_ptr(), alloc.layout) };
286        Ok(meta)
287    }
288
289    pub fn contains(&self, ptr: *const u8) -> bool {
290        !ptr.is_null() && self.live.contains_key(&(ptr as usize))
291    }
292
293    pub fn clear_all(&mut self) {
294        for (_, alloc) in self.live.drain() {
295            // SAFETY: owned by this allocator.
296            unsafe { dealloc(alloc.ptr.as_ptr(), alloc.layout) };
297        }
298        self.bytes_in_use = 0;
299    }
300}
301
302#[derive(Debug, Clone, Copy, PartialEq, Eq)]
303pub enum AllocError {
304    InvalidArgument,
305    InvalidAllocation,
306    OutOfResources,
307}
308
309fn round_up(value: usize, granule: usize) -> usize {
310    if granule <= 1 {
311        return value;
312    }
313    value.div_ceil(granule).saturating_mul(granule)
314}
315
316#[derive(Debug, Clone, PartialEq, Eq)]
317pub enum RegionInfoValue {
318    Segment(u32),
319    GlobalFlags(u32),
320    Size(usize),
321    AllocMaxSize(usize),
322    RuntimeAllocAllowed(bool),
323    Granule(usize),
324    Alignment(usize),
325}
326
327#[derive(Debug, Clone, PartialEq, Eq)]
328pub enum PoolInfoValue {
329    Segment(u32),
330    GlobalFlags(u32),
331    Size(usize),
332    RuntimeAllocAllowed(bool),
333    Granule(usize),
334    Alignment(usize),
335    AccessibleByAll(bool),
336    AllocMaxSize(usize),
337    Location(u32),
338    RecGranule(usize),
339}
340
341#[cfg(test)]
342mod tests {
343    use super::*;
344    use crate::handle::HandleKind;
345
346    #[test]
347    fn allocate_and_free_round_trip() {
348        let mut alloc = SoftGpuAllocator::new(SOFTGPU_POOL_BYTES);
349        let space = PackedHandle::pack(HandleKind::Region, 1, 0);
350        let ptr = alloc
351            .allocate(
352                space,
353                100,
354                SOFTGPU_ALLOC_GRANULE,
355                SOFTGPU_ALLOC_ALIGNMENT,
356                SOFTGPU_MAX_ALLOC_BYTES,
357            )
358            .unwrap();
359        assert!(!ptr.is_null());
360        let meta = alloc.lookup(ptr).unwrap();
361        assert!(meta.host_readable && meta.host_writable);
362        assert_eq!(meta.space, space);
363        assert!(alloc.contains(ptr));
364        assert!(alloc.bytes_in_use() >= 100);
365        alloc.free(ptr).unwrap();
366        assert_eq!(alloc.bytes_in_use(), 0);
367        assert!(alloc.lookup(ptr).is_none());
368    }
369
370    #[test]
371    fn rejects_oversize_alloc() {
372        let mut alloc = SoftGpuAllocator::new(SOFTGPU_POOL_BYTES);
373        let space = PackedHandle::pack(HandleKind::MemoryPool, 1, 0);
374        let err = alloc
375            .allocate(
376                space,
377                SOFTGPU_MAX_ALLOC_BYTES + 1,
378                SOFTGPU_ALLOC_GRANULE,
379                SOFTGPU_ALLOC_ALIGNMENT,
380                SOFTGPU_MAX_ALLOC_BYTES,
381            )
382            .unwrap_err();
383        assert_eq!(err, AllocError::InvalidAllocation);
384    }
385
386    #[test]
387    fn exhaustion_fails_closed() {
388        let mut alloc = SoftGpuAllocator::new(SOFTGPU_ALLOC_GRANULE);
389        let space = PackedHandle::pack(HandleKind::Region, 1, 0);
390        let ptr = alloc
391            .allocate(
392                space,
393                SOFTGPU_ALLOC_GRANULE,
394                SOFTGPU_ALLOC_GRANULE,
395                SOFTGPU_ALLOC_ALIGNMENT,
396                SOFTGPU_MAX_ALLOC_BYTES,
397            )
398            .unwrap();
399        let err = alloc
400            .allocate(
401                space,
402                SOFTGPU_ALLOC_GRANULE,
403                SOFTGPU_ALLOC_GRANULE,
404                SOFTGPU_ALLOC_ALIGNMENT,
405                SOFTGPU_MAX_ALLOC_BYTES,
406            )
407            .unwrap_err();
408        assert_eq!(err, AllocError::OutOfResources);
409        alloc.free(ptr).unwrap();
410    }
411
412    #[test]
413    fn free_unknown_pointer_fails() {
414        let mut alloc = SoftGpuAllocator::new(SOFTGPU_POOL_BYTES);
415        let err = alloc.free(std::ptr::dangling_mut::<u8>()).unwrap_err();
416        assert_eq!(err, AllocError::InvalidArgument);
417    }
418}