Skip to main content

softgpu_core/
runtime.rs

1//! Process-global SoftGPU runtime session (vendor-neutral).
2//!
3//! Thread-safe via a single mutex. Init is reference-counted. Handles are
4//! generation-bumping so destroy/recycle cannot revive stale IDs.
5
6use 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/// Errors returned by the vendor-neutral runtime (mapped to HSA at the edge).
34#[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/// SoftGPU-registered ISA kernel image (Phase 11 / v0.8 executable load).
97#[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
106/// SoftGPU runtime state.
107pub 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    /// Captured validated dispatches for offline replay (owned bytes).
119    dispatch_captures: Vec<DispatchDescriptor>,
120    /// kernel_object handle → SoftGPU ISA image (Phase 11 / v0.8).
121    isa_kernels: HashMap<u64, RegisteredIsaKernel>,
122    next_kernel_object: u64,
123    /// SoftGPU code-object readers (v0.8).
124    code_object_readers: HashMap<u64, crate::executable::CodeObjectReader>,
125    next_reader_id: u64,
126    /// SoftGPU executables (v0.8).
127    executables: HashMap<u64, crate::executable::SoftGpuExecutable>,
128    next_executable_id: u64,
129    /// executable_symbol handle → (executable_id, symbol_index).
130    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    /// Register a SoftGPU ISA kernel; returns `kernel_object` id for AQL packets.
163    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    /// Register ISA kernel with segment sizes (HSA symbol_get_info).
172    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    /// Register the SoftGPU llvm-mc `tiny_add` kernel image.
199    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    /// Owned packet captures suitable for offline `aql::replay_dispatch`.
373    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        // Cancel waiters before dropping signal/queue tables.
515        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            // SAFETY: buffer allocated by SoftGPU for this queue.
547            unsafe { dealloc(queue.packet_buffer, layout) };
548        }
549        // Doorbell signal is destroyed with the queue ownership table entry separately.
550        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    /// # Safety
888    ///
889    /// `dst` and `src` must be valid for `size` bytes; SoftGPU does not
890    /// validate OS-mapped pointers beyond SoftGPU allocations.
891    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        // SAFETY: caller guarantees valid non-overlapping regions of `size`.
904        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        // SoftGPU host memory is already CPU-visible; allow is a no-op success.
922        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        // HSA_AMD_MEMORY_POOL_ACCESS_ALLOWED_BY_DEFAULT
936        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            // Doorbell owned by a live queue — reject independent destroy.
979            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    /// Store a completion signal value without doorbell observe side effects.
1081    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        // HSA completion convention: producer waits for signal == 0.
1252        // SoftGPU stores 0 only as the experimental no-execution contract.
1253        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        // Reject does not store completion as success; still advance protocol.
1273        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                    // Monotonic processor progress; never move read_index backwards.
1283                    if next > q.read_index() {
1284                        q.store_read_index(next);
1285                    }
1286                    break;
1287                }
1288            }
1289        }
1290    }
1291
1292    /// Clone the signal Arc for waiting **without** holding the runtime lock.
1293    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    /// SoftGPU packet observation without doorbell (test / tooling).
1317    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            // SoftGPU supports single and multi; cooperative unsupported.
1347            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        // SAFETY: layout validated.
1380        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        // SAFETY: freshly allocated buffer of packet_bytes.
1386        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/// Typed agent info payload before ABI packing.
1574#[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
1609/// Process-global runtime holder.
1610static GLOBAL: OnceLock<Mutex<Option<Runtime>>> = OnceLock::new();
1611
1612fn global() -> &'static Mutex<Option<Runtime>> {
1613    GLOBAL.get_or_init(|| Mutex::new(None))
1614}
1615
1616/// Install the process runtime from a profile (idempotent replace only when down).
1617pub 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
1633/// Ensure a default runtime exists.
1634pub 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
1651/// Access the global runtime under the process mutex.
1652pub 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
1660/// Like `with_runtime` but auto-installs the default profile if missing.
1661pub 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}