Skip to main content

hrx/
runtime.rs

1//! GPU conveniences implemented over the owned native fabric.
2use crate::{Error, Result, Target, fabric};
3use std::{
4    cell::RefCell,
5    collections::BTreeMap,
6    path::Path,
7    sync::{Arc, Mutex},
8};
9
10/// Native entry's checked argument and workgroup layout.
11#[derive(Clone, Debug, Default)]
12pub struct ExportInfo {
13    /// Scalar bytes packed in declaration order, excluding alignment padding.
14    pub constant_byte_length: u32,
15    /// Number of explicit buffer arguments.
16    pub binding_count: u32,
17    /// Number of explicit native arguments.
18    pub parameter_count: u32,
19    /// Required workgroup dimensions; zero means unconstrained.
20    pub workgroup_size: [u32; 3],
21}
22/// Native engines and instrumentation fixed before a stream allocates or submits.
23#[derive(Clone, Debug, Default)]
24pub struct StreamOptions {
25    /// Protocol used for kernel dispatch and graph replay.
26    pub compute_engine: crate::execution::ComputeEngine,
27    /// Engine used for byte fills and copies.
28    pub copy_engine: crate::execution::CopyEngine,
29    /// Shared allocation ceiling, including AQL scratch and diagnostic storage.
30    pub memory_budget: Option<crate::residency::MemoryBudget>,
31    /// Requires AQL. Instrumented artifacts use this bounded report/shadow storage.
32    pub sanitizer: Option<fabric::SanitizerRuntimeOptions>,
33}
34#[derive(Clone)]
35enum Command {
36    Gpu(fabric::PreparedGpu, u8),
37    Aql(fabric::PreparedAql),
38}
39impl Command {
40    fn engine(&self) -> u8 {
41        match self {
42            Self::Gpu(_, engine) => *engine,
43            Self::Aql(_) => 2,
44        }
45    }
46    fn storage_bytes(&self) -> usize {
47        match self {
48            Self::Gpu(c, _) => c.storage_bytes(),
49            Self::Aql(c) => c.storage_bytes(),
50        }
51    }
52}
53#[derive(Clone)]
54enum Completion {
55    Gpu(fabric::Completion, u8),
56    Aql(fabric::AqlCompletion),
57}
58impl Completion {
59    fn engine(&self) -> u8 {
60        match self {
61            Self::Gpu(_, engine) => *engine,
62            Self::Aql(_) => 2,
63        }
64    }
65    fn refresh(&self) -> Result<bool> {
66        match self {
67            Self::Gpu(c, _) => c.refresh(),
68            Self::Aql(c) => c.refresh(),
69        }
70    }
71    fn wait(&self) -> Result<()> {
72        match self {
73            Self::Gpu(c, _) => c.wait(),
74            Self::Aql(c) => c.wait(),
75        }
76    }
77    fn wait_timeout(&self, timeout: std::time::Duration) -> Result<bool> {
78        match self {
79            Self::Gpu(c, _) => c.wait_timeout(timeout),
80            Self::Aql(c) => c.wait_timeout(timeout),
81        }
82    }
83}
84struct Inner {
85    device: fabric::Device,
86    queue: fabric::Queue,
87    aql: Option<fabric::AqlQueue>,
88    sdma: Option<fabric::Queue>,
89    sanitizer: Option<fabric::SanitizerRuntimeOptions>,
90    timeline: StreamTimeline,
91    free: Mutex<Vec<fabric::Buffer>>,
92    dispatch_cache: Mutex<Vec<CachedDispatch>>,
93    transfer_cache: Mutex<Vec<CachedTransfer>>,
94}
95#[derive(Default)]
96struct StreamTimeline {
97    producer: Mutex<()>,
98    last: Mutex<Option<Completion>>,
99}
100impl StreamTimeline {
101    fn submit(&self, dispatch: impl FnOnce() -> Result<Completion>) -> Result<()> {
102        // Serialize publication without blocking observers while another stream
103        // occupies the shared queue. Only published work enters the timeline.
104        let _producer = self
105            .producer
106            .lock()
107            .map_err(|_| Error::DeviceLost("stream producer poisoned".into()))?;
108        let done = dispatch()?;
109        *self
110            .last
111            .lock()
112            .map_err(|_| Error::DeviceLost("stream timeline poisoned".into()))? = Some(done);
113        Ok(())
114    }
115    fn snapshot(&self) -> Result<Option<Completion>> {
116        Ok(self
117            .last
118            .lock()
119            .map_err(|_| Error::DeviceLost("stream timeline poisoned".into()))?
120            .clone())
121    }
122}
123impl Inner {
124    fn submit(&self, command: &Command) -> Result<()> {
125        self.timeline.submit(|| {
126            if let Some(previous) = self.timeline.snapshot()?
127                && previous.engine() != command.engine()
128            {
129                previous.wait()?;
130            }
131            match command {
132                Command::Gpu(c, engine) => {
133                    let _uses = c.retain_parts()?;
134                    let mut last = None;
135                    for part in c.parts() {
136                        let done = Completion::Gpu(unsafe { part.dispatch_part_wait() }?, *engine);
137                        // Record every accepted part under the stream producer lock.
138                        // A later error must leave this prefix visible to fences/drop.
139                        *self
140                            .timeline
141                            .last
142                            .lock()
143                            .map_err(|_| Error::DeviceLost("stream timeline poisoned".into()))? =
144                            Some(done.clone());
145                        last = Some(done);
146                    }
147                    Ok(last.expect("prepared command has a final part"))
148                }
149                Command::Aql(c) => match unsafe { c.dispatch() } {
150                    Err(Error::Busy(_)) => {
151                        self.wait()?;
152                        Ok(Completion::Aql(unsafe { c.dispatch() }?))
153                    }
154                    result => result.map(Completion::Aql),
155                },
156            }
157        })
158    }
159    fn drain_for_drop(&self) -> bool {
160        self.timeline.snapshot().ok().is_some_and(|last| {
161            last.as_ref().is_none_or(|done| {
162                done.wait_timeout(std::time::Duration::from_secs(10))
163                    .unwrap_or(false)
164            })
165        })
166    }
167    fn wait(&self) -> Result<()> {
168        if let Some(done) = self.timeline.snapshot()? {
169            done.wait()?;
170        }
171        Ok(())
172    }
173}
174impl Inner {
175    fn prepare_fill(self: &Arc<Self>, dst: View<'_>, value: u8) -> Result<Command> {
176        owns(self, dst.owner)?;
177        let mut cache = self
178            .transfer_cache
179            .lock()
180            .map_err(|_| Error::DeviceLost("transfer cache poisoned".into()))?;
181        if let Some(entry) = cache.iter().find(|entry| {
182            entry.destination.same_backing(&dst.owner.native)
183                && entry.destination_offset == dst.offset
184                && entry.length == dst.length
185                && entry.source.is_none()
186                && entry.value == value
187        }) {
188            return Ok(entry.command.clone());
189        }
190        let command = Command::Gpu(
191            self.sdma.as_ref().unwrap_or(&self.queue).prepare_fill(
192                &dst.owner.native,
193                dst.offset,
194                dst.length,
195                value,
196            )?,
197            u8::from(self.sdma.is_some()),
198        );
199        // Other streams may use this device-scoped allocation. Only cache when
200        // its owning stream can evict the entry when the public buffer drops.
201        if Arc::ptr_eq(&dst.owner.owner, self) {
202            if cache.len() == 128 {
203                cache.remove(0);
204            }
205            cache.push(CachedTransfer {
206                destination: dst.owner.native.clone(),
207                destination_offset: dst.offset,
208                length: dst.length,
209                source: None,
210                value,
211                command: command.clone(),
212            });
213        }
214        Ok(command)
215    }
216    fn prepare_copy(self: &Arc<Self>, dst: View<'_>, src: View<'_>) -> Result<Command> {
217        owns(self, dst.owner)?;
218        owns(self, src.owner)?;
219        if dst.len() != src.len() {
220            return Err(Error::Message("copy requires equal spans".into()));
221        }
222        let mut cache = self
223            .transfer_cache
224            .lock()
225            .map_err(|_| Error::DeviceLost("transfer cache poisoned".into()))?;
226        if let Some(entry) = cache.iter().find(|entry| {
227            entry.destination.same_backing(&dst.owner.native)
228                && entry.destination_offset == dst.offset
229                && entry.length == dst.length
230                && entry.source.as_ref().is_some_and(|(buffer, offset)| {
231                    buffer.same_backing(&src.owner.native) && *offset == src.offset
232                })
233        }) {
234            return Ok(entry.command.clone());
235        }
236        let command = Command::Gpu(
237            self.sdma.as_ref().unwrap_or(&self.queue).prepare_copy(
238                &dst.owner.native,
239                dst.offset,
240                &src.owner.native,
241                src.offset,
242                src.length,
243            )?,
244            u8::from(self.sdma.is_some()),
245        );
246        if Arc::ptr_eq(&dst.owner.owner, self) && Arc::ptr_eq(&src.owner.owner, self) {
247            if cache.len() == 128 {
248                cache.remove(0);
249            }
250            cache.push(CachedTransfer {
251                destination: dst.owner.native.clone(),
252                destination_offset: dst.offset,
253                length: dst.length,
254                source: Some((src.owner.native.clone(), src.offset)),
255                value: 0,
256                command: command.clone(),
257            });
258        }
259        Ok(command)
260    }
261    unsafe fn prepare_dispatch(
262        self: &Arc<Self>,
263        kernel: &Kernel,
264        grid: [u32; 3],
265        block: [u32; 3],
266        constants: &Constants,
267        bindings: &[View<'_>],
268        arena: Option<&mut fabric::GraphArena>,
269    ) -> Result<Command> {
270        if kernel.device_id() != self.device.id() {
271            return Err(Error::Message("kernel belongs to another device".into()));
272        }
273        validate_export_launch(&kernel.info, grid, block)?;
274        if constants.len != kernel.info.constant_byte_length as usize
275            || bindings.len() != kernel.info.binding_count as usize
276        {
277            return Err(Error::Message(
278                "kernel binding or constant byte count mismatch".into(),
279            ));
280        }
281        for view in bindings {
282            owns(self, view.owner)?;
283        }
284        if let Some(queue) = &self.aql {
285            return Ok(Command::Aql(unsafe {
286                kernel.prepare_aql(queue, grid, block, constants, bindings)
287            }?));
288        }
289        let mut args = Vec::with_capacity(kernel.layout.len());
290        let mut scalar = 0;
291        let mut binding = 0;
292        for &(kind, size) in kernel.layout.iter() {
293            if kind == 1 {
294                args.push(fabric::Argument::Value(
295                    &constants.bytes[scalar..scalar + size],
296                ));
297                scalar += size;
298            } else {
299                let view = bindings[binding];
300                args.push(fabric::Argument::Buffer(&view.owner.native, view.offset));
301                binding += 1;
302            }
303        }
304        unsafe {
305            match arena {
306                Some(arena) => self.queue.prepare_graph(
307                    &kernel.native,
308                    grid,
309                    block.map(|v| v as u16),
310                    &args,
311                    arena,
312                ),
313                None => self
314                    .queue
315                    .prepare(&kernel.native, grid, block.map(|v| v as u16), &args),
316            }
317            .map(|c| Command::Gpu(c, 0))
318        }
319    }
320}
321
322/// An owned GPU address and execution domain.
323#[derive(Clone)]
324pub struct Device {
325    native: fabric::Device,
326}
327impl Device {
328    /// Activate an exact GPU ordinal in the native provider.
329    pub fn open(index: i32) -> Result<Self> {
330        Self::open_with_lifetime(index, fabric::NativeLifetime::Instance)
331    }
332    /// Activate a GPU in the selected native ownership domain.
333    pub fn open_with_lifetime(index: i32, lifetime: fabric::NativeLifetime) -> Result<Self> {
334        let index = usize::try_from(index)
335            .map_err(|_| Error::Message("GPU index must be nonnegative".into()))?;
336        Ok(Self {
337            native: fabric::Device::open_with_lifetime(fabric::Engine::Gpu, index, lifetime)?,
338        })
339    }
340    /// Require the workload's exact target before creating execution resources.
341    pub fn open_for(index: i32, target: &str) -> Result<Self> {
342        let device = Self::open(index)?;
343        if device.target().as_str() != target {
344            return Err(Error::Unsupported(format!(
345                "workload requires {target}, found {}",
346                device.target().as_str()
347            )));
348        }
349        Ok(device)
350    }
351    /// Exact compiler deployment key.
352    pub fn target(&self) -> &Target {
353        self.native.target()
354    }
355    /// Set the maximum number of native queues shared by this device's streams.
356    ///
357    /// The count must be positive. Call before creating the first stream on this
358    /// device; changing the count afterward returns [`Error::Busy`]. Repeating
359    /// the current count is allowed. Clones and reopened handles to this live
360    /// device share the setting, including streams created by [`Stream::open`].
361    /// Queues are created lazily, and allocation can fail at the device's limit.
362    pub fn set_stream_queue_count(&self, count: usize) -> Result<()> {
363        self.native.set_stream_queue_count(count)
364    }
365    /// Maximum native queue count for this device's streams (eight by default).
366    pub fn stream_queue_count(&self) -> Result<usize> {
367        self.native.stream_queue_count()
368    }
369    /// Create an ordered stream assigned to a bounded pool of native queues.
370    /// Assignment is round-robin, without priority or load awareness. A long
371    /// command or event wait can delay other streams assigned to the same queue.
372    pub fn stream(&self) -> Result<Stream> {
373        self.stream_with_options(StreamOptions::default())
374    }
375    /// Create a stream whose eager dispatches and recorded graphs use the selected engines.
376    /// Engine changes fence on the host. SDMA selects coherent allocation backing.
377    pub fn stream_with_options(&self, mut options: StreamOptions) -> Result<Stream> {
378        if options.sanitizer.is_some()
379            && !matches!(
380                options.compute_engine,
381                crate::execution::ComputeEngine::Aql { .. }
382            )
383        {
384            return Err(Error::Unsupported("stream sanitizers require AQL".into()));
385        }
386        if let Some(sanitizer) = &mut options.sanitizer {
387            sanitizer.memory_budget = options.memory_budget.clone();
388        }
389        let aql = match options.compute_engine {
390            crate::execution::ComputeEngine::Pm4 => None,
391            crate::execution::ComputeEngine::Aql {
392                maximum_private_bytes,
393            } => Some(
394                self.native
395                    .aql_queue_budgeted(maximum_private_bytes, options.memory_budget.as_ref())?,
396            ),
397        };
398        let sdma = match options.copy_engine {
399            crate::execution::CopyEngine::Compute => None,
400            crate::execution::CopyEngine::Sdma => Some(self.native.sdma_queue()?),
401        };
402        Ok(Stream {
403            inner: Arc::new(Inner {
404                device: self.native.clone(),
405                queue: self.native.stream_queue()?,
406                aql,
407                sdma,
408                sanitizer: options.sanitizer,
409                timeline: StreamTimeline::default(),
410                free: Mutex::new(Vec::with_capacity(16)),
411                dispatch_cache: Mutex::new(Vec::with_capacity(64)),
412                transfer_cache: Mutex::new(Vec::with_capacity(128)),
413            }),
414            staging: Vec::new(),
415            staging_pool: Vec::new(),
416            scratch: BTreeMap::new(),
417            scratch_bytes: 0,
418            scratch_limit: 256 * 1024 * 1024,
419            budget: options.memory_budget,
420            budget_uses: RefCell::new(BudgetUses::default()),
421        })
422    }
423    /// Access the owned native domain for explicit cross-engine allocations.
424    pub fn native(&self) -> &fabric::Device {
425        &self.native
426    }
427}
428/// Device backing with allocation-budget ownership.
429pub struct Buffer {
430    pub(crate) native: fabric::Buffer,
431    bytes: usize,
432    owner: Arc<Inner>,
433    reservation: Option<Arc<crate::residency::MemoryReservation>>,
434    poolable: bool,
435}
436impl Drop for Buffer {
437    fn drop(&mut self) {
438        // Cached commands are only useful while the application owns their
439        // bindings. Eviction also keeps pooled or budgeted backing reclaimable.
440        if let Ok(mut cache) = self.owner.dispatch_cache.lock() {
441            cache.retain(|entry| {
442                !entry
443                    .bindings
444                    .iter()
445                    .any(|(buffer, _, _)| buffer.same_backing(&self.native))
446            });
447        }
448        if let Ok(mut cache) = self.owner.transfer_cache.lock() {
449            cache.retain(|entry| {
450                !entry.destination.same_backing(&self.native)
451                    && !entry
452                        .source
453                        .as_ref()
454                        .is_some_and(|(buffer, _)| buffer.same_backing(&self.native))
455            });
456        }
457        if self.poolable
458            && self.reservation.is_none()
459            && self.native.exclusively_owned()
460            && self.bytes <= 64 * 1024 * 1024
461            && let Ok(mut pool) = self.owner.free.lock()
462        {
463            let used: usize = pool.iter().map(fabric::Buffer::len).sum();
464            if pool.len() < 16 && self.bytes <= (64 * 1024 * 1024usize).saturating_sub(used) {
465                pool.push(self.native.clone());
466            }
467        }
468    }
469}
470/// Checked borrowed logical range in an owned buffer.
471#[derive(Clone, Copy, Debug)]
472pub struct View<'a> {
473    owner: &'a Buffer,
474    offset: usize,
475    length: usize,
476}
477impl<'a> View<'a> {
478    /// Select a checked range relative to this view.
479    pub fn slice(self, offset: usize, length: usize) -> Result<Self> {
480        checked_span(offset, length, self.length)?;
481        Ok(Self {
482            owner: self.owner,
483            offset: self.offset + offset,
484            length,
485        })
486    }
487    /// Owning allocation.
488    pub fn owner(self) -> &'a Buffer {
489        self.owner
490    }
491    /// Absolute offset in the allocation.
492    pub fn offset(self) -> usize {
493        self.offset
494    }
495    /// Logical byte length.
496    pub fn len(self) -> usize {
497        self.length
498    }
499    /// Whether the range is empty.
500    pub fn is_empty(self) -> bool {
501        self.length == 0
502    }
503}
504impl Buffer {
505    // Tracked storage accounts for its own lifetime; its backing must not move
506    // into an uncharged stream pool when that owner releases its reservation.
507    pub(crate) fn into_unpooled(mut self) -> Self {
508        self.poolable = false;
509        self
510    }
511    #[cfg(feature = "npu")]
512    pub(crate) fn share_with(mut self, device: &fabric::Device) -> Result<Self> {
513        self.poolable = false;
514        if self.native.device_address(device).is_err() {
515            self.native = self
516                .owner
517                .device
518                .fabric()
519                .share_owned(&self.native, &[self.owner.device.clone(), device.clone()])?;
520        }
521        Ok(self)
522    }
523    pub(crate) fn charged_to(&self, budget: &crate::residency::MemoryBudget) -> bool {
524        self.reservation
525            .as_ref()
526            .is_some_and(|reservation| budget.contains(reservation))
527    }
528    pub(crate) fn device_id(&self) -> usize {
529        self.owner.device.id()
530    }
531    /// Allocation's exposed byte length.
532    pub fn bytes(&self) -> usize {
533        self.bytes
534    }
535    /// Whole allocation as a logical binding.
536    pub fn binding(&self) -> View<'_> {
537        View {
538            owner: self,
539            offset: 0,
540            length: self.bytes,
541        }
542    }
543    /// Checked subrange.
544    pub fn try_slice(&self, offset: usize, length: usize) -> Result<View<'_>> {
545        self.binding().slice(offset, length)
546    }
547    /// Checked subrange, panicking on invalid bounds.
548    pub fn slice(&self, offset: usize, length: usize) -> View<'_> {
549        self.try_slice(offset, length)
550            .expect("slice exceeds allocation")
551    }
552    /// Mapped host address. Dereferencing requires external synchronization.
553    /// Ordinary allocations additionally require `cache_control` before host
554    /// reads (including partial cache-line updates) and after host writes;
555    /// `allocate_shared` omits that requirement.
556    pub fn device_ptr(&self) -> Result<*mut std::ffi::c_void> {
557        Ok(self.native.host_pointer().cast())
558    }
559    /// Publish host writes (`flush = true`) or acquire device writes (`false`).
560    /// Stream upload/read operations perform these transitions automatically.
561    ///
562    /// # Safety
563    /// The caller must exclude conflicting host/device access for the entire
564    /// transition and retain the allocation until all accesses have completed.
565    pub unsafe fn cache_control(&self, flush: bool, offset: usize, length: usize) -> Result<()> {
566        unsafe { self.native.cache_control(flush, offset, length) }
567    }
568    pub(crate) fn allocation_address(&self) -> Result<u64> {
569        self.native.device_address(&self.owner.device)
570    }
571}
572/// Trusted native GPU entry and its declaration-order argument layout.
573#[derive(Clone)]
574pub struct Kernel {
575    native: fabric::Kernel,
576    info: ExportInfo,
577    layout: Arc<[(u32, usize)]>,
578    symbol: Arc<str>,
579    artifact: Option<Arc<crate::loom::Artifact>>,
580}
581impl Kernel {
582    /// Collect owned sanitizer reports after all native uses have retired.
583    /// Returns `Busy` while work is pending and `Unsupported` for ordinary kernels.
584    pub fn sanitizer_reports(&self) -> Result<fabric::SanitizerReports> {
585        self.native.sanitizer_reports()
586    }
587    pub(crate) unsafe fn prepare_aql(
588        &self,
589        queue: &fabric::AqlQueue,
590        grid: [u32; 3],
591        block: [u32; 3],
592        constants: &Constants,
593        bindings: &[View<'_>],
594    ) -> Result<fabric::PreparedAql> {
595        validate_export_launch(&self.info, grid, block)?;
596        if constants.len != self.info.constant_byte_length as usize
597            || bindings.len() != self.info.binding_count as usize
598        {
599            return Err(Error::Message(
600                "kernel binding or constant byte count mismatch".into(),
601            ));
602        }
603        let mut args = Vec::with_capacity(self.layout.len());
604        let mut scalar = 0;
605        let mut binding = 0;
606        for &(kind, size) in self.layout.iter() {
607            if kind == 1 {
608                args.push(fabric::Argument::Value(
609                    &constants.bytes[scalar..scalar + size],
610                ));
611                scalar += size;
612            } else {
613                let view = bindings[binding];
614                args.push(fabric::Argument::Buffer(&view.owner.native, view.offset));
615                binding += 1;
616            }
617        }
618        unsafe { queue.prepare(&self.native, grid, block.map(|v| v as u16), &args) }
619    }
620    pub(crate) fn device_id(&self) -> usize {
621        self.native.device().id()
622    }
623    /// Checked argument and launch metadata.
624    pub fn info(&self) -> &ExportInfo {
625        &self.info
626    }
627    /// Selected native entry name.
628    pub fn symbol(&self) -> &str {
629        &self.symbol
630    }
631    /// Create an independent evaluator of this artifact's exact export.
632    pub fn launch_program(&self) -> Result<crate::loom::LaunchProgram> {
633        self.artifact
634            .as_ref()
635            .ok_or_else(|| {
636                Error::Unsupported("raw code objects have no compiler launch companion".into())
637            })?
638            .launch_program(self.symbol())
639    }
640    /// Evaluate and validate compiler launch geometry during preparation.
641    pub fn launch_config(
642        &self,
643        workload_argument_bits: &[u64],
644    ) -> Result<crate::loom::LaunchConfig> {
645        let config = self.launch_program()?.evaluate(workload_argument_bits)?;
646        self.validate_launch_config(&config)?;
647        Ok(config)
648    }
649    fn validate_launch_config(&self, config: &crate::loom::LaunchConfig) -> Result<()> {
650        validate_export_launch(&self.info, config.workgroup_count, config.workgroup_size)?;
651        if config.workgroup_cluster_size != [1; 3] {
652            return Err(Error::Unsupported(
653                "clustered launches are not admitted by the PM4 runtime".into(),
654            ));
655        }
656        if config.subgroup_size != self.native.subgroup_size()
657            || config.workgroup_storage_bytes != self.native.workgroup_storage_bytes()
658        {
659            return Err(Error::Unsupported(
660                "launch program requirements differ from the loaded executable".into(),
661            ));
662        }
663        Ok(())
664    }
665    fn from_native(native: fabric::Kernel, symbol: &str) -> Result<Self> {
666        let layout = native.argument_layout()?;
667        let mut info = ExportInfo {
668            workgroup_size: native.workgroup_size(),
669            parameter_count: layout.len() as u32,
670            ..Default::default()
671        };
672        for &(kind, size) in &layout {
673            match kind {
674                1 => {
675                    info.constant_byte_length =
676                        info.constant_byte_length
677                            .checked_add(size as u32)
678                            .ok_or_else(|| Error::Message("constant layout overflow".into()))?
679                }
680                2 => info.binding_count += 1,
681                _ => {
682                    return Err(Error::Unsupported(
683                        "native argument kind is unsupported".into(),
684                    ));
685                }
686            }
687        }
688        Ok(Self {
689            native,
690            info,
691            layout: layout.into(),
692            symbol: symbol.into(),
693            artifact: None,
694        })
695    }
696}
697#[derive(Default)]
698struct BudgetUses(std::collections::HashMap<usize, Arc<crate::residency::MemoryReservation>>);
699impl BudgetUses {
700    fn retain(&mut self, views: &[View<'_>]) {
701        for view in views {
702            if let Some(reservation) = &view.owner.reservation {
703                self.0
704                    .entry(Arc::as_ptr(reservation) as usize)
705                    .or_insert_with(|| reservation.clone());
706            }
707        }
708    }
709    fn clear(&mut self) {
710        self.0.clear();
711    }
712}
713const DISPATCH_CACHE_ENTRIES: usize = 1024;
714const DISPATCH_CACHE_BYTES: usize = 16 << 20;
715
716struct CachedDispatch {
717    kernel: Kernel,
718    grid: [u32; 3],
719    block: [u32; 3],
720    constants: Constants,
721    bindings: Vec<(fabric::Buffer, usize, usize)>,
722    command: Command,
723}
724impl CachedDispatch {
725    fn matches(
726        &self,
727        kernel: &Kernel,
728        grid: [u32; 3],
729        block: [u32; 3],
730        constants: &Constants,
731        bindings: &[View<'_>],
732    ) -> bool {
733        self.kernel.native.same_entry(&kernel.native)
734            && self.grid == grid
735            && self.block == block
736            && self.constants.as_bytes() == constants.as_bytes()
737            && self.bindings.len() == bindings.len()
738            && self
739                .bindings
740                .iter()
741                .zip(bindings)
742                .all(|((buffer, offset, length), view)| {
743                    buffer.same_backing(&view.owner.native)
744                        && *offset == view.offset
745                        && *length == view.length
746                })
747    }
748}
749struct CachedTransfer {
750    destination: fabric::Buffer,
751    destination_offset: usize,
752    length: usize,
753    source: Option<(fabric::Buffer, usize)>,
754    value: u8,
755    command: Command,
756}
757/// An ordered GPU stream with explicit prepared commands and bounded pools.
758pub struct Stream {
759    inner: Arc<Inner>,
760    staging: Vec<Buffer>,
761    staging_pool: Vec<Buffer>,
762    scratch: BTreeMap<usize, Vec<Buffer>>,
763    scratch_bytes: usize,
764    scratch_limit: usize,
765    budget: Option<crate::residency::MemoryBudget>,
766    budget_uses: RefCell<BudgetUses>,
767}
768impl Stream {
769    /// Open the first qualified GPU.
770    pub fn open() -> Result<Self> {
771        Device::open(0)?.stream()
772    }
773    /// Stable native device identity.
774    pub fn device_id(&self) -> usize {
775        self.inner.device.id()
776    }
777    /// Stream identity, unique while it remains live.
778    pub fn id(&self) -> usize {
779        Arc::as_ptr(&self.inner) as usize
780    }
781    /// Compiler deployment key.
782    pub fn target(&self) -> &Target {
783        self.inner.device.target()
784    }
785    /// Attach a shared allocation ceiling.
786    pub fn with_memory_budget(mut self, budget: crate::residency::MemoryBudget) -> Self {
787        self.budget = Some(budget);
788        self
789    }
790    /// Selected allocation ceiling.
791    pub fn memory_budget(&self) -> Option<&crate::residency::MemoryBudget> {
792        self.budget.as_ref()
793    }
794    fn reserve(&self, bytes: usize) -> Result<Option<Arc<crate::residency::MemoryReservation>>> {
795        self.budget
796            .as_ref()
797            .map(|budget| budget.reserve(bytes.max(1)).map(Arc::new))
798            .transpose()
799    }
800    fn owns(&self, buffer: &Buffer) -> Result<()> {
801        owns(&self.inner, buffer)
802    }
803    /// Allocate initialized native GPU-visible storage with unspecified contents.
804    /// Empty requests round to one.
805    pub fn allocate(&self, bytes: usize) -> Result<Buffer> {
806        self.allocate_initialized(bytes.max(1), None)
807    }
808    /// Allocate an owned copy of a nonempty byte slice and publish it to the GPU.
809    /// Fresh backing is initialized directly, without a preliminary zero fill.
810    /// This does not wait for unrelated stream work; no existing buffer changes.
811    pub fn allocate_from(&self, data: &[u8]) -> Result<Buffer> {
812        if data.is_empty() {
813            return Err(Error::Message("initial data must be nonempty".into()));
814        }
815        self.allocate_initialized(data.len(), Some(data))
816    }
817    fn allocate_initialized(&self, bytes: usize, data: Option<&[u8]>) -> Result<Buffer> {
818        if self.inner.sdma.is_some() {
819            let buffer = self.allocate_shared(bytes)?;
820            if let Some(data) = data {
821                buffer.native.write(0, data)?;
822            }
823            return Ok(buffer);
824        }
825        let reservation = self.reserve(bytes)?;
826        let reused = if reservation.is_none() {
827            let mut pool = self
828                .inner
829                .free
830                .lock()
831                .map_err(|_| Error::DeviceLost("allocation pool poisoned".into()))?;
832            pool.iter()
833                .position(|buffer| buffer.len() == bytes)
834                .map(|index| pool.swap_remove(index))
835        } else {
836            None
837        };
838        let native = match reused {
839            Some(native) => {
840                if let Some(data) = data {
841                    native.write(0, data)?;
842                }
843                native
844            }
845            None => {
846                let fabric = self.inner.device.fabric();
847                let devices = std::slice::from_ref(&self.inner.device);
848                match data {
849                    Some(data) => fabric.allocate_from(data, devices)?,
850                    None => fabric.allocate(bytes, devices)?,
851                }
852            }
853        };
854        Ok(Buffer {
855            native,
856            bytes,
857            owner: self.inner.clone(),
858            reservation,
859            poolable: true,
860        })
861    }
862    /// Allocate initialized storage containing zero bytes.
863    pub fn allocate_zeroed(&self, bytes: usize) -> Result<Buffer> {
864        let buffer = self.allocate(bytes)?;
865        buffer.native.zero()?;
866        Ok(buffer)
867    }
868    /// Allocate GPU-coherent host storage for direct, synchronized host access.
869    pub fn allocate_shared(&self, bytes: usize) -> Result<Buffer> {
870        let bytes = bytes.max(1);
871        let reservation = self.reserve(bytes)?;
872        let native = self
873            .inner
874            .device
875            .fabric()
876            .allocate_shared(bytes, std::slice::from_ref(&self.inner.device))?;
877        Ok(Buffer {
878            native,
879            bytes,
880            owner: self.inner.clone(),
881            reservation,
882            poolable: false,
883        })
884    }
885    #[cfg(feature = "npu")]
886    pub(crate) fn allocate_for(&self, bytes: usize, devices: &[fabric::Device]) -> Result<Buffer> {
887        let reservation = self.reserve(bytes)?;
888        let native = self.inner.device.fabric().allocate(bytes, devices)?;
889        native.device_address(&self.inner.device)?;
890        Ok(Buffer {
891            native,
892            bytes,
893            owner: self.inner.clone(),
894            reservation,
895            poolable: false,
896        })
897    }
898    #[cfg(target_os = "linux")]
899    pub(crate) fn native_device(&self) -> &fabric::Device {
900        &self.inner.device
901    }
902    #[cfg(target_os = "linux")]
903    pub(crate) fn storage_buffer(&self, native: fabric::Buffer) -> Result<Buffer> {
904        native.device_address(&self.inner.device)?;
905        Ok(Buffer {
906            bytes: native.len(),
907            native,
908            owner: self.inner.clone(),
909            reservation: None,
910            poolable: false,
911        })
912    }
913    /// Wait for all preceding native work and reclaim transfer staging.
914    pub fn synchronize(&mut self) -> Result<()> {
915        self.inner.wait()?;
916        self.reclaim_staging();
917        self.budget_uses.get_mut().clear();
918        Ok(())
919    }
920    /// Snapshot preceding published work without a host wait. A submission still
921    /// waiting for shared queue capacity is not included.
922    pub fn record_event(&mut self) -> Result<Event> {
923        Ok(Event {
924            device: self.device_id(),
925            done: self.inner.timeline.snapshot()?,
926        })
927    }
928    /// Order subsequent stream work after an immutable event.
929    /// Events refer only to already-published native commands; they cannot name
930    /// future work from a scheduler callback or an unlaunched graph.
931    pub fn wait_event(&mut self, event: &Event) -> Result<()> {
932        if event.device != self.device_id() {
933            return Err(Error::Message("event belongs to another device".into()));
934        }
935        if let Some(done) = &event.done {
936            match done {
937                Completion::Gpu(done, 0)
938                    if self.inner.aql.is_none() && self.inner.sdma.is_none() =>
939                {
940                    self.inner
941                        .submit(&Command::Gpu(self.inner.queue.prepare_wait(done)?, 0))?;
942                }
943                _ => done.wait()?,
944            }
945        }
946        Ok(())
947    }
948    /// Observe the current native submission prefix.
949    pub fn submit(&mut self) -> Result<Submission<'_>> {
950        Ok(Submission { stream: self })
951    }
952    /// Write a checked range after draining this stream.
953    pub fn upload_blocking(&mut self, dst: View<'_>, bytes: &[u8]) -> Result<()> {
954        self.owns(dst.owner)?;
955        checked_span(0, bytes.len(), dst.len())?;
956        self.synchronize()?;
957        dst.owner.native.write(dst.offset, bytes)
958    }
959    /// Write at an allocation-relative byte offset.
960    pub fn upload_blocking_at(&mut self, dst: &Buffer, offset: usize, bytes: &[u8]) -> Result<()> {
961        self.upload_blocking(dst.try_slice(offset, bytes.len())?, bytes)
962    }
963    /// Read a checked range after draining this stream.
964    pub fn read_blocking(&mut self, src: View<'_>, bytes: &mut [u8]) -> Result<()> {
965        self.owns(src.owner)?;
966        checked_span(0, bytes.len(), src.len())?;
967        self.synchronize()?;
968        src.owner.native.read(src.offset, bytes)
969    }
970    /// Read at an allocation-relative byte offset.
971    pub fn read_blocking_at(
972        &mut self,
973        src: &Buffer,
974        offset: usize,
975        bytes: &mut [u8],
976    ) -> Result<()> {
977        self.read_blocking(src.try_slice(offset, bytes.len())?, bytes)
978    }
979    fn prepare_fill(&self, dst: View<'_>, value: u8) -> Result<Command> {
980        self.inner.prepare_fill(dst, value)
981    }
982    fn prepare_copy(&self, dst: View<'_>, src: View<'_>) -> Result<Command> {
983        self.inner.prepare_copy(dst, src)
984    }
985    /// Enqueue a native byte-pattern fill.
986    pub fn fill(&self, dst: View<'_>, value: u8) -> Result<()> {
987        self.budget_uses.borrow_mut().retain(&[dst]);
988        self.inner.submit(&self.prepare_fill(dst, value)?)
989    }
990    /// Enqueue a native copy between equal, non-overlapping ranges.
991    pub fn copy(&self, dst: View<'_>, src: View<'_>) -> Result<()> {
992        self.budget_uses.borrow_mut().retain(&[dst, src]);
993        self.inner.submit(&self.prepare_copy(dst, src)?)
994    }
995    /// Enqueue a host upload using bounded, reusable staging chunks.
996    /// The full destination is checked before any chunk is submitted.
997    pub fn upload(&mut self, dst: View<'_>, bytes: &[u8]) -> Result<()> {
998        self.owns(dst.owner)?;
999        checked_span(0, bytes.len(), dst.len())?;
1000        for (index, chunk) in bytes.chunks(STAGING_LIMIT).enumerate() {
1001            self.upload_chunk(dst.slice(index * STAGING_LIMIT, chunk.len())?, chunk)?;
1002        }
1003        Ok(())
1004    }
1005    fn upload_chunk(&mut self, dst: View<'_>, bytes: &[u8]) -> Result<()> {
1006        if self.staging.len() >= 8
1007            || self
1008                .staging
1009                .iter()
1010                .map(|b| b.bytes)
1011                .sum::<usize>()
1012                .saturating_add(bytes.len())
1013                > STAGING_LIMIT
1014        {
1015            self.synchronize()?;
1016        }
1017        let staging = if let Some(index) = self
1018            .staging_pool
1019            .iter()
1020            .enumerate()
1021            .filter(|(_, b)| b.bytes >= bytes.len())
1022            .min_by_key(|(_, b)| b.bytes)
1023            .map(|(index, _)| index)
1024        {
1025            self.staging_pool.swap_remove(index)
1026        } else {
1027            self.allocate(bytes.len())?
1028        };
1029        staging.native.write(0, bytes)?;
1030        self.copy(
1031            dst.slice(0, bytes.len())?,
1032            staging.try_slice(0, bytes.len())?,
1033        )?;
1034        self.staging.push(staging);
1035        Ok(())
1036    }
1037    /// Enqueue a checked upload at an allocation-relative offset.
1038    pub fn upload_at(&mut self, dst: &Buffer, offset: usize, bytes: &[u8]) -> Result<()> {
1039        self.upload(dst.try_slice(offset, bytes.len())?, bytes)
1040    }
1041    fn reclaim_staging(&mut self) {
1042        let mut cached = self.staging_pool.iter().map(|b| b.bytes).sum::<usize>();
1043        for buffer in self.staging.drain(..) {
1044            if buffer.bytes > STAGING_LIMIT {
1045                continue;
1046            }
1047            while self.staging_pool.len() >= 8
1048                || buffer.bytes > STAGING_LIMIT.saturating_sub(cached)
1049            {
1050                let index = self
1051                    .staging_pool
1052                    .iter()
1053                    .enumerate()
1054                    .min_by_key(|(_, b)| b.bytes)
1055                    .map(|(i, _)| i)
1056                    .unwrap();
1057                cached -= self.staging_pool.swap_remove(index).bytes;
1058            }
1059            cached += buffer.bytes;
1060            self.staging_pool.push(buffer);
1061        }
1062    }
1063    /// Load a trusted native code object from disk.
1064    ///
1065    /// # Safety
1066    /// Native code must obey its declared memory and argument contract.
1067    pub unsafe fn load(&self, path: &Path, symbol: &str) -> Result<Kernel> {
1068        Kernel::from_native(
1069            unsafe { self.inner.device.load_bytes(&std::fs::read(path)?, symbol) }?,
1070            symbol,
1071        )
1072    }
1073    /// Load a compiler artifact for this exact device target.
1074    ///
1075    /// # Safety
1076    /// Native code must obey its declared memory and argument contract.
1077    pub unsafe fn load_artifact(&self, artifact: &crate::loom::Artifact) -> Result<Kernel> {
1078        if let Some(options) = &self.inner.sanitizer {
1079            return unsafe { self.load_sanitized_artifact(artifact, options) };
1080        }
1081        let mut kernel = Kernel::from_native(
1082            unsafe { self.inner.device.load(artifact) }?,
1083            artifact.symbol(),
1084        )?;
1085        kernel.artifact = Some(Arc::new(artifact.clone()));
1086        Ok(kernel)
1087    }
1088    pub(crate) unsafe fn load_sanitized_artifact(
1089        &self,
1090        artifact: &crate::loom::Artifact,
1091        options: &fabric::SanitizerRuntimeOptions,
1092    ) -> Result<Kernel> {
1093        let mut kernel = Kernel::from_native(
1094            unsafe { self.inner.device.load_sanitized(artifact, options) }?,
1095            artifact.symbol(),
1096        )?;
1097        kernel.artifact = Some(Arc::new(artifact.clone()));
1098        Ok(kernel)
1099    }
1100    /// Load an artifact and evaluate its compiler-authored launch configuration.
1101    /// All evaluation and allocation happens before dispatch preparation.
1102    /// # Safety
1103    /// Native code must obey the caller's buffer-access contract for this workload.
1104    pub unsafe fn load_artifact_for_workload(
1105        &self,
1106        artifact: &crate::loom::Artifact,
1107        workload_argument_bits: &[u64],
1108    ) -> Result<(Kernel, crate::loom::LaunchConfig)> {
1109        let kernel = unsafe { self.load_artifact(artifact) }?;
1110        let config = kernel.launch_config(workload_argument_bits)?;
1111        Ok((kernel, config))
1112    }
1113    unsafe fn prepare_dispatch(
1114        &self,
1115        kernel: &Kernel,
1116        grid: [u32; 3],
1117        block: [u32; 3],
1118        constants: &Constants,
1119        bindings: &[View<'_>],
1120        arena: Option<&mut fabric::GraphArena>,
1121    ) -> Result<Command> {
1122        unsafe {
1123            self.inner
1124                .prepare_dispatch(kernel, grid, block, constants, bindings, arena)
1125        }
1126    }
1127    /// Enqueue a kernel invocation with declaration-order constants and buffers.
1128    ///
1129    /// # Safety
1130    /// Dimensions, constants, and actual kernel accesses must obey each binding's range.
1131    pub unsafe fn dispatch(
1132        &self,
1133        kernel: &Kernel,
1134        grid: [u32; 3],
1135        block: [u32; 3],
1136        constants: &Constants,
1137        bindings: &[View<'_>],
1138    ) -> Result<()> {
1139        self.budget_uses.borrow_mut().retain(bindings);
1140        // Owned buffers evict their cached commands before releasing backing or
1141        // budget charges. Queued uses keep their reservations until the fence.
1142        if bindings
1143            .iter()
1144            .all(|view| Arc::ptr_eq(&view.owner.owner, &self.inner))
1145        {
1146            let mut cache = self
1147                .inner
1148                .dispatch_cache
1149                .lock()
1150                .map_err(|_| Error::DeviceLost("dispatch cache poisoned".into()))?;
1151            if let Some(entry) = cache
1152                .iter()
1153                .find(|entry| entry.matches(kernel, grid, block, constants, bindings))
1154            {
1155                return self.inner.submit(&entry.command);
1156            }
1157            let command =
1158                unsafe { self.prepare_dispatch(kernel, grid, block, constants, bindings, None) }?;
1159            self.inner.submit(&command)?;
1160            let bytes = command.storage_bytes();
1161            if bytes > DISPATCH_CACHE_BYTES {
1162                return Ok(());
1163            }
1164            let mut retained: usize = cache
1165                .iter()
1166                .map(|entry| entry.command.storage_bytes())
1167                .sum();
1168            while cache.len() >= DISPATCH_CACHE_ENTRIES || retained + bytes > DISPATCH_CACHE_BYTES {
1169                retained -= cache.remove(0).command.storage_bytes();
1170            }
1171            cache.push(CachedDispatch {
1172                kernel: kernel.clone(),
1173                grid,
1174                block,
1175                constants: constants.clone(),
1176                bindings: bindings
1177                    .iter()
1178                    .map(|view| (view.owner.native.clone(), view.offset, view.length))
1179                    .collect(),
1180                command,
1181            });
1182            Ok(())
1183        } else {
1184            self.inner.submit(&unsafe {
1185                self.prepare_dispatch(kernel, grid, block, constants, bindings, None)
1186            }?)
1187        }
1188    }
1189    /// Reuse scratch storage under this queue's execution order.
1190    pub fn scratch(&mut self, bytes: usize) -> Result<Buffer> {
1191        let choice = self
1192            .scratch
1193            .range(bytes.max(1)..=bytes.saturating_mul(2).max(1))
1194            .next()
1195            .map(|(&size, _)| size);
1196        if let Some(size) = choice {
1197            let list = self.scratch.get_mut(&size).unwrap();
1198            let buffer = list.pop().unwrap();
1199            if list.is_empty() {
1200                self.scratch.remove(&size);
1201            }
1202            self.scratch_bytes -= buffer.bytes;
1203            Ok(buffer)
1204        } else {
1205            self.allocate(bytes)
1206        }
1207    }
1208    /// Return this stream's allocation after recording its last use.
1209    pub fn recycle(&mut self, buffer: Buffer) -> Result<()> {
1210        if !Arc::ptr_eq(&buffer.owner, &self.inner) {
1211            return Err(Error::Message("scratch belongs to another stream".into()));
1212        }
1213        if buffer.bytes <= self.scratch_limit.saturating_sub(self.scratch_bytes) {
1214            self.scratch_bytes += buffer.bytes;
1215            self.scratch.entry(buffer.bytes).or_default().push(buffer);
1216        }
1217        Ok(())
1218    }
1219    /// Bound retained scratch storage, evicting large entries first.
1220    pub fn set_scratch_limit(&mut self, bytes: usize) {
1221        self.scratch_limit = bytes;
1222        while self.scratch_bytes > bytes {
1223            if let Some((_, list)) = self.scratch.pop_last() {
1224                self.scratch_bytes -= list.iter().map(|b| b.bytes).sum::<usize>();
1225            } else {
1226                break;
1227            }
1228        }
1229    }
1230    /// Queue an owned readback.
1231    pub fn read(&mut self, source: View<'_>) -> Result<Readback> {
1232        let buffer = self.allocate(source.len())?;
1233        if !source.is_empty() {
1234            self.copy(buffer.try_slice(0, source.len())?, source)?;
1235        }
1236        Ok(Readback {
1237            buffer,
1238            length: source.len(),
1239        })
1240    }
1241}
1242impl Drop for Stream {
1243    fn drop(&mut self) {
1244        if !self.inner.drain_for_drop() {
1245            std::mem::forget(std::mem::take(&mut self.staging));
1246            std::mem::forget(std::mem::take(self.budget_uses.get_mut()));
1247        }
1248    }
1249}
1250/// Immutable native completion point.
1251#[derive(Clone)]
1252pub struct Event {
1253    device: usize,
1254    done: Option<Completion>,
1255}
1256impl Event {
1257    /// Poll terminal completion.
1258    pub fn is_complete(&self) -> Result<bool> {
1259        self.done.as_ref().map_or(Ok(true), Completion::refresh)
1260    }
1261    /// Wait on the host for preceding work.
1262    pub fn synchronize(&self) -> Result<()> {
1263        self.done.as_ref().map_or(Ok(()), Completion::wait)
1264    }
1265}
1266/// Borrowed completion observer that also reclaims completed staging.
1267#[must_use]
1268pub struct Submission<'a> {
1269    stream: &'a mut Stream,
1270}
1271impl Submission<'_> {
1272    /// Poll submitted work and reclaim its completed staging.
1273    pub fn is_complete(&mut self) -> Result<bool> {
1274        let done = self
1275            .stream
1276            .inner
1277            .timeline
1278            .snapshot()?
1279            .as_ref()
1280            .map_or(Ok(true), Completion::refresh)?;
1281        if done {
1282            self.stream.reclaim_staging();
1283            self.stream.budget_uses.get_mut().clear();
1284        }
1285        Ok(done)
1286    }
1287    /// Wait and reclaim staging.
1288    pub fn wait(self) -> Result<()> {
1289        self.stream.synchronize()
1290    }
1291}
1292/// Owned destination retained through native readback completion.
1293#[must_use]
1294pub struct Readback {
1295    buffer: Buffer,
1296    length: usize,
1297}
1298impl Readback {
1299    /// Wait on the originating stream and return initialized bytes.
1300    pub fn wait(self, stream: &mut Stream) -> Result<Vec<u8>> {
1301        if !Arc::ptr_eq(&self.buffer.owner, &stream.inner) {
1302            return Err(Error::Message("readback belongs to another stream".into()));
1303        }
1304        stream.synchronize()?;
1305        let mut bytes = vec![0; self.length];
1306        self.buffer.native.read(0, &mut bytes)?;
1307        Ok(bytes)
1308    }
1309}
1310fn owns(inner: &Arc<Inner>, buffer: &Buffer) -> Result<()> {
1311    if inner.device.id() == buffer.device_id() {
1312        Ok(())
1313    } else {
1314        Err(Error::Message("buffer belongs to another device".into()))
1315    }
1316}
1317pub(crate) fn checked_span(offset: usize, length: usize, capacity: usize) -> Result<()> {
1318    if offset.checked_add(length).is_none_or(|end| end > capacity) {
1319        Err(Error::Message(format!(
1320            "span {offset}+{length} exceeds {capacity} bytes"
1321        )))
1322    } else {
1323        Ok(())
1324    }
1325}
1326pub(crate) fn validate_launch(grid: [u32; 3], block: [u32; 3]) -> Result<()> {
1327    if grid.contains(&0)
1328        || block.contains(&0)
1329        || block
1330            .iter()
1331            .try_fold(1u32, |n, b| n.checked_mul(*b))
1332            .is_none_or(|n| n > 1024)
1333    {
1334        Err(Error::Message("invalid GPU launch dimensions".into()))
1335    } else {
1336        Ok(())
1337    }
1338}
1339pub(crate) fn validate_export_launch(
1340    info: &ExportInfo,
1341    grid: [u32; 3],
1342    block: [u32; 3],
1343) -> Result<()> {
1344    validate_launch(grid, block)?;
1345    if info
1346        .workgroup_size
1347        .iter()
1348        .zip(block)
1349        .any(|(&expected, actual)| expected != 0 && expected != actual)
1350    {
1351        Err(Error::Message(format!(
1352            "workgroup size {block:?} disagrees with compiled size {:?}",
1353            info.workgroup_size
1354        )))
1355    } else {
1356        Ok(())
1357    }
1358}
1359const STAGING_LIMIT: usize = 64 * 1024 * 1024;
1360
1361/// Explicit scalar widths, packed in declaration order for HRX binding dispatch.
1362/// Unlike direct kernargs this format has no implicit alignment or pointer slots.
1363/// Export metadata gives only the total byte count. Mixed scalar widths must
1364/// come from the kernel's argument contract.
1365#[derive(Clone, Debug)]
1366pub struct Constants {
1367    bytes: [u8; 256],
1368    len: usize,
1369}
1370impl Default for Constants {
1371    fn default() -> Self {
1372        Self {
1373            bytes: [0; 256],
1374            len: 0,
1375        }
1376    }
1377}
1378impl Constants {
1379    /// Pack an entry point whose scalar arguments are all unsigned Loom indices.
1380    ///
1381    /// Loom lowers a homogeneous index list to 32- or 64-bit slots depending on
1382    /// its range analysis. This adapter is only for that model contract; mixed
1383    /// scalar types must use `push` with their explicit native widths.
1384    ///
1385    /// Neither [`ExportInfo`] nor the compiler's resource report carries per-slot
1386    /// scalar types — only `constant_byte_length` and `parameter_count` — so no
1387    /// API here can build a mixed-width constant block by construction. A width
1388    /// inferred by dividing the byte count is wrong for a mixed `u32`/`u64`
1389    /// signature, and wrong quietly. Take each width from the source that
1390    /// declared it.
1391    pub fn indices(kernel: &Kernel, values: &[u32]) -> Result<Self> {
1392        let size = kernel.info.constant_byte_length as usize;
1393        let mut constants = Self::new();
1394        if values.is_empty() {
1395            if size != 0 {
1396                return Err(Error::Message("missing index constants".into()));
1397            }
1398            return Ok(constants);
1399        }
1400        if size == values.len() * 4 {
1401            for &value in values {
1402                constants.push(value)?;
1403            }
1404        } else if size == values.len() * 8 {
1405            for &value in values {
1406                constants.push(u64::from(value))?;
1407            }
1408        } else {
1409            return Err(Error::Message(
1410                "export is not a homogeneous 32/64-bit index list".into(),
1411            ));
1412        }
1413        Ok(constants)
1414    }
1415    /// Create an empty constant block.
1416    pub fn new() -> Self {
1417        Self::default()
1418    }
1419    #[must_use = "constant packing can fail when capacity is exceeded"]
1420    /// Append a scalar at its explicit width; fails if the 256-byte capacity is exceeded.
1421    pub fn push<T: Scalar>(&mut self, value: T) -> Result<()> {
1422        let bytes = value.bytes();
1423        checked_span(self.len, bytes.as_ref().len(), self.bytes.len())?;
1424        self.bytes[self.len..self.len + bytes.as_ref().len()].copy_from_slice(bytes.as_ref());
1425        self.len += bytes.as_ref().len();
1426        Ok(())
1427    }
1428    /// Construct an explicitly contracted scalar block from packed bytes.
1429    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
1430        let mut result = Self::new();
1431        checked_span(0, bytes.len(), result.bytes.len())?;
1432        result.bytes[..bytes.len()].copy_from_slice(bytes);
1433        result.len = bytes.len();
1434        Ok(result)
1435    }
1436    /// The packed constant bytes in declaration order.
1437    pub fn as_bytes(&self) -> &[u8] {
1438        &self.bytes[..self.len]
1439    }
1440}
1441mod sealed {
1442    pub trait Sealed {}
1443}
1444/// A supported fixed-width scalar for little-endian binding constants.
1445pub trait Scalar: sealed::Sealed {
1446    /// The scalar's fixed-size byte representation.
1447    type Bytes: AsRef<[u8]>;
1448    /// Encode this value in little-endian order.
1449    fn bytes(self) -> Self::Bytes;
1450}
1451macro_rules! scalars {
1452    ($($ty:ty => $n:literal),*) => {
1453        $(
1454            impl sealed::Sealed for $ty {}
1455            impl Scalar for $ty {
1456                type Bytes = [u8; $n];
1457                fn bytes(self) -> Self::Bytes { self.to_le_bytes() }
1458            }
1459        )*
1460    };
1461}
1462scalars!(u32 => 4, i32 => 4, f32 => 4, u64 => 8, i64 => 8, f64 => 8);
1463
1464/// A dependency node branded with its recording's identity.
1465#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1466pub struct Node {
1467    graph: u64,
1468    index: u32,
1469}
1470static NEXT_GRAPH_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
1471/// Owned native commands being recorded under checked dependency edges.
1472pub struct Graph<'a> {
1473    inner: Arc<Inner>,
1474    _borrow: std::marker::PhantomData<&'a Stream>,
1475    id: u64,
1476    nodes: Vec<(Recorded, Option<u32>)>,
1477    bindings: std::collections::HashMap<usize, usize>,
1478    arena: fabric::GraphArena,
1479    budget_uses: BudgetUses,
1480}
1481enum Recorded {
1482    Prepared(Command),
1483    Join,
1484}
1485/// Reusable prepared GPU graph and its last immutable completion point.
1486pub struct GraphExec {
1487    inner: Arc<Inner>,
1488    commands: Vec<Command>,
1489    profile: Option<fabric::profile::ProfileCapture>,
1490    last: Option<Completion>,
1491    failed: bool,
1492    budget_uses: BudgetUses,
1493}
1494impl Stream {
1495    /// Begin a dependency-checked recording borrowing this stream.
1496    pub fn graph(&self) -> Result<Graph<'_>> {
1497        self.owned_graph()
1498    }
1499    /// Begin a recording that owns its execution domain and each prepared command.
1500    /// Buffers and kernels need only live through their recording call. Native
1501    /// allocations and residency charges remain held by the graph. Pooled leases
1502    /// still require caller-controlled reuse and ordering through final replay.
1503    pub fn owned_graph(&self) -> Result<Graph<'static>> {
1504        Ok(Graph {
1505            inner: self.inner.clone(),
1506            _borrow: std::marker::PhantomData,
1507            id: NEXT_GRAPH_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
1508            nodes: Vec::new(),
1509            bindings: std::collections::HashMap::new(),
1510            arena: fabric::GraphArena::default(),
1511            budget_uses: BudgetUses::default(),
1512        })
1513    }
1514    /// Replay an instrumented graph, wait for retirement, and read GPU times.
1515    /// Ordinary graphs return an error; construct with `Graph::finish_profiled`.
1516    pub fn launch_profiled(&mut self, graph: &mut GraphExec) -> Result<fabric::DeviceProfile> {
1517        if graph.profile.is_none() {
1518            return Err(Error::Message(
1519                "graph was not prepared for profiling".into(),
1520            ));
1521        }
1522        self.launch(graph)?;
1523        self.synchronize()?;
1524        graph.profile.as_mut().unwrap().read()
1525    }
1526    /// Replay fixed native commands on the originating stream.
1527    pub fn launch(&mut self, graph: &mut GraphExec) -> Result<()> {
1528        if !Arc::ptr_eq(&graph.inner, &self.inner) {
1529            return Err(Error::Message("graph belongs to another stream".into()));
1530        }
1531        if graph.failed {
1532            return Err(Error::DeviceLost(
1533                "graph has an unretired failed launch".into(),
1534            ));
1535        }
1536        graph.failed = true;
1537        for command in &graph.commands {
1538            self.inner.submit(command)?;
1539        }
1540        graph.last = self.inner.timeline.snapshot()?;
1541        graph.failed = false;
1542        Ok(())
1543    }
1544}
1545impl<'a> Graph<'a> {
1546    /// Total exposed bytes of distinct allocations referenced by recorded nodes.
1547    /// Repeated views and pooled reuse of the same backing count only once.
1548    pub fn binding_bytes(&self) -> usize {
1549        self.bindings.values().sum()
1550    }
1551    fn retain_bindings(&mut self, bindings: &[View<'_>]) {
1552        self.budget_uses.retain(bindings);
1553        for view in bindings {
1554            self.bindings
1555                .insert(view.owner.native.identity(), view.owner.bytes());
1556        }
1557    }
1558
1559    fn validate_dependencies(&self, after: &[Node]) -> Result<()> {
1560        for node in after {
1561            if node.graph != self.id || node.index as usize >= self.nodes.len() {
1562                return Err(Error::Message(
1563                    "dependency node belongs to another graph".into(),
1564                ));
1565            }
1566        }
1567        if self.nodes.len() >= u32::MAX as usize {
1568            return Err(Error::Message("graph node capacity exceeded".into()));
1569        }
1570        Ok(())
1571    }
1572    fn record(&mut self, command: Recorded, after: &[Node]) -> Node {
1573        let index = self.nodes.len() as u32;
1574        self.nodes
1575            .push((command, after.iter().map(|node| node.index).max()));
1576        Node {
1577            graph: self.id,
1578            index,
1579        }
1580    }
1581    /// Record a byte fill after checked predecessor nodes.
1582    pub fn fill(&mut self, after: &[Node], dst: View<'_>, pattern: u8) -> Result<Node> {
1583        self.validate_dependencies(after)?;
1584        owns(&self.inner, dst.owner)?;
1585        if dst.is_empty() {
1586            return Err(Error::Message("empty graph fill".into()));
1587        }
1588        let command = self.inner.prepare_fill(dst, pattern)?;
1589        self.retain_bindings(&[dst]);
1590        Ok(self.record(Recorded::Prepared(command), after))
1591    }
1592    /// Record a non-overlapping byte copy after checked predecessor nodes.
1593    pub fn copy(&mut self, after: &[Node], dst: View<'_>, src: View<'_>) -> Result<Node> {
1594        self.validate_dependencies(after)?;
1595        owns(&self.inner, dst.owner)?;
1596        owns(&self.inner, src.owner)?;
1597        if dst.len() != src.len() || dst.is_empty() {
1598            return Err(Error::Message(
1599                "graph copy requires equal nonempty spans".into(),
1600            ));
1601        }
1602        let command = self.inner.prepare_copy(dst, src)?;
1603        self.retain_bindings(&[dst, src]);
1604        Ok(self.record(Recorded::Prepared(command), after))
1605    }
1606    /// Record a fixed native kernel invocation.
1607    ///
1608    /// # Safety
1609    /// Kernel accesses must obey bindings and constants; dependency edges must
1610    /// order conflicting accesses for every replay.
1611    pub unsafe fn dispatch(
1612        &mut self,
1613        after: &[Node],
1614        kernel: &Kernel,
1615        grid: [u32; 3],
1616        block: [u32; 3],
1617        constants: &Constants,
1618        bindings: &[View<'_>],
1619    ) -> Result<Node> {
1620        self.validate_dependencies(after)?;
1621        let command = unsafe {
1622            self.inner.prepare_dispatch(
1623                kernel,
1624                grid,
1625                block,
1626                constants,
1627                bindings,
1628                Some(&mut self.arena),
1629            )
1630        }?;
1631        self.retain_bindings(bindings);
1632        Ok(self.record(Recorded::Prepared(command), after))
1633    }
1634    /// Record a dependency join with no native payload.
1635    pub fn join(&mut self, after: &[Node]) -> Result<Node> {
1636        self.validate_dependencies(after)?;
1637        Ok(self.record(Recorded::Join, after))
1638    }
1639    /// Transfer recorded commands and allocation charges into owned replay state.
1640    /// Large graphs use ordered batches within the native indirect-buffer limit.
1641    pub fn finish(self) -> Result<GraphExec> {
1642        self.finish_inner(None)
1643    }
1644    /// Prepare a diagnostic graph with device-clock markers around each payload.
1645    /// Supply one label per non-join node. Instrumentation adds completion
1646    /// barriers and must not be used as end-to-end performance evidence.
1647    pub fn finish_profiled(self, labels: &[String]) -> Result<GraphExec> {
1648        self.finish_inner(Some(labels))
1649    }
1650    fn finish_inner(self, labels: Option<&[String]>) -> Result<GraphExec> {
1651        let mut ordered = Vec::new();
1652        let mut completed = 0usize;
1653        for (index, (node, dependency)) in self.nodes.into_iter().enumerate() {
1654            let command = match node {
1655                Recorded::Prepared(command) => command,
1656                Recorded::Join => continue,
1657            };
1658            let barrier = dependency.is_some_and(|node| node as usize >= completed);
1659            if barrier {
1660                completed = index;
1661            }
1662            ordered.push((command, barrier));
1663        }
1664        // Every batch is prepared before submission. The queue orders batches,
1665        // including dependencies and scratch aliases that cross a split.
1666        let (commands, profile) = if self.inner.aql.is_none() && self.inner.sdma.is_none() {
1667            let native: Vec<_> = ordered
1668                .into_iter()
1669                .map(|(command, barrier)| match command {
1670                    Command::Gpu(c, 0) => (c, barrier),
1671                    _ => unreachable!("PM4 stream only prepares PM4 commands"),
1672                })
1673                .collect();
1674            let (commands, profile) =
1675                unsafe { self.inner.queue.prepare_graph_batches(&native, labels) }?;
1676            (
1677                commands.into_iter().map(|c| Command::Gpu(c, 0)).collect(),
1678                profile,
1679            )
1680        } else {
1681            if labels.is_some() {
1682                return Err(Error::Unsupported(
1683                    "device graph profiling requires PM4 compute and compute copies".into(),
1684                ));
1685            }
1686            (
1687                ordered.into_iter().map(|(command, _)| command).collect(),
1688                None,
1689            )
1690        };
1691        Ok(GraphExec {
1692            inner: self.inner.clone(),
1693            commands,
1694            profile,
1695            last: None,
1696            failed: false,
1697            budget_uses: self.budget_uses,
1698        })
1699    }
1700}
1701impl Drop for GraphExec {
1702    fn drop(&mut self) {
1703        if self.failed
1704            || self.last.as_ref().is_some_and(|done| {
1705                !done
1706                    .wait_timeout(std::time::Duration::from_secs(10))
1707                    .unwrap_or(false)
1708            })
1709        {
1710            std::mem::forget(std::mem::take(&mut self.budget_uses));
1711        }
1712    }
1713}
1714impl std::fmt::Debug for Device {
1715    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1716        f.debug_struct("Device")
1717            .field("target", self.target())
1718            .finish_non_exhaustive()
1719    }
1720}
1721impl std::fmt::Debug for Buffer {
1722    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1723        f.debug_struct("Buffer")
1724            .field("bytes", &self.bytes)
1725            .finish_non_exhaustive()
1726    }
1727}
1728impl std::fmt::Debug for Kernel {
1729    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1730        f.debug_struct("Kernel")
1731            .field("symbol", &self.symbol)
1732            .finish_non_exhaustive()
1733    }
1734}
1735impl std::fmt::Debug for Stream {
1736    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1737        f.debug_struct("Stream")
1738            .field("scratch_bytes", &self.scratch_bytes)
1739            .finish_non_exhaustive()
1740    }
1741}
1742impl std::fmt::Debug for Graph<'_> {
1743    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1744        f.debug_struct("Graph")
1745            .field("nodes", &self.nodes.len())
1746            .finish_non_exhaustive()
1747    }
1748}
1749impl std::fmt::Debug for GraphExec {
1750    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1751        f.debug_struct("GraphExec").finish_non_exhaustive()
1752    }
1753}
1754impl std::fmt::Debug for Readback {
1755    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1756        f.debug_struct("Readback").finish_non_exhaustive()
1757    }
1758}
1759impl std::fmt::Debug for Submission<'_> {
1760    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1761        f.debug_struct("Submission").finish_non_exhaustive()
1762    }
1763}
1764
1765#[cfg(test)]
1766mod tests {
1767    #[test]
1768    fn observing_a_stream_does_not_wait_for_unpublished_work() {
1769        use std::{
1770            sync::{Arc, mpsc},
1771            time::Duration,
1772        };
1773        let timeline = Arc::new(super::StreamTimeline::default());
1774        let (started, running) = mpsc::channel();
1775        let (release, gate) = mpsc::channel();
1776        let producer = timeline.clone();
1777        let worker = std::thread::spawn(move || {
1778            producer.submit(|| {
1779                started.send(()).unwrap();
1780                gate.recv().unwrap();
1781                Err(crate::Error::Busy(
1782                    "simulated queue capacity failure".into(),
1783                ))
1784            })
1785        });
1786        running.recv_timeout(Duration::from_secs(1)).unwrap();
1787        let observer = timeline.clone();
1788        let (seen, observed) = mpsc::channel();
1789        let reader = std::thread::spawn(move || seen.send(observer.snapshot()).unwrap());
1790        let snapshot = observed.recv_timeout(Duration::from_secs(1));
1791        // Always release the producer before asserting, even if locking regresses.
1792        release.send(()).unwrap();
1793        assert!(matches!(worker.join().unwrap(), Err(crate::Error::Busy(_))));
1794        reader.join().unwrap();
1795        assert!(
1796            snapshot
1797                .expect("observer blocked on unpublished work")
1798                .unwrap()
1799                .is_none()
1800        );
1801        assert!(timeline.snapshot().unwrap().is_none());
1802    }
1803
1804    #[test]
1805    fn compiled_workgroup_dimensions_are_enforced() {
1806        let info = super::ExportInfo {
1807            workgroup_size: [64, 2, 1],
1808            ..Default::default()
1809        };
1810        assert!(super::validate_export_launch(&info, [1; 3], [64, 2, 1]).is_ok());
1811        assert!(super::validate_export_launch(&info, [1; 3], [128, 1, 1]).is_err());
1812        assert!(super::validate_export_launch(&info, [0, 1, 1], [64, 2, 1]).is_err());
1813        let dynamic = super::ExportInfo {
1814            workgroup_size: [0, 2, 1],
1815            ..Default::default()
1816        };
1817        assert!(super::validate_export_launch(&dynamic, [1; 3], [32, 2, 1]).is_ok());
1818        assert!(super::validate_export_launch(&dynamic, [1; 3], [32, 1, 1]).is_err());
1819    }
1820    /// The bounds arithmetic, without a device: `offset + length` must not wrap into a pass.
1821    #[test]
1822    fn a_slice_past_the_allocation_is_refused_even_when_the_sum_wraps() {
1823        let checked = |offset, length, bytes| super::checked_span(offset, length, bytes).is_ok();
1824        assert!(checked(0, 8, 8));
1825        assert!(checked(4, 4, 8));
1826        assert!(!checked(4, 5, 8));
1827        // the wrapping case: 8 bytes must not accept an offset near the top of the address space
1828        assert!(!checked(usize::MAX, 2, 8));
1829        assert!(!checked(usize::MAX - 1, 4, 8));
1830    }
1831}
1832
1833#[cfg(test)]
1834mod staging_tests {
1835    use super::*;
1836
1837    #[test]
1838    #[ignore = "requires gfx1151 and native SDMA"]
1839    fn split_transfer_failure_leaves_accepted_prefix_on_stream_timeline() -> Result<()> {
1840        let manager = crate::residency::ResidencyManager::new(512 << 20)?;
1841        {
1842            let mut stream = Device::open(0)?.stream_with_options(StreamOptions {
1843                copy_engine: crate::execution::CopyEngine::Sdma,
1844                memory_budget: Some(manager.budget()),
1845                ..Default::default()
1846            })?;
1847            let buffer = stream.allocate((256 << 20) + 17)?;
1848            stream.upload_blocking_at(&buffer, buffer.bytes() - 1, &[0])?;
1849            let command = stream.inner.prepare_fill(buffer.binding(), 0x39)?;
1850            let Command::Gpu(native, _) = &command else {
1851                unreachable!()
1852            };
1853            assert!(native.parts().count() > 1);
1854            native.exhaust_final_timeline();
1855            assert!(stream.inner.submit(&command).is_err());
1856            assert!(stream.inner.timeline.snapshot()?.is_some());
1857            stream.synchronize()?;
1858            let mut first = [0];
1859            stream.read_blocking(buffer.slice(0, 1), &mut first)?;
1860            assert_eq!(first, [0x39]);
1861            let mut last = [0];
1862            stream.read_blocking(buffer.slice(buffer.bytes() - 1, 1), &mut last)?;
1863            assert_eq!(last, [0]);
1864        }
1865        assert_eq!(manager.statistics().reserved_bytes, 0);
1866        Ok(())
1867    }
1868
1869    #[test]
1870    #[ignore = "requires gfx1151"]
1871    fn large_uploads_bound_staging_and_validate_before_publication() -> Result<()> {
1872        let manager = crate::residency::ResidencyManager::new(5 * STAGING_LIMIT)?;
1873        {
1874            let mut stream = Stream::open()?.with_memory_budget(manager.budget());
1875            let bytes = 2 * STAGING_LIMIT + 19;
1876            let buffer = stream.allocate(bytes + 16)?;
1877            stream.fill(buffer.binding(), 0x79)?;
1878            let data: Vec<_> = (0..bytes).map(|i| (i ^ (i >> 20)) as u8).collect();
1879            assert!(stream.upload(buffer.slice(17, bytes - 1), &data).is_err());
1880            let mut first = [0];
1881            stream.read_blocking(buffer.slice(0, 1), &mut first)?;
1882            assert_eq!(first, [0x79]);
1883            stream.upload(buffer.slice(3, bytes), &data)?;
1884            assert!(stream.staging.iter().map(|b| b.bytes).sum::<usize>() <= STAGING_LIMIT);
1885            let mut actual = vec![0; bytes + 16];
1886            stream.read_blocking(buffer.binding(), &mut actual)?;
1887            assert_eq!(&actual[..3], &[0x79; 3]);
1888            assert_eq!(&actual[3..3 + bytes], data);
1889            assert!(actual[3 + bytes..].iter().all(|&b| b == 0x79));
1890            assert!(stream.staging_pool.iter().map(|b| b.bytes).sum::<usize>() <= STAGING_LIMIT);
1891        }
1892        assert_eq!(manager.statistics().reserved_bytes, 0);
1893        Ok(())
1894    }
1895
1896    #[test]
1897    #[ignore = "requires gfx1151"]
1898    fn initialized_allocations_publish_all_bytes_and_preserve_pool_and_budget_rules() -> Result<()>
1899    {
1900        let device = Device::open(0)?;
1901        let mut stream = device.stream()?;
1902        assert!(stream.allocate_from(&[]).is_err());
1903        assert!(device.native.fabric().allocate_from(&[], &[]).is_err());
1904        // Odd lengths exercise the final partial cache line. Repeated rounds
1905        // deliberately recover the same pooled backing after GPU writes.
1906        for len in [1, 63, 65, 4097] {
1907            let mut prior = None;
1908            for round in 0..3 {
1909                let data: Vec<u8> = (0..len).map(|i| (i * 37 + round * 19) as u8).collect();
1910                let input = stream.allocate_from(&data)?;
1911                if let Some(prior) = prior {
1912                    assert_eq!(input.native.identity(), prior);
1913                }
1914                prior = Some(input.native.identity());
1915                let output = stream.allocate_zeroed(len)?;
1916                stream.copy(output.binding(), input.binding())?;
1917                let mut actual = vec![0; len];
1918                stream.read_blocking(output.binding(), &mut actual)?;
1919                assert_eq!(actual, data);
1920                stream.fill(input.binding(), 0xa5)?;
1921                // Move the retained completion prefix to unrelated backing.
1922                stream.fill(output.binding(), 0)?;
1923                stream.synchronize()?;
1924                drop(input);
1925                // Keep output out of the pool so the next input must reuse
1926                // the backing just written by the GPU.
1927                drop(output.into_unpooled());
1928            }
1929        }
1930        let residency = crate::residency::ResidencyManager::new(4097)?;
1931        let budget = residency.budget();
1932        let mut budgeted = device.stream()?.with_memory_budget(budget.clone());
1933        let input = budgeted.allocate_from(&vec![0x71; 4097])?;
1934        assert_eq!(budget.reserved_bytes(), 4097);
1935        assert!(budgeted.allocate_from(&[1]).is_err());
1936        assert_eq!(budget.reserved_bytes(), 4097);
1937        let mut actual = vec![0; 4097];
1938        budgeted.read_blocking(input.binding(), &mut actual)?;
1939        assert_eq!(actual, vec![0x71; 4097]);
1940        drop(input);
1941        assert_eq!(budget.reserved_bytes(), 0);
1942        Ok(())
1943    }
1944
1945    #[test]
1946    #[ignore = "requires gfx1151"]
1947    fn budgeted_commands_replay_and_evict_without_releasing_pending_charges() -> Result<()> {
1948        let residency = crate::residency::ResidencyManager::new(4096)?;
1949        let device = Device::open(0)?;
1950        let mut stream = device.stream()?.with_memory_budget(residency.budget());
1951        let mut foreign = device.stream()?;
1952        let compiler = crate::loom::Compiler::for_stream(None, &stream)?;
1953        let mut spec = crate::loom::Specialization::new("add_f32");
1954        spec.set_config("add.grid", "1");
1955        let artifact = compiler
1956            .module(include_str!("../tests/kernels/add_f32.loom"))
1957            .compile(&spec)?;
1958        // SAFETY: the trusted fixture adds a scalar to exactly 64 f32 elements.
1959        let kernel = unsafe { stream.load_artifact(&artifact)? };
1960        let buffer = stream.allocate(256)?;
1961        stream.fill(buffer.binding(), 0)?;
1962        let constants = |delta: f32| -> Result<Constants> {
1963            let mut constants = Constants::new();
1964            match kernel.layout[0].1 {
1965                4 => constants.push(64u32)?,
1966                8 => constants.push(64u64)?,
1967                _ => unreachable!("index width"),
1968            }
1969            constants.push(delta)?;
1970            Ok(constants)
1971        };
1972        // More entries than the old cache could hold, with distinct constants.
1973        for _ in 0..2 {
1974            for delta in 1..=100 {
1975                unsafe {
1976                    stream.dispatch(
1977                        &kernel,
1978                        [1; 3],
1979                        [256, 1, 1],
1980                        &constants(delta as f32)?,
1981                        &[buffer.binding()],
1982                    )?;
1983                }
1984            }
1985            stream.synchronize()?;
1986            assert_eq!(stream.inner.dispatch_cache.lock().unwrap().len(), 100);
1987        }
1988        let mut actual = [0u8; 256];
1989        stream.read_blocking(buffer.binding(), &mut actual)?;
1990        assert!(
1991            actual
1992                .as_chunks::<4>()
1993                .0
1994                .iter()
1995                .all(|bytes| f32::from_le_bytes(*bytes) == 10100.)
1996        );
1997        assert_eq!(residency.statistics().reserved_bytes, 256);
1998        for delta in 101..=1124 {
1999            unsafe {
2000                stream.dispatch(
2001                    &kernel,
2002                    [1; 3],
2003                    [256, 1, 1],
2004                    &constants(delta as f32)?,
2005                    &[buffer.binding()],
2006                )?;
2007            }
2008        }
2009        stream.synchronize()?;
2010        {
2011            let cache = stream.inner.dispatch_cache.lock().unwrap();
2012            assert_eq!(cache.len(), DISPATCH_CACHE_ENTRIES);
2013            assert!(
2014                cache
2015                    .iter()
2016                    .map(|entry| entry.command.storage_bytes())
2017                    .sum::<usize>()
2018                    <= DISPATCH_CACHE_BYTES
2019            );
2020        }
2021        // A foreign stream cannot retain a command whose owner cannot evict it.
2022        unsafe {
2023            foreign.dispatch(
2024                &kernel,
2025                [1; 3],
2026                [256, 1, 1],
2027                &constants(1.)?,
2028                &[buffer.binding()],
2029            )?;
2030        }
2031        foreign.synchronize()?;
2032        assert!(foreign.inner.dispatch_cache.lock().unwrap().is_empty());
2033        // Removing public backing purges both caches; its queued reservation
2034        // survives until the stream explicitly observes completion.
2035        unsafe {
2036            stream.dispatch(
2037                &kernel,
2038                [1; 3],
2039                [256, 1, 1],
2040                &constants(1.)?,
2041                &[buffer.binding()],
2042            )?;
2043        }
2044        drop(buffer);
2045        assert!(stream.inner.dispatch_cache.lock().unwrap().is_empty());
2046        assert!(stream.inner.transfer_cache.lock().unwrap().is_empty());
2047        assert_eq!(residency.statistics().reserved_bytes, 256);
2048        stream.synchronize()?;
2049        assert_eq!(residency.statistics().reserved_bytes, 0);
2050        Ok(())
2051    }
2052
2053    #[test]
2054    #[ignore = "requires gfx1151"]
2055    fn a_scratch_pool_only_accepts_the_stream_that_allocated_the_buffer() -> Result<()> {
2056        let device = Device::open(0)?;
2057        let mut owner = device.stream()?;
2058        let mut other = device.stream()?;
2059        let private = other.scratch(4096)?;
2060        let private_raw = private.allocation_address()?;
2061        other.recycle(private)?;
2062
2063        // Buffers are device-scoped, so another stream may read and write this one.
2064        let buffer = owner.allocate(4096)?;
2065        owner.fill(buffer.binding(), 0x5a)?;
2066        let ready = owner.record_event()?;
2067        other.wait_event(&ready)?;
2068        let mut seen = [0u8; 16];
2069        other.read_blocking(buffer.binding().slice(0, 16)?, &mut seen)?;
2070        assert_eq!(seen, [0x5a; 16], "a foreign stream can use the allocation");
2071
2072        // A scratch pool is still one stream's private free list, not shared state.
2073        assert!(
2074            other
2075                .recycle(buffer)
2076                .unwrap_err()
2077                .to_string()
2078                .contains("another stream")
2079        );
2080        assert!(owner.scratch.is_empty());
2081        assert_eq!(other.scratch_bytes, 4096);
2082        assert_eq!(other.scratch(4096)?.allocation_address()?, private_raw);
2083        Ok(())
2084    }
2085
2086    #[test]
2087    #[ignore = "requires gfx1151"]
2088    fn completed_staging_is_reclaimed_and_reused_on_every_completion_path() -> Result<()> {
2089        let mut stream = Stream::open()?;
2090        let buffer = stream.allocate(1024)?;
2091        stream.upload(buffer.binding(), &[7; 1024])?;
2092        let original = stream.staging[0].allocation_address()?;
2093        let mut output = [0; 1024];
2094        stream.read_blocking(buffer.binding(), &mut output)?;
2095        assert_eq!(output, [7; 1024]);
2096        assert!(stream.staging.is_empty());
2097        stream.upload(buffer.binding(), &[9; 1024])?;
2098        assert_eq!(stream.staging[0].allocation_address()?, original);
2099        stream.upload_blocking(buffer.binding(), &[11; 1024])?;
2100        assert!(stream.staging.is_empty());
2101        stream.upload(buffer.binding(), &[12; 512])?;
2102        assert_eq!(stream.staging[0].allocation_address()?, original);
2103        stream.synchronize()?;
2104        stream.upload(buffer.binding(), &[13; 1024])?;
2105        stream.inner.wait()?; // Establish completion without the cleanup being tested.
2106        assert!(stream.submit()?.is_complete()?);
2107        assert!(stream.staging.is_empty());
2108        for _ in 0..32 {
2109            stream.upload(buffer.binding(), &[1; 1024])?;
2110            assert!(stream.staging.len() <= 8);
2111        }
2112        stream.synchronize()?;
2113        assert!(stream.staging_pool.len() <= 8);
2114        assert!(stream.staging_pool.iter().map(|b| b.bytes).sum::<usize>() <= STAGING_LIMIT);
2115        Ok(())
2116    }
2117
2118    #[test]
2119    #[ignore = "requires gfx1151"]
2120    fn uploads_batch_until_pressure_and_reuse_the_smallest_capacity() -> Result<()> {
2121        let mut stream = Stream::open()?;
2122        let buffer = stream.allocate(4096)?;
2123        for size in [1024, 2048, 4096] {
2124            stream.upload(buffer.binding(), &vec![7; size])?;
2125        }
2126        assert_eq!(stream.staging.len(), 3);
2127        let medium = stream.staging[1].allocation_address()?;
2128        stream.synchronize()?;
2129        stream.upload(buffer.binding(), &[9; 1500])?;
2130        assert_eq!(stream.staging[0].allocation_address()?, medium);
2131        for _ in 0..7 {
2132            stream.upload(buffer.binding(), &[11; 1024])?;
2133        }
2134        assert_eq!(stream.staging.len(), 8);
2135        stream.upload(buffer.binding(), &[13; 1024])?;
2136        assert_eq!(stream.staging.len(), 1);
2137        stream.synchronize()?;
2138        Ok(())
2139    }
2140
2141    #[test]
2142    #[ignore = "requires gfx1151"]
2143    fn larger_uploads_replace_small_completed_staging() -> Result<()> {
2144        let mut stream = Stream::open()?;
2145        let buffer = stream.allocate(16 << 20)?;
2146        for _ in 0..8 {
2147            stream.upload(buffer.binding(), &[7; 1024])?;
2148        }
2149        stream.synchronize()?;
2150        assert_eq!(stream.staging_pool.len(), 8);
2151
2152        let source = vec![0x35; 1 << 20];
2153        stream.upload(buffer.binding(), &source)?;
2154        let large = stream.staging[0].allocation_address()?;
2155        stream.synchronize()?;
2156        let replacement = vec![0xa9; source.len()];
2157        stream.upload(buffer.binding(), &replacement)?;
2158        assert_eq!(stream.staging[0].allocation_address()?, large);
2159        let mut output = vec![0; source.len()];
2160        stream.read_blocking(buffer.try_slice(0, output.len())?, &mut output)?;
2161        assert_eq!(output, replacement);
2162        assert!(stream.staging_pool.len() <= 8);
2163        assert!(stream.staging_pool.iter().map(|b| b.bytes).sum::<usize>() <= STAGING_LIMIT);
2164
2165        // Larger packets also exercise eviction at the byte ceiling, before
2166        // the cache reaches its eight-entry ceiling.
2167        let source = vec![0x63; 16 << 20];
2168        for _ in 0..9 {
2169            stream.upload(buffer.binding(), &source)?;
2170        }
2171        let mut output = vec![0; source.len()];
2172        stream.read_blocking(buffer.binding(), &mut output)?;
2173        assert_eq!(output, source);
2174        assert!(stream.staging_pool.len() <= 8);
2175        assert!(stream.staging_pool.iter().map(|b| b.bytes).sum::<usize>() <= STAGING_LIMIT);
2176        Ok(())
2177    }
2178}
2179
2180#[cfg(test)]
2181mod dag_probe {
2182    use super::*;
2183
2184    /// Declared edges cost scheduling time, so the graph API must be able to omit
2185    /// the ones a workload does not need. Latency-bound by construction: 64 tiny
2186    /// fills, so bandwidth cannot confound the comparison.
2187    #[test]
2188    #[ignore = "requires gfx1151"]
2189    fn independent_nodes_beat_a_serial_chain() -> Result<()> {
2190        const NODES: usize = 64;
2191        let mut stream = Stream::open()?;
2192        let buffers: Vec<Buffer> = (0..NODES)
2193            .map(|_| stream.allocate(4096))
2194            .collect::<Result<_>>()?;
2195        let mut timings = Vec::new();
2196        for chained in [true, false] {
2197            let mut graph = stream.graph()?;
2198            let mut previous: Option<Node> = None;
2199            for buffer in &buffers {
2200                let after: &[Node] = match (chained, &previous) {
2201                    (true, Some(node)) => std::slice::from_ref(node),
2202                    _ => &[],
2203                };
2204                previous = Some(graph.fill(after, buffer.binding(), 0x3c)?);
2205            }
2206            let mut exec = graph.finish()?;
2207            for _ in 0..3 {
2208                stream.launch(&mut exec)?;
2209                stream.synchronize()?;
2210            }
2211            let mut samples = Vec::new();
2212            for _ in 0..9 {
2213                let start = std::time::Instant::now();
2214                stream.launch(&mut exec)?;
2215                stream.synchronize()?;
2216                samples.push(start.elapsed().as_secs_f64() * 1e6);
2217            }
2218            samples.sort_by(f64::total_cmp);
2219            println!(
2220                "{:<12} {NODES} nodes: {:8.1} us total, {:6.2} us/node",
2221                if chained { "serial" } else { "independent" },
2222                samples[4],
2223                samples[4] / NODES as f64
2224            );
2225            timings.push(samples[4]);
2226            let mut seen = [0u8; 4096];
2227            stream.read_blocking(buffers[NODES - 1].binding(), &mut seen)?;
2228            assert_eq!(seen, [0x3c; 4096], "every node ran");
2229        }
2230        // A margin rather than a strict inequality: the property under test is
2231        // that omitting edges still reaches the runtime, not that a shared CI
2232        // runner is quiet. The measured gap is ~1.7x, so 1.2x is a wide floor.
2233        assert!(
2234            timings[1] * 1.2 < timings[0],
2235            "omitting edges should be materially cheaper: {timings:?}"
2236        );
2237        Ok(())
2238    }
2239}
2240
2241#[cfg(test)]
2242mod engine_tests {
2243    use super::*;
2244    use crate::execution::{ComputeEngine, CopyEngine};
2245
2246    #[test]
2247    #[ignore = "requires native compiler and gfx1151"]
2248    fn engines_order_eager_graph_replay_and_foreign_events() -> Result<()> {
2249        let device = Device::open(0)?;
2250        let compiler = crate::loom::Compiler::for_target(None, device.target())?;
2251        let artifact = compiler
2252            .module(include_str!("../tests/kernels/add_f32.loom"))
2253            .compile(&crate::loom::Specialization::new("add_f32").with_config("add.grid", "1"))?;
2254        for compute_engine in [
2255            ComputeEngine::Pm4,
2256            ComputeEngine::Aql {
2257                maximum_private_bytes: 0,
2258            },
2259        ] {
2260            for copy_engine in [CopyEngine::Compute, CopyEngine::Sdma] {
2261                let mut stream = device.stream_with_options(StreamOptions {
2262                    compute_engine,
2263                    copy_engine,
2264                    ..Default::default()
2265                })?;
2266                let kernel = unsafe { stream.load_artifact(&artifact) }?;
2267                let mut constants = Constants::new();
2268                match kernel.layout[0].1 {
2269                    4 => constants.push(64u32)?,
2270                    8 => constants.push(64u64)?,
2271                    _ => unreachable!(),
2272                }
2273                constants.push(1f32)?;
2274                let a = stream.allocate(256)?;
2275                let b = stream.allocate(256)?;
2276                stream.fill(a.binding(), 0)?;
2277                unsafe {
2278                    stream.dispatch(&kernel, [1; 3], [256, 1, 1], &constants, &[a.binding()])?;
2279                }
2280                stream.copy(b.binding(), a.binding())?;
2281                let mut graph = stream.owned_graph()?;
2282                let add = unsafe {
2283                    graph.dispatch(
2284                        &[],
2285                        &kernel,
2286                        [1; 3],
2287                        [256, 1, 1],
2288                        &constants,
2289                        &[b.binding()],
2290                    )?
2291                };
2292                graph.copy(&[add], a.binding(), b.binding())?;
2293                let mut graph = graph.finish()?;
2294                for _ in 0..3 {
2295                    stream.launch(&mut graph)?;
2296                }
2297                let event = stream.record_event()?;
2298                let mut observer = device.stream()?;
2299                observer.wait_event(&event)?;
2300                let mut bytes = [0; 256];
2301                observer.read_blocking(a.binding(), &mut bytes)?;
2302                assert!(
2303                    bytes
2304                        .as_chunks::<4>()
2305                        .0
2306                        .iter()
2307                        .all(|v| f32::from_le_bytes(*v) == 4.),
2308                    "{compute_engine:?} / {copy_engine:?}"
2309                );
2310                stream.synchronize()?;
2311            }
2312        }
2313        Ok(())
2314    }
2315}