Skip to main content

hrx/fabric/
queue.rs

1mod sdma;
2use super::*;
3use std::{
4    collections::VecDeque,
5    sync::{
6        Mutex, Weak,
7        atomic::{AtomicU32, AtomicU64, Ordering},
8    },
9    time::{Duration, Instant},
10};
11
12/// A serialized host producer for one native GPU PM4 queue.
13#[derive(Clone)]
14pub struct Queue(Arc<QueueInner>);
15
16// Weak slots avoid a device -> queue -> device ownership cycle. Streams,
17// prepared commands and completions keep their assigned queue alive.
18pub(super) struct StreamQueues {
19    slots: Vec<Weak<QueueInner>>,
20    limit: usize,
21    next: usize,
22}
23impl Default for StreamQueues {
24    fn default() -> Self {
25        Self {
26            slots: Vec::new(),
27            // See benchmarks/queue-pool for the gfx1151 throughput/latency tradeoff.
28            limit: 8,
29            next: 0,
30        }
31    }
32}
33impl StreamQueues {
34    fn set_limit(&mut self, count: usize) -> Result<()> {
35        if count == 0 {
36            return Err(Error::Message("stream queue count must be positive".into()));
37        }
38        if count != self.limit && !self.slots.is_empty() {
39            return Err(Error::Busy(
40                "configure stream queues before creating the first stream".into(),
41            ));
42        }
43        self.limit = count;
44        Ok(())
45    }
46}
47struct QueueInner {
48    device: Device,
49    family: u32,
50    raw: *mut amdf_user_queue_t,
51    mapping: *mut amdf_user_queue_mapping_t,
52    info: amdf_user_queue_mapping_info_t,
53    state: Mutex<QueueState>,
54    transfers: Mutex<Option<[Kernel; 4]>>,
55}
56struct QueueState {
57    write: u64,
58    pending: VecDeque<Pending>,
59}
60struct Pending {
61    work: Arc<Work>,
62    value: u32,
63}
64struct Work {
65    fence: Buffer,
66    _kernels: Vec<Kernel>,
67    buffers: Vec<Buffer>,
68    leases: Mutex<Vec<memory::DeviceUse>>,
69    retired: AtomicU32,
70    active: AtomicU32,
71    dependencies: Vec<(Arc<Work>, u32)>,
72}
73// Command publication has one mutex-protected producer. Status observation is
74// documented as concurrent; mapped native pointers remain valid until drop.
75unsafe impl Send for QueueInner {}
76unsafe impl Sync for QueueInner {}
77
78/// Native execution completion retaining all indirect resources.
79#[derive(Clone)]
80pub struct Completion {
81    queue: Queue,
82    work: Arc<Work>,
83    value: u32,
84}
85impl Completion {
86    /// Read cached retirement without native queries, locks, or progress work.
87    /// Call `refresh` or `wait` to advance retirement.
88    pub fn is_complete(&self) -> Result<bool> {
89        Ok(self.work.retired.load(Ordering::Acquire) >= self.value)
90    }
91    /// Observe the execution fence and retire completed resource leases.
92    /// This performs bounded progress work; ring consumption is not completion.
93    pub fn refresh(&self) -> Result<bool> {
94        if !self.queue.0.poll(&self.work, self.value)? {
95            return Ok(false);
96        }
97        // A completed prefix must release every earlier submission's leases,
98        // even when callers observed only the last completion point.
99        let mut state = self
100            .queue
101            .0
102            .state
103            .lock()
104            .map_err(|_| Error::DeviceLost("queue producer poisoned".into()))?;
105        while let Some(work) = state.pending.front() {
106            if !self.queue.0.poll(&work.work, work.value)? {
107                break;
108            }
109            state.pending.pop_front();
110        }
111        Ok(true)
112    }
113    /// Wait without cancelling or releasing pending resources on timeout.
114    pub fn wait_timeout(&self, timeout: Duration) -> Result<bool> {
115        let start = Instant::now();
116        loop {
117            if self.refresh()? {
118                return Ok(true);
119            }
120            if start.elapsed() >= timeout {
121                return Ok(false);
122            }
123            std::thread::yield_now();
124        }
125    }
126    /// Wait until execution retires or the provider reports failure.
127    pub fn wait(&self) -> Result<()> {
128        while !self.refresh()? {
129            std::thread::yield_now();
130        }
131        Ok(())
132    }
133}
134impl Work {
135    fn retire(&self, value: u32) -> Result<bool> {
136        if self.retired.load(Ordering::Acquire) >= value {
137            return Ok(true);
138        }
139        let mut leases = self
140            .leases
141            .lock()
142            .map_err(|_| Error::DeviceLost("submission ownership poisoned".into()))?;
143        if self.retired.load(Ordering::Acquire) >= value {
144            return Ok(true);
145        }
146        let completed = self.fence.fence_value()?;
147        if completed >= value {
148            for (dependency, point) in &self.dependencies {
149                if !dependency.retire(*point)? {
150                    return Ok(false);
151                }
152            }
153            if completed >= self.active.load(Ordering::Acquire) {
154                leases.clear();
155            }
156            self.retired.store(completed, Ordering::Release);
157            return Ok(true);
158        }
159        Ok(false)
160    }
161}
162impl QueueInner {
163    fn poll(&self, work: &Work, value: u32) -> Result<bool> {
164        if work.retire(value)? {
165            return Ok(true);
166        }
167        let api = self.device.0.endpoint.0.instance.api.core;
168        let mut status = amdf_user_queue_status_t {
169            type_: AMDF_STRUCTURE_TYPE_USER_QUEUE_STATUS,
170            structure_size: size_of::<amdf_user_queue_status_t>() as u32,
171            ..Default::default()
172        };
173        check("user_queue_query_status", unsafe {
174            entry!(api, user_queue_query_status)(self.raw, &mut status)
175        })?;
176        if status.state != AMDF_QUEUE_STATE_ACTIVE {
177            return Err(Error::DeviceLost(format!(
178                "PM4 queue failed with native status {}",
179                status.terminal_status
180            )));
181        }
182        Ok(false)
183    }
184}
185impl Drop for QueueInner {
186    fn drop(&mut self) {
187        let pending = std::mem::take(
188            &mut self
189                .state
190                .get_mut()
191                .unwrap_or_else(|e| e.into_inner())
192                .pending,
193        );
194        let deadline = Instant::now() + Duration::from_secs(10);
195        for work in &pending {
196            loop {
197                match self.poll(&work.work, work.value) {
198                    Ok(true) => break,
199                    Ok(false) if Instant::now() < deadline => std::thread::yield_now(),
200                    Ok(false) | Err(_) => {
201                        // Failure is not retirement. Leak the entire native
202                        // ownership chain instead of unmapping reachable memory.
203                        std::mem::forget(pending);
204                        std::mem::forget(self.device.clone());
205                        return;
206                    }
207                }
208            }
209        }
210        let api = self.device.0.endpoint.0.instance.api.core;
211        unsafe {
212            if !self.mapping.is_null() && api.user_queue_mapping_destroy.unwrap()(self.mapping) != 0
213            {
214                std::mem::forget(self.device.clone());
215                return;
216            }
217            if !self.raw.is_null() && api.user_queue_destroy.unwrap()(self.raw) != 0 {
218                std::mem::forget(self.device.clone());
219            }
220        }
221    }
222}
223
224fn header(opcode: u32, count: usize) -> u32 {
225    (3 << 30) | (((count - 2) as u32) << 16) | (opcode << 8)
226}
227fn barrier(words: &mut Vec<u32>) {
228    // Native PM4 v1/GCR system release and acquire. GLI_INV=1 invalidates
229    // every instruction-cache line; 3 means FIRST_LAST, not ALL. A full address
230    // range with FIRST_LAST can retain instructions from retired code whose
231    // allocation address is later reused. See Mesa's GCR_GLI_INV packet enum
232    // (src/amd/registers/pkt3.json) and PAL's BuildAcquireMemInternal.
233    const GLI_INV_ALL: u32 = 1;
234    words.extend([
235        header(0x46, 2),
236        7 | (4 << 8),
237        header(0x58, 8),
238        0,
239        u32::MAX,
240        0xff,
241        0,
242        0,
243        0x0a,
244        GLI_INV_ALL | (1 << 4) | (1 << 5) | (1 << 7) | (1 << 8) | (1 << 9) | (1 << 14) | (1 << 15),
245    ]);
246}
247fn pad(words: &mut Vec<u32>) {
248    let mut count = 8 - words.len() % 8;
249    if count == 1 {
250        count += 8;
251    }
252    words.push(header(0x10, count));
253    words.resize(words.len() + count - 1, 0);
254}
255
256// The native indirect-buffer size field is 20 bits. Padding needs up to nine
257// words; dependency barriers take ten and each optional marker at most 32.
258const INDIRECT_WORD_LIMIT: usize = 1 << 20;
259fn batch_ranges(
260    commands: impl IntoIterator<Item = (usize, bool)>,
261    profiled: bool,
262) -> Result<Vec<std::ops::Range<usize>>> {
263    let mut ranges = Vec::new();
264    let mut start = 0;
265    let mut end = 0;
266    let mut words = 0;
267    for (payload, dependency) in commands {
268        let size = payload
269            .checked_add(if dependency { 10 } else { 0 })
270            .and_then(|n| n.checked_add(if profiled { 2 * (10 + 32) } else { 0 }))
271            .filter(|n| *n < INDIRECT_WORD_LIMIT - 9)
272            .ok_or_else(|| {
273                Error::Unsupported("native command exceeds indirect command capacity".into())
274            })?;
275        if words + size >= INDIRECT_WORD_LIMIT - 9 {
276            ranges.push(start..end);
277            start = end;
278            words = 0;
279        }
280        words += size;
281        end += 1;
282    }
283    if start != end {
284        ranges.push(start..end);
285    }
286    Ok(ranges)
287}
288
289impl Device {
290    pub(crate) fn set_stream_queue_count(&self, count: usize) -> Result<()> {
291        self.0
292            .stream_queues
293            .lock()
294            .map_err(|_| Error::DeviceLost("stream queue pool poisoned".into()))?
295            .set_limit(count)
296    }
297
298    pub(crate) fn stream_queue_count(&self) -> Result<usize> {
299        Ok(self
300            .0
301            .stream_queues
302            .lock()
303            .map_err(|_| Error::DeviceLost("stream queue pool poisoned".into()))?
304            .limit)
305    }
306
307    pub(crate) fn stream_queue(&self) -> Result<Queue> {
308        let mut pool = self
309            .0
310            .stream_queues
311            .lock()
312            .map_err(|_| Error::DeviceLost("stream queue pool poisoned".into()))?;
313        let slot = pool.next;
314        if slot == pool.slots.len() {
315            pool.slots
316                .try_reserve(1)
317                .map_err(|_| Error::Message("stream queue pool allocation failed".into()))?;
318            let queue = self.queue()?;
319            pool.slots.push(Arc::downgrade(&queue.0));
320            pool.next = (slot + 1) % pool.limit;
321            return Ok(queue);
322        }
323        let queue = match pool.slots[slot].upgrade() {
324            Some(queue) => Queue(queue),
325            None => {
326                let queue = self.queue()?;
327                pool.slots[slot] = Arc::downgrade(&queue.0);
328                queue
329            }
330        };
331        pool.next = (slot + 1) % pool.limit;
332        Ok(queue)
333    }
334
335    /// Create a native PM4 queue with one serialized host producer.
336    pub fn queue(&self) -> Result<Queue> {
337        self.create_queue(AMDF_QUEUE_COMMAND_TYPE_GPU_PM4)
338    }
339    /// Create a native SDMA transfer queue without loading compute kernels.
340    pub fn sdma_queue(&self) -> Result<Queue> {
341        self.create_queue(AMDF_QUEUE_COMMAND_TYPE_GPU_SDMA)
342    }
343    fn create_queue(&self, command: u32) -> Result<Queue> {
344        if self.endpoint().engine() != Engine::Gpu {
345            return Err(Error::Unsupported(
346                "PM4 queues require an AMDGPU device".into(),
347            ));
348        }
349        let api = &self.0.endpoint.0.instance.api;
350        let _ = entry!(api.core, user_queue_destroy);
351        let _ = entry!(api.core, user_queue_mapping_destroy);
352        let mut endpoint = amdf_endpoint_info_t {
353            type_: AMDF_STRUCTURE_TYPE_ENDPOINT_INFO,
354            structure_size: size_of::<amdf_endpoint_info_t>() as u32,
355            ..Default::default()
356        };
357        check("endpoint_query_info", unsafe {
358            entry!(api.core, endpoint_query_info)(self.0.endpoint.0.raw, &mut endpoint)
359        })?;
360        for ordinal in 0..endpoint.queue_family_count {
361            let mut family = amdf_queue_family_info_t {
362                type_: AMDF_STRUCTURE_TYPE_QUEUE_FAMILY_INFO,
363                structure_size: size_of::<amdf_queue_family_info_t>() as u32,
364                ..Default::default()
365            };
366            check("endpoint_query_queue_family_info", unsafe {
367                entry!(api.core, endpoint_query_queue_family_info)(
368                    self.0.endpoint.0.raw,
369                    ordinal,
370                    &mut family,
371                )
372            })?;
373            if family.command_type != command
374                || family.format_version != AMDF_GPU_PM4_QUEUE_FORMAT_VERSION_1
375                || family.publication_modes & AMDF_QUEUE_PUBLICATION_MODE_USER == 0
376                || (command == AMDF_QUEUE_COMMAND_TYPE_GPU_PM4
377                    && family.format_features & AMDF_GPU_PM4_FORMAT_FEATURE_ACQUIRE_MEM_GCR as u64
378                        == 0)
379            {
380                continue;
381            }
382            let options = amdf_gpu_user_queue_create_info_t {
383                type_: HRX_AMDF_STRUCTURE_TYPE_GPU_USER_QUEUE_CREATE_INFO,
384                structure_size: size_of::<amdf_gpu_user_queue_create_info_t>() as u32,
385                queue_family_ordinal: ordinal,
386                priority: AMDF_QUEUE_PRIORITY_NORMAL,
387                producer_mode: AMDF_QUEUE_PRODUCER_MODE_SINGLE,
388                required_capabilities: AMDF_USER_QUEUE_CAPABILITY_HOST_PRODUCER as u64,
389                ring_byte_length: family.minimum_ring_byte_length,
390                ..Default::default()
391            };
392            let mut raw = ptr::null_mut();
393            check("gpu.user_queue_create", unsafe {
394                entry!(api.gpu, user_queue_create)(self.0.raw, &options, &mut raw)
395            })?;
396            if raw.is_null() {
397                return Err(missing("created queue"));
398            }
399            let mut queue = QueueInner {
400                device: self.clone(),
401                family: ordinal,
402                raw,
403                mapping: ptr::null_mut(),
404                info: amdf_user_queue_mapping_info_t::default(),
405                transfers: Mutex::new(None),
406                state: Mutex::new(QueueState {
407                    write: 0,
408                    pending: VecDeque::with_capacity(4096),
409                }),
410            };
411            check("user_queue_map", unsafe {
412                entry!(api.core, user_queue_map)(raw, ptr::null_mut(), &mut queue.mapping)
413            })?;
414            queue.info.type_ = AMDF_STRUCTURE_TYPE_USER_QUEUE_MAPPING_INFO;
415            queue.info.structure_size = size_of::<amdf_user_queue_mapping_info_t>() as u32;
416            check("user_queue_mapping_query_info", unsafe {
417                entry!(api.core, user_queue_mapping_query_info)(queue.mapping, &mut queue.info)
418            })?;
419            let info = &queue.info;
420            if info.command_type != command
421                || info.format_version != 1
422                || !info.ring_byte_length.is_power_of_two()
423                || info.ring_byte_length < 4096
424                || info.index_bits != 64
425                || info.doorbell_bits != 64
426                || [
427                    info.ring_address,
428                    info.read_index_address,
429                    info.write_index_address,
430                    info.doorbell_address,
431                ]
432                .iter()
433                .any(|address| *address == 0 || address % 8 != 0)
434            {
435                return Err(Error::Unsupported(
436                    "unexpected native GPU queue mapping contract".into(),
437                ));
438            }
439            return Ok(Queue(Arc::new(queue)));
440        }
441        Err(Error::Unsupported(
442            "no qualified native queue family for the requested engine".into(),
443        ))
444    }
445}
446
447/// Private graph construction storage. Nodes are never submitted individually.
448/// Argument pages become immutable before the graph can be launched.
449#[derive(Default)]
450pub(crate) struct GraphArena {
451    page: Option<Buffer>,
452    used: usize,
453    fence: Option<Buffer>,
454}
455impl GraphArena {
456    fn pack(&mut self, queue: &Queue, bytes: &[u8]) -> Result<(Buffer, usize, Buffer, usize)> {
457        let fabric = queue.device().fabric();
458        let size = bytes.len().max(64).next_multiple_of(64);
459        let mut allocated = 0;
460        if self
461            .page
462            .as_ref()
463            .is_none_or(|p| size > p.len() - self.used)
464        {
465            self.page =
466                Some(fabric.allocate(size.max(64 * 1024), std::slice::from_ref(queue.device()))?);
467            self.used = 0;
468            allocated += self.page.as_ref().unwrap().len();
469        }
470        let page = self.page.as_ref().unwrap();
471        let offset = self.used;
472        page.write(offset, bytes)?;
473        self.used += size;
474        if self.fence.is_none() {
475            self.fence = Some(fabric.allocate_shared(64, std::slice::from_ref(queue.device()))?);
476            allocated += 64;
477        }
478        Ok((
479            page.clone(),
480            offset,
481            self.fence.as_ref().unwrap().clone(),
482            allocated,
483        ))
484    }
485}
486
487impl Queue {
488    pub(super) fn identity(&self) -> usize {
489        Arc::as_ptr(&self.0) as usize
490    }
491    /// Prepare immutable commands and backing for repeated native execution.
492    ///
493    /// # Safety
494    /// Arguments and launch dimensions must satisfy the kernel's memory
495    /// contract. The caller orders conflicting accesses across other queues.
496    pub unsafe fn prepare(
497        &self,
498        kernel: &Kernel,
499        grid: [u32; 3],
500        block: [u16; 3],
501        arguments: &[Argument<'_>],
502    ) -> Result<PreparedGpu> {
503        unsafe { self.prepare_inner(kernel, grid, block, arguments, None) }
504    }
505    pub(crate) unsafe fn prepare_graph(
506        &self,
507        kernel: &Kernel,
508        grid: [u32; 3],
509        block: [u16; 3],
510        arguments: &[Argument<'_>],
511        arena: &mut GraphArena,
512    ) -> Result<PreparedGpu> {
513        unsafe { self.prepare_inner(kernel, grid, block, arguments, Some(arena)) }
514    }
515    unsafe fn prepare_inner(
516        &self,
517        kernel: &Kernel,
518        grid: [u32; 3],
519        block: [u16; 3],
520        arguments: &[Argument<'_>],
521        arena: Option<&mut GraphArena>,
522    ) -> Result<PreparedGpu> {
523        if self.0.info.command_type != AMDF_QUEUE_COMMAND_TYPE_GPU_PM4 {
524            return Err(Error::Unsupported(
525                "kernel dispatch requires a compute queue".into(),
526            ));
527        }
528        if !Arc::ptr_eq(&kernel.device().0, &self.0.device.0) {
529            return Err(Error::Message(
530                "kernel belongs to another native device".into(),
531            ));
532        }
533        let fabric = Fabric(self.0.device.0.endpoint.0.instance.clone());
534        let (packed, mut buffers) = kernel.arguments(arguments)?;
535        let (argument_buffer, argument_offset, fence, argument_storage) = if let Some(arena) = arena
536        {
537            arena.pack(self, &packed)?
538        } else {
539            let buffer =
540                fabric.allocate(packed.len().max(64), std::slice::from_ref(&self.0.device))?;
541            buffer.write(0, &packed)?;
542            {
543                let bytes = buffer.len() + 64;
544                (
545                    buffer,
546                    0,
547                    fabric.allocate_shared(64, std::slice::from_ref(&self.0.device))?,
548                    bytes,
549                )
550            }
551        };
552        let address = fence.device_address(&self.0.device)?;
553        let scratch = if kernel.0.info.private_bytes != 0 {
554            let mut info = amdf_gpu_endpoint_info_t {
555                type_: HRX_AMDF_STRUCTURE_TYPE_GPU_ENDPOINT_INFO,
556                structure_size: size_of::<amdf_gpu_endpoint_info_t>() as u32,
557                ..Default::default()
558            };
559            let api = &self.0.device.0.endpoint.0.instance.api;
560            check("GPU scratch topology", unsafe {
561                entry!(api.gpu, endpoint_query_info)(self.0.device.0.endpoint.0.raw, &mut info)
562            })?;
563            if info.topology.xcc_count != 1 {
564                return Err(Error::Unsupported(
565                    "scratch requires the qualified single-XCC topology".into(),
566                ));
567            }
568            let waves = info
569                .compute
570                .compute_unit_count
571                .checked_mul(info.compute.maximum_scratch_wave_count_per_compute_unit)
572                .ok_or_else(|| Error::Message("scratch topology overflow".into()))?;
573            let per_wave = (u64::from(kernel.0.info.private_bytes)
574                * u64::from(kernel.0.info.wave_size))
575            .div_ceil(256)
576                * 256;
577            let size = per_wave
578                .checked_mul(u64::from(waves))
579                .filter(|size| *size <= 512 * 1024 * 1024)
580                .ok_or_else(|| {
581                    Error::Unsupported("kernel scratch exceeds 512 MiB admission limit".into())
582                })?;
583            let buffer = fabric.allocate_access(
584                size as usize,
585                std::slice::from_ref(&self.0.device),
586                AMDF_MEMORY_ACCESS_READ | AMDF_MEMORY_ACCESS_WRITE,
587                256,
588            )?;
589            Some((buffer, waves, info.topology.shader_engine_count_per_xcc))
590        } else {
591            None
592        };
593        let mut words = Vec::with_capacity(160);
594        barrier(&mut words);
595        let dispatch_start = words.len();
596        words.extend(
597            kernel.dispatch_words(
598                grid,
599                block,
600                &packed,
601                argument_buffer.device_address(&self.0.device)? + argument_offset as u64,
602                scratch
603                    .as_ref()
604                    .map(|(buffer, waves, engines)| (buffer, *waves, *engines)),
605            )?,
606        );
607        let dispatch_end = words.len();
608        barrier(&mut words);
609        let fence_word = words.len() + 4;
610        words.extend([
611            header(0x37, 5),
612            (2 << 8) | (1 << 20),
613            address as u32,
614            (address >> 32) as u32,
615            1,
616        ]);
617        pad(&mut words);
618        // Arena pages are charged once, at their first graph node.
619        let storage_bytes =
620            argument_storage + scratch.as_ref().map_or(0, |(buffer, _, _)| buffer.len());
621        buffers.push(argument_buffer.clone());
622        if let Some((scratch, _, _)) = scratch {
623            buffers.push(scratch);
624        }
625        buffers.sort_by_key(|buffer| Arc::as_ptr(&buffer.0) as usize);
626        buffers.dedup_by(|a, b| Arc::ptr_eq(&a.0, &b.0));
627        let leases = Mutex::new(Vec::with_capacity(buffers.len()));
628        Ok(PreparedGpu {
629            prefix: Default::default(),
630            queue: self.clone(),
631            inner: Arc::new(PreparedInner {
632                storage_bytes,
633                work: Arc::new(Work {
634                    _kernels: vec![kernel.clone()],
635                    buffers,
636                    fence,
637                    leases,
638                    retired: AtomicU32::new(0),
639                    active: AtomicU32::new(0),
640                    dependencies: Vec::new(),
641                }),
642                words,
643                dispatch_range: dispatch_start..dispatch_end,
644                indirect_words: None,
645                fence_word,
646                previous: Mutex::new(0),
647            }),
648        })
649    }
650    /// Combine prepared dispatches into one immutable indirect command buffer.
651    /// A true flag establishes a system-memory dependency on preceding nodes.
652    ///
653    /// # Safety
654    /// The supplied barriers must order every conflicting access. Commands must
655    /// obey their kernel contracts and must not use this batch's storage elsewhere.
656    pub unsafe fn prepare_batch(&self, commands: &[(PreparedGpu, bool)]) -> Result<PreparedGpu> {
657        self.prepare_batch_inner(commands, None, 0)
658    }
659    /// Instrument every prepared command, including nested batches as one span.
660    /// Completion barriers serialize the sampled commands. This is diagnostic
661    /// execution; use ordinary batches for end-to-end performance decisions.
662    ///
663    /// # Safety
664    /// The same kernel and cross-queue contracts as `prepare_batch` apply.
665    pub unsafe fn prepare_profiled_batch(
666        &self,
667        commands: &[(PreparedGpu, bool)],
668        labels: &[String],
669    ) -> Result<ProfiledGpu> {
670        if commands.len() != labels.len() {
671            return Err(Error::Message("profile labels must match commands".into()));
672        }
673        let capture = profile::ProfileCapture::new(self, labels)?;
674        let commands = self.prepare_batch_inner(commands, Some(&capture), 0)?;
675        Ok(ProfiledGpu { commands, capture })
676    }
677    /// Prepare a graph as bounded submissions on this ordered queue. Build all
678    /// batches before launching any, retaining one shared timestamp capture.
679    /// Safety: the same kernel/access contracts as `prepare_batch` apply.
680    pub(crate) unsafe fn prepare_graph_batches(
681        &self,
682        commands: &[(PreparedGpu, bool)],
683        labels: Option<&[String]>,
684    ) -> Result<(Vec<PreparedGpu>, Option<profile::ProfileCapture>)> {
685        let capture = if let Some(labels) = labels {
686            if commands.len() != labels.len() || commands.is_empty() {
687                return Err(Error::Message(
688                    "profile labels must match nonempty commands".into(),
689                ));
690            }
691            Some(profile::ProfileCapture::new(self, labels)?)
692        } else {
693            None
694        };
695        let ranges = batch_ranges(
696            commands
697                .iter()
698                .map(|(command, dependency)| (command.dispatch_words().len(), *dependency)),
699            capture.is_some(),
700        )?;
701        let batches = ranges
702            .into_iter()
703            .map(|range| {
704                self.prepare_batch_inner(&commands[range.clone()], capture.as_ref(), range.start)
705            })
706            .collect::<Result<_>>()?;
707        Ok((batches, capture))
708    }
709    fn prepare_batch_inner(
710        &self,
711        commands: &[(PreparedGpu, bool)],
712        profile: Option<&profile::ProfileCapture>,
713        profile_start: usize,
714    ) -> Result<PreparedGpu> {
715        if self.0.info.command_type != AMDF_QUEUE_COMMAND_TYPE_GPU_PM4 {
716            return Err(Error::Unsupported(
717                "indirect compute batches require a PM4 queue".into(),
718            ));
719        }
720        if commands.is_empty() {
721            return Err(Error::Message("empty native command batch".into()));
722        }
723        let mut indirect = Vec::new();
724        let mut buffers = Vec::new();
725        let mut kernels = Vec::new();
726        let mut dependencies = Vec::new();
727        let timestamp_address = profile
728            .map(|p| p.buffer.device_address(self.device()))
729            .transpose()?;
730        if let Some(profile) = profile {
731            buffers.push(profile.buffer.clone());
732        }
733        for (index, (command, dependency)) in commands.iter().enumerate() {
734            if !Arc::ptr_eq(&command.queue.0, &self.0) {
735                return Err(Error::Message(
736                    "batch command belongs to another queue".into(),
737                ));
738            }
739            if *dependency {
740                barrier(&mut indirect);
741            }
742            if let (Some(profile), Some(address)) = (profile, timestamp_address) {
743                barrier(&mut indirect);
744                profile
745                    .api
746                    .emit(address + (profile_start + index) as u64 * 16, &mut indirect)?;
747            }
748            indirect.extend_from_slice(command.dispatch_words());
749            if let (Some(profile), Some(address)) = (profile, timestamp_address) {
750                barrier(&mut indirect);
751                profile.api.emit(
752                    address + (profile_start + index) as u64 * 16 + 8,
753                    &mut indirect,
754                )?;
755            }
756            buffers.extend(command.inner.work.buffers.iter().cloned());
757            kernels.extend(command.inner.work._kernels.iter().cloned());
758            dependencies.extend(command.inner.work.dependencies.iter().cloned());
759        }
760        pad(&mut indirect);
761        if indirect.len() >= INDIRECT_WORD_LIMIT {
762            return Err(Error::Unsupported(format!(
763                "native batch exceeds indirect command capacity: {} words for {} commands",
764                indirect.len(),
765                commands.len()
766            )));
767        }
768        let fabric = self.device().fabric();
769        let storage = fabric.allocate_access(
770            indirect.len() * 4,
771            std::slice::from_ref(self.device()),
772            AMDF_MEMORY_ACCESS_READ | AMDF_MEMORY_ACCESS_WRITE | AMDF_MEMORY_ACCESS_EXECUTE,
773            256,
774        )?;
775        // Native PM4 words are little endian on this supported x86_64 host.
776        storage.write(0, unsafe {
777            std::slice::from_raw_parts(indirect.as_ptr().cast(), indirect.len() * 4)
778        })?;
779        let ib_address = storage.device_address(self.device())?;
780        let storage_bytes = storage.len()
781            + 64
782            + profile.map_or(0, |capture| capture.buffer.len())
783            + commands
784                .iter()
785                .map(|(command, _)| command.storage_bytes())
786                .sum::<usize>();
787        buffers.push(storage);
788        buffers.sort_by_key(|buffer| Arc::as_ptr(&buffer.0) as usize);
789        buffers.dedup_by(|a, b| Arc::ptr_eq(&a.0, &b.0));
790        let fence = fabric.allocate_shared(64, std::slice::from_ref(self.device()))?;
791        let address = fence.device_address(self.device())?;
792        let mut words = Vec::new();
793        barrier(&mut words);
794        let dispatch_start = words.len();
795        words.extend([
796            header(0x3f, 4),
797            ib_address as u32,
798            (ib_address >> 32) as u32,
799            indirect.len() as u32 | (1 << 23),
800        ]);
801        let dispatch_end = words.len();
802        barrier(&mut words);
803        let fence_word = words.len() + 4;
804        words.extend([
805            header(0x37, 5),
806            (2 << 8) | (1 << 20),
807            address as u32,
808            (address >> 32) as u32,
809            1,
810        ]);
811        pad(&mut words);
812        let leases = Mutex::new(Vec::with_capacity(buffers.len()));
813        Ok(PreparedGpu {
814            prefix: Default::default(),
815            queue: self.clone(),
816            inner: Arc::new(PreparedInner {
817                storage_bytes,
818                work: Arc::new(Work {
819                    fence,
820                    _kernels: kernels,
821                    buffers,
822                    leases,
823                    retired: AtomicU32::new(0),
824                    active: AtomicU32::new(0),
825                    dependencies,
826                }),
827                words,
828                dispatch_range: dispatch_start..dispatch_end,
829                indirect_words: Some(indirect),
830                fence_word,
831                previous: Mutex::new(0),
832            }),
833        })
834    }
835    /// Prepare a device-side wait for an immutable completion point.
836    /// The source fence and all indirect resources remain owned through retirement.
837    pub fn prepare_wait(&self, completion: &Completion) -> Result<PreparedGpu> {
838        if self.device().id() != completion.queue.device().id() {
839            return Err(Error::Message("event belongs to another device".into()));
840        }
841        // The event orders the entire producer prefix, not just the buffers in
842        // its final command. Retire those leases when a consumer observes the
843        // wait, even if nobody polls the producer again. Snapshot work rather
844        // than retaining/locking its queue during retirement: queues may wait
845        // on their own earlier events, and cross-queue waits may form a chain.
846        let dependencies = {
847            let state = completion
848                .queue
849                .0
850                .state
851                .lock()
852                .map_err(|_| Error::DeviceLost("event producer poisoned".into()))?;
853            match state.pending.iter().position(|pending| {
854                Arc::ptr_eq(&pending.work, &completion.work) && pending.value == completion.value
855            }) {
856                Some(end) => state
857                    .pending
858                    .iter()
859                    .take(end + 1)
860                    .map(|pending| (pending.work.clone(), pending.value))
861                    .collect(),
862                None => vec![(completion.work.clone(), completion.value)],
863            }
864        };
865        if self.0.info.command_type == AMDF_QUEUE_COMMAND_TYPE_GPU_SDMA {
866            return self.prepare_sdma_wait(completion, dependencies);
867        }
868        let address = completion.work.fence.device_address(self.device())?;
869        let fence = self
870            .device()
871            .fabric()
872            .allocate_shared(64, std::slice::from_ref(self.device()))?;
873        let output = fence.device_address(self.device())?;
874        let mut words = Vec::with_capacity(40);
875        // WAIT_REG_MEM32, memory space, unsigned >= comparison, MEC polling.
876        words.extend([
877            header(0x3c, 7),
878            5 | (1 << 4),
879            address as u32,
880            (address >> 32) as u32,
881            completion.value,
882            u32::MAX,
883            4,
884        ]);
885        barrier(&mut words);
886        let dispatch_end = words.len();
887        let fence_word = words.len() + 4;
888        words.extend([
889            header(0x37, 5),
890            (2 << 8) | (1 << 20),
891            output as u32,
892            (output >> 32) as u32,
893            1,
894        ]);
895        pad(&mut words);
896        Ok(PreparedGpu {
897            prefix: Default::default(),
898            queue: self.clone(),
899            inner: Arc::new(PreparedInner {
900                storage_bytes: fence.len(),
901                work: Arc::new(Work {
902                    fence,
903                    _kernels: Vec::new(),
904                    buffers: Vec::new(),
905                    leases: Mutex::new(Vec::new()),
906                    retired: AtomicU32::new(0),
907                    active: AtomicU32::new(0),
908                    dependencies,
909                }),
910                words,
911                dispatch_range: 0..dispatch_end,
912                indirect_words: None,
913                fence_word,
914                previous: Mutex::new(0),
915            }),
916        })
917    }
918    /// Prepare and publish a single invocation.
919    ///
920    /// # Safety
921    /// The kernel and its arguments must obey the declared memory contract;
922    /// callers order conflicting accesses on other queues.
923    pub unsafe fn dispatch(
924        &self,
925        kernel: &Kernel,
926        grid: [u32; 3],
927        block: [u16; 3],
928        arguments: &[Argument<'_>],
929    ) -> Result<Completion> {
930        unsafe { self.prepare(kernel, grid, block, arguments)?.dispatch() }
931    }
932}
933
934/// Immutable bound GPU commands with reusable argument, scratch and fence storage.
935/// Replays remain ordered on the originating queue, including scratch accesses.
936#[derive(Clone)]
937pub struct PreparedGpu {
938    // SDMA streams must fit the ring, including wrap padding. The final part
939    // supplies the public completion; preceding parts retire on the same queue.
940    prefix: Option<Arc<[PreparedGpu]>>,
941    queue: Queue,
942    inner: Arc<PreparedInner>,
943}
944struct PreparedInner {
945    storage_bytes: usize,
946    work: Arc<Work>,
947    words: Vec<u32>,
948    dispatch_range: std::ops::Range<usize>,
949    // Flatten nested batches when composing graphs; PM4 IB nesting is bounded.
950    indirect_words: Option<Vec<u32>>,
951    fence_word: usize,
952    previous: Mutex<u32>,
953}
954impl PreparedGpu {
955    fn dispatch_words(&self) -> &[u32] {
956        self.inner
957            .indirect_words
958            .as_deref()
959            .unwrap_or_else(|| &self.inner.words[self.inner.dispatch_range.clone()])
960    }
961
962    fn reserve_fence(&mut self, reservation: Option<Arc<crate::residency::MemoryReservation>>) {
963        let inner = Arc::get_mut(&mut self.inner).expect("new prepared transfer");
964        let work = Arc::get_mut(&mut inner.work).expect("new transfer work");
965        Arc::get_mut(&mut work.fence.0)
966            .expect("new transfer fence")
967            .reservation = reservation;
968    }
969
970    /// Keep operands unavailable to host mappings between SDMA publications.
971    pub(crate) fn retain_parts(&self) -> Result<impl Drop> {
972        if self.prefix.is_none() {
973            return Ok(Vec::new());
974        }
975        self.inner
976            .work
977            .buffers
978            .iter()
979            .map(Buffer::retain_use)
980            .collect::<Result<Vec<_>>>()
981    }
982
983    #[cfg(test)]
984    pub(crate) fn exhaust_final_timeline(&self) {
985        *self.inner.previous.lock().unwrap() = u32::MAX;
986    }
987
988    /// Private argument, scratch and fence bytes, excluding operands, code and
989    /// provider allocation rounding. Used to bound retained command caches.
990    pub(crate) fn storage_bytes(&self) -> usize {
991        self.inner.storage_bytes
992            + self.prefix.as_ref().map_or(0, |parts| {
993                parts.iter().map(Self::storage_bytes).sum::<usize>()
994            })
995    }
996
997    pub(crate) fn parts(&self) -> impl Iterator<Item = &Self> {
998        self.prefix
999            .iter()
1000            .flat_map(|parts| parts.iter())
1001            .chain(std::iter::once(self))
1002    }
1003
1004    /// Submit one physical part; callers retain the logical transfer's guard
1005    /// and record each accepted completion before advancing to its next part.
1006    pub(crate) unsafe fn dispatch_part_wait(&self) -> Result<Completion> {
1007        unsafe { self.dispatch_one(true) }
1008    }
1009
1010    /// Submit previously prepared commands without creating native resources.
1011    /// Large SDMA transfers may wait for ring capacity between publications.
1012    /// A failure can leave an accepted prefix modified; it is drained before
1013    /// returning unless native completion itself fails.
1014    ///
1015    /// # Safety
1016    /// Callers order conflicting accesses on other queues and obey the native
1017    /// executable's binding contracts.
1018    pub unsafe fn dispatch(&self) -> Result<Completion> {
1019        if self.prefix.is_some() {
1020            unsafe { self.dispatch_wait() }
1021        } else {
1022            unsafe { self.dispatch_one(false) }
1023        }
1024    }
1025
1026    /// Wait for ring capacity while publishing all parts. Accepted work is
1027    /// drained if a later part fails; uncertain completion retains native owners.
1028    pub(crate) unsafe fn dispatch_wait(&self) -> Result<Completion> {
1029        let _uses = self.retain_parts()?;
1030        let mut last: Option<Completion> = None;
1031        for part in self.parts() {
1032            match unsafe { part.dispatch_one(true) } {
1033                Ok(done) => last = Some(done),
1034                Err(error) => {
1035                    if let Some(done) = last {
1036                        done.wait()?;
1037                    }
1038                    return Err(error);
1039                }
1040            }
1041        }
1042        Ok(last.expect("prepared command has a final part"))
1043    }
1044
1045    unsafe fn dispatch_one(&self, wait: bool) -> Result<Completion> {
1046        let mut previous = self
1047            .inner
1048            .previous
1049            .lock()
1050            .map_err(|_| Error::DeviceLost("prepared command poisoned".into()))?;
1051        let value = previous
1052            .checked_add(1)
1053            .ok_or_else(|| Error::DeviceLost("prepared completion timeline exhausted".into()))?;
1054        let words = &self.inner.words;
1055        let capacity = self.queue.0.info.ring_byte_length / 4;
1056        let sdma = self.queue.0.info.command_type == AMDF_QUEUE_COMMAND_TYPE_GPU_SDMA;
1057        let index_scale = if sdma { 4 } else { 1 };
1058        let (mut state, cursor, wrap, required) = loop {
1059            let mut state = self
1060                .queue
1061                .0
1062                .state
1063                .lock()
1064                .map_err(|_| Error::DeviceLost("queue producer poisoned".into()))?;
1065            // Reap completed submissions even if their public completion was dropped.
1066            while let Some(work) = state.pending.front() {
1067                if !self.queue.0.poll(&work.work, work.value)? {
1068                    break;
1069                }
1070                state.pending.pop_front();
1071            }
1072            let cursor = state.write & (capacity - 1);
1073            let wrap = if words.len() as u64 > capacity - cursor {
1074                capacity - cursor
1075            } else {
1076                0
1077            };
1078            let required = wrap + words.len() as u64;
1079            let read = unsafe {
1080                (&*(self.queue.0.info.read_index_address as *const AtomicU64))
1081                    .load(Ordering::Acquire)
1082            } / index_scale;
1083            let outstanding = state.write.wrapping_sub(read) & (capacity - 1);
1084            let full = if state.pending.len() >= 4096 {
1085                Some("native queue submission capacity")
1086            } else if required >= capacity - outstanding {
1087                Some("native command ring is full")
1088            } else {
1089                None
1090            };
1091            let Some(reason) = full else {
1092                break (state, cursor, wrap, required);
1093            };
1094            if !wait {
1095                return Err(Error::Busy(reason.into()));
1096            }
1097            let oldest = state.pending.front().map(|pending| Completion {
1098                queue: self.queue.clone(),
1099                work: pending.work.clone(),
1100                value: pending.value,
1101            });
1102            drop(state);
1103            wait_for_capacity(oldest)?;
1104        };
1105        let published = state
1106            .write
1107            .checked_add(required)
1108            .ok_or_else(|| Error::DeviceLost("native queue index exhausted".into()))?;
1109        let native_published = published
1110            .checked_mul(index_scale)
1111            .ok_or_else(|| Error::DeviceLost("native queue byte index exhausted".into()))?;
1112        let work = self.inner.work.clone();
1113        let mut leases = work
1114            .leases
1115            .lock()
1116            .map_err(|_| Error::DeviceLost("submission ownership poisoned".into()))?;
1117        if leases.is_empty() {
1118            for buffer in &work.buffers {
1119                match buffer.retain_use() {
1120                    Ok(lease) => leases.push(lease),
1121                    Err(error) => {
1122                        leases.clear();
1123                        return Err(error);
1124                    }
1125                }
1126            }
1127        }
1128        work.active.store(value, Ordering::Release);
1129        // Fixed capacity and reusable ownership storage are allocated at prepare.
1130        state.pending.push_back(Pending {
1131            work: work.clone(),
1132            value,
1133        });
1134        drop(leases);
1135        unsafe {
1136            let ring = self.queue.0.info.ring_address as *mut u32;
1137            if wrap != 0 {
1138                ptr::write_bytes(ring.add(cursor as usize), 0, wrap as usize);
1139                if !sdma {
1140                    ptr::write(ring.add(cursor as usize), header(0x10, wrap as usize));
1141                }
1142            }
1143            let cursor = if wrap == 0 { cursor as usize } else { 0 };
1144            ptr::copy_nonoverlapping(words.as_ptr(), ring.add(cursor), words.len());
1145            ptr::write(ring.add(cursor + self.inner.fence_word), value);
1146            (&*(self.queue.0.info.write_index_address as *const AtomicU64))
1147                .store(native_published, Ordering::Release);
1148            std::sync::atomic::fence(Ordering::SeqCst);
1149            ptr::write_volatile(
1150                self.queue.0.info.doorbell_address as *mut u64,
1151                native_published,
1152            );
1153        }
1154        state.write = published;
1155        *previous = value;
1156        Ok(Completion {
1157            queue: self.queue.clone(),
1158            work,
1159            value,
1160        })
1161    }
1162}
1163
1164fn wait_for_capacity(oldest: Option<Completion>) -> Result<()> {
1165    // A stale native read index or an invalid command must not cause an
1166    // unbounded spin when there is no submitted work whose fence we can await.
1167    oldest
1168        .ok_or_else(|| Error::Busy("native command ring is full without pending work".into()))?
1169        .wait()
1170}
1171
1172impl Queue {
1173    /// Exact native family used by this queue and its visibility recipes.
1174    pub fn family_ordinal(&self) -> u32 {
1175        self.0.family
1176    }
1177    /// Native command representation.
1178    pub fn command(&self) -> QueueCommand {
1179        if self.0.info.command_type == AMDF_QUEUE_COMMAND_TYPE_GPU_SDMA {
1180            QueueCommand::Sdma
1181        } else {
1182            QueueCommand::Pm4
1183        }
1184    }
1185    /// Device whose address domain this queue executes in.
1186    pub fn device(&self) -> &Device {
1187        &self.0.device
1188    }
1189    fn transfer_kernels(&self) -> Result<[Kernel; 4]> {
1190        let mut kernels = self
1191            .0
1192            .transfers
1193            .lock()
1194            .map_err(|_| Error::DeviceLost("transfer compiler poisoned".into()))?;
1195        if kernels.is_none() {
1196            let compiler = crate::loom::Compiler::for_target(None, self.device().target())?;
1197            let source =
1198                crate::loom::CxxSource::new("hrx-transfers.cpp", include_str!("transfers.cpp"));
1199            let module = compiler.import_cxx(source)?;
1200            // Compile an upper bound for blockIdx; these kernels never read
1201            // gridDim and guard every byte with the runtime length. Dispatching
1202            // the checked prefix below therefore preserves their memory contract.
1203            let request = |symbol: &str| {
1204                crate::loom::Specialization::new(symbol)
1205                    .with_config(format!("{symbol}.workgroup_count.x"), "16777216")
1206                    .with_config(format!("{symbol}.workgroup_count.y"), "1")
1207                    .with_config(format!("{symbol}.workgroup_count.z"), "1")
1208            };
1209            let mut entries = Vec::new();
1210            for symbol in ["hrx_copy", "hrx_fill", "hrx_copy_words", "hrx_fill_words"] {
1211                let artifact = module.compile(&request(symbol))?;
1212                // Crate-owned bounded kernels; prepared calls validate ranges and alignment.
1213                entries.push(unsafe { self.device().load(&artifact) }?);
1214            }
1215            *kernels = Some(entries.try_into().unwrap_or_else(|_| unreachable!()));
1216        }
1217        Ok(kernels.as_ref().unwrap().clone())
1218    }
1219    /// Prepare a copy of any nonempty, in-bounds, non-overlapping byte range.
1220    /// Large transfers are split to fit the selected engine. The returned
1221    /// completion covers every part; operands remain retained until retirement.
1222    pub fn prepare_copy(
1223        &self,
1224        destination: &Buffer,
1225        destination_offset: usize,
1226        source: &Buffer,
1227        source_offset: usize,
1228        length: usize,
1229    ) -> Result<PreparedGpu> {
1230        transfer_range(destination, destination_offset, length)?;
1231        transfer_range(source, source_offset, length)?;
1232        let dst = destination
1233            .device_address(self.device())?
1234            .checked_add(destination_offset as u64)
1235            .ok_or_else(|| Error::Message("copy address overflow".into()))?;
1236        let src = source
1237            .device_address(self.device())?
1238            .checked_add(source_offset as u64)
1239            .ok_or_else(|| Error::Message("copy address overflow".into()))?;
1240        if dst < src.saturating_add(length as u64) && src < dst.saturating_add(length as u64) {
1241            return Err(Error::Message("native copy ranges overlap".into()));
1242        }
1243        self.prepare_transfer(length, |offset, bytes| {
1244            self.prepare_copy_part(
1245                destination,
1246                destination_offset + offset,
1247                source,
1248                source_offset + offset,
1249                bytes,
1250            )
1251        })
1252    }
1253
1254    fn prepare_transfer(
1255        &self,
1256        length: usize,
1257        mut prepare: impl FnMut(usize, usize) -> Result<PreparedGpu>,
1258    ) -> Result<PreparedGpu> {
1259        let sdma = self.0.info.command_type == AMDF_QUEUE_COMMAND_TYPE_GPU_SDMA;
1260        let limit = if sdma {
1261            sdma::transfer_limit(self.0.info.ring_byte_length as usize)?
1262        } else {
1263            // Preserve alignment at chunk boundaries and the transfer kernels'
1264            // bounded 32-bit dispatch extent after rounding to 256 work items.
1265            (u32::MAX as usize) & !255
1266        };
1267        if length <= limit {
1268            return prepare(0, length);
1269        }
1270        let mut parts = transfer_chunks(length, limit)
1271            .map(|(offset, bytes)| prepare(offset, bytes))
1272            .collect::<Result<Vec<_>>>()?;
1273        let mut last = parts.pop().expect("validated nonempty transfer");
1274        if sdma {
1275            last.prefix = Some(parts.into());
1276            Ok(last)
1277        } else {
1278            parts.push(last);
1279            // PM4 indirect batches fit the ring regardless of payload size.
1280            // Each part addresses disjoint bytes within the validated span.
1281            let commands: Vec<_> = parts.into_iter().map(|part| (part, false)).collect();
1282            unsafe { self.prepare_batch(&commands) }
1283        }
1284    }
1285
1286    /// Prepare a byte copy between non-overlapping logical ranges.
1287    fn prepare_copy_part(
1288        &self,
1289        destination: &Buffer,
1290        destination_offset: usize,
1291        source: &Buffer,
1292        source_offset: usize,
1293        length: usize,
1294    ) -> Result<PreparedGpu> {
1295        transfer_range(destination, destination_offset, length)?;
1296        transfer_range(source, source_offset, length)?;
1297        if Arc::ptr_eq(&destination.0, &source.0)
1298            && destination_offset < source_offset + length
1299            && source_offset < destination_offset + length
1300        {
1301            return Err(Error::Message("native copy ranges overlap".into()));
1302        }
1303        if self.0.info.command_type == AMDF_QUEUE_COMMAND_TYPE_GPU_SDMA {
1304            return self.prepare_sdma_copy(
1305                destination,
1306                destination_offset,
1307                source,
1308                source_offset,
1309                length,
1310            );
1311        }
1312        let kernels = self.transfer_kernels()?;
1313        let wide = (destination_offset | source_offset | length) & 7 == 0;
1314        let copy = &kernels[if wide { 2 } else { 0 }];
1315        let elements = if wide { length / 8 } else { length };
1316        let count = (elements as u64).to_le_bytes();
1317        unsafe {
1318            self.prepare(
1319                copy,
1320                [elements.div_ceil(256) as u32, 1, 1],
1321                [256, 1, 1],
1322                &[
1323                    Argument::Buffer(destination, destination_offset),
1324                    Argument::Buffer(source, source_offset),
1325                    Argument::Value(&count),
1326                ],
1327            )
1328        }
1329    }
1330    pub(crate) fn prepare_copy_reserved(
1331        &self,
1332        destination: &Buffer,
1333        destination_offset: usize,
1334        source: &Buffer,
1335        source_offset: usize,
1336        length: usize,
1337        budget: Option<&crate::residency::MemoryBudget>,
1338    ) -> Result<PreparedGpu> {
1339        // Reserve before allocating native fences. One charge is shared by all
1340        // parts so it survives dropped wrappers and any accepted prefix.
1341        let count = if self.0.info.command_type == AMDF_QUEUE_COMMAND_TYPE_GPU_SDMA {
1342            length.div_ceil(sdma::transfer_limit(self.0.info.ring_byte_length as usize)?)
1343        } else {
1344            1
1345        };
1346        let bytes = count
1347            .checked_mul(64)
1348            .ok_or_else(|| Error::Message("transfer fence size overflow".into()))?;
1349        let reservation = budget
1350            .map(|budget| budget.reserve(bytes).map(Arc::new))
1351            .transpose()?;
1352        let mut command = self.prepare_copy(
1353            destination,
1354            destination_offset,
1355            source,
1356            source_offset,
1357            length,
1358        )?;
1359        if let Some(prefix) = &mut command.prefix {
1360            for part in Arc::get_mut(prefix).expect("new transfer prefix") {
1361                part.reserve_fence(reservation.clone());
1362            }
1363        }
1364        command.reserve_fence(reservation);
1365        Ok(command)
1366    }
1367    /// Prepare a byte fill, splitting large ranges for the selected engine.
1368    pub fn prepare_fill(
1369        &self,
1370        destination: &Buffer,
1371        offset: usize,
1372        length: usize,
1373        value: u8,
1374    ) -> Result<PreparedGpu> {
1375        transfer_range(destination, offset, length)?;
1376        self.prepare_transfer(length, |first, bytes| {
1377            self.prepare_fill_part(destination, offset + first, bytes, value)
1378        })
1379    }
1380
1381    /// Prepare a repeated byte fill over one logical range.
1382    fn prepare_fill_part(
1383        &self,
1384        destination: &Buffer,
1385        offset: usize,
1386        length: usize,
1387        value: u8,
1388    ) -> Result<PreparedGpu> {
1389        transfer_range(destination, offset, length)?;
1390        if self.0.info.command_type == AMDF_QUEUE_COMMAND_TYPE_GPU_SDMA {
1391            return self.prepare_sdma_fill(destination, offset, length, value);
1392        }
1393        let kernels = self.transfer_kernels()?;
1394        let wide = (offset | length) & 7 == 0;
1395        let fill = &kernels[if wide { 3 } else { 1 }];
1396        let elements = if wide { length / 8 } else { length };
1397        let count = (elements as u64).to_le_bytes();
1398        let value = u32::from(value).to_le_bytes();
1399        unsafe {
1400            self.prepare(
1401                fill,
1402                [elements.div_ceil(256) as u32, 1, 1],
1403                [256, 1, 1],
1404                &[
1405                    Argument::Buffer(destination, offset),
1406                    Argument::Value(&count),
1407                    Argument::Value(&value),
1408                ],
1409            )
1410        }
1411    }
1412}
1413fn transfer_chunks(length: usize, limit: usize) -> impl Iterator<Item = (usize, usize)> {
1414    (0..length)
1415        .step_by(limit)
1416        .map(move |offset| (offset, (length - offset).min(limit)))
1417}
1418
1419fn transfer_range(buffer: &Buffer, offset: usize, length: usize) -> Result<()> {
1420    if length == 0
1421        || offset
1422            .checked_add(length)
1423            .is_none_or(|end| end > buffer.len())
1424    {
1425        return Err(Error::Message(
1426            "native transfer range must be nonempty and in bounds".into(),
1427        ));
1428    }
1429    Ok(())
1430}
1431
1432#[cfg(test)]
1433mod event_tests {
1434    use super::*;
1435
1436    #[test]
1437    fn large_transfer_chunks_cover_boundaries_without_wrapping() {
1438        let limit = (u32::MAX as usize) & !255;
1439        for length in [
1440            1,
1441            limit - 1,
1442            limit,
1443            limit + 1,
1444            1usize << 32,
1445            (1usize << 33) + 19,
1446        ] {
1447            let mut end = 0;
1448            for (offset, bytes) in transfer_chunks(length, limit) {
1449                assert_eq!(offset, end);
1450                assert_eq!(offset % 8, 0);
1451                assert!((1..=limit).contains(&bytes));
1452                assert!(bytes.div_ceil(256) <= u32::MAX as usize / 256);
1453                end += bytes;
1454            }
1455            assert_eq!(end, length);
1456        }
1457    }
1458
1459    #[test]
1460    #[ignore = "requires gfx1151 and native SDMA"]
1461    fn split_transfer_failure_drains_prefix_and_retains_fence_budget() -> Result<()> {
1462        let gpu = Device::open(Engine::Gpu, 0)?;
1463        let queue = gpu.sdma_queue()?;
1464        let limit = sdma::transfer_limit(queue.0.info.ring_byte_length as usize)?;
1465        let bytes = 2 * limit + 17;
1466        let source = gpu
1467            .fabric()
1468            .allocate_shared(bytes, std::slice::from_ref(&gpu))?;
1469        let target = gpu
1470            .fabric()
1471            .allocate_shared(bytes, std::slice::from_ref(&gpu))?;
1472        // Probe both completed chunks and the unsubmitted tail.
1473        for offset in [0, limit, 2 * limit] {
1474            source.write(offset, &[0x51])?;
1475        }
1476        target.write(2 * limit, &[0])?;
1477        let manager = crate::residency::ResidencyManager::new(4096)?;
1478        let command =
1479            queue.prepare_copy_reserved(&target, 0, &source, 0, bytes, Some(&manager.budget()))?;
1480        assert_eq!(command.parts().count(), 3);
1481        assert_eq!(manager.statistics().reserved_bytes, 3 * 64);
1482        command.exhaust_final_timeline();
1483        assert!(matches!(
1484            unsafe { command.dispatch() },
1485            Err(Error::DeviceLost(_))
1486        ));
1487        for offset in [0, limit] {
1488            let mut actual = [0];
1489            target.read(offset, &mut actual)?;
1490            assert_eq!(actual, [0x51]);
1491        }
1492        let mut tail = [0];
1493        target.read(2 * limit, &mut tail)?;
1494        assert_eq!(tail, [0]);
1495        for part in command.prefix.as_ref().unwrap().iter() {
1496            assert_eq!(part.inner.work.retired.load(Ordering::Acquire), 1);
1497        }
1498        drop(command);
1499        assert_eq!(manager.statistics().reserved_bytes, 0);
1500        Ok(())
1501    }
1502
1503    #[test]
1504    fn a_full_ring_without_pending_work_returns_busy() {
1505        assert!(matches!(wait_for_capacity(None), Err(Error::Busy(_))));
1506    }
1507
1508    #[test]
1509    fn stream_queue_configuration_is_positive_and_frozen_after_first_use() -> Result<()> {
1510        let mut pool = StreamQueues::default();
1511        assert!(pool.set_limit(0).is_err());
1512        pool.set_limit(2)?;
1513        assert_eq!(pool.limit, 2);
1514        assert!(
1515            pool.slots.is_empty(),
1516            "configuration must not allocate queues"
1517        );
1518        // Even an expired slot records that streams have used this device.
1519        pool.slots.push(Weak::new());
1520        pool.set_limit(2)?;
1521        assert!(matches!(pool.set_limit(8), Err(Error::Busy(_))));
1522        assert_eq!(pool.limit, 2);
1523        Ok(())
1524    }
1525
1526    #[test]
1527    #[ignore = "requires gfx1151 and provisioned native libraries"]
1528    fn stream_queue_pool_is_bounded_shared_and_weak() -> Result<()> {
1529        let device = Device::open(Engine::Gpu, 0)?;
1530        device.set_stream_queue_count(2)?;
1531        let queues = std::thread::scope(|scope| {
1532            let workers: Vec<_> = (0..8)
1533                .map(|_| {
1534                    scope.spawn(|| {
1535                        let reopened = Device::open(Engine::Gpu, 0)?;
1536                        (0..16)
1537                            .map(|_| reopened.stream_queue())
1538                            .collect::<Result<Vec<_>>>()
1539                    })
1540                })
1541                .collect();
1542            workers
1543                .into_iter()
1544                .map(|worker| worker.join().unwrap())
1545                .collect::<Result<Vec<_>>>()
1546        })?;
1547        let identities: std::collections::HashSet<_> = queues
1548            .iter()
1549            .flatten()
1550            .map(|queue| Arc::as_ptr(&queue.0))
1551            .collect();
1552        assert_eq!(identities.len(), 2);
1553        let weak: Vec<_> = queues
1554            .iter()
1555            .flatten()
1556            .map(|queue| Arc::downgrade(&queue.0))
1557            .collect();
1558        drop(queues);
1559        assert!(weak.iter().all(|queue| queue.upgrade().is_none()));
1560        let replacement = device.stream_queue()?;
1561        assert_eq!(replacement.device().id(), device.id());
1562        Ok(())
1563    }
1564
1565    #[test]
1566    #[ignore = "requires gfx1151 and provisioned native libraries"]
1567    fn shared_queue_submission_waits_for_other_producers() -> Result<()> {
1568        let device = Device::open(Engine::Gpu, 0)?;
1569        let fabric = device.fabric();
1570        let queue = device.stream_queue()?;
1571        let gate = Completion {
1572            queue: queue.clone(),
1573            work: Arc::new(Work {
1574                fence: fabric.allocate_shared(64, std::slice::from_ref(&device))?,
1575                _kernels: Vec::new(),
1576                buffers: Vec::new(),
1577                leases: Mutex::new(Vec::new()),
1578                retired: AtomicU32::new(0),
1579                active: AtomicU32::new(1),
1580                dependencies: Vec::new(),
1581            }),
1582            value: 1,
1583        };
1584        let buffer = fabric.allocate(64, std::slice::from_ref(&device))?;
1585        let fill = queue.prepare_fill(&buffer, 0, 64, 19)?;
1586        let final_fill = queue.prepare_fill(&buffer, 0, 64, 42)?;
1587        unsafe { queue.prepare_wait(&gate)?.dispatch()? };
1588        let mut submitted = 0;
1589        loop {
1590            match unsafe { fill.dispatch() } {
1591                Ok(_) => submitted += 1,
1592                Err(Error::Busy(_)) => break,
1593                Err(error) => {
1594                    gate.work.fence.write(0, &1u32.to_le_bytes())?;
1595                    return Err(error);
1596                }
1597            }
1598        }
1599        let (sent, received) = std::sync::mpsc::channel();
1600        let worker = std::thread::spawn(move || {
1601            sent.send(unsafe { final_fill.dispatch_wait() }).unwrap();
1602        });
1603        let blocked = received.recv_timeout(Duration::from_millis(50));
1604        gate.work.fence.write(0, &1u32.to_le_bytes())?;
1605        worker.join().unwrap();
1606        assert!(submitted > 0);
1607        assert!(matches!(
1608            blocked,
1609            Err(std::sync::mpsc::RecvTimeoutError::Timeout)
1610        ));
1611        let done = received.recv().unwrap()?;
1612        assert!(done.wait_timeout(Duration::from_secs(5))?);
1613        let mut actual = [0; 64];
1614        buffer.read(0, &mut actual)?;
1615        assert_eq!(actual, [42; 64]);
1616        Ok(())
1617    }
1618
1619    #[test]
1620    fn graph_batch_ranges_reserve_barriers_markers_and_padding() -> Result<()> {
1621        assert!(batch_ranges([], false)?.is_empty());
1622        assert_eq!(batch_ranges([(100, false); 3], false)?, vec![0..3]);
1623        let near_limit = INDIRECT_WORD_LIMIT - 20;
1624        assert_eq!(
1625            batch_ranges([(near_limit, true), (1, false)], false)?,
1626            vec![0..1, 1..2]
1627        );
1628        assert!(batch_ranges([(INDIRECT_WORD_LIMIT - 9, false)], false).is_err());
1629        assert!(batch_ranges([(usize::MAX, true)], true).is_err());
1630        assert_eq!(
1631            batch_ranges([(INDIRECT_WORD_LIMIT / 2 - 10, false); 2], false)?,
1632            vec![0..2]
1633        );
1634        assert_eq!(
1635            batch_ranges([(INDIRECT_WORD_LIMIT / 2 - 10, false); 2], true)?,
1636            vec![0..1, 1..2]
1637        );
1638        Ok(())
1639    }
1640
1641    #[test]
1642    #[ignore = "requires gfx1151 and provisioned native libraries"]
1643    fn observing_an_event_retires_every_earlier_submission() -> Result<()> {
1644        let device = Device::open(Engine::Gpu, 0)?;
1645        let fabric = device.fabric();
1646        let producer = device.queue()?;
1647        let consumer = device.queue()?;
1648        // A host-controlled fence keeps the whole producer prefix outstanding
1649        // while the consumer snapshots its event. No timing race or slow kernel
1650        // is needed to keep the earlier buffer's lease alive.
1651        let gate = Completion {
1652            queue: device.queue()?,
1653            work: Arc::new(Work {
1654                fence: fabric.allocate_shared(64, std::slice::from_ref(&device))?,
1655                _kernels: Vec::new(),
1656                buffers: Vec::new(),
1657                leases: Mutex::new(Vec::new()),
1658                retired: AtomicU32::new(0),
1659                active: AtomicU32::new(1),
1660                dependencies: Vec::new(),
1661            }),
1662            value: 1,
1663        };
1664        let first = fabric.allocate(64, std::slice::from_ref(&device))?;
1665        let last = fabric.allocate(64, std::slice::from_ref(&device))?;
1666        unsafe { producer.prepare_wait(&gate)?.dispatch()? };
1667        unsafe {
1668            producer
1669                .prepare_fill(&first, 0, first.len(), 37)?
1670                .dispatch()?
1671        };
1672        let event = unsafe {
1673            producer
1674                .prepare_fill(&last, 0, last.len(), 91)?
1675                .dispatch()?
1676        };
1677        let observed = unsafe { consumer.prepare_wait(&event)?.dispatch()? };
1678        assert!(matches!(first.read(0, &mut [0; 64]), Err(Error::Busy(_))));
1679        gate.work.fence.write(0, &1u32.to_le_bytes())?;
1680        if !observed.wait_timeout(Duration::from_secs(5))? {
1681            std::mem::forget(observed);
1682            return Err(Error::DeviceLost("event prefix did not retire".into()));
1683        }
1684        // No producer wait or poll: consumer completion must retire all resources
1685        // ordered by the event, including a buffer absent from its last command.
1686        let mut bytes = [0; 64];
1687        first.read(0, &mut bytes)?;
1688        assert_eq!(bytes, [37; 64]);
1689        last.read(0, &mut bytes)?;
1690        assert_eq!(bytes, [91; 64]);
1691        Ok(())
1692    }
1693}