1use 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
20pub const SOFTGPU_POOL_BYTES: usize = 256 * 1024 * 1024;
22pub const SOFTGPU_MAX_ALLOC_BYTES: usize = 64 * 1024 * 1024;
24pub const SOFTGPU_ALLOC_GRANULE: usize = 4096;
25pub const SOFTGPU_ALLOC_ALIGNMENT: usize = 256;
26
27pub const REGION_SEGMENT_GLOBAL: u32 = 0;
29pub const REGION_FLAGS_FINE_KERNARG: u32 = 1 | 2;
31pub const REGION_FLAGS_COARSE: u32 = 4;
33
34pub const AMD_SEGMENT_GLOBAL: u32 = 0;
36pub const AMD_POOL_FLAGS_FINE_KERNARG: u32 = 1 | 2;
38pub 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#[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
105unsafe impl Send for Allocation {}
109
110#[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 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 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 unsafe {
194 std::ptr::copy_nonoverlapping(base.add(off), out.as_mut_ptr(), out.len());
195 }
196 Ok(())
197 }
198
199 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 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 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 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 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 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}