Skip to main content

hrx/fabric/
aql.rs

1//! Owned AQL v1 dispatch using libamdf's native packet and signal contracts.
2use super::*;
3use std::{
4    collections::VecDeque,
5    sync::{
6        Mutex,
7        atomic::{AtomicI64, AtomicU32, AtomicU64, Ordering},
8    },
9    time::{Duration, Instant},
10};
11
12/// A serialized AQL producer with fixed scratch capacity.
13#[derive(Clone)]
14pub struct AqlQueue(Arc<QueueState>);
15struct QueueState {
16    device: Device,
17    raw: *mut amdf_user_queue_t,
18    mapping: *mut amdf_user_queue_mapping_t,
19    info: amdf_user_queue_mapping_info_t,
20    scratch: Option<Buffer>,
21    maximum_private_bytes: u32,
22    budget: Option<crate::residency::MemoryBudget>,
23    producer: Mutex<Producer>,
24}
25struct Producer {
26    write: u64,
27    pending: VecDeque<(Arc<Work>, u64)>,
28}
29// One host publisher owns the ring under producer. Mapping and scratch remain
30// alive through queue destruction; completion observers only read owned signals.
31unsafe impl Send for QueueState {}
32unsafe impl Sync for QueueState {}
33struct Work {
34    signal: Buffer,
35    buffers: Vec<Buffer>,
36    _kernel: Kernel,
37    leases: Mutex<Vec<memory::DeviceUse>>,
38    active: AtomicU64,
39    retired: AtomicU64,
40}
41impl Work {
42    fn refresh(&self, point: u64) -> Result<bool> {
43        if self.retired.load(Ordering::Acquire) >= point {
44            return Ok(true);
45        }
46        let value = unsafe {
47            (&*self.signal.host_pointer().add(8).cast::<AtomicI64>()).load(Ordering::Acquire)
48        };
49        let completed = i64::MAX
50            .checked_sub(value)
51            .filter(|v| *v >= 0)
52            .ok_or_else(|| Error::DeviceLost("invalid AQL completion signal".into()))?
53            as u64;
54        if completed < point {
55            return Ok(false);
56        }
57        let mut leases = self
58            .leases
59            .lock()
60            .map_err(|_| Error::DeviceLost("AQL leases poisoned".into()))?;
61        if completed >= self.active.load(Ordering::Acquire) {
62            leases.clear();
63        }
64        self.retired.fetch_max(completed, Ordering::Release);
65        Ok(true)
66    }
67}
68impl Drop for QueueState {
69    fn drop(&mut self) {
70        let pending = std::mem::take(
71            &mut self
72                .producer
73                .get_mut()
74                .unwrap_or_else(|e| e.into_inner())
75                .pending,
76        );
77        let deadline = Instant::now() + Duration::from_secs(10);
78        for (work, point) in &pending {
79            loop {
80                match work.refresh(*point) {
81                    Ok(true) => break,
82                    Ok(false) if Instant::now() < deadline => std::thread::yield_now(),
83                    _ => {
84                        std::mem::forget(pending);
85                        std::mem::forget(self.device.clone());
86                        std::mem::forget(self.scratch.take());
87                        return;
88                    }
89                }
90            }
91        }
92        let api = self.device.0.endpoint.0.instance.api.core;
93        unsafe {
94            if (!self.mapping.is_null()
95                && api.user_queue_mapping_destroy.unwrap()(self.mapping) != 0)
96                || api.user_queue_destroy.unwrap()(self.raw) != 0
97            {
98                std::mem::forget(self.device.clone());
99                std::mem::forget(self.scratch.take());
100            }
101        }
102    }
103}
104/// Immutable AQL packets and bindings for repeated ordered dispatch.
105#[derive(Clone)]
106pub struct PreparedAql {
107    queue: AqlQueue,
108    work: Arc<Work>,
109    packets: Vec<[u32; 16]>,
110    next: Arc<Mutex<u64>>,
111    storage_bytes: usize,
112}
113/// A native AQL execution completion retaining code, arguments and payloads.
114#[derive(Clone)]
115pub struct AqlCompletion {
116    queue: AqlQueue,
117    work: Arc<Work>,
118    point: u64,
119}
120impl AqlCompletion {
121    /// Cached retirement only; no native query, lock or progress work.
122    pub fn is_complete(&self) -> bool {
123        self.work.retired.load(Ordering::Acquire) >= self.point
124    }
125    /// Observe native execution completion and release completed leases.
126    pub fn refresh(&self) -> Result<bool> {
127        if !self.work.refresh(self.point)? {
128            let api = self.queue.0.device.0.endpoint.0.instance.api.core;
129            let mut info = amdf_user_queue_status_t {
130                type_: AMDF_STRUCTURE_TYPE_USER_QUEUE_STATUS,
131                structure_size: size_of::<amdf_user_queue_status_t>() as u32,
132                ..Default::default()
133            };
134            check("AQL queue status", unsafe {
135                entry!(api, user_queue_query_status)(self.queue.0.raw, &mut info)
136            })?;
137            if info.state != AMDF_QUEUE_STATE_ACTIVE {
138                return Err(Error::DeviceLost(format!(
139                    "AQL queue status {}",
140                    info.terminal_status
141                )));
142            }
143            return Ok(false);
144        }
145        let mut producer = self
146            .queue
147            .0
148            .producer
149            .lock()
150            .map_err(|_| Error::DeviceLost("AQL producer poisoned".into()))?;
151        while let Some((work, point)) = producer.pending.front() {
152            if !work.refresh(*point)? {
153                break;
154            }
155            producer.pending.pop_front();
156        }
157        Ok(true)
158    }
159    /// Wait under a host deadline. Timeout does not cancel or release work.
160    pub fn wait_timeout(&self, timeout: Duration) -> Result<bool> {
161        let start = Instant::now();
162        loop {
163            if self.refresh()? {
164                return Ok(true);
165            }
166            if start.elapsed() >= timeout {
167                return Ok(false);
168            }
169            std::thread::yield_now();
170        }
171    }
172    /// Wait for execution completion, preserving resources on failure.
173    pub fn wait(&self) -> Result<()> {
174        while !self.refresh()? {
175            std::thread::yield_now();
176        }
177        Ok(())
178    }
179}
180impl Device {
181    /// Create an AQL queue with fixed per-workitem scratch capacity.
182    /// Zero admits kernels with no private segment. All physical scratch slots
183    /// are allocated before returning; dispatch never grows the pool.
184    pub fn aql_queue(&self, maximum_private_bytes: u32) -> Result<AqlQueue> {
185        self.aql_queue_budgeted(maximum_private_bytes, None)
186    }
187    /// Create a queue whose scratch and prepared dispatch storage share a byte ceiling.
188    pub fn aql_queue_budgeted(
189        &self,
190        maximum_private_bytes: u32,
191        budget: Option<&crate::residency::MemoryBudget>,
192    ) -> Result<AqlQueue> {
193        if self.endpoint().engine() != Engine::Gpu {
194            return Err(Error::Unsupported("AQL requires a GPU".into()));
195        }
196        let family = self
197            .endpoint()
198            .queue_capabilities()?
199            .into_iter()
200            .find(|f| {
201                f.command == QueueCommand::Aql
202                    && f.format_version == 1
203                    && f.user_publication
204                    && f.system_release
205                    && f.system_acquire
206            })
207            .ok_or_else(|| Error::Unsupported("no native AQL v1 family".into()))?;
208        let api = &self.0.endpoint.0.instance.api;
209        let _ = entry!(api.core, user_queue_destroy);
210        let _ = entry!(api.core, user_queue_mapping_destroy);
211        let mut scratch_info = amdf_gpu_queue_scratch_t::default();
212        let scratch = if maximum_private_bytes != 0 {
213            let mut endpoint = amdf_gpu_endpoint_info_t {
214                type_: HRX_AMDF_STRUCTURE_TYPE_GPU_ENDPOINT_INFO,
215                structure_size: size_of::<amdf_gpu_endpoint_info_t>() as u32,
216                ..Default::default()
217            };
218            check("AQL scratch topology", unsafe {
219                entry!(api.gpu, endpoint_query_info)(self.0.endpoint.0.raw, &mut endpoint)
220            })?;
221            let engines = u64::from(endpoint.topology.xcc_count)
222                * u64::from(endpoint.topology.shader_engine_count_per_xcc);
223            if engines == 0 {
224                return Err(Error::Unsupported("empty AQL scratch topology".into()));
225            }
226            let slots = u64::from(endpoint.compute.compute_unit_count).div_ceil(engines)
227                * engines
228                * u64::from(endpoint.compute.maximum_scratch_wave_count_per_compute_unit);
229            let bytes = (u64::from(maximum_private_bytes) * 64)
230                .next_multiple_of(1024)
231                .checked_mul(slots)
232                .filter(|bytes| {
233                    *bytes != 0 && *bytes <= 512 * 1024 * 1024 && slots <= u64::from(u32::MAX)
234                })
235                .ok_or_else(|| {
236                    Error::Unsupported("AQL scratch exceeds the 512 MiB admission limit".into())
237                })?;
238            let buffer = self.fabric().allocate_owned(
239                bytes as usize,
240                self,
241                AMDF_MEMORY_ACCESS_READ | AMDF_MEMORY_ACCESS_WRITE,
242                4096,
243                false,
244                budget,
245            )?;
246            scratch_info = amdf_gpu_queue_scratch_t {
247                memory: buffer.0.raw,
248                byte_length: bytes,
249                maximum_private_segment_byte_length: maximum_private_bytes,
250                maximum_wave_count: slots as u32,
251                ..Default::default()
252            };
253            Some(buffer)
254        } else {
255            None
256        };
257        let options = amdf_gpu_user_queue_create_info_t {
258            type_: HRX_AMDF_STRUCTURE_TYPE_GPU_USER_QUEUE_CREATE_INFO,
259            structure_size: size_of::<amdf_gpu_user_queue_create_info_t>() as u32,
260            queue_family_ordinal: family.ordinal,
261            priority: AMDF_QUEUE_PRIORITY_NORMAL,
262            producer_mode: AMDF_QUEUE_PRODUCER_MODE_SINGLE,
263            required_capabilities: AMDF_USER_QUEUE_CAPABILITY_HOST_PRODUCER as u64,
264            scratch: scratch_info,
265            ..Default::default()
266        };
267        let mut raw = ptr::null_mut();
268        check("AQL queue create", unsafe {
269            entry!(api.gpu, user_queue_create)(self.0.raw, &options, &mut raw)
270        })?;
271        if raw.is_null() {
272            return Err(missing("AQL queue"));
273        }
274        let mut queue = QueueState {
275            device: self.clone(),
276            raw,
277            mapping: ptr::null_mut(),
278            info: Default::default(),
279            scratch,
280            maximum_private_bytes,
281            budget: budget.cloned(),
282            producer: Mutex::new(Producer {
283                write: 0,
284                pending: VecDeque::with_capacity(4096),
285            }),
286        };
287        check("AQL queue map", unsafe {
288            entry!(api.core, user_queue_map)(raw, ptr::null_mut(), &mut queue.mapping)
289        })?;
290        queue.info.type_ = AMDF_STRUCTURE_TYPE_USER_QUEUE_MAPPING_INFO;
291        queue.info.structure_size = size_of::<amdf_user_queue_mapping_info_t>() as u32;
292        check("AQL mapping info", unsafe {
293            entry!(api.core, user_queue_mapping_query_info)(queue.mapping, &mut queue.info)
294        })?;
295        let info = &queue.info;
296        if info.command_type != AMDF_QUEUE_COMMAND_TYPE_GPU_AQL
297            || info.format_version != 1
298            || info.index_bits != 64
299            || info.doorbell_bits != 64
300            || !info.ring_byte_length.is_power_of_two()
301            || info.ring_byte_length < 128
302            || [
303                info.ring_address,
304                info.read_index_address,
305                info.write_index_address,
306                info.doorbell_address,
307            ]
308            .iter()
309            .any(|address| *address == 0 || address % 8 != 0)
310        {
311            return Err(Error::Unsupported("unexpected AQL mapping contract".into()));
312        }
313        Ok(AqlQueue(Arc::new(queue)))
314    }
315}
316impl AqlQueue {
317    /// Device owning this queue and its fixed scratch.
318    pub fn device(&self) -> &Device {
319        &self.0.device
320    }
321    /// Prepare a dispatch and its instruction-cache publication packets.
322    /// # Safety
323    /// Arguments and geometry must satisfy the trusted kernel's memory contract.
324    /// Caller orders conflicting accesses by other queues.
325    pub unsafe fn prepare(
326        &self,
327        kernel: &Kernel,
328        grid: [u32; 3],
329        block: [u16; 3],
330        arguments: &[Argument<'_>],
331    ) -> Result<PreparedAql> {
332        if !Arc::ptr_eq(&kernel.device().0, &self.device().0) {
333            return Err(Error::Message(
334                "AQL kernel belongs to another device".into(),
335            ));
336        }
337        if kernel.0.info.private_bytes > self.0.maximum_private_bytes {
338            return Err(Error::Unsupported(
339                "kernel exceeds fixed AQL scratch capacity".into(),
340            ));
341        }
342        let mut extents = [0; 3];
343        for axis in 0..3 {
344            if block[axis] == 0
345                || grid[axis] == 0
346                || (kernel.workgroup_size()[axis] != 0
347                    && kernel.workgroup_size()[axis] != u32::from(block[axis]))
348            {
349                return Err(Error::Message(
350                    "AQL launch geometry does not match kernel".into(),
351                ));
352            }
353            extents[axis] = grid[axis]
354                .checked_mul(u32::from(block[axis]))
355                .ok_or_else(|| Error::Message("AQL grid extent overflow".into()))?;
356        }
357        if block.iter().map(|v| u64::from(*v)).product::<u64>() > 1024 {
358            return Err(Error::Message(
359                "AQL workgroup exceeds gfx1151 capacity".into(),
360            ));
361        }
362        let specialized = kernel.for_aql(
363            grid,
364            self.0.info.ring_address,
365            self.0.info.ring_byte_length / 64 - 1,
366            arguments,
367        )?;
368        let clear = if let Some(state) = &specialized.0.race_state {
369            let (utility, clear_grid) =
370                state.clear_kernel(self.device(), kernel.shadow_budget())?;
371            // Utility is uninstrumented, so preparation does not recurse again.
372            Some(unsafe {
373                self.prepare(
374                    &utility,
375                    clear_grid,
376                    [64, 1, 1],
377                    &[Argument::Buffer(&state.shadow, 0)],
378                )
379            }?)
380        } else {
381            None
382        };
383        let kernel = &specialized;
384        let (packed, mut buffers) = kernel.arguments(arguments)?;
385        let fabric = self.device().fabric();
386        let kernarg = fabric.allocate_owned(
387            packed.len().max(64),
388            self.device(),
389            AMDF_MEMORY_ACCESS_READ | AMDF_MEMORY_ACCESS_WRITE,
390            64,
391            true,
392            self.0.budget.as_ref(),
393        )?;
394        kernarg.write(0, &packed)?;
395        let kernarg_address = kernarg.device_address(self.device())?;
396        let descriptor = kernel
397            .0
398            .code
399            .device_address(self.device())?
400            .checked_add(kernel.0.info.descriptor_offset)
401            .ok_or_else(|| Error::Message("AQL descriptor address overflow".into()))?;
402        // GFX11 GCR includes GLI_INV_ALL. The native system fences on dispatch
403        // order data; this explicit publication also handles reused code addresses.
404        let invalidation = [
405            0xc0065800u32,
406            0,
407            u32::MAX,
408            0xff,
409            0,
410            0,
411            0x0a,
412            1 | (1 << 4) | (1 << 5) | (1 << 7) | (1 << 8) | (1 << 9) | (1 << 14) | (1 << 15),
413        ];
414        let publication = fabric.allocate_owned(
415            4096,
416            self.device(),
417            AMDF_MEMORY_ACCESS_READ | AMDF_MEMORY_ACCESS_EXECUTE,
418            4096,
419            false,
420            self.0.budget.as_ref(),
421        )?;
422        publication.write(
423            0,
424            &invalidation
425                .into_iter()
426                .flat_map(u32::to_le_bytes)
427                .collect::<Vec<_>>(),
428        )?;
429        let ib = publication.device_address(self.device())?;
430        if ib >= (1 << 48) || ib + 32 > (1 << 48) {
431            return Err(Error::Unsupported(
432                "AQL publication IB is outside the 48-bit address domain".into(),
433            ));
434        }
435        let signal = fabric.allocate_owned(
436            64,
437            self.device(),
438            AMDF_MEMORY_ACCESS_READ | AMDF_MEMORY_ACCESS_WRITE,
439            64,
440            true,
441            self.0.budget.as_ref(),
442        )?;
443        signal.write(0, &1i64.to_le_bytes())?;
444        signal.write(8, &i64::MAX.to_le_bytes())?;
445        let signal_address = signal.device_address(self.device())?;
446        let mut publish = [0; 16];
447        publish[0] = (1 << 16) | (1 << 8);
448        publish[1] = 0xc0023f00;
449        publish[2] = ib as u32;
450        publish[3] = (ib >> 32) as u32;
451        publish[4] = (1 << 23) | 8;
452        publish[5] = 10;
453        let mut dispatch = [0; 16];
454        dispatch[0] = 2 | (1 << 8) | (2 << 9) | (2 << 11) | (3 << 16);
455        dispatch[1] = u32::from(block[0]) | (u32::from(block[1]) << 16);
456        dispatch[2] = u32::from(block[2]);
457        dispatch[3..6].copy_from_slice(&extents);
458        dispatch[6] = kernel.0.info.private_bytes;
459        dispatch[7] = kernel.0.info.local_bytes;
460        dispatch[8] = descriptor as u32;
461        dispatch[9] = (descriptor >> 32) as u32;
462        dispatch[10] = kernarg_address as u32;
463        dispatch[11] = (kernarg_address >> 32) as u32;
464        dispatch[14] = signal_address as u32;
465        dispatch[15] = (signal_address >> 32) as u32;
466        let mut packets = vec![publish];
467        if let Some(clear) = clear {
468            let mut packet = *clear.packets.last().unwrap();
469            packet[14..16].fill(0);
470            packets.push(packet);
471            // The outer completion covers the clear kernel and all of its
472            // bindings. The private utility completion signal is never submitted.
473            buffers.extend(clear.work.buffers.iter().cloned());
474        }
475        packets.push(dispatch);
476        buffers.extend([kernarg, publication]);
477        buffers.sort_by_key(Buffer::identity);
478        buffers.dedup_by(|a, b| a.same_backing(b));
479        let storage_bytes = signal.len()
480            + buffers
481                .iter()
482                .filter(|buffer| {
483                    !arguments.iter().any(|argument| {
484                        matches!(argument,
485                Argument::Buffer(bound, _) if buffer.same_backing(bound))
486                    })
487                })
488                .map(Buffer::len)
489                .sum::<usize>();
490        let leases = Mutex::new(Vec::with_capacity(buffers.len()));
491        Ok(PreparedAql {
492            queue: self.clone(),
493            work: Arc::new(Work {
494                signal,
495                buffers,
496                _kernel: kernel.clone(),
497                leases,
498                active: AtomicU64::new(0),
499                retired: AtomicU64::new(0),
500            }),
501            packets,
502            next: Arc::new(Mutex::new(0)),
503            storage_bytes,
504        })
505    }
506}
507impl PreparedAql {
508    pub(crate) fn storage_bytes(&self) -> usize {
509        self.storage_bytes
510    }
511
512    /// Publish prepared packets without allocating or growing scratch.
513    /// # Safety
514    /// Caller orders conflicting accesses on other queues and obeys the kernel contract.
515    pub unsafe fn dispatch(&self) -> Result<AqlCompletion> {
516        let mut next = self
517            .next
518            .lock()
519            .map_err(|_| Error::DeviceLost("AQL dispatch poisoned".into()))?;
520        let point = next
521            .checked_add(1)
522            .filter(|point| *point <= i64::MAX as u64)
523            .ok_or_else(|| Error::DeviceLost("AQL timeline exhausted".into()))?;
524        let mut producer = self
525            .queue
526            .0
527            .producer
528            .lock()
529            .map_err(|_| Error::DeviceLost("AQL producer poisoned".into()))?;
530        while let Some((work, point)) = producer.pending.front() {
531            if !work.refresh(*point)? {
532                break;
533            }
534            producer.pending.pop_front();
535        }
536        let info = &self.queue.0.info;
537        let capacity = info.ring_byte_length / 64;
538        let read =
539            unsafe { (&*(info.read_index_address as *const AtomicU64)).load(Ordering::Acquire) };
540        let end = producer
541            .write
542            .checked_add(self.packets.len() as u64)
543            .ok_or_else(|| Error::DeviceLost("AQL ring index exhausted".into()))?;
544        if producer.pending.len() == 4096 || end.saturating_sub(read) > capacity {
545            return Err(Error::Busy("AQL ring or pending capacity exhausted".into()));
546        }
547        let mut leases = self
548            .work
549            .leases
550            .lock()
551            .map_err(|_| Error::DeviceLost("AQL leases poisoned".into()))?;
552        if leases.is_empty() {
553            for buffer in &self.work.buffers {
554                match buffer.retain_use() {
555                    Ok(lease) => leases.push(lease),
556                    Err(error) => {
557                        leases.clear();
558                        return Err(error);
559                    }
560                }
561            }
562        }
563        self.work.active.store(point, Ordering::Release);
564        producer.pending.push_back((self.work.clone(), point));
565        unsafe {
566            (&*(info.write_index_address as *const AtomicU64)).store(end, Ordering::Release);
567            for (offset, packet) in self.packets.iter().enumerate() {
568                let index = producer.write + offset as u64;
569                let slot =
570                    (info.ring_address as *mut u32).add(((index & (capacity - 1)) * 16) as usize);
571                ptr::copy_nonoverlapping(packet.as_ptr().add(1), slot.add(1), 15);
572                (&*slot.cast::<AtomicU32>()).store(packet[0], Ordering::Release);
573                std::sync::atomic::fence(Ordering::SeqCst);
574                ptr::write_volatile(info.doorbell_address as *mut u64, index);
575            }
576        }
577        producer.write = end;
578        *next = point;
579        Ok(AqlCompletion {
580            queue: self.queue.clone(),
581            work: self.work.clone(),
582            point,
583        })
584    }
585}
586
587#[cfg(test)]
588mod sanitizer_tests {
589    use super::*;
590    use crate::loom::{
591        Compiler, CompilerOptions, SanitizerChecks, SanitizerOptions, SanitizerReporting,
592        Specialization,
593    };
594
595    #[test]
596    #[ignore = "requires current native compiler/bridge and gfx1151"]
597    fn address_shadow_guards_null_wrap_and_every_native_load_width() -> Result<()> {
598        let device = Device::open(Engine::Gpu, 0)?;
599        let queue = device.aql_queue(0)?;
600        let compiler = Compiler::shared(
601            None,
602            CompilerOptions {
603                sanitizer: SanitizerOptions {
604                    checks: SanitizerChecks {
605                        access: true,
606                        ..Default::default()
607                    },
608                    reporting: SanitizerReporting::ReportOnly,
609                },
610                ..Default::default()
611            },
612        )?;
613        for wave in [32, 64] {
614            for width in [1, 2, 4, 8, 16, 32, 64, 128] {
615                let source = format!(
616                    r#"
617amdgpu.target<gfx1151> @target {{subgroup_size = {wave}}}
618kernel.def target(@target) @check_range() {{
619  %one = index.constant 1 : index
620  kernel.launch.config workgroups(%one, %one, %one) workgroup_size(%one, %one, %one) : index
621}} launch(%input: buffer, %output: buffer) {{
622  %zero = index.constant 0 : offset
623  %index = index.constant 0 : index
624  %value = scalar.constant 9 : i32
625  %global = buffer.assume.memory_space<global> %input : buffer
626  %view = buffer.view %global[%zero] : buffer -> view<{width}xi8>
627  sanitizer.assert.access<read> %view[0] {{static_extents = [{width}]}} : view<{width}xi8>
628  %out = buffer.assume.memory_space<global> %output : buffer
629  %out_view = buffer.view %out[%zero] : buffer -> view<1xi32>
630  view.store %value, %out_view[%index] : i32, view<1xi32>
631  kernel.return
632}}
633"#
634                );
635                let artifact = compiler
636                    .module(&source)
637                    .compile(&Specialization::new("check_range"))?;
638                let kernel = unsafe {
639                    device.load_sanitized(&artifact, &SanitizerRuntimeOptions::default())
640                }?;
641                let input = device
642                    .fabric()
643                    .allocate(width + 1, std::slice::from_ref(&device))?;
644                let output = device.fabric().allocate(4, std::slice::from_ref(&device))?;
645                let prepared = unsafe {
646                    queue.prepare(
647                        &kernel,
648                        [1; 3],
649                        [1; 3],
650                        &[Argument::Buffer(&input, 0), Argument::Buffer(&output, 0)],
651                    )
652                }?;
653                let packet = prepared.packets.last().unwrap();
654                let kernarg_address = u64::from(packet[10]) | (u64::from(packet[11]) << 32);
655                let kernarg = prepared
656                    .work
657                    .buffers
658                    .iter()
659                    .find(|b| b.device_address(&device).ok() == Some(kernarg_address))
660                    .unwrap();
661                let base = input.device_address(&device)?;
662                for (pointer, valid) in [
663                    (base, true),
664                    (base + 1, true),
665                    (base + 2, false),
666                    (base - 1, false),
667                    (0, false),
668                    (1, false),
669                    (u64::MAX - 1, false),
670                    (u64::MAX - 7, false),
671                    (1 << 63, false),
672                ] {
673                    // Deliberately corrupt only a retired fixture's pointer. The
674                    // public API never accepts a naked pointer in a Buffer binding.
675                    // This kernel checks the range without dereferencing it.
676                    kernarg.write(0, &pointer.to_le_bytes())?;
677                    output.write(0, &17u32.to_le_bytes())?;
678                    let done = unsafe { prepared.dispatch() }?;
679                    assert!(
680                        done.wait_timeout(Duration::from_secs(10))?,
681                        "wave{wave} width{width} pointer{pointer:x}"
682                    );
683                    let reports = kernel.sanitizer_reports()?;
684                    assert_eq!(reports.dropped, 0);
685                    assert_eq!(
686                        reports.reports.len(),
687                        usize::from(!valid),
688                        "wave{wave} width{width} pointer{pointer:x}: {reports:?}"
689                    );
690                    if !valid {
691                        let address = reports.reports[0].address.as_ref().unwrap();
692                        assert_eq!(address.fault_address, pointer);
693                        assert_eq!(address.access_bytes, width as u64);
694                    }
695                    let mut actual = [0; 4];
696                    output.read(0, &mut actual)?;
697                    assert_eq!(u32::from_le_bytes(actual), if valid { 9 } else { 17 });
698                }
699            }
700        }
701        Ok(())
702    }
703
704    #[test]
705    #[ignore = "requires current native compiler/bridge and gfx1151"]
706    fn address_checks_keep_valid_and_invalid_lanes_separate() -> Result<()> {
707        let device = Device::open(Engine::Gpu, 0)?;
708        let queue = device.aql_queue(0)?;
709        let compiler = Compiler::shared(
710            None,
711            CompilerOptions {
712                sanitizer: SanitizerOptions {
713                    checks: SanitizerChecks {
714                        access: true,
715                        ..Default::default()
716                    },
717                    reporting: SanitizerReporting::ReportOnly,
718                },
719                ..Default::default()
720            },
721        )?;
722        for wave in [32, 64] {
723            let source = format!(
724                r#"
725amdgpu.target<gfx1151> @target {{subgroup_size = {wave}}}
726kernel.def target(@target) @check_lanes() {{
727  %one = index.constant 1 : index
728  %threads = index.constant 64 : index
729  kernel.launch.config workgroups(%one, %one, %one) workgroup_size(%threads, %one, %one) : index
730}} launch(%input: buffer, %output: buffer) {{
731  %zero = index.constant 0 : offset
732  %lane = kernel.workitem.id<x> : index
733  %value = scalar.constant 9 : i32
734  %global = buffer.assume.memory_space<global> %input : buffer
735  %view = buffer.view %global[%zero] : buffer -> view<64xi8>
736  sanitizer.assert.access<read> %view[%lane] : view<64xi8>
737  %out = buffer.assume.memory_space<global> %output : buffer
738  %out_view = buffer.view %out[%zero] : buffer -> view<64xi32>
739  view.store %value, %out_view[%lane] : i32, view<64xi32>
740  kernel.return
741}}
742"#
743            );
744            let artifact = compiler
745                .module(&source)
746                .compile(&Specialization::new("check_lanes"))?;
747            let kernel =
748                unsafe { device.load_sanitized(&artifact, &SanitizerRuntimeOptions::default()) }?;
749            let input = device
750                .fabric()
751                .allocate(33, std::slice::from_ref(&device))?;
752            let output = device
753                .fabric()
754                .allocate(256, std::slice::from_ref(&device))?;
755            output.write(0, &[0; 256])?;
756            let prepared = unsafe {
757                queue.prepare(
758                    &kernel,
759                    [1; 3],
760                    [64, 1, 1],
761                    &[Argument::Buffer(&input, 0), Argument::Buffer(&output, 0)],
762                )
763            }?;
764            let done = unsafe { prepared.dispatch() }?;
765            assert!(done.wait_timeout(Duration::from_secs(10))?);
766            let reports = kernel.sanitizer_reports()?;
767            assert_eq!(reports.dropped, 0);
768            assert!(!reports.reports.is_empty());
769            let base = input.device_address(&device)?;
770            for report in reports.reports {
771                let address = report.address.unwrap();
772                assert!((base + 33..base + 64).contains(&address.fault_address));
773                assert!((33..64).contains(&report.workitem_x));
774            }
775            if wave == 32 {
776                let mut first_wave = [0; 128];
777                output.read(0, &mut first_wave)?;
778                for word in first_wave.as_chunks::<4>().0 {
779                    assert_eq!(*word, 9u32.to_le_bytes());
780                }
781            }
782        }
783        Ok(())
784    }
785}