1#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum BufferUsage {
19 Vertex,
21 Index,
23 Uniform,
25 Storage,
27 Staging,
29}
30
31#[derive(Debug, Clone)]
37pub struct ComputeBuffer {
38 pub data: Vec<f32>,
40 pub usage: BufferUsage,
42 pub label: String,
44}
45
46impl ComputeBuffer {
47 pub fn new(size: usize, usage: BufferUsage, label: &str) -> Self {
49 Self {
50 data: vec![0.0_f32; size],
51 usage,
52 label: label.to_owned(),
53 }
54 }
55
56 pub fn write_f32(&mut self, offset: usize, values: &[f32]) {
61 let end = offset + values.len();
62 assert!(
63 end <= self.data.len(),
64 "write_f32: out-of-bounds write (offset={offset}, len={}, capacity={})",
65 values.len(),
66 self.data.len()
67 );
68 self.data[offset..end].copy_from_slice(values);
69 }
70
71 pub fn read_f32(&self, offset: usize, count: usize) -> Vec<f32> {
76 let end = offset + count;
77 assert!(
78 end <= self.data.len(),
79 "read_f32: out-of-bounds read (offset={offset}, count={count}, capacity={})",
80 self.data.len()
81 );
82 self.data[offset..end].to_vec()
83 }
84
85 pub fn byte_size(&self) -> usize {
87 self.data.len() * std::mem::size_of::<f32>()
88 }
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub struct WorkgroupSize {
98 pub x: u32,
100 pub y: u32,
102 pub z: u32,
104}
105
106impl WorkgroupSize {
107 pub fn dispatch_count(total: u32, workgroup: u32) -> u32 {
110 assert!(workgroup > 0, "workgroup size must be > 0");
111 total.div_ceil(workgroup)
112 }
113}
114
115impl Default for WorkgroupSize {
116 fn default() -> Self {
117 Self { x: 64, y: 1, z: 1 }
118 }
119}
120
121#[derive(Debug, Clone, PartialEq, Eq)]
127pub enum ComputeKernelKind {
128 VelocityUpdate,
130 PressureJacobi,
132 ParticleForce,
134 NeighborSearch,
136 Custom(String),
138}
139
140pub struct CpuComputeDispatch {
146 pub kernel: ComputeKernelKind,
148 pub workgroup_size: WorkgroupSize,
150}
151
152impl CpuComputeDispatch {
153 pub fn new(kernel: ComputeKernelKind, wg: WorkgroupSize) -> Self {
155 Self {
156 kernel,
157 workgroup_size: wg,
158 }
159 }
160
161 pub fn dispatch_velocity_update(
165 &self,
166 pos: &mut ComputeBuffer,
167 vel: &mut ComputeBuffer,
168 force: &ComputeBuffer,
169 mass: &ComputeBuffer,
170 dt: f32,
171 n: usize,
172 ) {
173 for i in 0..n {
174 pos.data[i] += vel.data[i] * dt;
175 vel.data[i] += force.data[i] / mass.data[i] * dt;
176 }
177 }
178
179 pub fn dispatch_pressure_jacobi(
186 &self,
187 p: &mut ComputeBuffer,
188 p_old: &ComputeBuffer,
189 rhs: &ComputeBuffer,
190 nx: usize,
191 ny: usize,
192 dx: f32,
193 ) {
194 let dx2 = dx * dx;
195 for j in 1..ny - 1 {
196 for i in 1..nx - 1 {
197 let idx = j * nx + i;
198 p.data[idx] = (p_old.data[idx + 1]
199 + p_old.data[idx - 1]
200 + p_old.data[idx + nx]
201 + p_old.data[idx - nx]
202 - dx2 * rhs.data[idx])
203 / 4.0;
204 }
205 }
206 }
207
208 pub fn dispatch_particle_force(
213 &self,
214 pos: &ComputeBuffer,
215 force: &mut ComputeBuffer,
216 eps: f32,
217 sigma: f32,
218 n: usize,
219 ) {
220 for v in force.data[..2 * n].iter_mut() {
222 *v = 0.0;
223 }
224
225 for i in 0..n {
226 for j in (i + 1)..n {
227 let dx = pos.data[2 * j] - pos.data[2 * i];
228 let dy = pos.data[2 * j + 1] - pos.data[2 * i + 1];
229 let r2 = dx * dx + dy * dy;
230 if r2 < 1e-12 {
231 continue;
232 }
233 let sr2 = (sigma * sigma) / r2;
234 let sr6 = sr2 * sr2 * sr2;
235 let sr12 = sr6 * sr6;
236 let fmag = 24.0 * eps / r2 * (2.0 * sr12 - sr6);
238 force.data[2 * i] -= fmag * dx;
239 force.data[2 * i + 1] -= fmag * dy;
240 force.data[2 * j] += fmag * dx;
241 force.data[2 * j + 1] += fmag * dy;
242 }
243 }
244 }
245}
246
247#[derive(Debug, Clone, Default)]
253pub struct GpuStats {
254 pub dispatch_count: u64,
256 pub bytes_transferred: u64,
258 pub kernel_time_ms: f64,
260}
261
262impl GpuStats {
263 pub fn new() -> Self {
265 Self::default()
266 }
267
268 pub fn record_dispatch(&mut self, bytes: u64, time_ms: f64) {
270 self.dispatch_count += 1;
271 self.bytes_transferred += bytes;
272 self.kernel_time_ms += time_ms;
273 }
274}
275
276pub fn jacobi_step_2d(
284 p_new: &mut [f32],
285 p_old: &[f32],
286 rhs: &[f32],
287 nx: usize,
288 ny: usize,
289 dx: f32,
290) {
291 let dx2 = dx * dx;
292 for j in 1..ny - 1 {
293 for i in 1..nx - 1 {
294 let idx = j * nx + i;
295 p_new[idx] = (p_old[idx + 1] + p_old[idx - 1] + p_old[idx + nx] + p_old[idx - nx]
296 - dx2 * rhs[idx])
297 / 4.0;
298 }
299 }
300}
301
302pub fn pressure_poisson_solve(
306 p: &mut [f32],
307 rhs: &[f32],
308 nx: usize,
309 ny: usize,
310 dx: f32,
311 n_iter: usize,
312) -> f32 {
313 let size = nx * ny;
314 let mut p_old = p.to_vec();
315
316 for _ in 0..n_iter {
317 jacobi_step_2d(p, &p_old, rhs, nx, ny, dx);
318 p_old.copy_from_slice(&p[..size]);
319 }
320
321 let dx2 = dx * dx;
323 let mut residual = 0.0_f32;
324 for j in 1..ny - 1 {
325 for i in 1..nx - 1 {
326 let idx = j * nx + i;
327 let lap = (p[idx + 1] + p[idx - 1] + p[idx + nx] + p[idx - nx] - 4.0 * p[idx]) / dx2;
328 let r = (lap - rhs[idx]).abs();
329 if r > residual {
330 residual = r;
331 }
332 }
333 }
334 residual
335}
336
337pub struct PipelineCache {
345 capacity: usize,
347 entries: Vec<(String, CpuComputeDispatch)>,
349}
350
351impl PipelineCache {
352 pub fn new(capacity: usize) -> Self {
354 Self {
355 capacity,
356 entries: Vec::new(),
357 }
358 }
359
360 pub fn insert(&mut self, key: &str, pipeline: CpuComputeDispatch) {
362 self.entries.retain(|(k, _)| k != key);
364 while self.entries.len() >= self.capacity {
366 self.entries.remove(0);
367 }
368 self.entries.push((key.to_owned(), pipeline));
369 }
370
371 pub fn get(&self, key: &str) -> Option<&CpuComputeDispatch> {
373 self.entries.iter().find(|(k, _)| k == key).map(|(_, v)| v)
374 }
375
376 pub fn len(&self) -> usize {
378 self.entries.len()
379 }
380
381 pub fn is_empty(&self) -> bool {
383 self.entries.is_empty()
384 }
385
386 pub fn clear(&mut self) {
388 self.entries.clear();
389 }
390}
391
392#[derive(Debug, Clone, Default)]
398pub struct PipelineStats {
399 pub total_dispatches: u64,
401 pub total_workgroups: u64,
403 pub total_invocations: u64,
405 pub cache_hits: u64,
407 pub cache_misses: u64,
409}
410
411impl PipelineStats {
412 pub fn record_dispatch(&mut self, num_workgroups: u64, wg_size: WorkgroupSize) {
414 self.total_dispatches += 1;
415 self.total_workgroups += num_workgroups;
416 self.total_invocations +=
417 num_workgroups * (wg_size.x as u64) * (wg_size.y as u64) * (wg_size.z as u64);
418 }
419
420 pub fn cache_hit_ratio(&self) -> f64 {
422 let total = self.cache_hits + self.cache_misses;
423 if total == 0 {
424 return f64::NAN;
425 }
426 self.cache_hits as f64 / total as f64
427 }
428}
429
430#[derive(Debug, Clone)]
436pub struct ComputePass {
437 pub label: String,
439 pub kernel: ComputeKernelKind,
441 pub workgroup_size: WorkgroupSize,
443 pub buffer_bindings: Vec<usize>,
445}
446
447#[derive(Debug)]
449pub struct MultiPassPipeline {
450 pub label: String,
452 pub passes: Vec<ComputePass>,
454}
455
456impl MultiPassPipeline {
457 pub fn new(label: &str) -> Self {
459 Self {
460 label: label.to_owned(),
461 passes: Vec::new(),
462 }
463 }
464
465 pub fn add_pass(&mut self, pass: ComputePass) {
467 self.passes.push(pass);
468 }
469
470 pub fn num_passes(&self) -> usize {
472 self.passes.len()
473 }
474}
475
476pub fn validate_resource_bindings(pass: &ComputePass, buffers: &[ComputeBuffer]) -> Vec<String> {
484 let mut errors = Vec::new();
485 let mut seen = std::collections::HashSet::new();
486 for &idx in &pass.buffer_bindings {
487 if idx >= buffers.len() {
488 errors.push(format!(
489 "Pass '{}': buffer binding {} is out of range (have {} buffers)",
490 pass.label,
491 idx,
492 buffers.len()
493 ));
494 }
495 if !seen.insert(idx) {
496 errors.push(format!(
497 "Pass '{}': Duplicate buffer binding {}",
498 pass.label, idx
499 ));
500 }
501 }
502 errors
503}
504
505pub fn validate_pipeline(pipeline: &MultiPassPipeline, buffers: &[ComputeBuffer]) -> Vec<String> {
507 let mut errors = Vec::new();
508 for pass in &pipeline.passes {
509 errors.extend(validate_resource_bindings(pass, buffers));
510 }
511 errors
512}
513
514pub fn sor_step_2d(
522 p: &mut [f32],
523 p_old: &[f32],
524 rhs: &[f32],
525 nx: usize,
526 ny: usize,
527 dx: f32,
528 omega: f32,
529) {
530 let dx2 = dx * dx;
531 for j in 1..ny - 1 {
532 for i in 1..nx - 1 {
533 let idx = j * nx + i;
534 let gs = (p_old[idx + 1] + p_old[idx - 1] + p_old[idx + nx] + p_old[idx - nx]
535 - dx2 * rhs[idx])
536 / 4.0;
537 p[idx] = (1.0 - omega) * p_old[idx] + omega * gs;
538 }
539 }
540}
541
542pub fn red_black_gauss_seidel_step(p: &mut [f32], rhs: &[f32], nx: usize, ny: usize, dx: f32) {
546 let dx2 = dx * dx;
547 for j in 1..ny - 1 {
549 for i in 1..nx - 1 {
550 if (i + j) % 2 == 0 {
551 let idx = j * nx + i;
552 p[idx] =
553 (p[idx + 1] + p[idx - 1] + p[idx + nx] + p[idx - nx] - dx2 * rhs[idx]) / 4.0;
554 }
555 }
556 }
557 for j in 1..ny - 1 {
559 for i in 1..nx - 1 {
560 if (i + j) % 2 == 1 {
561 let idx = j * nx + i;
562 p[idx] =
563 (p[idx + 1] + p[idx - 1] + p[idx + nx] + p[idx - nx] - dx2 * rhs[idx]) / 4.0;
564 }
565 }
566 }
567}
568
569pub fn compute_linf_residual(p: &[f32], rhs: &[f32], nx: usize, ny: usize, dx: f32) -> f32 {
571 let dx2 = dx * dx;
572 let mut residual = 0.0_f32;
573 for j in 1..ny - 1 {
574 for i in 1..nx - 1 {
575 let idx = j * nx + i;
576 let lap = (p[idx + 1] + p[idx - 1] + p[idx + nx] + p[idx - nx] - 4.0 * p[idx]) / dx2;
577 let r = (lap - rhs[idx]).abs();
578 if r > residual {
579 residual = r;
580 }
581 }
582 }
583 residual
584}
585
586pub fn dispatch_neighbor_search(positions: &[f32], n: usize, cutoff: f32) -> Vec<Vec<usize>> {
591 let cutoff2 = cutoff * cutoff;
592 let mut neighbors = vec![Vec::new(); n];
593 for i in 0..n {
594 for j in (i + 1)..n {
595 let dx = positions[2 * j] - positions[2 * i];
596 let dy = positions[2 * j + 1] - positions[2 * i + 1];
597 let r2 = dx * dx + dy * dy;
598 if r2 < cutoff2 {
599 neighbors[i].push(j);
600 neighbors[j].push(i);
601 }
602 }
603 }
604 neighbors
605}
606
607#[cfg(test)]
612mod tests {
613 use super::*;
614
615 #[test]
618 fn buffer_usage_eq() {
619 assert_eq!(BufferUsage::Storage, BufferUsage::Storage);
620 assert_ne!(BufferUsage::Vertex, BufferUsage::Index);
621 }
622
623 #[test]
624 fn buffer_usage_clone() {
625 let u = BufferUsage::Uniform;
626 assert_eq!(u.clone(), BufferUsage::Uniform);
627 }
628
629 #[test]
632 fn compute_buffer_new_zeroed() {
633 let buf = ComputeBuffer::new(8, BufferUsage::Storage, "test");
634 assert_eq!(buf.data.len(), 8);
635 assert!(buf.data.iter().all(|&v| v == 0.0));
636 assert_eq!(buf.label, "test");
637 }
638
639 #[test]
640 fn compute_buffer_byte_size() {
641 let buf = ComputeBuffer::new(4, BufferUsage::Uniform, "u");
642 assert_eq!(buf.byte_size(), 16);
643 }
644
645 #[test]
646 fn compute_buffer_write_read_roundtrip() {
647 let mut buf = ComputeBuffer::new(8, BufferUsage::Storage, "rw");
648 buf.write_f32(2, &[1.0, 2.0, 3.0]);
649 let out = buf.read_f32(2, 3);
650 assert_eq!(out, vec![1.0, 2.0, 3.0]);
651 }
652
653 #[test]
654 fn compute_buffer_write_at_offset_zero() {
655 let mut buf = ComputeBuffer::new(4, BufferUsage::Storage, "s");
656 buf.write_f32(0, &[9.0, 8.0, 7.0, 6.0]);
657 assert_eq!(buf.data, vec![9.0, 8.0, 7.0, 6.0]);
658 }
659
660 #[test]
661 #[should_panic(expected = "out-of-bounds write")]
662 fn compute_buffer_write_oob_panics() {
663 let mut buf = ComputeBuffer::new(4, BufferUsage::Storage, "oob");
664 buf.write_f32(3, &[1.0, 2.0]); }
666
667 #[test]
668 #[should_panic(expected = "out-of-bounds read")]
669 fn compute_buffer_read_oob_panics() {
670 let buf = ComputeBuffer::new(4, BufferUsage::Storage, "oob");
671 let _ = buf.read_f32(3, 2);
672 }
673
674 #[test]
677 fn workgroup_dispatch_count_exact() {
678 assert_eq!(WorkgroupSize::dispatch_count(64, 64), 1);
679 }
680
681 #[test]
682 fn workgroup_dispatch_count_ceil() {
683 assert_eq!(WorkgroupSize::dispatch_count(65, 64), 2);
684 assert_eq!(WorkgroupSize::dispatch_count(1, 64), 1);
685 }
686
687 #[test]
688 fn workgroup_dispatch_count_zero_total() {
689 assert_eq!(WorkgroupSize::dispatch_count(0, 64), 0);
690 }
691
692 #[test]
693 fn workgroup_default() {
694 let wg = WorkgroupSize::default();
695 assert_eq!(wg.x, 64);
696 assert_eq!(wg.y, 1);
697 assert_eq!(wg.z, 1);
698 }
699
700 #[test]
703 fn kernel_kind_custom_eq() {
704 let a = ComputeKernelKind::Custom("foo".into());
705 let b = ComputeKernelKind::Custom("foo".into());
706 assert_eq!(a, b);
707 }
708
709 #[test]
710 fn kernel_kind_variants_neq() {
711 assert_ne!(
712 ComputeKernelKind::VelocityUpdate,
713 ComputeKernelKind::PressureJacobi
714 );
715 }
716
717 #[test]
720 fn velocity_update_basic() {
721 let disp =
722 CpuComputeDispatch::new(ComputeKernelKind::VelocityUpdate, WorkgroupSize::default());
723 let n = 3;
724 let mut pos = ComputeBuffer::new(n, BufferUsage::Storage, "pos");
725 let mut vel = ComputeBuffer::new(n, BufferUsage::Storage, "vel");
726 let mut force = ComputeBuffer::new(n, BufferUsage::Storage, "force");
727 let mut mass = ComputeBuffer::new(n, BufferUsage::Storage, "mass");
728
729 pos.write_f32(0, &[0.0, 1.0, 2.0]);
730 vel.write_f32(0, &[1.0, 0.5, -1.0]);
731 force.write_f32(0, &[0.0, 1.0, 0.0]);
732 mass.write_f32(0, &[1.0, 2.0, 1.0]);
733
734 let dt = 0.1_f32;
735 disp.dispatch_velocity_update(&mut pos, &mut vel, &force, &mass, dt, n);
736
737 assert!((pos.data[0] - 0.1).abs() < 1e-6);
739 assert!((vel.data[0] - 1.0).abs() < 1e-6);
740 assert!((pos.data[1] - 1.05).abs() < 1e-6);
742 assert!((vel.data[1] - 0.55).abs() < 1e-6);
743 }
744
745 #[test]
746 fn velocity_update_zero_force() {
747 let disp =
748 CpuComputeDispatch::new(ComputeKernelKind::VelocityUpdate, WorkgroupSize::default());
749 let n = 2;
750 let mut pos = ComputeBuffer::new(n, BufferUsage::Storage, "pos");
751 let mut vel = ComputeBuffer::new(n, BufferUsage::Storage, "vel");
752 let force = ComputeBuffer::new(n, BufferUsage::Storage, "force");
753 let mut mass = ComputeBuffer::new(n, BufferUsage::Storage, "mass");
754
755 pos.write_f32(0, &[0.0, 0.0]);
756 vel.write_f32(0, &[2.0, -3.0]);
757 mass.write_f32(0, &[1.0, 1.0]);
758
759 disp.dispatch_velocity_update(&mut pos, &mut vel, &force, &mass, 0.5, n);
760
761 assert!((pos.data[0] - 1.0).abs() < 1e-6);
762 assert!((pos.data[1] - (-1.5)).abs() < 1e-6);
763 assert!((vel.data[0] - 2.0).abs() < 1e-6);
765 assert!((vel.data[1] - (-3.0)).abs() < 1e-6);
766 }
767
768 #[test]
771 fn pressure_jacobi_interior_update() {
772 let disp =
773 CpuComputeDispatch::new(ComputeKernelKind::PressureJacobi, WorkgroupSize::default());
774 let nx = 4;
775 let ny = 4;
776 let mut p = ComputeBuffer::new(nx * ny, BufferUsage::Storage, "p");
777 let mut p_old = ComputeBuffer::new(nx * ny, BufferUsage::Storage, "p_old");
778 let rhs = ComputeBuffer::new(nx * ny, BufferUsage::Storage, "rhs");
779
780 for v in p_old.data.iter_mut() {
782 *v = 1.0;
783 }
784
785 disp.dispatch_pressure_jacobi(&mut p, &p_old, &rhs, nx, ny, 1.0);
786
787 let idx = nx + 1;
789 assert!((p.data[idx] - 1.0).abs() < 1e-6);
790 }
791
792 #[test]
793 fn pressure_jacobi_boundary_unchanged() {
794 let disp =
795 CpuComputeDispatch::new(ComputeKernelKind::PressureJacobi, WorkgroupSize::default());
796 let nx = 5;
797 let ny = 5;
798 let mut p = ComputeBuffer::new(nx * ny, BufferUsage::Storage, "p");
799 let p_old = ComputeBuffer::new(nx * ny, BufferUsage::Storage, "p_old");
800 let rhs = ComputeBuffer::new(nx * ny, BufferUsage::Storage, "rhs");
801
802 disp.dispatch_pressure_jacobi(&mut p, &p_old, &rhs, nx, ny, 1.0);
804 assert_eq!(p.data[0], 0.0); assert_eq!(p.data[4], 0.0); }
807
808 #[test]
811 fn particle_force_zero_at_large_sep() {
812 let disp =
813 CpuComputeDispatch::new(ComputeKernelKind::ParticleForce, WorkgroupSize::default());
814 let n = 2;
815 let mut pos = ComputeBuffer::new(2 * n, BufferUsage::Storage, "pos");
817 let mut force = ComputeBuffer::new(2 * n, BufferUsage::Storage, "force");
818 pos.write_f32(0, &[0.0, 0.0, 1000.0, 0.0]);
819
820 disp.dispatch_particle_force(&pos, &mut force, 1.0, 1.0, n);
821 assert!(force.data[0].abs() < 1e-10);
823 }
824
825 #[test]
826 fn particle_force_newton3() {
827 let disp =
828 CpuComputeDispatch::new(ComputeKernelKind::ParticleForce, WorkgroupSize::default());
829 let n = 2;
830 let mut pos = ComputeBuffer::new(2 * n, BufferUsage::Storage, "pos");
831 let mut force = ComputeBuffer::new(2 * n, BufferUsage::Storage, "force");
832 pos.write_f32(0, &[0.0, 0.0, 1.5, 0.0]);
833
834 disp.dispatch_particle_force(&pos, &mut force, 1.0, 1.0, n);
835 assert!((force.data[0] + force.data[2]).abs() < 1e-5);
837 assert!((force.data[1] + force.data[3]).abs() < 1e-5);
838 }
839
840 #[test]
843 fn gpu_stats_initial_zero() {
844 let s = GpuStats::new();
845 assert_eq!(s.dispatch_count, 0);
846 assert_eq!(s.bytes_transferred, 0);
847 assert_eq!(s.kernel_time_ms, 0.0);
848 }
849
850 #[test]
851 fn gpu_stats_accumulate() {
852 let mut s = GpuStats::new();
853 s.record_dispatch(128, 0.5);
854 s.record_dispatch(256, 1.0);
855 assert_eq!(s.dispatch_count, 2);
856 assert_eq!(s.bytes_transferred, 384);
857 assert!((s.kernel_time_ms - 1.5).abs() < 1e-9);
858 }
859
860 #[test]
863 fn jacobi_step_2d_uniform_field() {
864 let nx = 4;
865 let ny = 4;
866 let size = nx * ny;
867 let mut p_new = vec![0.0_f32; size];
868 let p_old = vec![1.0_f32; size];
869 let rhs = vec![0.0_f32; size];
870
871 jacobi_step_2d(&mut p_new, &p_old, &rhs, nx, ny, 1.0);
872
873 for j in 1..ny - 1 {
875 for i in 1..nx - 1 {
876 assert!((p_new[j * nx + i] - 1.0).abs() < 1e-6);
877 }
878 }
879 }
880
881 #[test]
882 fn jacobi_step_2d_rhs_effect() {
883 let nx = 4;
884 let ny = 4;
885 let size = nx * ny;
886 let mut p_new = vec![0.0_f32; size];
887 let p_old = vec![4.0_f32; size];
888 let rhs = vec![4.0_f32; size];
890
891 jacobi_step_2d(&mut p_new, &p_old, &rhs, nx, ny, 1.0);
892 for j in 1..ny - 1 {
894 for i in 1..nx - 1 {
895 assert!((p_new[j * nx + i] - 3.0).abs() < 1e-6);
896 }
897 }
898 }
899
900 #[test]
903 fn pressure_poisson_zero_rhs_zero_bc() {
904 let nx = 5;
906 let ny = 5;
907 let mut p = vec![0.0_f32; nx * ny];
908 let rhs = vec![0.0_f32; nx * ny];
909 let residual = pressure_poisson_solve(&mut p, &rhs, nx, ny, 0.1, 50);
910 assert!(residual < 1e-6, "residual={residual}");
911 }
912
913 #[test]
914 fn pressure_poisson_residual_decreases() {
915 let nx = 6;
916 let ny = 6;
917 let mut p1 = vec![0.0_f32; nx * ny];
918 let mut p2 = p1.clone();
919 let rhs: Vec<f32> = (0..(nx * ny)).map(|k| (k as f32).sin()).collect();
920 let dx = 0.1;
921
922 let r1 = pressure_poisson_solve(&mut p1, &rhs, nx, ny, dx, 10);
923 let r2 = pressure_poisson_solve(&mut p2, &rhs, nx, ny, dx, 200);
924 assert!(
925 r2 <= r1 + 1e-4,
926 "more iterations should not increase residual (r1={r1}, r2={r2})"
927 );
928 }
929
930 #[test]
933 fn pipeline_cache_insert_and_get() {
934 let mut cache = PipelineCache::new(4);
935 let disp =
936 CpuComputeDispatch::new(ComputeKernelKind::VelocityUpdate, WorkgroupSize::default());
937 cache.insert("vel_update", disp);
938 assert!(cache.get("vel_update").is_some());
939 assert!(cache.get("nonexistent").is_none());
940 }
941
942 #[test]
943 fn pipeline_cache_eviction() {
944 let mut cache = PipelineCache::new(2);
945 let d1 =
946 CpuComputeDispatch::new(ComputeKernelKind::VelocityUpdate, WorkgroupSize::default());
947 let d2 =
948 CpuComputeDispatch::new(ComputeKernelKind::PressureJacobi, WorkgroupSize::default());
949 let d3 =
950 CpuComputeDispatch::new(ComputeKernelKind::ParticleForce, WorkgroupSize::default());
951 cache.insert("a", d1);
952 cache.insert("b", d2);
953 cache.insert("c", d3); assert!(cache.get("a").is_none());
955 assert!(cache.get("b").is_some());
956 assert!(cache.get("c").is_some());
957 }
958
959 #[test]
960 fn pipeline_cache_replace() {
961 let mut cache = PipelineCache::new(4);
962 let d1 =
963 CpuComputeDispatch::new(ComputeKernelKind::VelocityUpdate, WorkgroupSize::default());
964 let d2 = CpuComputeDispatch::new(
965 ComputeKernelKind::ParticleForce,
966 WorkgroupSize { x: 128, y: 1, z: 1 },
967 );
968 cache.insert("key", d1);
969 cache.insert("key", d2);
970 let entry = cache.get("key").unwrap();
971 assert_eq!(entry.kernel, ComputeKernelKind::ParticleForce);
972 }
973
974 #[test]
977 fn pipeline_stats_default() {
978 let stats = PipelineStats::default();
979 assert_eq!(stats.total_dispatches, 0);
980 assert_eq!(stats.total_workgroups, 0);
981 assert_eq!(stats.total_invocations, 0);
982 assert_eq!(stats.cache_hits, 0);
983 assert_eq!(stats.cache_misses, 0);
984 }
985
986 #[test]
987 fn pipeline_stats_record() {
988 let mut stats = PipelineStats::default();
989 stats.record_dispatch(4, WorkgroupSize { x: 64, y: 1, z: 1 });
990 assert_eq!(stats.total_dispatches, 1);
991 assert_eq!(stats.total_workgroups, 4);
992 assert_eq!(stats.total_invocations, 4 * 64);
993 }
994
995 #[test]
996 fn pipeline_stats_record_3d_workgroup() {
997 let mut stats = PipelineStats::default();
998 stats.record_dispatch(2, WorkgroupSize { x: 8, y: 8, z: 4 });
999 assert_eq!(stats.total_dispatches, 1);
1000 assert_eq!(stats.total_workgroups, 2);
1001 assert_eq!(stats.total_invocations, 2 * 8 * 8 * 4);
1002 }
1003
1004 #[test]
1005 fn pipeline_stats_cache_ratio() {
1006 let mut stats = PipelineStats::default();
1007 assert!(stats.cache_hit_ratio().is_nan() || stats.cache_hit_ratio() == 0.0);
1008 stats.cache_hits = 3;
1009 stats.cache_misses = 1;
1010 assert!((stats.cache_hit_ratio() - 0.75).abs() < 1e-6);
1011 }
1012
1013 #[test]
1016 fn multi_pass_empty() {
1017 let mp = MultiPassPipeline::new("empty");
1018 assert_eq!(mp.passes.len(), 0);
1019 assert_eq!(mp.label, "empty");
1020 }
1021
1022 #[test]
1023 fn multi_pass_execute_add_scale() {
1024 let mut mp = MultiPassPipeline::new("add_scale");
1027 mp.add_pass(ComputePass {
1028 label: "fill".into(),
1029 kernel: ComputeKernelKind::Custom("fill".into()),
1030 workgroup_size: WorkgroupSize::default(),
1031 buffer_bindings: vec![0],
1032 });
1033 mp.add_pass(ComputePass {
1034 label: "scale".into(),
1035 kernel: ComputeKernelKind::Custom("scale".into()),
1036 workgroup_size: WorkgroupSize::default(),
1037 buffer_bindings: vec![0],
1038 });
1039 assert_eq!(mp.passes.len(), 2);
1040 assert_eq!(mp.passes[0].label, "fill");
1041 assert_eq!(mp.passes[1].label, "scale");
1042 }
1043
1044 #[test]
1045 fn multi_pass_dispatch_velocity_chain() {
1046 let mut mp = MultiPassPipeline::new("vel_chain");
1048 mp.add_pass(ComputePass {
1049 label: "step1".into(),
1050 kernel: ComputeKernelKind::VelocityUpdate,
1051 workgroup_size: WorkgroupSize::default(),
1052 buffer_bindings: vec![0, 1, 2, 3],
1053 });
1054 mp.add_pass(ComputePass {
1055 label: "step2".into(),
1056 kernel: ComputeKernelKind::VelocityUpdate,
1057 workgroup_size: WorkgroupSize::default(),
1058 buffer_bindings: vec![0, 1, 2, 3],
1059 });
1060
1061 let n = 2;
1062 let mut pos = ComputeBuffer::new(n, BufferUsage::Storage, "pos");
1063 let mut vel = ComputeBuffer::new(n, BufferUsage::Storage, "vel");
1064 let force = ComputeBuffer::new(n, BufferUsage::Storage, "force");
1065 let mut mass = ComputeBuffer::new(n, BufferUsage::Storage, "mass");
1066
1067 pos.write_f32(0, &[0.0, 0.0]);
1068 vel.write_f32(0, &[1.0, 2.0]);
1069 mass.write_f32(0, &[1.0, 1.0]);
1070
1071 let dt = 0.1_f32;
1072
1073 for pass in &mp.passes {
1075 if pass.kernel == ComputeKernelKind::VelocityUpdate {
1076 let disp =
1077 CpuComputeDispatch::new(ComputeKernelKind::VelocityUpdate, pass.workgroup_size);
1078 disp.dispatch_velocity_update(&mut pos, &mut vel, &force, &mass, dt, n);
1079 }
1080 }
1081 assert!((pos.data[0] - 0.2).abs() < 1e-5);
1083 assert!((pos.data[1] - 0.4).abs() < 1e-5);
1084 }
1085
1086 #[test]
1089 fn validate_binding_valid() {
1090 let buffers = vec![
1091 ComputeBuffer::new(16, BufferUsage::Storage, "buf0"),
1092 ComputeBuffer::new(16, BufferUsage::Uniform, "buf1"),
1093 ];
1094 let pass = ComputePass {
1095 label: "test".into(),
1096 kernel: ComputeKernelKind::VelocityUpdate,
1097 workgroup_size: WorkgroupSize::default(),
1098 buffer_bindings: vec![0, 1],
1099 };
1100 let errors = validate_resource_bindings(&pass, &buffers);
1101 assert!(errors.is_empty());
1102 }
1103
1104 #[test]
1105 fn validate_binding_out_of_range() {
1106 let buffers = vec![ComputeBuffer::new(16, BufferUsage::Storage, "buf0")];
1107 let pass = ComputePass {
1108 label: "test".into(),
1109 kernel: ComputeKernelKind::VelocityUpdate,
1110 workgroup_size: WorkgroupSize::default(),
1111 buffer_bindings: vec![0, 5],
1112 };
1113 let errors = validate_resource_bindings(&pass, &buffers);
1114 assert_eq!(errors.len(), 1);
1115 assert!(errors[0].contains("out of range"));
1116 }
1117
1118 #[test]
1119 fn validate_binding_duplicate() {
1120 let buffers = vec![ComputeBuffer::new(16, BufferUsage::Storage, "buf0")];
1121 let pass = ComputePass {
1122 label: "test".into(),
1123 kernel: ComputeKernelKind::VelocityUpdate,
1124 workgroup_size: WorkgroupSize::default(),
1125 buffer_bindings: vec![0, 0],
1126 };
1127 let errors = validate_resource_bindings(&pass, &buffers);
1128 assert_eq!(errors.len(), 1);
1129 assert!(errors[0].contains("Duplicate"));
1130 }
1131
1132 #[test]
1133 fn validate_pipeline_all_passes() {
1134 let buffers = vec![ComputeBuffer::new(16, BufferUsage::Storage, "buf0")];
1135 let mut mp = MultiPassPipeline::new("test");
1136 mp.add_pass(ComputePass {
1137 label: "good".into(),
1138 kernel: ComputeKernelKind::VelocityUpdate,
1139 workgroup_size: WorkgroupSize::default(),
1140 buffer_bindings: vec![0],
1141 });
1142 mp.add_pass(ComputePass {
1143 label: "bad".into(),
1144 kernel: ComputeKernelKind::PressureJacobi,
1145 workgroup_size: WorkgroupSize::default(),
1146 buffer_bindings: vec![0, 3],
1147 });
1148 let errors = validate_pipeline(&mp, &buffers);
1149 assert_eq!(errors.len(), 1); }
1151
1152 #[test]
1155 fn compute_buffer_clone() {
1156 let mut buf = ComputeBuffer::new(4, BufferUsage::Storage, "orig");
1157 buf.write_f32(0, &[1.0, 2.0, 3.0, 4.0]);
1158 let cloned = buf.clone();
1159 assert_eq!(buf.data, cloned.data);
1160 assert_eq!(buf.label, cloned.label);
1161 }
1162
1163 #[test]
1164 fn compute_buffer_staging_usage() {
1165 let buf = ComputeBuffer::new(8, BufferUsage::Staging, "staging");
1166 assert_eq!(buf.usage, BufferUsage::Staging);
1167 assert_eq!(buf.byte_size(), 32);
1168 }
1169
1170 #[test]
1173 fn workgroup_dispatch_count_large() {
1174 assert_eq!(WorkgroupSize::dispatch_count(1024, 256), 4);
1175 assert_eq!(WorkgroupSize::dispatch_count(1025, 256), 5);
1176 }
1177
1178 #[test]
1181 fn gpu_stats_clone() {
1182 let mut s = GpuStats::new();
1183 s.record_dispatch(100, 1.5);
1184 let s2 = s.clone();
1185 assert_eq!(s.dispatch_count, s2.dispatch_count);
1186 assert_eq!(s.bytes_transferred, s2.bytes_transferred);
1187 assert!((s.kernel_time_ms - s2.kernel_time_ms).abs() < 1e-12);
1188 }
1189
1190 #[test]
1193 fn particle_force_repulsive_at_close_range() {
1194 let disp =
1195 CpuComputeDispatch::new(ComputeKernelKind::ParticleForce, WorkgroupSize::default());
1196 let n = 2;
1197 let mut pos = ComputeBuffer::new(2 * n, BufferUsage::Storage, "pos");
1198 let mut force = ComputeBuffer::new(2 * n, BufferUsage::Storage, "force");
1199 pos.write_f32(0, &[0.0, 0.0, 0.9, 0.0]);
1201 disp.dispatch_particle_force(&pos, &mut force, 1.0, 1.0, n);
1202 assert!(
1204 force.data[0] < 0.0,
1205 "expected repulsive force, got {}",
1206 force.data[0]
1207 );
1208 }
1209
1210 #[test]
1211 fn particle_force_three_particles() {
1212 let disp =
1213 CpuComputeDispatch::new(ComputeKernelKind::ParticleForce, WorkgroupSize::default());
1214 let n = 3;
1215 let mut pos = ComputeBuffer::new(2 * n, BufferUsage::Storage, "pos");
1216 let mut force = ComputeBuffer::new(2 * n, BufferUsage::Storage, "force");
1217 pos.write_f32(0, &[0.0, 0.0, 2.0, 0.0, 1.0, 1.732]);
1219 disp.dispatch_particle_force(&pos, &mut force, 1.0, 1.0, n);
1220
1221 let fx_total = force.data[0] + force.data[2] + force.data[4];
1223 let fy_total = force.data[1] + force.data[3] + force.data[5];
1224 assert!(fx_total.abs() < 1e-5, "fx_total={fx_total}");
1225 assert!(fy_total.abs() < 1e-5, "fy_total={fy_total}");
1226 }
1227
1228 #[test]
1231 fn pressure_poisson_uniform_rhs() {
1232 let nx = 8;
1233 let ny = 8;
1234 let mut p = vec![0.0_f32; nx * ny];
1235 let rhs = vec![1.0_f32; nx * ny];
1236 let residual = pressure_poisson_solve(&mut p, &rhs, nx, ny, 0.1, 500);
1237 assert!(residual < 10.0, "residual={residual}");
1239 }
1240
1241 #[test]
1244 fn sor_step_uniform_field() {
1245 let nx = 4;
1246 let ny = 4;
1247 let mut p = vec![0.0_f32; nx * ny];
1248 let rhs = vec![0.0_f32; nx * ny];
1249 let p_ref = vec![1.0_f32; nx * ny];
1250 sor_step_2d(&mut p, &p_ref, &rhs, nx, ny, 1.0, 1.0);
1251 for j in 1..ny - 1 {
1253 for i in 1..nx - 1 {
1254 assert!((p[j * nx + i] - 1.0).abs() < 1e-6);
1255 }
1256 }
1257 }
1258
1259 #[test]
1260 fn sor_step_over_relaxation() {
1261 let nx = 6;
1263 let ny = 6;
1264 let rhs = vec![0.0_f32; nx * ny];
1265
1266 let mut p_ref = vec![0.0_f32; nx * ny];
1268 for i in 0..nx {
1269 p_ref[i] = 1.0;
1270 p_ref[(ny - 1) * nx + i] = 1.0;
1271 }
1272 for j in 0..ny {
1273 p_ref[j * nx] = 1.0;
1274 p_ref[j * nx + nx - 1] = 1.0;
1275 }
1276
1277 let mut p_jac = vec![0.0_f32; nx * ny];
1278 let mut p_sor = vec![0.0_f32; nx * ny];
1279 sor_step_2d(&mut p_jac, &p_ref, &rhs, nx, ny, 1.0, 1.0);
1280 sor_step_2d(&mut p_sor, &p_ref, &rhs, nx, ny, 1.0, 1.5);
1281
1282 let idx = nx + 1; assert!(
1286 (p_sor[idx] - p_jac[idx]).abs() > 0.01,
1287 "SOR and Jacobi should differ: SOR={}, Jac={}",
1288 p_sor[idx],
1289 p_jac[idx]
1290 );
1291 }
1292
1293 #[test]
1296 fn red_black_gs_uniform() {
1297 let nx = 6;
1298 let ny = 6;
1299 let mut p = vec![1.0_f32; nx * ny];
1300 let rhs = vec![0.0_f32; nx * ny];
1301 red_black_gauss_seidel_step(&mut p, &rhs, nx, ny, 1.0);
1302 for j in 1..ny - 1 {
1304 for i in 1..nx - 1 {
1305 assert!((p[j * nx + i] - 1.0).abs() < 1e-6);
1306 }
1307 }
1308 }
1309
1310 #[test]
1311 fn red_black_gs_converges() {
1312 let nx = 8;
1313 let ny = 8;
1314 let mut p = vec![0.0_f32; nx * ny];
1315 let rhs = vec![0.0_f32; nx * ny];
1316 for i in 0..nx {
1318 p[i] = 1.0;
1319 p[(ny - 1) * nx + i] = 1.0;
1320 }
1321 for j in 0..ny {
1322 p[j * nx] = 1.0;
1323 p[j * nx + nx - 1] = 1.0;
1324 }
1325 for _ in 0..200 {
1327 red_black_gauss_seidel_step(&mut p, &rhs, nx, ny, 1.0);
1328 }
1329 let center = p[(ny / 2) * nx + nx / 2];
1330 assert!((center - 1.0).abs() < 0.01, "center={center}");
1331 }
1332
1333 #[test]
1336 fn linf_residual_zero_for_exact() {
1337 let nx = 4;
1339 let ny = 4;
1340 let p = vec![1.0_f32; nx * ny];
1341 let rhs = vec![0.0_f32; nx * ny];
1342 let res = compute_linf_residual(&p, &rhs, nx, ny, 1.0);
1343 assert!(res < 1e-6, "res={res}");
1344 }
1345
1346 #[test]
1347 fn linf_residual_nonzero_for_wrong() {
1348 let nx = 4;
1349 let ny = 4;
1350 let mut p = vec![0.0_f32; nx * ny];
1351 p[nx + 1] = 100.0; let rhs = vec![0.0_f32; nx * ny];
1353 let res = compute_linf_residual(&p, &rhs, nx, ny, 1.0);
1354 assert!(res > 1.0, "expected large residual, got {res}");
1355 }
1356
1357 #[test]
1360 fn dispatch_neighbor_search_basic() {
1361 let n = 4;
1362 let positions = vec![
1363 0.0_f32, 0.0, 0.5, 0.0, 5.0, 5.0, 0.3, 0.3, ];
1368 let neighbors = dispatch_neighbor_search(&positions, n, 1.0);
1369 assert!(neighbors[0].contains(&1));
1371 assert!(neighbors[0].contains(&3));
1372 assert!(neighbors[2].is_empty());
1374 }
1375
1376 #[test]
1377 fn dispatch_neighbor_search_all_close() {
1378 let n = 3;
1379 let positions = vec![0.0_f32, 0.0, 0.1, 0.0, 0.0, 0.1];
1380 let neighbors = dispatch_neighbor_search(&positions, n, 1.0);
1381 assert_eq!(neighbors[0].len(), 2);
1383 assert_eq!(neighbors[1].len(), 2);
1384 assert_eq!(neighbors[2].len(), 2);
1385 }
1386
1387 #[test]
1388 fn dispatch_neighbor_search_none() {
1389 let n = 2;
1390 let positions = vec![0.0_f32, 0.0, 100.0, 100.0];
1391 let neighbors = dispatch_neighbor_search(&positions, n, 1.0);
1392 assert!(neighbors[0].is_empty());
1393 assert!(neighbors[1].is_empty());
1394 }
1395}