1use crate::agent::{AgentInfoAttr, AgentKind, VirtualAgent};
7use crate::aql::{
8 parse_supported_packet, DispatchDescriptor, KernargClass, DIAGNOSTIC_COMPLETE_NO_EXECUTION,
9 DIAGNOSTIC_REJECTED, KERNEL_SUCCESS,
10};
11use crate::error::{Error, ErrorCategory};
12use crate::fidelity::FidelityLevel;
13use crate::handle::{HandleKind, PackedHandle};
14use crate::memory::{
15 AllocError, MemorySpace, MemoryViewKind, PoolInfoValue, RegionInfoValue, SoftGpuAllocator,
16 AMD_POOL_FLAGS_COARSE, AMD_POOL_FLAGS_FINE_KERNARG, AMD_POOL_LOCATION_CPU,
17 AMD_POOL_LOCATION_GPU, AMD_SEGMENT_GLOBAL, REGION_FLAGS_COARSE, REGION_FLAGS_FINE_KERNARG,
18 REGION_SEGMENT_GLOBAL, SOFTGPU_ALLOC_ALIGNMENT, SOFTGPU_ALLOC_GRANULE, SOFTGPU_MAX_ALLOC_BYTES,
19 SOFTGPU_POOL_BYTES,
20};
21use crate::profile::DeviceProfile;
22use crate::queue::{
23 init_packet_buffer, is_power_of_two, HsaQueueAbi, PacketObservation, SoftGpuQueue,
24 AQL_PACKET_BYTES, QUEUE_FEATURE_KERNEL_DISPATCH, QUEUE_TYPE_MULTI, SOFTGPU_QUEUES_MAX,
25};
26use crate::signal::{SignalCondition, SignalWaitOutcome, SoftGpuSignal};
27use crate::trace::{SharedTrace, TraceEvent, TraceLog, TraceSink};
28use softgpu_amd_isa::{run_code_1d, IsaMemory, WaveSize, TINY_ADD_TEXT};
29use std::alloc::{alloc_zeroed, dealloc, Layout};
30use std::collections::HashMap;
31use std::sync::{Mutex, OnceLock};
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub enum RuntimeError {
36 NotInitialized,
37 RefcountOverflow,
38 InvalidArgument,
39 InvalidAgent,
40 InvalidRegion,
41 InvalidPool,
42 InvalidSignal,
43 InvalidQueue,
44 InvalidQueueCreation,
45 InvalidAllocation,
46 OutOfResources,
47 UnsupportedAttribute,
48 Internal(String),
49}
50
51impl RuntimeError {
52 pub fn as_str(&self) -> &'static str {
53 match self {
54 Self::NotInitialized => "not_initialized",
55 Self::RefcountOverflow => "refcount_overflow",
56 Self::InvalidArgument => "invalid_argument",
57 Self::InvalidAgent => "invalid_agent",
58 Self::InvalidRegion => "invalid_region",
59 Self::InvalidPool => "invalid_pool",
60 Self::InvalidSignal => "invalid_signal",
61 Self::InvalidQueue => "invalid_queue",
62 Self::InvalidQueueCreation => "invalid_queue_creation",
63 Self::InvalidAllocation => "invalid_allocation",
64 Self::OutOfResources => "out_of_resources",
65 Self::UnsupportedAttribute => "unsupported_attribute",
66 Self::Internal(_) => "internal",
67 }
68 }
69}
70
71struct AgentSlot {
72 generation: u32,
73 live: bool,
74 agent: Option<VirtualAgent>,
75}
76
77struct SpaceSlot {
78 generation: u32,
79 live: bool,
80 space: Option<MemorySpace>,
81}
82
83struct SignalSlot {
84 generation: u32,
85 live: bool,
86 signal: Option<std::sync::Arc<SoftGpuSignal>>,
87}
88
89struct QueueSlot {
90 #[allow(dead_code)]
91 generation: u32,
92 live: bool,
93 queue: Option<SoftGpuQueue>,
94}
95
96#[derive(Debug, Clone)]
98pub struct RegisteredIsaKernel {
99 pub name: String,
100 pub code: Vec<u8>,
101 pub kernarg_segment_size: u32,
102 pub group_segment_size: u32,
103 pub private_segment_size: u32,
104}
105
106pub struct Runtime {
108 refcount: u32,
109 profile: DeviceProfile,
110 agents: Vec<AgentSlot>,
111 regions: Vec<SpaceSlot>,
112 pools: Vec<SpaceSlot>,
113 signals: Vec<SignalSlot>,
114 queues: Vec<QueueSlot>,
115 allocator: SoftGpuAllocator,
116 next_generation: u32,
117 next_queue_id: u64,
118 dispatch_captures: Vec<DispatchDescriptor>,
120 isa_kernels: HashMap<u64, RegisteredIsaKernel>,
122 next_kernel_object: u64,
123 code_object_readers: HashMap<u64, crate::executable::CodeObjectReader>,
125 next_reader_id: u64,
126 executables: HashMap<u64, crate::executable::SoftGpuExecutable>,
128 next_executable_id: u64,
129 executable_symbols: HashMap<u64, (u64, usize)>,
131 next_symbol_id: u64,
132 trace: SharedTrace,
133}
134
135impl Runtime {
136 pub fn new(profile: DeviceProfile) -> Result<Self, Error> {
137 profile.validate()?;
138 Ok(Self {
139 refcount: 0,
140 profile,
141 agents: Vec::new(),
142 regions: Vec::new(),
143 pools: Vec::new(),
144 signals: Vec::new(),
145 queues: Vec::new(),
146 allocator: SoftGpuAllocator::new(SOFTGPU_POOL_BYTES),
147 next_generation: 1,
148 next_queue_id: 1,
149 dispatch_captures: Vec::new(),
150 isa_kernels: HashMap::new(),
151 next_kernel_object: 1,
152 code_object_readers: HashMap::new(),
153 next_reader_id: 1,
154 executables: HashMap::new(),
155 next_executable_id: 1,
156 executable_symbols: HashMap::new(),
157 next_symbol_id: 1,
158 trace: SharedTrace::new(512),
159 })
160 }
161
162 pub fn register_isa_kernel(
164 &mut self,
165 name: impl Into<String>,
166 code: Vec<u8>,
167 ) -> Result<u64, RuntimeError> {
168 self.register_isa_kernel_ex(name, code, 0, 0, 0)
169 }
170
171 pub fn register_isa_kernel_ex(
173 &mut self,
174 name: impl Into<String>,
175 code: Vec<u8>,
176 kernarg_segment_size: u32,
177 group_segment_size: u32,
178 private_segment_size: u32,
179 ) -> Result<u64, RuntimeError> {
180 if code.is_empty() {
181 return Err(RuntimeError::InvalidArgument);
182 }
183 let id = self.next_kernel_object;
184 self.next_kernel_object = self.next_kernel_object.saturating_add(1);
185 self.isa_kernels.insert(
186 id,
187 RegisteredIsaKernel {
188 name: name.into(),
189 code,
190 kernarg_segment_size,
191 group_segment_size,
192 private_segment_size,
193 },
194 );
195 Ok(id)
196 }
197
198 pub fn register_builtin_tiny_add(&mut self) -> Result<u64, RuntimeError> {
200 self.register_isa_kernel_ex("tiny_add", TINY_ADD_TEXT.to_vec(), 16, 0, 0)
201 }
202
203 pub fn lookup_isa_kernel(&self, kernel_object: u64) -> Option<&RegisteredIsaKernel> {
204 self.isa_kernels.get(&kernel_object)
205 }
206
207 pub fn code_object_reader_create_from_memory(
208 &mut self,
209 bytes: &[u8],
210 ) -> Result<u64, RuntimeError> {
211 if bytes.is_empty() {
212 return Err(RuntimeError::InvalidArgument);
213 }
214 let id = self.next_reader_id;
215 self.next_reader_id = self.next_reader_id.saturating_add(1);
216 self.code_object_readers.insert(
217 id,
218 crate::executable::CodeObjectReader {
219 bytes: bytes.to_vec(),
220 },
221 );
222 Ok(id)
223 }
224
225 pub fn code_object_reader_destroy(&mut self, reader_id: u64) -> Result<(), RuntimeError> {
226 if self.code_object_readers.remove(&reader_id).is_none() {
227 return Err(RuntimeError::InvalidArgument);
228 }
229 Ok(())
230 }
231
232 pub fn executable_create(&mut self) -> Result<u64, RuntimeError> {
233 let id = self.next_executable_id;
234 self.next_executable_id = self.next_executable_id.saturating_add(1);
235 self.executables.insert(
236 id,
237 crate::executable::SoftGpuExecutable {
238 frozen: false,
239 symbols: Vec::new(),
240 },
241 );
242 Ok(id)
243 }
244
245 pub fn executable_destroy(&mut self, exec_id: u64) -> Result<(), RuntimeError> {
246 let Some(exec) = self.executables.remove(&exec_id) else {
247 return Err(RuntimeError::InvalidArgument);
248 };
249 self.executable_symbols
250 .retain(|_, (eid, _)| *eid != exec_id);
251 for sym in exec.symbols {
252 self.isa_kernels.remove(&sym.kernel_object);
253 }
254 Ok(())
255 }
256
257 pub fn executable_load_agent_code_object(
258 &mut self,
259 exec_id: u64,
260 reader_id: u64,
261 ) -> Result<(), RuntimeError> {
262 let frozen = self
263 .executables
264 .get(&exec_id)
265 .map(|e| e.frozen)
266 .ok_or(RuntimeError::InvalidArgument)?;
267 if frozen {
268 return Err(RuntimeError::InvalidArgument);
269 }
270 let bytes = self
271 .code_object_readers
272 .get(&reader_id)
273 .map(|r| r.bytes.clone())
274 .ok_or(RuntimeError::InvalidArgument)?;
275 let kernels = crate::executable::parse_agent_kernels(&bytes)
276 .map_err(|_| RuntimeError::InvalidArgument)?;
277 let mut symbols = Vec::new();
278 for k in kernels {
279 let ko = self.register_isa_kernel_ex(
280 k.name.clone(),
281 k.text,
282 k.kernarg_segment_size,
283 k.group_segment_size,
284 k.private_segment_size,
285 )?;
286 symbols.push(crate::executable::ExecutableSymbol {
287 name: k.name,
288 symbol: k.symbol,
289 kernel_object: ko,
290 kernarg_segment_size: k.kernarg_segment_size,
291 kernarg_segment_align: k.kernarg_segment_align.max(1),
292 group_segment_size: k.group_segment_size,
293 private_segment_size: k.private_segment_size,
294 launch_abi: k.launch_abi,
295 });
296 }
297 let exec = self
298 .executables
299 .get_mut(&exec_id)
300 .ok_or(RuntimeError::InvalidArgument)?;
301 exec.symbols.extend(symbols);
302 Ok(())
303 }
304
305 pub fn executable_freeze(&mut self, exec_id: u64) -> Result<(), RuntimeError> {
306 let Some(exec) = self.executables.get_mut(&exec_id) else {
307 return Err(RuntimeError::InvalidArgument);
308 };
309 if exec.symbols.is_empty() {
310 return Err(RuntimeError::InvalidArgument);
311 }
312 exec.frozen = true;
313 Ok(())
314 }
315
316 pub fn executable_get_symbol_by_name(
317 &mut self,
318 exec_id: u64,
319 name: &str,
320 ) -> Result<u64, RuntimeError> {
321 let Some(exec) = self.executables.get(&exec_id) else {
322 return Err(RuntimeError::InvalidArgument);
323 };
324 if !exec.frozen {
325 return Err(RuntimeError::InvalidArgument);
326 }
327 let idx_map = crate::executable::index_symbols(&exec.symbols);
328 let Some(&sym_idx) = idx_map.get(name) else {
329 return Err(RuntimeError::InvalidArgument);
330 };
331 let id = self.next_symbol_id;
332 self.next_symbol_id = self.next_symbol_id.saturating_add(1);
333 self.executable_symbols.insert(id, (exec_id, sym_idx));
334 Ok(id)
335 }
336
337 pub fn executable_symbol_kernel_object(&self, symbol_id: u64) -> Result<u64, RuntimeError> {
338 let Some(&(exec_id, sym_idx)) = self.executable_symbols.get(&symbol_id) else {
339 return Err(RuntimeError::InvalidArgument);
340 };
341 let Some(exec) = self.executables.get(&exec_id) else {
342 return Err(RuntimeError::InvalidArgument);
343 };
344 let Some(sym) = exec.symbols.get(sym_idx) else {
345 return Err(RuntimeError::InvalidArgument);
346 };
347 Ok(sym.kernel_object)
348 }
349
350 pub fn executable_symbol_info(
351 &self,
352 symbol_id: u64,
353 ) -> Result<&crate::executable::ExecutableSymbol, RuntimeError> {
354 let Some(&(exec_id, sym_idx)) = self.executable_symbols.get(&symbol_id) else {
355 return Err(RuntimeError::InvalidArgument);
356 };
357 let Some(exec) = self.executables.get(&exec_id) else {
358 return Err(RuntimeError::InvalidArgument);
359 };
360 exec.symbols
361 .get(sym_idx)
362 .ok_or(RuntimeError::InvalidArgument)
363 }
364
365 pub fn executable_is_frozen(&self, exec_id: u64) -> Result<bool, RuntimeError> {
366 self.executables
367 .get(&exec_id)
368 .map(|e| e.frozen)
369 .ok_or(RuntimeError::InvalidArgument)
370 }
371
372 pub fn dispatch_captures(&self) -> &[DispatchDescriptor] {
374 &self.dispatch_captures
375 }
376
377 pub fn profile(&self) -> &DeviceProfile {
378 &self.profile
379 }
380
381 pub fn trace_snapshot(&self) -> Vec<TraceEvent> {
382 self.trace.snapshot()
383 }
384
385 pub fn init(&mut self) -> Result<(), RuntimeError> {
386 if self.refcount == u32::MAX {
387 return Err(RuntimeError::RefcountOverflow);
388 }
389 if self.refcount == 0 {
390 self.bootstrap();
391 let seq = self.trace.with_log(TraceLog::next_seq);
392 self.trace.with_log(|log| {
393 log.record(TraceEvent::RuntimeInit {
394 seq,
395 refcount: 1,
396 profile_id: self.profile.profile_id.clone(),
397 profile_revision: self.profile.profile_revision.clone(),
398 fidelity: TraceLog::fidelity_label(self.profile.max_fidelity),
399 });
400 });
401 }
402 self.refcount += 1;
403 Ok(())
404 }
405
406 pub fn shut_down(&mut self) -> Result<(), RuntimeError> {
407 if self.refcount == 0 {
408 return Err(RuntimeError::NotInitialized);
409 }
410 self.refcount -= 1;
411 let seq = self.trace.with_log(TraceLog::next_seq);
412 self.trace.with_log(|log| {
413 log.record(TraceEvent::RuntimeShutdown {
414 seq,
415 refcount: self.refcount,
416 });
417 });
418 if self.refcount == 0 {
419 self.teardown();
420 }
421 Ok(())
422 }
423
424 pub fn is_initialized(&self) -> bool {
425 self.refcount > 0
426 }
427
428 fn bootstrap(&mut self) {
429 self.teardown();
430 let generation = self.alloc_generation();
431 let index = 0u32;
432 let handle = PackedHandle::pack(HandleKind::Agent, generation, index);
433 let agent = VirtualAgent::gpu_from_profile(handle, &self.profile);
434 self.agents.push(AgentSlot {
435 generation,
436 live: true,
437 agent: Some(agent),
438 });
439 self.bootstrap_memory(handle);
440 }
441
442 fn bootstrap_memory(&mut self, agent_handle: PackedHandle) {
443 let specs = [
444 (
445 HandleKind::Region,
446 MemoryViewKind::RegionFineKernarg,
447 REGION_SEGMENT_GLOBAL,
448 REGION_FLAGS_FINE_KERNARG,
449 None,
450 ),
451 (
452 HandleKind::Region,
453 MemoryViewKind::RegionCoarse,
454 REGION_SEGMENT_GLOBAL,
455 REGION_FLAGS_COARSE,
456 None,
457 ),
458 (
459 HandleKind::MemoryPool,
460 MemoryViewKind::PoolFineHost,
461 AMD_SEGMENT_GLOBAL,
462 AMD_POOL_FLAGS_FINE_KERNARG,
463 Some(AMD_POOL_LOCATION_CPU),
464 ),
465 (
466 HandleKind::MemoryPool,
467 MemoryViewKind::PoolCoarseDevice,
468 AMD_SEGMENT_GLOBAL,
469 AMD_POOL_FLAGS_COARSE,
470 Some(AMD_POOL_LOCATION_GPU),
471 ),
472 ];
473 for (kind, view, segment, flags, location) in specs {
474 let generation = self.alloc_generation();
475 let table = if kind == HandleKind::Region {
476 &mut self.regions
477 } else {
478 &mut self.pools
479 };
480 let index = table.len() as u32;
481 let handle = PackedHandle::pack(kind, generation, index);
482 let space = MemorySpace {
483 handle,
484 kind: view,
485 agent_handle,
486 segment,
487 global_flags: flags,
488 size_bytes: SOFTGPU_POOL_BYTES,
489 alloc_max_size: SOFTGPU_MAX_ALLOC_BYTES,
490 runtime_alloc_allowed: true,
491 granule: SOFTGPU_ALLOC_GRANULE,
492 alignment: SOFTGPU_ALLOC_ALIGNMENT,
493 location,
494 };
495 table.push(SpaceSlot {
496 generation,
497 live: true,
498 space: Some(space),
499 });
500 }
501 }
502
503 fn alloc_generation(&mut self) -> u32 {
504 let generation = self.next_generation;
505 self.next_generation = if generation >= 0x00FF_FFFF {
506 1
507 } else {
508 generation + 1
509 };
510 generation
511 }
512
513 fn teardown(&mut self) {
514 for slot in &self.signals {
516 if let Some(sig) = slot.signal.as_ref() {
517 sig.cancel();
518 }
519 }
520 let mut doorbells = Vec::new();
521 while let Some(slot) = self.queues.pop() {
522 if let Some(q) = slot.queue {
523 doorbells.push(q.doorbell);
524 self.destroy_queue_resources(q);
525 }
526 }
527 for doorbell in doorbells {
528 let _ = self.signal_destroy_doorbell(doorbell);
529 }
530 self.signals.clear();
531 self.allocator.clear_all();
532 self.regions.clear();
533 self.pools.clear();
534 let live_count = self.agents.iter().filter(|s| s.live).count();
535 for _ in 0..live_count {
536 let _ = self.alloc_generation();
537 }
538 self.agents.clear();
539 self.dispatch_captures.clear();
540 }
541
542 fn destroy_queue_resources(&mut self, queue: SoftGpuQueue) {
543 if !queue.packet_buffer.is_null() {
544 let layout = Layout::from_size_align(queue.packet_bytes, AQL_PACKET_BYTES)
545 .unwrap_or_else(|_| Layout::from_size_align(AQL_PACKET_BYTES, 8).unwrap());
546 unsafe { dealloc(queue.packet_buffer, layout) };
548 }
549 let _ = queue;
551 }
552
553 fn resolve_agent(&self, handle: PackedHandle) -> Result<&VirtualAgent, RuntimeError> {
554 if handle.is_invalid() || handle.kind() != Some(HandleKind::Agent) {
555 return Err(RuntimeError::InvalidAgent);
556 }
557 let idx = handle.index() as usize;
558 let slot = self.agents.get(idx).ok_or(RuntimeError::InvalidAgent)?;
559 if !slot.live || slot.generation != handle.generation() {
560 return Err(RuntimeError::InvalidAgent);
561 }
562 slot.agent.as_ref().ok_or(RuntimeError::InvalidAgent)
563 }
564
565 fn resolve_space(
566 &self,
567 handle: PackedHandle,
568 kind: HandleKind,
569 ) -> Result<&MemorySpace, RuntimeError> {
570 let make_err = || {
571 if kind == HandleKind::Region {
572 RuntimeError::InvalidRegion
573 } else {
574 RuntimeError::InvalidPool
575 }
576 };
577 if handle.is_invalid() || handle.kind() != Some(kind) {
578 return Err(make_err());
579 }
580 let table = if kind == HandleKind::Region {
581 &self.regions
582 } else {
583 &self.pools
584 };
585 let slot = table.get(handle.index() as usize).ok_or_else(make_err)?;
586 if !slot.live || slot.generation != handle.generation() {
587 return Err(make_err());
588 }
589 slot.space.as_ref().ok_or_else(make_err)
590 }
591
592 fn resolve_signal(
593 &self,
594 handle: PackedHandle,
595 ) -> Result<std::sync::Arc<SoftGpuSignal>, RuntimeError> {
596 if handle.is_invalid() || handle.kind() != Some(HandleKind::Signal) {
597 return Err(RuntimeError::InvalidSignal);
598 }
599 let slot = self
600 .signals
601 .get(handle.index() as usize)
602 .ok_or(RuntimeError::InvalidSignal)?;
603 if !slot.live || slot.generation != handle.generation() {
604 return Err(RuntimeError::InvalidSignal);
605 }
606 slot.signal
607 .as_ref()
608 .cloned()
609 .ok_or(RuntimeError::InvalidSignal)
610 }
611
612 fn resolve_queue_by_abi(&self, abi: *const HsaQueueAbi) -> Result<&SoftGpuQueue, RuntimeError> {
613 if abi.is_null() {
614 return Err(RuntimeError::InvalidQueue);
615 }
616 for slot in &self.queues {
617 if !slot.live {
618 continue;
619 }
620 if let Some(q) = slot.queue.as_ref() {
621 if std::ptr::eq(q.abi_ptr(), abi) {
622 return Ok(q);
623 }
624 }
625 }
626 Err(RuntimeError::InvalidQueue)
627 }
628
629 pub fn iterate_agents<F>(&self, mut callback: F) -> Result<(), RuntimeError>
630 where
631 F: FnMut(&VirtualAgent) -> Result<(), RuntimeError>,
632 {
633 if !self.is_initialized() {
634 return Err(RuntimeError::NotInitialized);
635 }
636 let seq = self.trace.with_log(TraceLog::next_seq);
637 self.trace.with_log(|log| {
638 log.record(TraceEvent::AgentIterateBegin {
639 seq,
640 agent_count: self.agents.iter().filter(|s| s.live).count(),
641 });
642 });
643 for slot in &self.agents {
644 if !slot.live {
645 continue;
646 }
647 let agent = slot.agent.as_ref().ok_or(RuntimeError::Internal(
648 "live agent slot missing agent".into(),
649 ))?;
650 let seq = self.trace.with_log(TraceLog::next_seq);
651 self.trace.with_log(|log| {
652 log.record(TraceEvent::AgentIterateVisit {
653 seq,
654 agent_handle: agent.handle.raw(),
655 kind: agent.kind.as_str().to_string(),
656 });
657 });
658 callback(agent)?;
659 }
660 Ok(())
661 }
662
663 pub fn agent_get_info(
664 &self,
665 handle: PackedHandle,
666 attr: AgentInfoAttr,
667 ) -> Result<AgentInfoValue, RuntimeError> {
668 if !self.is_initialized() {
669 return Err(RuntimeError::NotInitialized);
670 }
671 let agent = match self.resolve_agent(handle) {
672 Ok(a) => a,
673 Err(err) => {
674 self.trace_get_info(handle, attr, err.as_str());
675 return Err(err);
676 }
677 };
678 let value = match attr {
679 AgentInfoAttr::Name => AgentInfoValue::Name(agent.name.clone()),
680 AgentInfoAttr::VendorName => AgentInfoValue::VendorName(agent.vendor_name.clone()),
681 AgentInfoAttr::Feature => AgentInfoValue::Feature(agent.feature_mask),
682 AgentInfoAttr::Device => AgentInfoValue::Device(agent.kind),
683 AgentInfoAttr::VersionMajor => AgentInfoValue::U16(1),
684 AgentInfoAttr::VersionMinor => AgentInfoValue::U16(2),
685 AgentInfoAttr::QueuesMax => AgentInfoValue::U32(agent.queues_max),
686 AgentInfoAttr::QueueMinSize => AgentInfoValue::U32(agent.queue_min_size),
687 AgentInfoAttr::QueueMaxSize => AgentInfoValue::U32(agent.queue_max_size),
688 AgentInfoAttr::QueueType => AgentInfoValue::U32(agent.queue_type),
689 };
690 self.trace_get_info(handle, attr, "ok");
691 Ok(value)
692 }
693
694 fn trace_get_info(&self, handle: PackedHandle, attr: AgentInfoAttr, outcome: &str) {
695 let seq = self.trace.with_log(TraceLog::next_seq);
696 self.trace.with_log(|log| {
697 log.record(TraceEvent::AgentGetInfo {
698 seq,
699 agent_handle: handle.raw(),
700 attribute: attr.as_str().to_string(),
701 outcome: outcome.to_string(),
702 });
703 });
704 }
705
706 pub fn iterate_regions<F>(
707 &self,
708 agent: PackedHandle,
709 mut callback: F,
710 ) -> Result<(), RuntimeError>
711 where
712 F: FnMut(&MemorySpace) -> Result<(), RuntimeError>,
713 {
714 if !self.is_initialized() {
715 return Err(RuntimeError::NotInitialized);
716 }
717 let _ = self.resolve_agent(agent)?;
718 for slot in &self.regions {
719 if !slot.live {
720 continue;
721 }
722 let space = slot
723 .space
724 .as_ref()
725 .ok_or(RuntimeError::Internal("live region missing".into()))?;
726 if space.agent_handle != agent {
727 continue;
728 }
729 callback(space)?;
730 }
731 Ok(())
732 }
733
734 pub fn iterate_pools<F>(&self, agent: PackedHandle, mut callback: F) -> Result<(), RuntimeError>
735 where
736 F: FnMut(&MemorySpace) -> Result<(), RuntimeError>,
737 {
738 if !self.is_initialized() {
739 return Err(RuntimeError::NotInitialized);
740 }
741 let _ = self.resolve_agent(agent)?;
742 for slot in &self.pools {
743 if !slot.live {
744 continue;
745 }
746 let space = slot
747 .space
748 .as_ref()
749 .ok_or(RuntimeError::Internal("live pool missing".into()))?;
750 if space.agent_handle != agent {
751 continue;
752 }
753 callback(space)?;
754 }
755 Ok(())
756 }
757
758 pub fn region_get_info(
759 &self,
760 handle: PackedHandle,
761 attr: RegionInfoAttr,
762 ) -> Result<RegionInfoValue, RuntimeError> {
763 if !self.is_initialized() {
764 return Err(RuntimeError::NotInitialized);
765 }
766 let space = self.resolve_space(handle, HandleKind::Region)?;
767 Ok(match attr {
768 RegionInfoAttr::Segment => RegionInfoValue::Segment(space.segment),
769 RegionInfoAttr::GlobalFlags => RegionInfoValue::GlobalFlags(space.global_flags),
770 RegionInfoAttr::Size => RegionInfoValue::Size(space.size_bytes),
771 RegionInfoAttr::AllocMaxSize => RegionInfoValue::AllocMaxSize(space.alloc_max_size),
772 RegionInfoAttr::RuntimeAllocAllowed => {
773 RegionInfoValue::RuntimeAllocAllowed(space.runtime_alloc_allowed)
774 }
775 RegionInfoAttr::Granule => RegionInfoValue::Granule(space.granule),
776 RegionInfoAttr::Alignment => RegionInfoValue::Alignment(space.alignment),
777 })
778 }
779
780 pub fn pool_get_info(
781 &self,
782 handle: PackedHandle,
783 attr: PoolInfoAttr,
784 ) -> Result<PoolInfoValue, RuntimeError> {
785 if !self.is_initialized() {
786 return Err(RuntimeError::NotInitialized);
787 }
788 let space = self.resolve_space(handle, HandleKind::MemoryPool)?;
789 Ok(match attr {
790 PoolInfoAttr::Segment => PoolInfoValue::Segment(space.segment),
791 PoolInfoAttr::GlobalFlags => PoolInfoValue::GlobalFlags(space.global_flags),
792 PoolInfoAttr::Size => PoolInfoValue::Size(space.size_bytes),
793 PoolInfoAttr::RuntimeAllocAllowed => {
794 PoolInfoValue::RuntimeAllocAllowed(space.runtime_alloc_allowed)
795 }
796 PoolInfoAttr::Granule => PoolInfoValue::Granule(space.granule),
797 PoolInfoAttr::Alignment => PoolInfoValue::Alignment(space.alignment),
798 PoolInfoAttr::AccessibleByAll => PoolInfoValue::AccessibleByAll(true),
799 PoolInfoAttr::AllocMaxSize => PoolInfoValue::AllocMaxSize(space.alloc_max_size),
800 PoolInfoAttr::Location => {
801 PoolInfoValue::Location(space.location.unwrap_or(AMD_POOL_LOCATION_CPU))
802 }
803 PoolInfoAttr::RecGranule => PoolInfoValue::RecGranule(space.granule),
804 })
805 }
806
807 pub fn memory_allocate(
808 &mut self,
809 space_handle: PackedHandle,
810 size: usize,
811 ) -> Result<*mut u8, RuntimeError> {
812 if !self.is_initialized() {
813 return Err(RuntimeError::NotInitialized);
814 }
815 let kind = space_handle.kind().ok_or(RuntimeError::InvalidArgument)?;
816 if kind != HandleKind::Region && kind != HandleKind::MemoryPool {
817 return Err(RuntimeError::InvalidArgument);
818 }
819 let space = self.resolve_space(space_handle, kind)?.clone();
820 if !space.runtime_alloc_allowed {
821 return Err(RuntimeError::InvalidAllocation);
822 }
823 let result = self.allocator.allocate(
824 space.handle,
825 size,
826 space.granule,
827 space.alignment,
828 space.alloc_max_size,
829 );
830 let (ptr, outcome) = match &result {
831 Ok(p) => (*p as u64, "ok"),
832 Err(AllocError::InvalidArgument) => (0, "invalid_argument"),
833 Err(AllocError::InvalidAllocation) => (0, "invalid_allocation"),
834 Err(AllocError::OutOfResources) => (0, "out_of_resources"),
835 };
836 let seq = self.trace.with_log(TraceLog::next_seq);
837 self.trace.with_log(|log| {
838 log.record(TraceEvent::MemoryAllocate {
839 seq,
840 space_handle: space_handle.raw(),
841 size,
842 ptr,
843 outcome: outcome.into(),
844 });
845 });
846 result.map_err(|e| match e {
847 AllocError::InvalidArgument => RuntimeError::InvalidArgument,
848 AllocError::InvalidAllocation => RuntimeError::InvalidAllocation,
849 AllocError::OutOfResources => RuntimeError::OutOfResources,
850 })
851 }
852
853 pub fn memory_free(&mut self, ptr: *mut u8) -> Result<(), RuntimeError> {
854 if !self.is_initialized() {
855 return Err(RuntimeError::NotInitialized);
856 }
857 let result = self.allocator.free(ptr);
858 let outcome = match &result {
859 Ok(_) => "ok",
860 Err(_) => "invalid_argument",
861 };
862 let seq = self.trace.with_log(TraceLog::next_seq);
863 self.trace.with_log(|log| {
864 log.record(TraceEvent::MemoryFree {
865 seq,
866 ptr: ptr as u64,
867 outcome: outcome.into(),
868 });
869 });
870 result
871 .map(|_| ())
872 .map_err(|_| RuntimeError::InvalidArgument)
873 }
874
875 pub fn allocation_lookup(
876 &self,
877 ptr: *const u8,
878 ) -> Result<crate::memory::AllocationMeta, RuntimeError> {
879 if !self.is_initialized() {
880 return Err(RuntimeError::NotInitialized);
881 }
882 self.allocator
883 .lookup(ptr)
884 .ok_or(RuntimeError::InvalidArgument)
885 }
886
887 pub unsafe fn memory_copy(
892 &self,
893 dst: *mut u8,
894 src: *const u8,
895 size: usize,
896 ) -> Result<(), RuntimeError> {
897 if !self.is_initialized() {
898 return Err(RuntimeError::NotInitialized);
899 }
900 if dst.is_null() || src.is_null() {
901 return Err(RuntimeError::InvalidArgument);
902 }
903 unsafe {
905 std::ptr::copy_nonoverlapping(src, dst, size);
906 }
907 Ok(())
908 }
909
910 pub fn agents_allow_access(
911 &self,
912 agents: &[PackedHandle],
913 _ptr: *const u8,
914 ) -> Result<(), RuntimeError> {
915 if !self.is_initialized() {
916 return Err(RuntimeError::NotInitialized);
917 }
918 for agent in agents {
919 let _ = self.resolve_agent(*agent)?;
920 }
921 Ok(())
923 }
924
925 pub fn agent_memory_pool_access(
926 &self,
927 agent: PackedHandle,
928 pool: PackedHandle,
929 ) -> Result<u32, RuntimeError> {
930 if !self.is_initialized() {
931 return Err(RuntimeError::NotInitialized);
932 }
933 let _ = self.resolve_agent(agent)?;
934 let _ = self.resolve_space(pool, HandleKind::MemoryPool)?;
935 Ok(1)
937 }
938
939 pub fn signal_create(&mut self, initial: i64) -> Result<PackedHandle, RuntimeError> {
940 if !self.is_initialized() {
941 return Err(RuntimeError::NotInitialized);
942 }
943 let generation = self.alloc_generation();
944 let index = self.signals.len() as u32;
945 let handle = PackedHandle::pack(HandleKind::Signal, generation, index);
946 self.signals.push(SignalSlot {
947 generation,
948 live: true,
949 signal: Some(std::sync::Arc::new(SoftGpuSignal::new(handle, initial))),
950 });
951 let seq = self.trace.with_log(TraceLog::next_seq);
952 self.trace.with_log(|log| {
953 log.record(TraceEvent::SignalCreate {
954 seq,
955 signal_handle: handle.raw(),
956 initial,
957 });
958 });
959 Ok(handle)
960 }
961
962 pub fn signal_destroy(&mut self, handle: PackedHandle) -> Result<(), RuntimeError> {
963 if !self.is_initialized() {
964 return Err(RuntimeError::NotInitialized);
965 }
966 if handle.is_invalid() || handle.kind() != Some(HandleKind::Signal) {
967 return Err(RuntimeError::InvalidSignal);
968 }
969 let idx = handle.index() as usize;
970 let slot = self
971 .signals
972 .get_mut(idx)
973 .ok_or(RuntimeError::InvalidSignal)?;
974 if !slot.live || slot.generation != handle.generation() {
975 return Err(RuntimeError::InvalidSignal);
976 }
977 if slot.signal.as_ref().map(|s| s.is_doorbell).unwrap_or(false) {
978 return Err(RuntimeError::InvalidArgument);
980 }
981 if let Some(sig) = slot.signal.as_ref() {
982 sig.cancel();
983 }
984 slot.live = false;
985 slot.signal = None;
986 let _ = self.alloc_generation();
987 let seq = self.trace.with_log(TraceLog::next_seq);
988 self.trace.with_log(|log| {
989 log.record(TraceEvent::SignalDestroy {
990 seq,
991 signal_handle: handle.raw(),
992 });
993 });
994 Ok(())
995 }
996
997 pub fn signal_load(&self, handle: PackedHandle) -> Result<i64, RuntimeError> {
998 if !self.is_initialized() {
999 return Err(RuntimeError::NotInitialized);
1000 }
1001 Ok(self.resolve_signal(handle)?.load())
1002 }
1003
1004 pub fn signal_store(&mut self, handle: PackedHandle, value: i64) -> Result<(), RuntimeError> {
1005 if !self.is_initialized() {
1006 return Err(RuntimeError::NotInitialized);
1007 }
1008 let sig = self.resolve_signal(handle)?;
1009 if sig.is_cancelled() {
1010 return Err(RuntimeError::InvalidSignal);
1011 }
1012 let is_doorbell = sig.is_doorbell;
1013 let queue_id = sig.queue_id;
1014 sig.store(value);
1015 if is_doorbell {
1016 if let Some(qid) = queue_id {
1017 let mut pending: Vec<PacketObservation> = Vec::new();
1018 let mut observe_err: Option<String> = None;
1019 for slot in &self.queues {
1020 if let Some(q) = slot.queue.as_ref() {
1021 if q.id == qid {
1022 q.note_doorbell_store();
1023 let seq = self.trace.with_log(TraceLog::next_seq);
1024 self.trace.with_log(|log| {
1025 log.record(TraceEvent::QueueDoorbell {
1026 seq,
1027 queue_id: qid,
1028 value,
1029 });
1030 });
1031 match q.observe_packets() {
1032 Ok(obs) => pending = obs,
1033 Err(err) => observe_err = Some(format!("{err:?}")),
1034 }
1035 break;
1036 }
1037 }
1038 }
1039 if let Some(detail) = observe_err {
1040 let seq = self.trace.with_log(TraceLog::next_seq);
1041 self.trace.with_log(|log| {
1042 log.record(TraceEvent::PacketValidateFailed {
1043 seq,
1044 queue_id: qid,
1045 detail,
1046 });
1047 });
1048 }
1049 for p in pending {
1050 self.process_observed_packet(qid, &p);
1051 }
1052 }
1053 }
1054 Ok(())
1055 }
1056
1057 fn classify_kernarg(&self, addr: u64) -> KernargClass {
1058 if addr == 0 {
1059 return KernargClass::Null;
1060 }
1061 match self.allocator.lookup(addr as *const u8) {
1062 Some(meta) => KernargClass::SoftGpu {
1063 alloc_id: Some(meta.alloc_id),
1064 addr,
1065 },
1066 None => KernargClass::ForeignOpaque { addr },
1067 }
1068 }
1069
1070 fn kernarg_class_label(k: &KernargClass) -> String {
1071 match k {
1072 KernargClass::Null => "null".into(),
1073 KernargClass::SoftGpu { alloc_id, .. } => {
1074 format!("softgpu:{}", alloc_id.unwrap_or(0))
1075 }
1076 KernargClass::ForeignOpaque { .. } => "foreign_opaque".into(),
1077 }
1078 }
1079
1080 fn signal_store_plain(&self, handle: PackedHandle, value: i64) -> Result<(), RuntimeError> {
1082 let sig = self.resolve_signal(handle)?;
1083 if sig.is_cancelled() || sig.is_doorbell {
1084 return Err(RuntimeError::InvalidSignal);
1085 }
1086 sig.store(value);
1087 Ok(())
1088 }
1089
1090 fn process_observed_packet(&mut self, queue_id: u64, obs: &PacketObservation) {
1091 let seq = self.trace.with_log(TraceLog::next_seq);
1092 self.trace.with_log(|log| {
1093 log.record(TraceEvent::PacketObserved {
1094 seq,
1095 queue_id,
1096 packet_index: obs.packet_index,
1097 packet_type: obs.packet_type,
1098 });
1099 });
1100
1101 let parsed = parse_supported_packet(&obs.bytes, obs.packet_index, |addr| {
1102 self.classify_kernarg(addr)
1103 });
1104
1105 match parsed {
1106 Ok(desc) => {
1107 let seq = self.trace.with_log(TraceLog::next_seq);
1108 self.trace.with_log(|log| {
1109 log.record(TraceEvent::DispatchValidated {
1110 seq,
1111 queue_id,
1112 packet_index: desc.packet_index,
1113 packet_type: desc.packet_type.as_u16(),
1114 dimensions: desc.dimensions,
1115 workgroup_size: desc.workgroup_size,
1116 grid_size: desc.grid_size,
1117 private_segment_size: desc.private_segment_size,
1118 group_segment_size: desc.group_segment_size,
1119 kernel_object: desc.kernel_object,
1120 kernarg_class: Self::kernarg_class_label(&desc.kernarg),
1121 completion_signal: desc.completion_signal,
1122 });
1123 });
1124 self.try_dispatch_or_diagnose(queue_id, &desc);
1125 self.dispatch_captures.push(desc);
1126 }
1127 Err(err) => {
1128 let seq = self.trace.with_log(TraceLog::next_seq);
1129 self.trace.with_log(|log| {
1130 log.record(TraceEvent::DispatchRejected {
1131 seq,
1132 queue_id,
1133 packet_index: obs.packet_index,
1134 packet_type: obs.packet_type,
1135 detail: format!("{err:?}"),
1136 contract: DIAGNOSTIC_REJECTED.into(),
1137 });
1138 });
1139 self.apply_diagnostic_reject(queue_id, obs.packet_index);
1140 }
1141 }
1142 }
1143
1144 fn try_dispatch_or_diagnose(&mut self, queue_id: u64, desc: &DispatchDescriptor) {
1145 if desc.packet_type != crate::aql::PacketType::KernelDispatch {
1146 self.apply_diagnostic_complete(queue_id, desc);
1147 return;
1148 }
1149 let Some(kernel) = self.isa_kernels.get(&desc.kernel_object).cloned() else {
1150 self.apply_diagnostic_complete(queue_id, desc);
1151 return;
1152 };
1153 let KernargClass::SoftGpu { addr, .. } = desc.kernarg else {
1154 self.apply_diagnostic_complete(queue_id, desc);
1155 return;
1156 };
1157 let grid_x = desc.grid_size[0];
1158 if grid_x == 0 {
1159 self.apply_diagnostic_complete(queue_id, desc);
1160 return;
1161 }
1162
1163 struct AllocMem<'a>(&'a mut SoftGpuAllocator);
1164 impl IsaMemory for AllocMem<'_> {
1165 fn load_u32(&self, addr: u64) -> softgpu_amd_isa::Result<u32> {
1166 let mut buf = [0u8; 4];
1167 self.0.read_bytes_at(addr, &mut buf).map_err(|_| {
1168 softgpu_amd_isa::IsaError::new(
1169 softgpu_amd_isa::IsaErrorKind::Trap,
1170 format!("SoftGPU alloc load_u32 OOB/unknown addr=0x{addr:x}"),
1171 )
1172 })?;
1173 Ok(u32::from_le_bytes(buf))
1174 }
1175 fn store_u32(&mut self, addr: u64, value: u32) -> softgpu_amd_isa::Result<()> {
1176 self.0
1177 .write_bytes_at(addr, &value.to_le_bytes())
1178 .map_err(|_| {
1179 softgpu_amd_isa::IsaError::new(
1180 softgpu_amd_isa::IsaErrorKind::Trap,
1181 format!("SoftGPU alloc store_u32 OOB/unknown addr=0x{addr:x}"),
1182 )
1183 })
1184 }
1185 fn load_u64(&self, addr: u64) -> softgpu_amd_isa::Result<u64> {
1186 let mut buf = [0u8; 8];
1187 self.0.read_bytes_at(addr, &mut buf).map_err(|_| {
1188 softgpu_amd_isa::IsaError::new(
1189 softgpu_amd_isa::IsaErrorKind::Trap,
1190 format!("SoftGPU alloc load_u64 OOB/unknown addr=0x{addr:x}"),
1191 )
1192 })?;
1193 Ok(u64::from_le_bytes(buf))
1194 }
1195 fn store_u64(&mut self, addr: u64, value: u64) -> softgpu_amd_isa::Result<()> {
1196 self.0
1197 .write_bytes_at(addr, &value.to_le_bytes())
1198 .map_err(|_| {
1199 softgpu_amd_isa::IsaError::new(
1200 softgpu_amd_isa::IsaErrorKind::Trap,
1201 format!("SoftGPU alloc store_u64 OOB/unknown addr=0x{addr:x}"),
1202 )
1203 })
1204 }
1205 }
1206
1207 let mut mem = AllocMem(&mut self.allocator);
1208 match run_code_1d(&kernel.code, &mut mem, addr, grid_x, WaveSize::Wave32) {
1209 Ok(_) => self.apply_kernel_success(queue_id, desc, &kernel.name),
1210 Err(e) => {
1211 let seq = self.trace.with_log(TraceLog::next_seq);
1212 self.trace.with_log(|log| {
1213 log.record(TraceEvent::DispatchRejected {
1214 seq,
1215 queue_id,
1216 packet_index: desc.packet_index,
1217 packet_type: desc.packet_type.as_u16(),
1218 detail: e.to_string(),
1219 contract: DIAGNOSTIC_REJECTED.into(),
1220 });
1221 });
1222 self.apply_diagnostic_reject(queue_id, desc.packet_index);
1223 }
1224 }
1225 }
1226
1227 fn apply_kernel_success(
1228 &mut self,
1229 queue_id: u64,
1230 desc: &DispatchDescriptor,
1231 kernel_name: &str,
1232 ) {
1233 if desc.completion_signal != 0 {
1234 let handle = PackedHandle::from_raw(desc.completion_signal);
1235 let _ = self.signal_store_plain(handle, 0);
1236 }
1237 self.advance_packet_processor(queue_id, desc.packet_index);
1238 let seq = self.trace.with_log(TraceLog::next_seq);
1239 self.trace.with_log(|log| {
1240 log.record(TraceEvent::DiagnosticComplete {
1241 seq,
1242 queue_id,
1243 packet_index: desc.packet_index,
1244 completion_signal: desc.completion_signal,
1245 contract: KERNEL_SUCCESS.into(),
1246 note: format!("softgpu_isa_kernel:{kernel_name}"),
1247 });
1248 });
1249 }
1250 fn apply_diagnostic_complete(&mut self, queue_id: u64, desc: &DispatchDescriptor) {
1251 if desc.completion_signal != 0 {
1254 let handle = PackedHandle::from_raw(desc.completion_signal);
1255 let _ = self.signal_store_plain(handle, 0);
1256 }
1257 self.advance_packet_processor(queue_id, desc.packet_index);
1258 let seq = self.trace.with_log(TraceLog::next_seq);
1259 self.trace.with_log(|log| {
1260 log.record(TraceEvent::DiagnosticComplete {
1261 seq,
1262 queue_id,
1263 packet_index: desc.packet_index,
1264 completion_signal: desc.completion_signal,
1265 contract: DIAGNOSTIC_COMPLETE_NO_EXECUTION.into(),
1266 note: "not_kernel_success".into(),
1267 });
1268 });
1269 }
1270
1271 fn apply_diagnostic_reject(&mut self, queue_id: u64, packet_index: u64) {
1272 self.advance_packet_processor(queue_id, packet_index);
1274 }
1275
1276 fn advance_packet_processor(&mut self, queue_id: u64, packet_index: u64) {
1277 for slot in &self.queues {
1278 if let Some(q) = slot.queue.as_ref() {
1279 if q.id == queue_id {
1280 q.invalidate_packet_slot(packet_index);
1281 let next = packet_index.saturating_add(1);
1282 if next > q.read_index() {
1284 q.store_read_index(next);
1285 }
1286 break;
1287 }
1288 }
1289 }
1290 }
1291
1292 pub fn signal_arc(
1294 &self,
1295 handle: PackedHandle,
1296 ) -> Result<std::sync::Arc<SoftGpuSignal>, RuntimeError> {
1297 if !self.is_initialized() {
1298 return Err(RuntimeError::NotInitialized);
1299 }
1300 self.resolve_signal(handle)
1301 }
1302
1303 pub fn signal_wait(
1304 &self,
1305 handle: PackedHandle,
1306 condition: u32,
1307 compare: i64,
1308 timeout_hint_ns: u64,
1309 wait_state_hint: u32,
1310 ) -> Result<SignalWaitOutcome, RuntimeError> {
1311 let sig = self.signal_arc(handle)?;
1312 let cond = SignalCondition::from_u32(condition).ok_or(RuntimeError::InvalidArgument)?;
1313 Ok(sig.wait(cond, compare, timeout_hint_ns, wait_state_hint))
1314 }
1315
1316 pub fn queue_observe(
1318 &self,
1319 abi: *const HsaQueueAbi,
1320 ) -> Result<Vec<crate::queue::PacketObservation>, RuntimeError> {
1321 if !self.is_initialized() {
1322 return Err(RuntimeError::NotInitialized);
1323 }
1324 let q = self.resolve_queue_by_abi(abi)?;
1325 q.observe_packets()
1326 .map_err(|e| RuntimeError::Internal(format!("{e:?}")))
1327 }
1328
1329 pub fn queue_create(
1330 &mut self,
1331 agent: PackedHandle,
1332 size: u32,
1333 type_: u32,
1334 ) -> Result<*mut HsaQueueAbi, RuntimeError> {
1335 if !self.is_initialized() {
1336 return Err(RuntimeError::NotInitialized);
1337 }
1338 let agent_ref = self.resolve_agent(agent)?;
1339 if size == 0 || !is_power_of_two(size) {
1340 return Err(RuntimeError::InvalidArgument);
1341 }
1342 if size < agent_ref.queue_min_size || size > agent_ref.queue_max_size {
1343 return Err(RuntimeError::InvalidArgument);
1344 }
1345 if type_ != QUEUE_TYPE_MULTI && type_ != 1 {
1346 return Err(RuntimeError::InvalidQueueCreation);
1348 }
1349 let live_queues = self.queues.iter().filter(|s| s.live).count() as u32;
1350 if live_queues >= SOFTGPU_QUEUES_MAX {
1351 return Err(RuntimeError::OutOfResources);
1352 }
1353
1354 let queue_id = self.next_queue_id;
1355 self.next_queue_id = self.next_queue_id.saturating_add(1);
1356
1357 let generation = self.alloc_generation();
1358 let index = self.signals.len() as u32;
1359 let doorbell = PackedHandle::pack(HandleKind::Signal, generation, index);
1360 self.signals.push(SignalSlot {
1361 generation,
1362 live: true,
1363 signal: Some(std::sync::Arc::new(SoftGpuSignal::new_doorbell(
1364 doorbell, 0, queue_id,
1365 ))),
1366 });
1367 let seq = self.trace.with_log(TraceLog::next_seq);
1368 self.trace.with_log(|log| {
1369 log.record(TraceEvent::SignalCreate {
1370 seq,
1371 signal_handle: doorbell.raw(),
1372 initial: 0,
1373 });
1374 });
1375
1376 let packet_bytes = (size as usize).saturating_mul(AQL_PACKET_BYTES);
1377 let layout = Layout::from_size_align(packet_bytes, AQL_PACKET_BYTES)
1378 .map_err(|_| RuntimeError::OutOfResources)?;
1379 let packet_ptr = unsafe { alloc_zeroed(layout) };
1381 if packet_ptr.is_null() {
1382 let _ = self.signal_destroy_doorbell(doorbell);
1383 return Err(RuntimeError::OutOfResources);
1384 }
1385 unsafe {
1387 init_packet_buffer(std::slice::from_raw_parts_mut(packet_ptr, packet_bytes));
1388 }
1389
1390 let generation = self.alloc_generation();
1391 let index = self.queues.len() as u32;
1392 let handle = PackedHandle::pack(HandleKind::Queue, generation, index);
1393
1394 let abi = Box::new(HsaQueueAbi {
1395 type_,
1396 features: QUEUE_FEATURE_KERNEL_DISPATCH,
1397 base_address: packet_ptr,
1398 doorbell_signal: doorbell.raw(),
1399 size,
1400 reserved1: 0,
1401 id: queue_id,
1402 });
1403
1404 let queue = SoftGpuQueue::new(
1405 handle,
1406 agent,
1407 queue_id,
1408 abi,
1409 packet_ptr,
1410 packet_bytes,
1411 doorbell,
1412 );
1413 let abi_ptr = queue.abi_ptr();
1414 self.queues.push(QueueSlot {
1415 generation,
1416 live: true,
1417 queue: Some(queue),
1418 });
1419
1420 let seq = self.trace.with_log(TraceLog::next_seq);
1421 self.trace.with_log(|log| {
1422 log.record(TraceEvent::QueueCreate {
1423 seq,
1424 queue_id,
1425 size,
1426 agent_handle: agent.raw(),
1427 });
1428 });
1429 Ok(abi_ptr)
1430 }
1431
1432 fn signal_destroy_doorbell(&mut self, handle: PackedHandle) -> Result<(), RuntimeError> {
1433 let idx = handle.index() as usize;
1434 let slot = self
1435 .signals
1436 .get_mut(idx)
1437 .ok_or(RuntimeError::InvalidSignal)?;
1438 if !slot.live || slot.generation != handle.generation() {
1439 return Err(RuntimeError::InvalidSignal);
1440 }
1441 if let Some(sig) = slot.signal.as_ref() {
1442 sig.cancel();
1443 }
1444 slot.live = false;
1445 slot.signal = None;
1446 let _ = self.alloc_generation();
1447 Ok(())
1448 }
1449
1450 pub fn queue_destroy(&mut self, abi: *mut HsaQueueAbi) -> Result<(), RuntimeError> {
1451 if !self.is_initialized() {
1452 return Err(RuntimeError::NotInitialized);
1453 }
1454 if abi.is_null() {
1455 return Err(RuntimeError::InvalidQueue);
1456 }
1457 let mut found = None;
1458 for (i, slot) in self.queues.iter().enumerate() {
1459 if !slot.live {
1460 continue;
1461 }
1462 if let Some(q) = slot.queue.as_ref() {
1463 if q.abi_ptr() == abi {
1464 found = Some(i);
1465 break;
1466 }
1467 }
1468 }
1469 let idx = found.ok_or(RuntimeError::InvalidQueue)?;
1470 let slot = &mut self.queues[idx];
1471 let Some(mut queue) = slot.queue.take() else {
1472 return Err(RuntimeError::InvalidQueue);
1473 };
1474 slot.live = false;
1475 let queue_id = queue.id;
1476 let doorbell = queue.doorbell;
1477 if !queue.packet_buffer.is_null() {
1478 let layout = Layout::from_size_align(queue.packet_bytes, AQL_PACKET_BYTES)
1479 .unwrap_or_else(|_| Layout::from_size_align(AQL_PACKET_BYTES, 8).unwrap());
1480 unsafe { dealloc(queue.packet_buffer, layout) };
1481 queue.packet_buffer = std::ptr::null_mut();
1482 }
1483 let _ = self.signal_destroy_doorbell(doorbell);
1484 let _ = self.alloc_generation();
1485 let seq = self.trace.with_log(TraceLog::next_seq);
1486 self.trace.with_log(|log| {
1487 log.record(TraceEvent::QueueDestroy { seq, queue_id });
1488 });
1489 Ok(())
1490 }
1491
1492 pub fn queue_load_write_index(&self, abi: *const HsaQueueAbi) -> Result<u64, RuntimeError> {
1493 if !self.is_initialized() {
1494 return Err(RuntimeError::NotInitialized);
1495 }
1496 Ok(self.resolve_queue_by_abi(abi)?.write_index())
1497 }
1498
1499 pub fn queue_load_read_index(&self, abi: *const HsaQueueAbi) -> Result<u64, RuntimeError> {
1500 if !self.is_initialized() {
1501 return Err(RuntimeError::NotInitialized);
1502 }
1503 Ok(self.resolve_queue_by_abi(abi)?.read_index())
1504 }
1505
1506 pub fn queue_store_write_index(
1507 &self,
1508 abi: *const HsaQueueAbi,
1509 value: u64,
1510 ) -> Result<(), RuntimeError> {
1511 if !self.is_initialized() {
1512 return Err(RuntimeError::NotInitialized);
1513 }
1514 let q = self.resolve_queue_by_abi(abi)?;
1515 q.store_write_index(value);
1516 let seq = self.trace.with_log(TraceLog::next_seq);
1517 self.trace.with_log(|log| {
1518 log.record(TraceEvent::QueueIndexStore {
1519 seq,
1520 queue_id: q.id,
1521 which: "write".into(),
1522 value,
1523 });
1524 });
1525 Ok(())
1526 }
1527
1528 pub fn queue_store_read_index(
1529 &self,
1530 abi: *const HsaQueueAbi,
1531 value: u64,
1532 ) -> Result<(), RuntimeError> {
1533 if !self.is_initialized() {
1534 return Err(RuntimeError::NotInitialized);
1535 }
1536 let q = self.resolve_queue_by_abi(abi)?;
1537 q.store_read_index(value);
1538 let seq = self.trace.with_log(TraceLog::next_seq);
1539 self.trace.with_log(|log| {
1540 log.record(TraceEvent::QueueIndexStore {
1541 seq,
1542 queue_id: q.id,
1543 which: "read".into(),
1544 value,
1545 });
1546 });
1547 Ok(())
1548 }
1549
1550 pub fn queue_doorbell_count(&self, abi: *const HsaQueueAbi) -> Result<u64, RuntimeError> {
1551 if !self.is_initialized() {
1552 return Err(RuntimeError::NotInitialized);
1553 }
1554 Ok(self.resolve_queue_by_abi(abi)?.doorbell_store_count())
1555 }
1556
1557 pub fn record_unsupported(&self, api: &str, detail: &str) {
1558 let seq = self.trace.with_log(TraceLog::next_seq);
1559 self.trace.with_log(|log| {
1560 log.record(TraceEvent::Unsupported {
1561 seq,
1562 api: api.to_string(),
1563 detail: detail.to_string(),
1564 });
1565 });
1566 }
1567
1568 pub fn max_fidelity(&self) -> FidelityLevel {
1569 self.profile.max_fidelity
1570 }
1571}
1572
1573#[derive(Debug, Clone, PartialEq, Eq)]
1575pub enum AgentInfoValue {
1576 Name(String),
1577 VendorName(String),
1578 Feature(u32),
1579 Device(AgentKind),
1580 U16(u16),
1581 U32(u32),
1582}
1583
1584#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1585pub enum RegionInfoAttr {
1586 Segment,
1587 GlobalFlags,
1588 Size,
1589 AllocMaxSize,
1590 RuntimeAllocAllowed,
1591 Granule,
1592 Alignment,
1593}
1594
1595#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1596pub enum PoolInfoAttr {
1597 Segment,
1598 GlobalFlags,
1599 Size,
1600 RuntimeAllocAllowed,
1601 Granule,
1602 Alignment,
1603 AccessibleByAll,
1604 AllocMaxSize,
1605 Location,
1606 RecGranule,
1607}
1608
1609static GLOBAL: OnceLock<Mutex<Option<Runtime>>> = OnceLock::new();
1611
1612fn global() -> &'static Mutex<Option<Runtime>> {
1613 GLOBAL.get_or_init(|| Mutex::new(None))
1614}
1615
1616pub fn install_runtime(profile: DeviceProfile) -> Result<(), Error> {
1618 let mut guard = global()
1619 .lock()
1620 .map_err(|_| Error::new(ErrorCategory::Internal, "runtime mutex poisoned"))?;
1621 if let Some(rt) = guard.as_ref() {
1622 if rt.is_initialized() {
1623 return Err(Error::new(
1624 ErrorCategory::Internal,
1625 "cannot replace SoftGPU runtime while initialized",
1626 ));
1627 }
1628 }
1629 *guard = Some(Runtime::new(profile)?);
1630 Ok(())
1631}
1632
1633pub fn ensure_default_runtime() -> Result<(), Error> {
1635 let mut guard = global()
1636 .lock()
1637 .map_err(|_| Error::new(ErrorCategory::Internal, "runtime mutex poisoned"))?;
1638 if guard.is_none() {
1639 let profile = if let Ok(path) = std::env::var("SOFTGPU_PROFILE") {
1640 DeviceProfile::load_path(path)?
1641 } else {
1642 DeviceProfile::parse_bytes(DEFAULT_GENERIC_PROFILE.as_bytes())?
1643 };
1644 *guard = Some(Runtime::new(profile)?);
1645 }
1646 Ok(())
1647}
1648
1649const DEFAULT_GENERIC_PROFILE: &str = include_str!("../embedded/softgpu-generic-v0.json");
1650
1651pub fn with_runtime<R>(f: impl FnOnce(&mut Runtime) -> R) -> Result<R, RuntimeError> {
1653 let mut guard = global()
1654 .lock()
1655 .unwrap_or_else(|poisoned| poisoned.into_inner());
1656 let rt = guard.as_mut().ok_or(RuntimeError::NotInitialized)?;
1657 Ok(f(rt))
1658}
1659
1660pub fn with_runtime_autostart<R>(f: impl FnOnce(&mut Runtime) -> R) -> Result<R, RuntimeError> {
1662 ensure_default_runtime().map_err(|e| RuntimeError::Internal(e.to_string()))?;
1663 with_runtime(f)
1664}
1665
1666#[cfg(test)]
1667mod tests {
1668 use super::*;
1669 use crate::agent::AGENT_FEATURE_KERNEL_DISPATCH;
1670 use crate::profile::{ProfileField, ProfileIdentity, ResourceLimits};
1671 use crate::queue::{
1672 QUEUE_TYPE_MULTI, SOFTGPU_QUEUES_MAX, SOFTGPU_QUEUE_MAX_SIZE, SOFTGPU_QUEUE_MIN_SIZE,
1673 };
1674 use std::sync::Barrier;
1675 use std::thread;
1676
1677 fn test_profile() -> DeviceProfile {
1678 DeviceProfile {
1679 schema_version: 1,
1680 profile_id: "test-gpu".into(),
1681 profile_revision: "0".into(),
1682 identity: ProfileIdentity {
1683 vendor: "SoftGPU".into(),
1684 product_name: "Test GPU".into(),
1685 architecture_family: "softgpu-abstract".into(),
1686 llvm_target: ProfileField::unknown(),
1687 },
1688 max_fidelity: FidelityLevel::Abi,
1689 conformance_allowed: false,
1690 resource_limits: ResourceLimits::default(),
1691 analytical_performance: Default::default(),
1692 quirks: vec![],
1693 }
1694 }
1695
1696 #[test]
1697 fn init_enumerates_one_gpu_agent() {
1698 let mut rt = Runtime::new(test_profile()).unwrap();
1699 rt.init().unwrap();
1700 let mut count = 0;
1701 rt.iterate_agents(|agent| {
1702 count += 1;
1703 assert_eq!(agent.kind, AgentKind::Gpu);
1704 assert_eq!(agent.feature_mask, AGENT_FEATURE_KERNEL_DISPATCH);
1705 Ok(())
1706 })
1707 .unwrap();
1708 assert_eq!(count, 1);
1709 let name = rt
1710 .agent_get_info(
1711 rt.agents[0].agent.as_ref().unwrap().handle,
1712 AgentInfoAttr::Name,
1713 )
1714 .unwrap();
1715 assert_eq!(name, AgentInfoValue::Name("Test GPU".into()));
1716 rt.shut_down().unwrap();
1717 }
1718
1719 #[test]
1720 fn path_c_regions_and_pools_allocate() {
1721 let mut rt = Runtime::new(test_profile()).unwrap();
1722 rt.init().unwrap();
1723 let agent = rt.agents[0].agent.as_ref().unwrap().handle;
1724 let mut region = None;
1725 rt.iterate_regions(agent, |space| {
1726 if space.kind == MemoryViewKind::RegionFineKernarg {
1727 region = Some(space.handle);
1728 }
1729 Ok(())
1730 })
1731 .unwrap();
1732 let region = region.expect("fine region");
1733 let ptr = rt.memory_allocate(region, 128).unwrap();
1734 assert!(!ptr.is_null());
1735 rt.memory_free(ptr).unwrap();
1736
1737 let mut pool = None;
1738 rt.iterate_pools(agent, |space| {
1739 if space.kind == MemoryViewKind::PoolFineHost {
1740 pool = Some(space.handle);
1741 }
1742 Ok(())
1743 })
1744 .unwrap();
1745 let pool = pool.expect("fine pool");
1746 let ptr = rt.memory_allocate(pool, 256).unwrap();
1747 rt.memory_free(ptr).unwrap();
1748 rt.shut_down().unwrap();
1749 }
1750
1751 #[test]
1752 fn signal_and_queue_observe_doorbell() {
1753 let mut rt = Runtime::new(test_profile()).unwrap();
1754 rt.init().unwrap();
1755 let agent = rt.agents[0].agent.as_ref().unwrap().handle;
1756 let q = rt
1757 .queue_create(agent, SOFTGPU_QUEUE_MIN_SIZE, QUEUE_TYPE_MULTI)
1758 .unwrap();
1759 assert!(!q.is_null());
1760 let doorbell = unsafe { (*q).doorbell_signal };
1761 rt.signal_store(PackedHandle::from_raw(doorbell), 1)
1762 .unwrap();
1763 assert_eq!(rt.queue_doorbell_count(q).unwrap(), 1);
1764 rt.queue_store_write_index(q, 1).unwrap();
1765 assert_eq!(rt.queue_load_write_index(q).unwrap(), 1);
1766 rt.queue_destroy(q).unwrap();
1767 rt.shut_down().unwrap();
1768 }
1769
1770 #[test]
1771 fn stale_handle_after_shutdown_fails() {
1772 let mut rt = Runtime::new(test_profile()).unwrap();
1773 rt.init().unwrap();
1774 let handle = rt.agents[0].agent.as_ref().unwrap().handle;
1775 rt.shut_down().unwrap();
1776 let err = rt.agent_get_info(handle, AgentInfoAttr::Name).unwrap_err();
1777 assert_eq!(err, RuntimeError::NotInitialized);
1778 rt.init().unwrap();
1779 let err = rt.agent_get_info(handle, AgentInfoAttr::Name).unwrap_err();
1780 assert_eq!(err, RuntimeError::InvalidAgent);
1781 rt.shut_down().unwrap();
1782 }
1783
1784 #[test]
1785 fn forged_handle_rejected() {
1786 let mut rt = Runtime::new(test_profile()).unwrap();
1787 rt.init().unwrap();
1788 let forged = PackedHandle::from_raw(0xDEAD_BEEF_DEAD_BEEF);
1789 assert_eq!(
1790 rt.agent_get_info(forged, AgentInfoAttr::Device)
1791 .unwrap_err(),
1792 RuntimeError::InvalidAgent
1793 );
1794 rt.shut_down().unwrap();
1795 }
1796
1797 #[test]
1798 fn concurrent_init_and_iterate() {
1799 let mut rt = Runtime::new(test_profile()).unwrap();
1800 rt.init().unwrap();
1801 let shared = Mutex::new(rt);
1802 let barrier = Barrier::new(4);
1803 thread::scope(|scope| {
1804 for _ in 0..4 {
1805 scope.spawn(|| {
1806 barrier.wait();
1807 let guard = shared.lock().unwrap();
1808 guard
1809 .iterate_agents(|agent| {
1810 assert_eq!(agent.kind, AgentKind::Gpu);
1811 Ok(())
1812 })
1813 .unwrap();
1814 });
1815 }
1816 });
1817 shared.lock().unwrap().shut_down().unwrap();
1818 }
1819
1820 #[test]
1821 fn traces_include_profile_and_fidelity() {
1822 let mut rt = Runtime::new(test_profile()).unwrap();
1823 rt.init().unwrap();
1824 let events = rt.trace_snapshot();
1825 assert!(events.iter().any(|e| matches!(
1826 e,
1827 TraceEvent::RuntimeInit {
1828 fidelity,
1829 profile_id,
1830 ..
1831 } if fidelity == "abi" && profile_id == "test-gpu"
1832 )));
1833 rt.shut_down().unwrap();
1834 }
1835
1836 #[test]
1837 fn softgpu_queue_limits_are_software_defaults() {
1838 assert!(SOFTGPU_QUEUE_MIN_SIZE.is_power_of_two());
1839 assert!(SOFTGPU_QUEUE_MAX_SIZE.is_power_of_two());
1840 const { assert!(SOFTGPU_QUEUES_MAX > 0) };
1841 }
1842}