Skip to main content

oxiphysics_gpu/compute/
functions.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use super::types::{
6    BufferBinding, BufferHandle, BufferId, BufferUsage, PipelineBarrier, WarpDivergenceRecord,
7};
8
9/// Trait for a compute backend (GPU or CPU fallback).
10pub trait ComputeBackend {
11    /// Human-readable name of this backend.
12    fn name(&self) -> &str;
13    /// Allocate a buffer that can hold `size` f64 elements.
14    fn create_buffer(&self, size: usize) -> BufferHandle;
15    /// Write `data` into the buffer referenced by `handle`.
16    fn write_buffer(&self, handle: BufferHandle, data: &[f64]);
17    /// Read the full contents of the buffer referenced by `handle`.
18    fn read_buffer(&self, handle: BufferHandle) -> Vec<f64>;
19    /// Dispatch a compute kernel over `work_size` work items.
20    fn dispatch(&self, kernel: &dyn ComputeKernel, work_size: usize);
21}
22/// Trait for a compute kernel that can be dispatched on a backend.
23pub trait ComputeKernel {
24    /// Human-readable name of this kernel.
25    fn name(&self) -> &str;
26    /// Execute the kernel over `work_size` work items.
27    ///
28    /// * `inputs`  – read-only input slices.
29    /// * `outputs` – mutable output vectors (pre-allocated by the caller).
30    /// * `work_size` – number of logical work items.
31    fn execute(&self, inputs: &[&[f64]], outputs: &mut [Vec<f64>], work_size: usize);
32}
33/// Compute the number of workgroups needed for a 1D dispatch.
34pub fn compute_num_workgroups(total_items: u32, workgroup_size: u32) -> u32 {
35    total_items.div_ceil(workgroup_size)
36}
37/// Compute workgroup counts for a 3D dispatch.
38pub fn compute_num_workgroups_3d(total: [u32; 3], workgroup_size: [u32; 3]) -> [u32; 3] {
39    [
40        total[0].div_ceil(workgroup_size[0]),
41        total[1].div_ceil(workgroup_size[1]),
42        total[2].div_ceil(workgroup_size[2]),
43    ]
44}
45/// Determine the required pipeline barrier between two kernel passes.
46///
47/// If the output buffers of pass A overlap with the input buffers of pass B,
48/// a read-after-write barrier is needed.
49pub fn required_barrier(
50    pass_a_outputs: &[BufferId],
51    pass_b_inputs: &[BufferId],
52) -> PipelineBarrier {
53    let overlap = pass_a_outputs.iter().any(|out| pass_b_inputs.contains(out));
54    if overlap {
55        PipelineBarrier::StorageReadAfterWrite
56    } else {
57        PipelineBarrier::None
58    }
59}
60/// Detect whether any buffers in a pass alias the same storage.
61///
62/// Two bindings alias if they reference the same `BufferId` with incompatible
63/// usages (e.g., one is write and the other is read in the same pass).
64pub fn detect_aliasing(bindings: &[BufferBinding]) -> Vec<(u32, u32)> {
65    let mut conflicts = Vec::new();
66    for i in 0..bindings.len() {
67        for j in (i + 1)..bindings.len() {
68            if bindings[i].buffer_id == bindings[j].buffer_id {
69                let write_i = matches!(
70                    bindings[i].usage,
71                    BufferUsage::WriteOnly | BufferUsage::ReadWrite
72                );
73                let read_j = matches!(
74                    bindings[j].usage,
75                    BufferUsage::ReadOnly | BufferUsage::ReadWrite
76                );
77                let write_j = matches!(
78                    bindings[j].usage,
79                    BufferUsage::WriteOnly | BufferUsage::ReadWrite
80                );
81                let read_i = matches!(
82                    bindings[i].usage,
83                    BufferUsage::ReadOnly | BufferUsage::ReadWrite
84                );
85                if write_i && read_j || write_j && read_i {
86                    conflicts.push((bindings[i].binding, bindings[j].binding));
87                }
88            }
89        }
90    }
91    conflicts
92}
93/// Simulate warp divergence by analysing a boolean predicate over work items.
94///
95/// Groups work items into warps and checks if all threads take the same branch.
96/// Returns a [`WarpDivergenceRecord`].
97pub fn analyse_warp_divergence(predicates: &[bool], warp_size: usize) -> WarpDivergenceRecord {
98    if predicates.is_empty() || warp_size == 0 {
99        return WarpDivergenceRecord::default();
100    }
101    let mut total = 0u64;
102    let mut divergent = 0u64;
103    let n_warps = predicates.len().div_ceil(warp_size);
104    for w in 0..n_warps {
105        let start = w * warp_size;
106        let end = (start + warp_size).min(predicates.len());
107        let slice = &predicates[start..end];
108        total += 1;
109        let all_true = slice.iter().all(|&v| v);
110        let all_false = slice.iter().all(|&v| !v);
111        if !all_true && !all_false {
112            divergent += 1;
113        }
114    }
115    WarpDivergenceRecord {
116        total_branches: total,
117        divergent_branches: divergent,
118    }
119}
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use crate::CpuBackend;
124    use crate::compute::ComputeDispatcher;
125    use crate::compute::ComputePass;
126    use crate::compute::GpuBuffer;
127    use crate::compute::GpuCommand;
128    use crate::compute::GpuCommandEncoder;
129    use crate::compute::GpuError;
130    use crate::compute::KernelSpec;
131    use crate::compute::MemoryBandwidthModel;
132    use crate::compute::OccupancyModel;
133    use crate::compute::ResourceLifecycle;
134    use crate::compute::TimelineSemaphore;
135    #[test]
136    fn cpu_backend_buffer_roundtrip() {
137        let backend = CpuBackend::new();
138        let buf = backend.create_buffer(4);
139        backend.write_buffer(buf, &[1.0, 2.0, 3.0, 4.0]);
140        let data = backend.read_buffer(buf);
141        assert_eq!(data, vec![1.0, 2.0, 3.0, 4.0]);
142    }
143    #[test]
144    fn dispatcher_buffer_write_read_roundtrip() {
145        let mut d = ComputeDispatcher::new();
146        let id = d.create_buffer(5, None);
147        d.write_buffer(id, &[1.0, 2.0, 3.0, 4.0, 5.0]).unwrap();
148        let out = d.read_buffer(id).unwrap();
149        assert_eq!(out, vec![1.0, 2.0, 3.0, 4.0, 5.0]);
150    }
151    #[test]
152    fn dispatcher_buffer_initial_data() {
153        let mut d = ComputeDispatcher::new();
154        let id = d.create_buffer(3, Some(&[10.0, 20.0, 30.0]));
155        let out = d.read_buffer(id).unwrap();
156        assert_eq!(out, vec![10.0, 20.0, 30.0]);
157    }
158    #[test]
159    fn dispatcher_invalid_buffer_read_errors() {
160        let d = ComputeDispatcher::new();
161        let bad_id = BufferId(99);
162        assert_eq!(d.read_buffer(bad_id), Err(GpuError::InvalidBuffer(bad_id)));
163    }
164    #[test]
165    fn dispatch_map_identity() {
166        let mut d = ComputeDispatcher::new();
167        let src = d.create_buffer(4, Some(&[1.0, 2.0, 3.0, 4.0]));
168        let dst = d.create_buffer(4, None);
169        d.dispatch_map(src, dst, |x| x).unwrap();
170        assert_eq!(d.read_buffer(dst).unwrap(), vec![1.0, 2.0, 3.0, 4.0]);
171    }
172    #[test]
173    fn dispatch_map_scale_by_two() {
174        let mut d = ComputeDispatcher::new();
175        let src = d.create_buffer(3, Some(&[1.0, 2.0, 3.0]));
176        let dst = d.create_buffer(3, None);
177        d.dispatch_map(src, dst, |x| x * 2.0).unwrap();
178        assert_eq!(d.read_buffer(dst).unwrap(), vec![2.0, 4.0, 6.0]);
179    }
180    #[test]
181    fn dispatch_reduce_sum() {
182        let mut d = ComputeDispatcher::new();
183        let id = d.create_buffer(5, Some(&[1.0, 2.0, 3.0, 4.0, 5.0]));
184        let sum = d.dispatch_reduce(id, |a, b| a + b).unwrap();
185        assert!((sum - 15.0).abs() < 1e-12);
186    }
187    #[test]
188    fn dispatch_reduce_max() {
189        let mut d = ComputeDispatcher::new();
190        let id = d.create_buffer(5, Some(&[3.0, 1.0, 7.0, 2.0, 5.0]));
191        let max = d.dispatch_reduce(id, f64::max).unwrap();
192        assert!((max - 7.0).abs() < 1e-12);
193    }
194    #[test]
195    fn dispatch_reduce_empty_errors() {
196        let mut d = ComputeDispatcher::new();
197        let id = d.create_buffer(0, None);
198        assert_eq!(
199            d.dispatch_reduce(id, |a, b| a + b),
200            Err(GpuError::EmptyBuffer)
201        );
202    }
203    #[test]
204    fn sph_density_single_particle_self_contribution_positive() {
205        let mut d = ComputeDispatcher::new();
206        let pos = d.create_buffer(3, Some(&[0.0, 0.0, 0.0]));
207        let mass = d.create_buffer(1, Some(&[1.0]));
208        let out = d.create_buffer(1, None);
209        d.dispatch_sph_density(pos, mass, 1.0, out).unwrap();
210        let density = d.read_buffer(out).unwrap();
211        assert_eq!(density.len(), 1);
212        assert!((density[0] - 1.0).abs() < 1e-12);
213    }
214    #[test]
215    fn sph_density_two_particles_within_kernel_positive() {
216        let mut d = ComputeDispatcher::new();
217        let pos = d.create_buffer(6, Some(&[0.0, 0.0, 0.0, 0.5, 0.0, 0.0]));
218        let mass = d.create_buffer(2, Some(&[1.0, 1.0]));
219        let out = d.create_buffer(2, None);
220        d.dispatch_sph_density(pos, mass, 2.0, out).unwrap();
221        let density = d.read_buffer(out).unwrap();
222        assert_eq!(density.len(), 2);
223        assert!(
224            density[0] > 0.0,
225            "density[0] should be positive: {}",
226            density[0]
227        );
228        assert!(
229            density[1] > 0.0,
230            "density[1] should be positive: {}",
231            density[1]
232        );
233    }
234    #[test]
235    fn sph_density_particles_outside_kernel_zero_cross_contribution() {
236        let mut d = ComputeDispatcher::new();
237        let pos = d.create_buffer(6, Some(&[0.0, 0.0, 0.0, 100.0, 0.0, 0.0]));
238        let mass = d.create_buffer(2, Some(&[1.0, 1.0]));
239        let out = d.create_buffer(2, None);
240        d.dispatch_sph_density(pos, mass, 1.0, out).unwrap();
241        let density = d.read_buffer(out).unwrap();
242        assert!((density[0] - 1.0).abs() < 1e-12);
243        assert!((density[1] - 1.0).abs() < 1e-12);
244    }
245    #[test]
246    fn kernel_spec_creation() {
247        let b0 = BufferId(0);
248        let b1 = BufferId(1);
249        let spec = KernelSpec::new("sph_density", 64, vec![b0, b1]);
250        assert_eq!(spec.name, "sph_density");
251        assert_eq!(spec.workgroup_size, [64, 1, 1]);
252        assert_eq!(spec.buffer_bindings.len(), 2);
253    }
254    #[test]
255    fn gpu_buffer_new_zeros() {
256        let buf = GpuBuffer::new(8);
257        assert_eq!(buf.size, 8);
258        assert!(buf.data.iter().all(|&v| v == 0.0));
259    }
260    #[test]
261    fn test_buffer_binding_shorthands() {
262        let id = BufferId(5);
263        let br = BufferBinding::read(0, id);
264        assert_eq!(br.usage, BufferUsage::ReadOnly);
265        let bw = BufferBinding::write(1, id);
266        assert_eq!(bw.usage, BufferUsage::WriteOnly);
267        let brw = BufferBinding::read_write(2, id);
268        assert_eq!(brw.usage, BufferUsage::ReadWrite);
269        let bu = BufferBinding::uniform(3, id);
270        assert_eq!(bu.usage, BufferUsage::Uniform);
271    }
272    #[test]
273    fn test_kernel_spec_3d_workgroup() {
274        let spec = KernelSpec::with_workgroup_3d("test", [8, 8, 4], vec![]);
275        assert_eq!(spec.workgroup_size, [8, 8, 4]);
276        assert_eq!(spec.threads_per_workgroup(), 256);
277    }
278    #[test]
279    fn test_kernel_spec_num_workgroups() {
280        let spec = KernelSpec::new("test", 64, vec![]);
281        assert_eq!(spec.num_workgroups_x(100), 2);
282        assert_eq!(spec.num_workgroups_x(64), 1);
283        assert_eq!(spec.num_workgroups_x(65), 2);
284    }
285    #[test]
286    fn test_gpu_buffer_fill_and_clear() {
287        let mut buf = GpuBuffer::new(5);
288        buf.fill(42.0);
289        assert!(buf.data.iter().all(|&v| (v - 42.0).abs() < 1e-12));
290        buf.clear();
291        assert!(buf.data.iter().all(|&v| v == 0.0));
292    }
293    #[test]
294    fn test_gpu_buffer_byte_size() {
295        let buf = GpuBuffer::new(10);
296        assert_eq!(buf.byte_size(), 80);
297    }
298    #[test]
299    fn test_gpu_buffer_as_slice() {
300        let buf = GpuBuffer::from_data(vec![1.0, 2.0, 3.0]);
301        assert_eq!(buf.as_slice(), &[1.0, 2.0, 3.0]);
302    }
303    #[test]
304    fn test_cpu_backend_num_buffers() {
305        let backend = CpuBackend::new();
306        assert_eq!(backend.num_buffers(), 0);
307        backend.create_buffer(10);
308        assert_eq!(backend.num_buffers(), 1);
309        backend.create_buffer(5);
310        assert_eq!(backend.num_buffers(), 2);
311    }
312    #[test]
313    fn test_cpu_backend_total_elements() {
314        let backend = CpuBackend::new();
315        backend.create_buffer(10);
316        backend.create_buffer(5);
317        assert_eq!(backend.total_elements(), 15);
318    }
319    #[test]
320    fn test_dispatcher_num_buffers() {
321        let mut d = ComputeDispatcher::new();
322        assert_eq!(d.num_buffers(), 0);
323        d.create_buffer(5, None);
324        assert_eq!(d.num_buffers(), 1);
325    }
326    #[test]
327    fn test_dispatcher_has_buffer() {
328        let mut d = ComputeDispatcher::new();
329        let id = d.create_buffer(5, None);
330        assert!(d.has_buffer(id));
331        assert!(!d.has_buffer(BufferId(999)));
332    }
333    #[test]
334    fn test_dispatcher_buffer_size() {
335        let mut d = ComputeDispatcher::new();
336        let id = d.create_buffer(7, None);
337        assert_eq!(d.buffer_size(id).unwrap(), 7);
338    }
339    #[test]
340    fn test_dispatcher_destroy_buffer() {
341        let mut d = ComputeDispatcher::new();
342        let id = d.create_buffer(5, None);
343        assert!(d.has_buffer(id));
344        d.destroy_buffer(id).unwrap();
345        assert!(!d.has_buffer(id));
346    }
347    #[test]
348    fn test_dispatcher_destroy_invalid_buffer_errors() {
349        let mut d = ComputeDispatcher::new();
350        assert_eq!(
351            d.destroy_buffer(BufferId(42)),
352            Err(GpuError::InvalidBuffer(BufferId(42)))
353        );
354    }
355    #[test]
356    fn test_dispatcher_copy_buffer() {
357        let mut d = ComputeDispatcher::new();
358        let src = d.create_buffer(3, Some(&[1.0, 2.0, 3.0]));
359        let dst = d.create_buffer(3, None);
360        d.copy_buffer(src, dst).unwrap();
361        assert_eq!(d.read_buffer(dst).unwrap(), vec![1.0, 2.0, 3.0]);
362    }
363    #[test]
364    fn test_dispatcher_copy_buffer_size_mismatch() {
365        let mut d = ComputeDispatcher::new();
366        let src = d.create_buffer(3, Some(&[1.0, 2.0, 3.0]));
367        let dst = d.create_buffer(5, None);
368        assert!(d.copy_buffer(src, dst).is_err());
369    }
370    #[test]
371    fn test_dispatch_map_indexed() {
372        let mut d = ComputeDispatcher::new();
373        let src = d.create_buffer(4, Some(&[10.0, 20.0, 30.0, 40.0]));
374        let dst = d.create_buffer(4, None);
375        d.dispatch_map_indexed(src, dst, |i, x| x + i as f64)
376            .unwrap();
377        assert_eq!(d.read_buffer(dst).unwrap(), vec![10.0, 21.0, 32.0, 43.0]);
378    }
379    #[test]
380    fn test_dispatch_zip_map() {
381        let mut d = ComputeDispatcher::new();
382        let a = d.create_buffer(3, Some(&[1.0, 2.0, 3.0]));
383        let b = d.create_buffer(3, Some(&[10.0, 20.0, 30.0]));
384        let out = d.create_buffer(3, None);
385        d.dispatch_zip_map(a, b, out, |x, y| x + y).unwrap();
386        assert_eq!(d.read_buffer(out).unwrap(), vec![11.0, 22.0, 33.0]);
387    }
388    #[test]
389    fn test_compute_pass_recording() {
390        let mut pass = ComputePass::new();
391        assert_eq!(pass.num_commands(), 0);
392        pass.dispatch("density", 1000);
393        pass.dispatch("force", 1000);
394        pass.dispatch("integrate", 1000);
395        assert_eq!(pass.num_commands(), 3);
396        assert_eq!(pass.total_work_items(), 3000);
397        assert_eq!(pass.commands()[0].0, "density");
398        assert_eq!(pass.commands()[1].1, 1000);
399    }
400    #[test]
401    fn test_compute_pass_clear() {
402        let mut pass = ComputePass::new();
403        pass.dispatch("test", 100);
404        assert_eq!(pass.num_commands(), 1);
405        pass.clear();
406        assert_eq!(pass.num_commands(), 0);
407    }
408    #[test]
409    fn test_resource_lifecycle_tracking() {
410        let mut lifecycle = ResourceLifecycle::new();
411        assert!(lifecycle.is_empty());
412        let id = BufferId(0);
413        lifecycle.record_create(id, 100);
414        lifecycle.record_write(id);
415        lifecycle.record_write(id);
416        lifecycle.record_read(id);
417        assert_eq!(lifecycle.len(), 4);
418        assert_eq!(lifecycle.count_writes(id), 2);
419        assert_eq!(lifecycle.count_reads(id), 1);
420    }
421    #[test]
422    fn test_resource_lifecycle_clear() {
423        let mut lifecycle = ResourceLifecycle::new();
424        lifecycle.record_create(BufferId(0), 10);
425        lifecycle.clear();
426        assert!(lifecycle.is_empty());
427    }
428    #[test]
429    fn test_compute_num_workgroups() {
430        assert_eq!(compute_num_workgroups(100, 64), 2);
431        assert_eq!(compute_num_workgroups(64, 64), 1);
432        assert_eq!(compute_num_workgroups(1, 64), 1);
433    }
434    #[test]
435    fn test_compute_num_workgroups_3d() {
436        let wg = compute_num_workgroups_3d([100, 100, 100], [8, 8, 8]);
437        assert_eq!(wg, [13, 13, 13]);
438    }
439    #[test]
440    fn test_gpu_error_display() {
441        let e = GpuError::InvalidBuffer(BufferId(5));
442        assert!(format!("{e}").contains("5"));
443        let e2 = GpuError::SizeMismatch {
444            expected: 10,
445            got: 5,
446        };
447        assert!(format!("{e2}").contains("10"));
448        let e3 = GpuError::EmptyBuffer;
449        assert!(format!("{e3}").contains("empty"));
450        let e4 = GpuError::NotFound("test".to_string());
451        assert!(format!("{e4}").contains("test"));
452    }
453    #[test]
454    fn test_command_encoder_basic() {
455        let mut enc = GpuCommandEncoder::new("test_pass");
456        assert_eq!(enc.label(), "test_pass");
457        assert_eq!(enc.command_count(), 0);
458        enc.dispatch_compute("density", [64, 1, 1]);
459        enc.dispatch_compute("force", [64, 1, 1]);
460        enc.insert_barrier(PipelineBarrier::StorageReadAfterWrite);
461        assert_eq!(enc.command_count(), 3);
462    }
463    #[test]
464    fn test_command_encoder_reset() {
465        let mut enc = GpuCommandEncoder::new("enc");
466        enc.dispatch_compute("k", [1, 1, 1]);
467        enc.reset();
468        assert_eq!(enc.command_count(), 0);
469    }
470    #[test]
471    fn test_command_encoder_submit_copies() {
472        let mut enc = GpuCommandEncoder::new("enc");
473        let mut d = ComputeDispatcher::new();
474        let src = d.create_buffer(3, Some(&[1.0, 2.0, 3.0]));
475        let dst = d.create_buffer(3, None);
476        enc.copy_buffer(src, dst, 3);
477        enc.submit(&mut d).unwrap();
478        assert_eq!(d.read_buffer(dst).unwrap(), vec![1.0, 2.0, 3.0]);
479    }
480    #[test]
481    fn test_command_encoder_push_constant() {
482        let mut enc = GpuCommandEncoder::new("enc");
483        enc.push_constant("dt", 0.001);
484        assert_eq!(enc.command_count(), 1);
485        match &enc.commands()[0] {
486            GpuCommand::PushConstant { name, value } => {
487                assert_eq!(name, "dt");
488                assert!((value - 0.001).abs() < 1e-15);
489            }
490            _ => panic!("expected PushConstant"),
491        }
492    }
493    #[test]
494    fn test_required_barrier_overlap() {
495        let a_out = vec![BufferId(0), BufferId(1)];
496        let b_in = vec![BufferId(1), BufferId(2)];
497        let barrier = required_barrier(&a_out, &b_in);
498        assert_eq!(barrier, PipelineBarrier::StorageReadAfterWrite);
499    }
500    #[test]
501    fn test_required_barrier_no_overlap() {
502        let a_out = vec![BufferId(0)];
503        let b_in = vec![BufferId(5)];
504        let barrier = required_barrier(&a_out, &b_in);
505        assert_eq!(barrier, PipelineBarrier::None);
506    }
507    #[test]
508    fn test_detect_aliasing_conflict() {
509        let bindings = vec![
510            BufferBinding::write(0, BufferId(10)),
511            BufferBinding::read(1, BufferId(10)),
512        ];
513        let conflicts = detect_aliasing(&bindings);
514        assert!(!conflicts.is_empty(), "should detect aliasing conflict");
515    }
516    #[test]
517    fn test_detect_aliasing_no_conflict() {
518        let bindings = vec![
519            BufferBinding::read(0, BufferId(10)),
520            BufferBinding::read(1, BufferId(11)),
521        ];
522        let conflicts = detect_aliasing(&bindings);
523        assert!(conflicts.is_empty(), "no conflict expected");
524    }
525    #[test]
526    fn test_detect_aliasing_same_buffer_two_reads() {
527        let bindings = vec![
528            BufferBinding::read(0, BufferId(5)),
529            BufferBinding::read(1, BufferId(5)),
530        ];
531        let conflicts = detect_aliasing(&bindings);
532        assert!(conflicts.is_empty());
533    }
534    #[test]
535    fn test_timeline_semaphore_signal_and_wait() {
536        let mut sem = TimelineSemaphore::new();
537        assert_eq!(sem.current_value(), 0);
538        sem.signal(1);
539        assert_eq!(sem.current_value(), 1);
540        assert!(sem.wait(1));
541        assert!(!sem.wait(2));
542        sem.signal(3);
543        assert!(sem.wait(3));
544        assert_eq!(sem.signal_count(), 2);
545    }
546    #[test]
547    fn test_timeline_semaphore_default() {
548        let sem = TimelineSemaphore::default();
549        assert_eq!(sem.current_value(), 0);
550    }
551    #[test]
552    fn test_occupancy_full_when_unconstrained() {
553        let model = OccupancyModel::mid_range();
554        let occ = model.estimate_occupancy(64, 0, 32);
555        assert!(
556            occ > 0.5,
557            "occupancy should be high for small workgroup, got {occ}"
558        );
559    }
560    #[test]
561    fn test_occupancy_limited_by_shared_memory() {
562        let model = OccupancyModel::mid_range();
563        let occ = model.estimate_occupancy(64, model.shared_mem_per_cu, 1);
564        let occ_limited = model.estimate_occupancy(64, model.shared_mem_per_cu / 2, 1);
565        assert!(
566            occ <= occ_limited,
567            "more smem usage should give lower or equal occupancy"
568        );
569    }
570    #[test]
571    fn test_occupancy_bounded_to_one() {
572        let model = OccupancyModel::mid_range();
573        let occ = model.estimate_occupancy(1, 0, 0);
574        assert!((0.0..=1.0).contains(&occ));
575    }
576    #[test]
577    fn test_peak_gflops_positive() {
578        let model = OccupancyModel::mid_range();
579        let gflops = model.peak_gflops(1500.0);
580        assert!(gflops > 0.0);
581    }
582    #[test]
583    fn test_warp_divergence_none() {
584        let predicates = vec![true; 32];
585        let rec = analyse_warp_divergence(&predicates, 32);
586        assert_eq!(rec.divergent_branches, 0);
587        assert!((rec.divergence_rate()).abs() < 1e-12);
588    }
589    #[test]
590    fn test_warp_divergence_full() {
591        let predicates: Vec<bool> = (0..32).map(|i| i % 2 == 0).collect();
592        let rec = analyse_warp_divergence(&predicates, 32);
593        assert_eq!(rec.divergent_branches, 1);
594        assert!((rec.divergence_rate() - 1.0).abs() < 1e-12);
595    }
596    #[test]
597    fn test_warp_divergence_penalty() {
598        let rec = WarpDivergenceRecord {
599            total_branches: 10,
600            divergent_branches: 5,
601        };
602        let penalty = rec.performance_penalty(32);
603        assert!(
604            penalty > 1.0 && penalty < 2.0,
605            "penalty should be > 1, got {penalty}"
606        );
607    }
608    #[test]
609    fn test_warp_divergence_empty() {
610        let rec = analyse_warp_divergence(&[], 32);
611        assert_eq!(rec.total_branches, 0);
612        assert!((rec.divergence_rate()).abs() < 1e-12);
613    }
614    #[test]
615    fn test_memory_bandwidth_arithmetic_intensity() {
616        let intensity = MemoryBandwidthModel::arithmetic_intensity(1000.0, 100.0);
617        assert!((intensity - 10.0).abs() < 1e-12);
618    }
619    #[test]
620    fn test_memory_bandwidth_zero_bytes() {
621        let intensity = MemoryBandwidthModel::arithmetic_intensity(100.0, 0.0);
622        assert!(intensity.is_infinite());
623    }
624    #[test]
625    fn test_roofline_bandwidth_bound() {
626        let model = MemoryBandwidthModel::mid_range();
627        let perf = model.roofline_performance(0.1);
628        let expected = 0.1 * model.peak_bandwidth_gbs;
629        assert!(
630            (perf - expected).abs() < 1e-6,
631            "bandwidth-bound perf mismatch"
632        );
633    }
634    #[test]
635    fn test_roofline_compute_bound() {
636        let model = MemoryBandwidthModel::mid_range();
637        let perf = model.roofline_performance(1e9);
638        assert!((perf - model.peak_compute_gflops).abs() < 1e-6);
639    }
640    #[test]
641    fn test_is_bandwidth_bound() {
642        let model = MemoryBandwidthModel::mid_range();
643        let ridge = model.peak_compute_gflops / model.peak_bandwidth_gbs;
644        assert!(model.is_bandwidth_bound(ridge * 0.5));
645        assert!(!model.is_bandwidth_bound(ridge * 2.0));
646    }
647    #[test]
648    fn test_estimated_runtime_ms_positive() {
649        let model = MemoryBandwidthModel::mid_range();
650        let t = model.estimated_runtime_ms(1e12, 1e9);
651        assert!(t > 0.0 && t.is_finite());
652    }
653    #[test]
654    fn test_reduction_tree_sum() {
655        let mut d = ComputeDispatcher::new();
656        let buf = d.create_buffer(4, Some(&[1.0, 2.0, 3.0, 4.0]));
657        let result = d.dispatch_reduction_tree(buf).unwrap();
658        assert!(
659            (result - 10.0).abs() < 1e-12,
660            "sum should be 10, got {result}"
661        );
662    }
663    #[test]
664    fn test_reduction_tree_empty() {
665        let mut d = ComputeDispatcher::new();
666        let buf = d.create_buffer(0, Some(&[]));
667        let result = d.dispatch_reduction_tree(buf).unwrap();
668        assert_eq!(result, 0.0);
669    }
670    #[test]
671    fn test_reduction_tree_single_element() {
672        let mut d = ComputeDispatcher::new();
673        let buf = d.create_buffer(1, Some(&[42.0]));
674        let result = d.dispatch_reduction_tree(buf).unwrap();
675        assert!((result - 42.0).abs() < 1e-12);
676    }
677    #[test]
678    fn test_reduction_tree_power_of_two() {
679        let data: Vec<f64> = (1..=8).map(|x| x as f64).collect();
680        let mut d = ComputeDispatcher::new();
681        let buf = d.create_buffer(8, Some(&data));
682        let result = d.dispatch_reduction_tree(buf).unwrap();
683        assert!((result - 36.0).abs() < 1e-12, "1+2+…+8=36, got {result}");
684    }
685    #[test]
686    fn test_inclusive_scan_basic() {
687        let mut d = ComputeDispatcher::new();
688        let buf_in = d.create_buffer(4, Some(&[1.0, 2.0, 3.0, 4.0]));
689        let buf_out = d.create_buffer(4, None);
690        d.dispatch_inclusive_scan(buf_in, buf_out).unwrap();
691        let result = d.read_buffer(buf_out).unwrap();
692        let expected = [1.0, 3.0, 6.0, 10.0];
693        for (a, b) in result.iter().zip(expected.iter()) {
694            assert!((a - b).abs() < 1e-12, "mismatch: {a} vs {b}");
695        }
696    }
697    #[test]
698    fn test_inclusive_scan_single() {
699        let mut d = ComputeDispatcher::new();
700        let buf_in = d.create_buffer(1, Some(&[7.0]));
701        let buf_out = d.create_buffer(1, None);
702        d.dispatch_inclusive_scan(buf_in, buf_out).unwrap();
703        let result = d.read_buffer(buf_out).unwrap();
704        assert!((result[0] - 7.0).abs() < 1e-12);
705    }
706    #[test]
707    fn test_radix_sort_basic() {
708        let data = vec![5.0, 1.0, 3.0, 2.0, 4.0];
709        let mut d = ComputeDispatcher::new();
710        let buf = d.create_buffer(5, Some(&data));
711        let sorted = d.dispatch_radix_sort(buf).unwrap();
712        for w in sorted.windows(2) {
713            assert!(w[0] <= w[1], "not sorted: {} > {}", w[0], w[1]);
714        }
715    }
716    #[test]
717    fn test_radix_sort_empty() {
718        let mut d = ComputeDispatcher::new();
719        let buf = d.create_buffer(0, Some(&[]));
720        let sorted = d.dispatch_radix_sort(buf).unwrap();
721        assert!(sorted.is_empty());
722    }
723    #[test]
724    fn test_radix_sort_already_sorted() {
725        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
726        let mut d = ComputeDispatcher::new();
727        let buf = d.create_buffer(5, Some(&data));
728        let sorted = d.dispatch_radix_sort(buf).unwrap();
729        assert_eq!(sorted, data);
730    }
731    #[test]
732    fn test_radix_sort_length_preserved() {
733        let data: Vec<f64> = (0..16).map(|i| (16 - i) as f64).collect();
734        let mut d = ComputeDispatcher::new();
735        let buf = d.create_buffer(16, Some(&data));
736        let sorted = d.dispatch_radix_sort(buf).unwrap();
737        assert_eq!(sorted.len(), 16);
738    }
739}