Skip to main content

oxicuda_levelzero/
command_list.rs

1//! Host-side Level Zero command-list recording and dispatch planning.
2//!
3//! A Level Zero `ze_command_list_handle_t` is *recorded* on the host: the
4//! application appends memory copies, kernel launches, and barriers, then
5//! *closes* the list and submits it to a queue. The recording itself —
6//! validating ordinals, computing `ze_group_count_t` dispatch dimensions,
7//! sequencing barriers and events — is pure host-side logic that this module
8//! models as an in-memory [`CommandListRecorder`]. The only device-gated step
9//! is replaying the recorded commands through `zeCommandListAppend*`.
10//!
11//! This module also provides:
12//! - [`GroupCountPlanner`] — converts a global problem size into the
13//!   `(group_count_x, group_count_y, group_count_z)` launch grid for a given
14//!   workgroup size, mirroring `zeKernelSuggestGroupSize` arithmetic.
15//! - [`CommandQueueGroupTable`] — selects a compute vs. copy queue-group
16//!   ordinal from a queried `zeDeviceGetCommandQueueGroupProperties` table.
17//! - [`EventPoolDesc`] / [`Barrier`] — event-pool sizing and barrier
18//!   dependency construction.
19//! - [`ReusableCommandList`] — models `zeCommandListReset` + re-record for
20//!   repeated launches without re-allocation.
21
22use crate::error::{LevelZeroError, LevelZeroResult};
23
24// ─── Group-count dispatch planning ───────────────────────────
25
26/// A `ze_group_count_t` — the number of workgroups launched in each dimension.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub struct GroupCount {
29    /// Workgroups in X.
30    pub x: u32,
31    /// Workgroups in Y.
32    pub y: u32,
33    /// Workgroups in Z.
34    pub z: u32,
35}
36
37impl GroupCount {
38    /// Total number of workgroups across all three dimensions.
39    #[must_use]
40    pub fn total(&self) -> u64 {
41        u64::from(self.x) * u64::from(self.y) * u64::from(self.z)
42    }
43}
44
45/// Plans `ze_group_count_t` launch grids from global problem sizes.
46///
47/// Given a per-dimension workgroup (local) size, `plan_*` computes the smallest
48/// group count whose `group_count * local_size >= global_size`, i.e. the
49/// ceiling division used by `zeCommandListAppendLaunchKernel`. The kernel's
50/// bounds check (`if (gid < count)`) masks the overhang.
51#[derive(Debug, Clone, Copy)]
52pub struct GroupCountPlanner {
53    local_x: u32,
54    local_y: u32,
55    local_z: u32,
56}
57
58impl GroupCountPlanner {
59    /// Create a 1-D planner with the given workgroup size.
60    pub fn new_1d(local_x: u32) -> LevelZeroResult<Self> {
61        Self::new_3d(local_x, 1, 1)
62    }
63
64    /// Create a 3-D planner. Every local dimension must be non-zero.
65    pub fn new_3d(local_x: u32, local_y: u32, local_z: u32) -> LevelZeroResult<Self> {
66        if local_x == 0 || local_y == 0 || local_z == 0 {
67            return Err(LevelZeroError::InvalidArgument(
68                "workgroup size must be non-zero in every dimension".into(),
69            ));
70        }
71        Ok(Self {
72            local_x,
73            local_y,
74            local_z,
75        })
76    }
77
78    /// The configured local (workgroup) size.
79    #[must_use]
80    pub fn local_size(&self) -> (u32, u32, u32) {
81        (self.local_x, self.local_y, self.local_z)
82    }
83
84    /// Plan a 1-D launch grid covering `global_x` work-items.
85    pub fn plan_1d(&self, global_x: u64) -> LevelZeroResult<GroupCount> {
86        self.plan_3d(global_x, 1, 1)
87    }
88
89    /// Plan a 3-D launch grid covering `(global_x, global_y, global_z)`
90    /// work-items.
91    ///
92    /// Returns [`LevelZeroError::InvalidArgument`] if any dimension's group
93    /// count would overflow `u32` (the Level Zero limit).
94    pub fn plan_3d(
95        &self,
96        global_x: u64,
97        global_y: u64,
98        global_z: u64,
99    ) -> LevelZeroResult<GroupCount> {
100        let x = ceil_div_u32(global_x, self.local_x)?;
101        let y = ceil_div_u32(global_y, self.local_y)?;
102        let z = ceil_div_u32(global_z, self.local_z)?;
103        Ok(GroupCount { x, y, z })
104    }
105}
106
107/// Ceiling division `ceil(num / den)` clamped to the `u32` group-count limit.
108fn ceil_div_u32(num: u64, den: u32) -> LevelZeroResult<u32> {
109    if num == 0 {
110        return Ok(0);
111    }
112    let groups = num.div_ceil(u64::from(den));
113    u32::try_from(groups).map_err(|_| {
114        LevelZeroError::InvalidArgument(format!("group count {groups} exceeds u32 dispatch limit"))
115    })
116}
117
118// ─── Command-queue group selection ───────────────────────────
119
120/// One entry of `zeDeviceGetCommandQueueGroupProperties`.
121#[derive(Debug, Clone, PartialEq, Eq)]
122pub struct CommandQueueGroupInfo {
123    /// The ordinal passed to command-list / command-queue descriptors.
124    pub ordinal: u32,
125    /// `true` when this group can execute compute kernels.
126    pub compute: bool,
127    /// `true` when this group can execute memory-copy commands.
128    pub copy: bool,
129    /// Number of physical queues in this group.
130    pub num_queues: u32,
131}
132
133/// A queried command-queue-group property table for one device.
134///
135/// Intel GPUs expose a primary compute+copy group (ordinal 0) and, on
136/// discrete parts, dedicated copy-only "link" engines. Selecting the right
137/// ordinal lets copies overlap compute on a separate engine.
138#[derive(Debug, Clone, Default)]
139pub struct CommandQueueGroupTable {
140    groups: Vec<CommandQueueGroupInfo>,
141}
142
143impl CommandQueueGroupTable {
144    /// Build a table from queue-group records.
145    #[must_use]
146    pub fn new(groups: Vec<CommandQueueGroupInfo>) -> Self {
147        Self { groups }
148    }
149
150    /// All queue groups, in driver-reported order.
151    #[must_use]
152    pub fn groups(&self) -> &[CommandQueueGroupInfo] {
153        &self.groups
154    }
155
156    /// Select an ordinal that can run compute kernels (lowest ordinal first).
157    pub fn select_compute_ordinal(&self) -> LevelZeroResult<u32> {
158        self.groups
159            .iter()
160            .filter(|g| g.compute)
161            .min_by_key(|g| g.ordinal)
162            .map(|g| g.ordinal)
163            .ok_or_else(|| {
164                LevelZeroError::Unsupported("device exposes no compute queue group".into())
165            })
166    }
167
168    /// Select an ordinal for asynchronous copies.
169    ///
170    /// Prefers a **copy-only** group (so it does not contend with compute);
171    /// falls back to any copy-capable group. This is the basis for async copy
172    /// queues discovered separately from compute queues.
173    pub fn select_copy_ordinal(&self) -> LevelZeroResult<u32> {
174        // First choice: a dedicated copy-only engine.
175        if let Some(g) = self
176            .groups
177            .iter()
178            .filter(|g| g.copy && !g.compute)
179            .min_by_key(|g| g.ordinal)
180        {
181            return Ok(g.ordinal);
182        }
183        // Fallback: any copy-capable group.
184        self.groups
185            .iter()
186            .filter(|g| g.copy)
187            .min_by_key(|g| g.ordinal)
188            .map(|g| g.ordinal)
189            .ok_or_else(|| LevelZeroError::Unsupported("device exposes no copy queue group".into()))
190    }
191
192    /// `true` when a dedicated copy-only engine exists (enabling overlap).
193    #[must_use]
194    pub fn has_dedicated_copy_engine(&self) -> bool {
195        self.groups.iter().any(|g| g.copy && !g.compute)
196    }
197}
198
199// ─── Event pools and barriers ────────────────────────────────
200
201/// Memory/host visibility scope for events in a pool (`ze_event_pool_flags_t`).
202#[derive(Debug, Clone, Copy, PartialEq, Eq)]
203pub enum EventScope {
204    /// Events visible only to the device (`0`).
205    Device,
206    /// `ZE_EVENT_POOL_FLAG_HOST_VISIBLE` — the host can wait on the event.
207    HostVisible,
208    /// `ZE_EVENT_POOL_FLAG_KERNEL_TIMESTAMP` — events capture kernel timestamps.
209    KernelTimestamp,
210}
211
212impl EventScope {
213    /// The `ze_event_pool_flags_t` bitmask for this scope.
214    #[must_use]
215    pub fn ze_flags(self) -> u32 {
216        match self {
217            EventScope::Device => 0,
218            EventScope::HostVisible => 0x1,
219            EventScope::KernelTimestamp => 0x2,
220        }
221    }
222}
223
224/// Descriptor for a `zeEventPoolCreate` call.
225///
226/// An event pool is a fixed-capacity arena of events. The recorder hands out
227/// monotonically-increasing event indices via [`EventPoolDesc::alloc_event`]
228/// until the pool is exhausted, modelling the index bookkeeping that the
229/// device-gated `zeEventCreate` performs.
230#[derive(Debug, Clone)]
231pub struct EventPoolDesc {
232    /// Maximum number of events the pool can hold.
233    pub capacity: u32,
234    /// Visibility scope applied to all events.
235    pub scope: EventScope,
236    /// Next free event index.
237    next_index: u32,
238}
239
240impl EventPoolDesc {
241    /// Create a pool descriptor with `capacity` slots and `scope` visibility.
242    pub fn new(capacity: u32, scope: EventScope) -> LevelZeroResult<Self> {
243        if capacity == 0 {
244            return Err(LevelZeroError::InvalidArgument(
245                "event pool capacity must be > 0".into(),
246            ));
247        }
248        Ok(Self {
249            capacity,
250            scope,
251            next_index: 0,
252        })
253    }
254
255    /// Reserve the next event index in the pool.
256    ///
257    /// Returns [`LevelZeroError::OutOfMemory`] when the pool is exhausted.
258    pub fn alloc_event(&mut self) -> LevelZeroResult<u32> {
259        if self.next_index >= self.capacity {
260            return Err(LevelZeroError::OutOfMemory);
261        }
262        let idx = self.next_index;
263        self.next_index += 1;
264        Ok(idx)
265    }
266
267    /// Number of events still available in the pool.
268    #[must_use]
269    pub fn remaining(&self) -> u32 {
270        self.capacity - self.next_index
271    }
272
273    /// `true` when host-side `zeEventHostSynchronize` is permitted.
274    #[must_use]
275    pub fn is_host_visible(&self) -> bool {
276        matches!(self.scope, EventScope::HostVisible)
277    }
278}
279
280/// A barrier appended into a command list.
281///
282/// Models `zeCommandListAppendBarrier`: it signals `signal_event` (if any)
283/// once all `wait_events` have completed. With no wait events it is a global
284/// execution barrier across the list.
285#[derive(Debug, Clone, PartialEq, Eq)]
286pub struct Barrier {
287    /// Event signalled when the barrier completes (`None` = no signal).
288    pub signal_event: Option<u32>,
289    /// Events that must complete before the barrier passes.
290    pub wait_events: Vec<u32>,
291}
292
293impl Barrier {
294    /// A global barrier with no event dependencies.
295    #[must_use]
296    pub fn global() -> Self {
297        Self {
298            signal_event: None,
299            wait_events: Vec::new(),
300        }
301    }
302
303    /// A barrier that signals `event` once recorded work completes.
304    #[must_use]
305    pub fn signalling(event: u32) -> Self {
306        Self {
307            signal_event: Some(event),
308            wait_events: Vec::new(),
309        }
310    }
311
312    /// A barrier that waits on `events` before passing.
313    #[must_use]
314    pub fn waiting(events: Vec<u32>) -> Self {
315        Self {
316            signal_event: None,
317            wait_events: events,
318        }
319    }
320}
321
322// ─── Recorded commands ───────────────────────────────────────
323
324/// A single command appended into a [`CommandListRecorder`].
325#[derive(Debug, Clone, PartialEq, Eq)]
326pub enum RecordedCommand {
327    /// `zeCommandListAppendMemoryCopy` of `bytes` bytes from src→dst handles.
328    MemoryCopy {
329        /// Destination buffer handle.
330        dst: u64,
331        /// Source buffer handle.
332        src: u64,
333        /// Number of bytes copied.
334        bytes: u64,
335    },
336    /// `zeCommandListAppendMemoryFill` writing `pattern_len`-byte pattern.
337    MemoryFill {
338        /// Destination buffer handle.
339        dst: u64,
340        /// Bytes filled.
341        bytes: u64,
342        /// Pattern width in bytes.
343        pattern_len: u32,
344    },
345    /// `zeCommandListAppendLaunchKernel` with a name and launch grid.
346    LaunchKernel {
347        /// Entry-point name of the launched kernel.
348        kernel: String,
349        /// The launch grid.
350        groups: GroupCount,
351        /// Event signalled on completion (`None` = no signal).
352        signal_event: Option<u32>,
353    },
354    /// `zeCommandListAppendBarrier`.
355    Barrier(Barrier),
356}
357
358/// State of a command list in the host-side model.
359#[derive(Debug, Clone, Copy, PartialEq, Eq)]
360pub enum CommandListState {
361    /// Open for `append_*` calls.
362    Recording,
363    /// Closed; ready for submission, no further appends until reset.
364    Closed,
365}
366
367/// An in-memory recording of a Level Zero command list.
368///
369/// Appends are buffered in order; [`close`](Self::close) seals the list (after
370/// which appends are rejected) and [`reset`](Self::reset) clears it back to the
371/// recording state for reuse — exactly the `zeCommandListReset` lifecycle.
372#[derive(Debug, Clone)]
373pub struct CommandListRecorder {
374    /// Compute/copy queue-group ordinal this list targets.
375    ordinal: u32,
376    commands: Vec<RecordedCommand>,
377    state: CommandListState,
378}
379
380impl CommandListRecorder {
381    /// Begin recording a command list targeting queue-group `ordinal`.
382    #[must_use]
383    pub fn new(ordinal: u32) -> Self {
384        Self {
385            ordinal,
386            commands: Vec::new(),
387            state: CommandListState::Recording,
388        }
389    }
390
391    /// The queue-group ordinal this list was created against.
392    #[must_use]
393    pub fn ordinal(&self) -> u32 {
394        self.ordinal
395    }
396
397    /// Current state of the list.
398    #[must_use]
399    pub fn state(&self) -> CommandListState {
400        self.state
401    }
402
403    /// Number of recorded commands.
404    #[must_use]
405    pub fn len(&self) -> usize {
406        self.commands.len()
407    }
408
409    /// `true` when no commands have been recorded.
410    #[must_use]
411    pub fn is_empty(&self) -> bool {
412        self.commands.is_empty()
413    }
414
415    /// The recorded command sequence.
416    #[must_use]
417    pub fn commands(&self) -> &[RecordedCommand] {
418        &self.commands
419    }
420
421    fn ensure_recording(&self) -> LevelZeroResult<()> {
422        if self.state != CommandListState::Recording {
423            return Err(LevelZeroError::CommandListError(
424                "command list is closed; reset before appending".into(),
425            ));
426        }
427        Ok(())
428    }
429
430    /// Append a host→device or device→device memory copy.
431    pub fn append_memory_copy(&mut self, dst: u64, src: u64, bytes: u64) -> LevelZeroResult<()> {
432        self.ensure_recording()?;
433        if bytes == 0 {
434            return Err(LevelZeroError::InvalidArgument(
435                "memory copy size must be > 0".into(),
436            ));
437        }
438        self.commands
439            .push(RecordedCommand::MemoryCopy { dst, src, bytes });
440        Ok(())
441    }
442
443    /// Append a memory fill writing a repeating pattern.
444    pub fn append_memory_fill(
445        &mut self,
446        dst: u64,
447        bytes: u64,
448        pattern_len: u32,
449    ) -> LevelZeroResult<()> {
450        self.ensure_recording()?;
451        if pattern_len == 0 {
452            return Err(LevelZeroError::InvalidArgument(
453                "fill pattern length must be > 0".into(),
454            ));
455        }
456        if bytes % u64::from(pattern_len) != 0 {
457            return Err(LevelZeroError::InvalidArgument(
458                "fill size must be a multiple of the pattern length".into(),
459            ));
460        }
461        self.commands.push(RecordedCommand::MemoryFill {
462            dst,
463            bytes,
464            pattern_len,
465        });
466        Ok(())
467    }
468
469    /// Append a kernel launch with the given grid and optional signal event.
470    pub fn append_launch_kernel(
471        &mut self,
472        kernel: impl Into<String>,
473        groups: GroupCount,
474        signal_event: Option<u32>,
475    ) -> LevelZeroResult<()> {
476        self.ensure_recording()?;
477        if groups.total() == 0 {
478            return Err(LevelZeroError::InvalidArgument(
479                "kernel launch grid must contain at least one workgroup".into(),
480            ));
481        }
482        self.commands.push(RecordedCommand::LaunchKernel {
483            kernel: kernel.into(),
484            groups,
485            signal_event,
486        });
487        Ok(())
488    }
489
490    /// Append an execution/event barrier.
491    pub fn append_barrier(&mut self, barrier: Barrier) -> LevelZeroResult<()> {
492        self.ensure_recording()?;
493        self.commands.push(RecordedCommand::Barrier(barrier));
494        Ok(())
495    }
496
497    /// Close the list, sealing it for submission.
498    pub fn close(&mut self) -> LevelZeroResult<()> {
499        self.ensure_recording()?;
500        self.state = CommandListState::Closed;
501        Ok(())
502    }
503
504    /// Reset the list back to the recording state, discarding all commands.
505    pub fn reset(&mut self) {
506        self.commands.clear();
507        self.state = CommandListState::Recording;
508    }
509
510    /// Number of kernel-launch commands recorded.
511    #[must_use]
512    pub fn launch_count(&self) -> usize {
513        self.commands
514            .iter()
515            .filter(|c| matches!(c, RecordedCommand::LaunchKernel { .. }))
516            .count()
517    }
518}
519
520// ─── Reusable command list ───────────────────────────────────
521
522/// A command list designed for repeated reset-and-re-record launch loops.
523///
524/// Mirrors the L0 §3.5.6 pattern: record once, submit, then `zeCommandListReset`
525/// and re-record for the next iteration *without re-allocating* the list. The
526/// [`reuse_count`](Self::reuse_count) tracks how many times the underlying list
527/// has been recycled, which a backend uses to amortise allocation cost.
528#[derive(Debug, Clone)]
529pub struct ReusableCommandList {
530    inner: CommandListRecorder,
531    reuse_count: u64,
532}
533
534impl ReusableCommandList {
535    /// Create a reusable list targeting queue-group `ordinal`.
536    #[must_use]
537    pub fn new(ordinal: u32) -> Self {
538        Self {
539            inner: CommandListRecorder::new(ordinal),
540            reuse_count: 0,
541        }
542    }
543
544    /// Mutable access to the underlying recorder for the current iteration.
545    pub fn recorder_mut(&mut self) -> &mut CommandListRecorder {
546        &mut self.inner
547    }
548
549    /// Shared access to the underlying recorder.
550    #[must_use]
551    pub fn recorder(&self) -> &CommandListRecorder {
552        &self.inner
553    }
554
555    /// How many times the list has been reset for reuse.
556    #[must_use]
557    pub fn reuse_count(&self) -> u64 {
558        self.reuse_count
559    }
560
561    /// Reset for the next iteration, incrementing the reuse counter.
562    ///
563    /// Returns [`LevelZeroError::CommandListError`] if the list was never
564    /// closed (resetting an open recording would lose un-submitted work).
565    pub fn recycle(&mut self) -> LevelZeroResult<()> {
566        if self.inner.state() != CommandListState::Closed {
567            return Err(LevelZeroError::CommandListError(
568                "close the command list before recycling it".into(),
569            ));
570        }
571        self.inner.reset();
572        self.reuse_count += 1;
573        Ok(())
574    }
575}
576
577// ─── Tests ───────────────────────────────────────────────────
578
579#[cfg(test)]
580mod tests {
581    use super::*;
582
583    #[test]
584    fn group_count_total() {
585        let g = GroupCount { x: 4, y: 2, z: 3 };
586        assert_eq!(g.total(), 24);
587    }
588
589    #[test]
590    fn planner_1d_ceil_div() {
591        let p = GroupCountPlanner::new_1d(256).unwrap();
592        assert_eq!(p.local_size(), (256, 1, 1));
593        // 1000 / 256 = ceil 4.
594        assert_eq!(p.plan_1d(1000).unwrap(), GroupCount { x: 4, y: 1, z: 1 });
595        // Exact multiple → no overhang.
596        assert_eq!(p.plan_1d(512).unwrap(), GroupCount { x: 2, y: 1, z: 1 });
597        // Zero work-items → zero groups.
598        assert_eq!(p.plan_1d(0).unwrap(), GroupCount { x: 0, y: 1, z: 1 });
599    }
600
601    #[test]
602    fn planner_3d_per_dimension() {
603        let p = GroupCountPlanner::new_3d(16, 16, 1).unwrap();
604        let g = p.plan_3d(100, 50, 8).unwrap();
605        assert_eq!(g.x, 7); // ceil(100/16)
606        assert_eq!(g.y, 4); // ceil(50/16)
607        assert_eq!(g.z, 8); // ceil(8/1)
608    }
609
610    #[test]
611    fn planner_rejects_zero_local_and_overflow() {
612        assert!(GroupCountPlanner::new_3d(0, 1, 1).is_err());
613        let p = GroupCountPlanner::new_1d(1).unwrap();
614        // 2^33 work-items / local 1 overflows u32 group count.
615        assert!(matches!(
616            p.plan_1d(1u64 << 33),
617            Err(LevelZeroError::InvalidArgument(_))
618        ));
619    }
620
621    #[test]
622    fn queue_group_selection() {
623        let table = CommandQueueGroupTable::new(vec![
624            CommandQueueGroupInfo {
625                ordinal: 0,
626                compute: true,
627                copy: true,
628                num_queues: 4,
629            },
630            CommandQueueGroupInfo {
631                ordinal: 1,
632                compute: false,
633                copy: true,
634                num_queues: 1,
635            },
636        ]);
637        assert_eq!(table.select_compute_ordinal().unwrap(), 0);
638        // Dedicated copy engine (ordinal 1) preferred for copies.
639        assert_eq!(table.select_copy_ordinal().unwrap(), 1);
640        assert!(table.has_dedicated_copy_engine());
641        assert_eq!(table.groups().len(), 2);
642    }
643
644    #[test]
645    fn queue_group_fallback_to_shared_copy() {
646        let table = CommandQueueGroupTable::new(vec![CommandQueueGroupInfo {
647            ordinal: 0,
648            compute: true,
649            copy: true,
650            num_queues: 1,
651        }]);
652        // No dedicated engine → falls back to the compute+copy group.
653        assert_eq!(table.select_copy_ordinal().unwrap(), 0);
654        assert!(!table.has_dedicated_copy_engine());
655    }
656
657    #[test]
658    fn queue_group_errors_when_missing_capability() {
659        let copy_only = CommandQueueGroupTable::new(vec![CommandQueueGroupInfo {
660            ordinal: 0,
661            compute: false,
662            copy: true,
663            num_queues: 1,
664        }]);
665        assert!(matches!(
666            copy_only.select_compute_ordinal(),
667            Err(LevelZeroError::Unsupported(_))
668        ));
669
670        let compute_only = CommandQueueGroupTable::new(vec![CommandQueueGroupInfo {
671            ordinal: 0,
672            compute: true,
673            copy: false,
674            num_queues: 1,
675        }]);
676        assert!(matches!(
677            compute_only.select_copy_ordinal(),
678            Err(LevelZeroError::Unsupported(_))
679        ));
680    }
681
682    #[test]
683    fn event_pool_allocation() {
684        let mut pool = EventPoolDesc::new(2, EventScope::HostVisible).unwrap();
685        assert!(pool.is_host_visible());
686        assert_eq!(pool.scope.ze_flags(), 0x1);
687        assert_eq!(pool.remaining(), 2);
688        assert_eq!(pool.alloc_event().unwrap(), 0);
689        assert_eq!(pool.alloc_event().unwrap(), 1);
690        assert_eq!(pool.remaining(), 0);
691        assert!(matches!(
692            pool.alloc_event(),
693            Err(LevelZeroError::OutOfMemory)
694        ));
695    }
696
697    #[test]
698    fn event_pool_rejects_zero_capacity() {
699        assert!(EventPoolDesc::new(0, EventScope::Device).is_err());
700        assert_eq!(EventScope::KernelTimestamp.ze_flags(), 0x2);
701        assert_eq!(EventScope::Device.ze_flags(), 0);
702    }
703
704    #[test]
705    fn barrier_constructors() {
706        let g = Barrier::global();
707        assert!(g.signal_event.is_none());
708        assert!(g.wait_events.is_empty());
709
710        let s = Barrier::signalling(3);
711        assert_eq!(s.signal_event, Some(3));
712
713        let w = Barrier::waiting(vec![1, 2]);
714        assert_eq!(w.wait_events, vec![1, 2]);
715        assert!(w.signal_event.is_none());
716    }
717
718    #[test]
719    fn recorder_records_in_order() {
720        let mut rec = CommandListRecorder::new(0);
721        assert_eq!(rec.ordinal(), 0);
722        assert!(rec.is_empty());
723        assert_eq!(rec.state(), CommandListState::Recording);
724
725        rec.append_memory_copy(2, 1, 256).unwrap();
726        rec.append_launch_kernel("gemm_f32", GroupCount { x: 4, y: 1, z: 1 }, Some(0))
727            .unwrap();
728        rec.append_barrier(Barrier::global()).unwrap();
729        assert_eq!(rec.len(), 3);
730        assert_eq!(rec.launch_count(), 1);
731
732        match &rec.commands()[0] {
733            RecordedCommand::MemoryCopy { dst, src, bytes } => {
734                assert_eq!((*dst, *src, *bytes), (2, 1, 256));
735            }
736            other => panic!("expected MemoryCopy, got {other:?}"),
737        }
738    }
739
740    #[test]
741    fn recorder_validates_args() {
742        let mut rec = CommandListRecorder::new(0);
743        assert!(rec.append_memory_copy(1, 0, 0).is_err());
744        assert!(rec.append_memory_fill(1, 10, 0).is_err());
745        assert!(
746            rec.append_memory_fill(1, 10, 4).is_err(),
747            "10 not a multiple of 4"
748        );
749        rec.append_memory_fill(1, 12, 4).unwrap();
750        assert!(
751            rec.append_launch_kernel("k", GroupCount { x: 0, y: 1, z: 1 }, None)
752                .is_err()
753        );
754    }
755
756    #[test]
757    fn recorder_close_blocks_append_until_reset() {
758        let mut rec = CommandListRecorder::new(0);
759        rec.append_memory_copy(2, 1, 128).unwrap();
760        rec.close().unwrap();
761        assert_eq!(rec.state(), CommandListState::Closed);
762        assert!(matches!(
763            rec.append_memory_copy(3, 1, 64),
764            Err(LevelZeroError::CommandListError(_))
765        ));
766        // Double close is rejected.
767        assert!(rec.close().is_err());
768
769        rec.reset();
770        assert_eq!(rec.state(), CommandListState::Recording);
771        assert!(rec.is_empty());
772        rec.append_memory_copy(3, 1, 64).unwrap();
773    }
774
775    #[test]
776    fn reusable_command_list_recycle_loop() {
777        let mut list = ReusableCommandList::new(0);
778        assert_eq!(list.reuse_count(), 0);
779
780        for expected in 0..3u64 {
781            assert_eq!(list.reuse_count(), expected);
782            let rec = list.recorder_mut();
783            rec.append_launch_kernel("k", GroupCount { x: 1, y: 1, z: 1 }, None)
784                .unwrap();
785            rec.close().unwrap();
786            list.recycle().unwrap();
787        }
788        assert_eq!(list.reuse_count(), 3);
789        // After recycle the recorder is empty and recording again.
790        assert!(list.recorder().is_empty());
791        assert_eq!(list.recorder().state(), CommandListState::Recording);
792    }
793
794    #[test]
795    fn reusable_rejects_recycle_while_open() {
796        let mut list = ReusableCommandList::new(0);
797        list.recorder_mut()
798            .append_launch_kernel("k", GroupCount { x: 1, y: 1, z: 1 }, None)
799            .unwrap();
800        // Not closed yet → recycle must fail to avoid losing work.
801        assert!(matches!(
802            list.recycle(),
803            Err(LevelZeroError::CommandListError(_))
804        ));
805        assert_eq!(list.reuse_count(), 0);
806    }
807}