Skip to main content

oxiphysics_gpu/
lib.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! GPU acceleration backends for the OxiPhysics engine.
5//!
6//! This crate provides a GPU compute abstraction layer that can work with any
7//! backend, with a CPU fallback as the default implementation. No heavy GPU
8//! dependencies (such as wgpu) are required.
9
10mod error;
11pub use error::*;
12
13pub mod bvh;
14pub mod cell_list;
15pub mod compute;
16pub mod compute_pipeline;
17pub mod flux_compute;
18pub mod gpu_bench;
19pub mod grid_reduce;
20pub mod kernels;
21pub mod lbm_gpu;
22pub mod neural_compute;
23pub mod parallel;
24pub mod parallel_sort;
25pub mod particle_system;
26pub mod pipeline;
27pub mod sdf_compute;
28pub mod shader_registry;
29pub mod shaders;
30pub mod sparse_gpu;
31pub mod sph_gpu;
32
33pub use compute::{BufferHandle, ComputeBackend, ComputeKernel, CpuBackend};
34pub use neural_compute::*;
35pub use particle_system::functions::*;
36pub use particle_system::types::*;
37pub use sparse_gpu::*;
38
39// ── GPU compute utility functions ───────────────────────────────────────────
40
41/// Compute the optimal work group size for a given total work item count.
42///
43/// Rounds up `total` to the next multiple of `group_size`.
44pub fn dispatch_count(total: usize, group_size: usize) -> usize {
45    if group_size == 0 {
46        return 0;
47    }
48    total.div_ceil(group_size)
49}
50
51/// Compute the padded buffer size to meet alignment requirements.
52///
53/// Returns the smallest multiple of `alignment` that is >= `size`.
54pub fn aligned_size(size: usize, alignment: usize) -> usize {
55    if alignment == 0 {
56        return size;
57    }
58    size.div_ceil(alignment) * alignment
59}
60
61/// Flatten a 3D dispatch (x, y, z) into a linear index, given grid dimensions.
62pub fn linear_index_3d(x: usize, y: usize, z: usize, dim_x: usize, dim_y: usize) -> usize {
63    z * dim_x * dim_y + y * dim_x + x
64}
65
66/// Convert a linear index back to 3D coordinates.
67pub fn index_3d_from_linear(index: usize, dim_x: usize, dim_y: usize) -> (usize, usize, usize) {
68    let z = index / (dim_x * dim_y);
69    let rem = index % (dim_x * dim_y);
70    let y = rem / dim_x;
71    let x = rem % dim_x;
72    (x, y, z)
73}
74
75/// A simple timer utility for profiling GPU-like dispatches.
76#[derive(Debug, Clone)]
77pub struct DispatchTimer {
78    /// Label for this dispatch.
79    pub label: String,
80    /// Elapsed time in seconds (set after timing).
81    pub elapsed_secs: f64,
82}
83
84impl DispatchTimer {
85    /// Create a new timer with the given label.
86    pub fn new(label: impl Into<String>) -> Self {
87        Self {
88            label: label.into(),
89            elapsed_secs: 0.0,
90        }
91    }
92
93    /// Record elapsed time.
94    pub fn record(&mut self, elapsed: f64) {
95        self.elapsed_secs = elapsed;
96    }
97}
98
99/// Estimate memory bandwidth in GB/s.
100///
101/// * `bytes_transferred` - Total bytes read + written.
102/// * `elapsed_secs` - Elapsed time in seconds.
103pub fn bandwidth_gb_s(bytes_transferred: usize, elapsed_secs: f64) -> f64 {
104    if elapsed_secs <= 0.0 {
105        return 0.0;
106    }
107    (bytes_transferred as f64) / elapsed_secs / 1e9
108}
109
110/// Compute the number of elements that fit in a given memory budget.
111///
112/// * `budget_bytes` - Available memory in bytes.
113/// * `element_size` - Size of one element in bytes.
114pub fn elements_in_budget(budget_bytes: usize, element_size: usize) -> usize {
115    if element_size == 0 {
116        return 0;
117    }
118    budget_bytes / element_size
119}
120
121// ── GPU buffer utilities ─────────────────────────────────────────────────────
122
123/// Stride (in bytes) of a row in a 2-D buffer, given the element count per row
124/// and the required alignment.
125///
126/// This mirrors `wgpuDeviceGetSupportedSurfaceFormats` style pitch calculation.
127pub fn row_pitch(elements_per_row: usize, element_size: usize, alignment: usize) -> usize {
128    let raw = elements_per_row * element_size;
129    aligned_size(raw, alignment)
130}
131
132/// Compute the 2-D buffer size (rows × pitch) for a texture-like allocation.
133pub fn buffer_size_2d(
134    width: usize,
135    height: usize,
136    element_size: usize,
137    row_alignment: usize,
138) -> usize {
139    row_pitch(width, element_size, row_alignment) * height
140}
141
142/// Round `value` up to the next power of two.
143///
144/// Returns `value` unchanged when it is already a power of two.
145/// Returns 1 when `value` is 0.
146pub fn next_power_of_two(value: usize) -> usize {
147    if value == 0 {
148        return 1;
149    }
150    let mut p = 1usize;
151    while p < value {
152        p <<= 1;
153    }
154    p
155}
156
157/// True when `value` is a power of two (including 1).
158pub fn is_power_of_two(value: usize) -> bool {
159    value != 0 && (value & (value - 1)) == 0
160}
161
162/// Log2 of a power-of-two value.  Panics in debug mode if `v` is not a power
163/// of two.
164pub fn log2_pow2(v: usize) -> u32 {
165    debug_assert!(is_power_of_two(v), "{v} is not a power of two");
166    v.trailing_zeros()
167}
168
169// ── Work-group tiling helpers ─────────────────────────────────────────────────
170
171/// Divides a 2-D problem of `(width × height)` into tiles of `(tw × th)` and
172/// returns `(tiles_x, tiles_y)`.
173///
174/// Each dimension is rounded up so the full problem is covered.
175pub fn tile_count_2d(width: usize, height: usize, tw: usize, th: usize) -> (usize, usize) {
176    let tx = width.div_ceil(tw);
177    let ty = height.div_ceil(th);
178    (tx, ty)
179}
180
181/// Total number of tiles for a 2-D problem.
182pub fn total_tiles_2d(width: usize, height: usize, tw: usize, th: usize) -> usize {
183    let (tx, ty) = tile_count_2d(width, height, tw, th);
184    tx * ty
185}
186
187/// Convert a flat tile index back to `(tile_x, tile_y)` for a grid with
188/// `tiles_x` columns.
189pub fn tile_index_to_2d(flat: usize, tiles_x: usize) -> (usize, usize) {
190    (flat % tiles_x, flat / tiles_x)
191}
192
193// ── Numeric helpers used across GPU kernels ──────────────────────────────────
194
195/// Clamp `v` to `[lo, hi]`.
196pub fn clamp_f64(v: f64, lo: f64, hi: f64) -> f64 {
197    v.max(lo).min(hi)
198}
199
200/// Smooth-step function: `3t² - 2t³` with `t = (v - lo) / (hi - lo)`.
201pub fn smoothstep(lo: f64, hi: f64, v: f64) -> f64 {
202    let t = clamp_f64((v - lo) / (hi - lo), 0.0, 1.0);
203    t * t * (3.0 - 2.0 * t)
204}
205
206/// Smoother-step (Ken Perlin's quintic): `6t⁵ − 15t⁴ + 10t³`.
207pub fn smootherstep(lo: f64, hi: f64, v: f64) -> f64 {
208    let t = clamp_f64((v - lo) / (hi - lo), 0.0, 1.0);
209    t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
210}
211
212/// Linear interpolation: `a + t*(b-a)`.
213pub fn lerp(a: f64, b: f64, t: f64) -> f64 {
214    a + t * (b - a)
215}
216
217/// Inverse lerp: returns `t` such that `lerp(a, b, t) == v`, or `0` if `a==b`.
218pub fn inv_lerp(a: f64, b: f64, v: f64) -> f64 {
219    if (b - a).abs() < f64::EPSILON {
220        return 0.0;
221    }
222    (v - a) / (b - a)
223}
224
225// ── FP utilities ─────────────────────────────────────────────────────────────
226
227/// Safe reciprocal: returns `1/x` when `|x| > eps`, else `0`.
228pub fn safe_recip(x: f64, eps: f64) -> f64 {
229    if x.abs() > eps { 1.0 / x } else { 0.0 }
230}
231
232/// Safe square root: clamps negative values to 0 before taking sqrt.
233pub fn safe_sqrt(x: f64) -> f64 {
234    x.max(0.0).sqrt()
235}
236
237/// Wrap an angle in radians to `(-π, π]`.
238pub fn wrap_angle(theta: f64) -> f64 {
239    use std::f64::consts::PI;
240    let mut t = theta % (2.0 * PI);
241    if t > PI {
242        t -= 2.0 * PI;
243    }
244    if t <= -PI {
245        t += 2.0 * PI;
246    }
247    t
248}
249
250// ── Vector math (3-D, f64) ────────────────────────────────────────────────────
251
252/// Compute the dot product of two 3-element arrays.
253pub fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
254    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
255}
256
257/// Compute the cross product of two 3-element arrays.
258pub fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
259    [
260        a[1] * b[2] - a[2] * b[1],
261        a[2] * b[0] - a[0] * b[2],
262        a[0] * b[1] - a[1] * b[0],
263    ]
264}
265
266/// Length of a 3-D vector.
267pub fn length3(v: [f64; 3]) -> f64 {
268    dot3(v, v).sqrt()
269}
270
271/// Normalise a 3-D vector.  Returns the zero vector if the length is < eps.
272pub fn normalize3(v: [f64; 3]) -> [f64; 3] {
273    let len = length3(v);
274    if len < 1e-15 {
275        return [0.0; 3];
276    }
277    [v[0] / len, v[1] / len, v[2] / len]
278}
279
280/// Reflect vector `d` about normal `n` (both assumed normalised).
281pub fn reflect3(d: [f64; 3], n: [f64; 3]) -> [f64; 3] {
282    let dn2 = 2.0 * dot3(d, n);
283    [d[0] - dn2 * n[0], d[1] - dn2 * n[1], d[2] - dn2 * n[2]]
284}
285
286// ── Parallel prefix sum (scan) ───────────────────────────────────────────────
287
288/// Parallel prefix sum (scan) on a slice of f64 values.
289///
290/// Returns a new vector where `result[i] = sum(data[0..i])`.
291/// This is the exclusive scan variant.
292pub fn exclusive_scan(data: &[f64]) -> Vec<f64> {
293    let mut result = Vec::with_capacity(data.len());
294    let mut acc = 0.0;
295    for &v in data {
296        result.push(acc);
297        acc += v;
298    }
299    result
300}
301
302/// Inclusive scan: `result[i] = sum(data[0..=i])`.
303pub fn inclusive_scan(data: &[f64]) -> Vec<f64> {
304    let mut result = Vec::with_capacity(data.len());
305    let mut acc = 0.0;
306    for &v in data {
307        acc += v;
308        result.push(acc);
309    }
310    result
311}
312
313/// Parallel reduce: compute the sum of all elements.
314pub fn reduce_sum(data: &[f64]) -> f64 {
315    data.iter().copied().sum()
316}
317
318/// Parallel reduce: compute the maximum of all elements.
319pub fn reduce_max(data: &[f64]) -> f64 {
320    data.iter().copied().fold(f64::NEG_INFINITY, f64::max)
321}
322
323/// Parallel reduce: compute the minimum of all elements.
324pub fn reduce_min(data: &[f64]) -> f64 {
325    data.iter().copied().fold(f64::INFINITY, f64::min)
326}
327
328#[cfg(test)]
329mod gpu_util_tests {
330    use super::*;
331    use std::f64::consts::PI;
332
333    #[test]
334    fn test_dispatch_count_exact() {
335        assert_eq!(dispatch_count(256, 64), 4);
336    }
337
338    #[test]
339    fn test_dispatch_count_remainder() {
340        assert_eq!(dispatch_count(257, 64), 5);
341    }
342
343    #[test]
344    fn test_dispatch_count_zero_group() {
345        assert_eq!(dispatch_count(100, 0), 0);
346    }
347
348    #[test]
349    fn test_aligned_size_exact() {
350        assert_eq!(aligned_size(256, 64), 256);
351    }
352
353    #[test]
354    fn test_aligned_size_pad() {
355        assert_eq!(aligned_size(257, 64), 320);
356    }
357
358    #[test]
359    fn test_aligned_size_zero_alignment() {
360        assert_eq!(aligned_size(100, 0), 100);
361    }
362
363    #[test]
364    fn test_linear_index_3d() {
365        // Grid 4x3x2
366        assert_eq!(linear_index_3d(0, 0, 0, 4, 3), 0);
367        assert_eq!(linear_index_3d(3, 2, 1, 4, 3), 12 + 2 * 4 + 3);
368    }
369
370    #[test]
371    fn test_index_3d_roundtrip() {
372        let (dx, dy) = (4, 3);
373        for z in 0..2 {
374            for y in 0..dy {
375                for x in 0..dx {
376                    let idx = linear_index_3d(x, y, z, dx, dy);
377                    let (rx, ry, rz) = index_3d_from_linear(idx, dx, dy);
378                    assert_eq!((rx, ry, rz), (x, y, z));
379                }
380            }
381        }
382    }
383
384    #[test]
385    fn test_dispatch_timer() {
386        let mut timer = DispatchTimer::new("test");
387        assert_eq!(timer.label, "test");
388        timer.record(0.5);
389        assert!((timer.elapsed_secs - 0.5).abs() < 1e-10);
390    }
391
392    #[test]
393    fn test_bandwidth_gb_s() {
394        // 1 GB in 1 second = 1 GB/s
395        let bw = bandwidth_gb_s(1_000_000_000, 1.0);
396        assert!((bw - 1.0).abs() < 1e-6);
397    }
398
399    #[test]
400    fn test_bandwidth_zero_time() {
401        assert!((bandwidth_gb_s(1000, 0.0)).abs() < 1e-10);
402    }
403
404    #[test]
405    fn test_elements_in_budget() {
406        assert_eq!(elements_in_budget(1024, 4), 256);
407        assert_eq!(elements_in_budget(1024, 0), 0);
408    }
409
410    #[test]
411    fn test_exclusive_scan() {
412        let data = [1.0, 2.0, 3.0, 4.0];
413        let result = exclusive_scan(&data);
414        assert_eq!(result, vec![0.0, 1.0, 3.0, 6.0]);
415    }
416
417    #[test]
418    fn test_inclusive_scan() {
419        let data = [1.0, 2.0, 3.0, 4.0];
420        let result = inclusive_scan(&data);
421        assert_eq!(result, vec![1.0, 3.0, 6.0, 10.0]);
422    }
423
424    #[test]
425    fn test_reduce_sum() {
426        assert!((reduce_sum(&[1.0, 2.0, 3.0]) - 6.0).abs() < 1e-10);
427    }
428
429    #[test]
430    fn test_reduce_max() {
431        assert!((reduce_max(&[1.0, 5.0, 3.0]) - 5.0).abs() < 1e-10);
432    }
433
434    #[test]
435    fn test_reduce_min() {
436        assert!((reduce_min(&[1.0, 5.0, 3.0]) - 1.0).abs() < 1e-10);
437    }
438
439    #[test]
440    fn test_exclusive_scan_empty() {
441        let result = exclusive_scan(&[]);
442        assert!(result.is_empty());
443    }
444
445    #[test]
446    fn test_inclusive_scan_single() {
447        let result = inclusive_scan(&[42.0]);
448        assert_eq!(result, vec![42.0]);
449    }
450
451    // ── Buffer utility tests ─────────────────────────────────────────────
452
453    #[test]
454    fn test_row_pitch_aligned() {
455        // 128 elements × 4 bytes = 512 bytes, already aligned to 256
456        assert_eq!(row_pitch(128, 4, 256), 512);
457    }
458
459    #[test]
460    fn test_row_pitch_needs_padding() {
461        // 100 × 4 = 400; aligned to 256 => 512
462        assert_eq!(row_pitch(100, 4, 256), 512);
463    }
464
465    #[test]
466    fn test_buffer_size_2d() {
467        // 4 rows × pitch(64 elems × 4 bytes, align 256) = 4 × 256 = 1024
468        assert_eq!(buffer_size_2d(64, 4, 4, 256), 1024);
469    }
470
471    #[test]
472    fn test_next_power_of_two() {
473        assert_eq!(next_power_of_two(0), 1);
474        assert_eq!(next_power_of_two(1), 1);
475        assert_eq!(next_power_of_two(5), 8);
476        assert_eq!(next_power_of_two(8), 8);
477        assert_eq!(next_power_of_two(9), 16);
478    }
479
480    #[test]
481    fn test_is_power_of_two() {
482        assert!(is_power_of_two(1));
483        assert!(is_power_of_two(16));
484        assert!(!is_power_of_two(0));
485        assert!(!is_power_of_two(7));
486    }
487
488    #[test]
489    fn test_log2_pow2() {
490        assert_eq!(log2_pow2(1), 0);
491        assert_eq!(log2_pow2(2), 1);
492        assert_eq!(log2_pow2(256), 8);
493    }
494
495    #[test]
496    fn test_tile_count_2d_exact() {
497        let (tx, ty) = tile_count_2d(64, 64, 16, 16);
498        assert_eq!(tx, 4);
499        assert_eq!(ty, 4);
500    }
501
502    #[test]
503    fn test_tile_count_2d_remainder() {
504        let (tx, ty) = tile_count_2d(65, 65, 16, 16);
505        assert_eq!(tx, 5);
506        assert_eq!(ty, 5);
507    }
508
509    #[test]
510    fn test_total_tiles_2d() {
511        assert_eq!(total_tiles_2d(64, 64, 16, 16), 16);
512    }
513
514    #[test]
515    fn test_tile_index_to_2d() {
516        // tiles_x = 4; flat=5 => (1, 1)
517        assert_eq!(tile_index_to_2d(5, 4), (1, 1));
518        assert_eq!(tile_index_to_2d(0, 4), (0, 0));
519    }
520
521    // ── Numeric helpers tests ────────────────────────────────────────────
522
523    #[test]
524    fn test_smoothstep_edges() {
525        assert!((smoothstep(0.0, 1.0, 0.0) - 0.0).abs() < 1e-12);
526        assert!((smoothstep(0.0, 1.0, 1.0) - 1.0).abs() < 1e-12);
527    }
528
529    #[test]
530    fn test_smoothstep_midpoint() {
531        // at t=0.5: 3*(0.25) - 2*(0.125) = 0.75 - 0.25 = 0.5
532        assert!((smoothstep(0.0, 1.0, 0.5) - 0.5).abs() < 1e-12);
533    }
534
535    #[test]
536    fn test_smootherstep_edges() {
537        assert!((smootherstep(0.0, 1.0, 0.0)).abs() < 1e-12);
538        assert!((smootherstep(0.0, 1.0, 1.0) - 1.0).abs() < 1e-12);
539    }
540
541    #[test]
542    fn test_lerp_inv_lerp_roundtrip() {
543        let a = 10.0;
544        let b = 20.0;
545        let t = 0.3;
546        let v = lerp(a, b, t);
547        assert!((inv_lerp(a, b, v) - t).abs() < 1e-12);
548    }
549
550    #[test]
551    fn test_safe_recip_normal() {
552        assert!((safe_recip(2.0, 1e-9) - 0.5).abs() < 1e-12);
553    }
554
555    #[test]
556    fn test_safe_recip_near_zero() {
557        assert!((safe_recip(1e-15, 1e-9)).abs() < 1e-12);
558    }
559
560    #[test]
561    fn test_safe_sqrt_positive() {
562        assert!((safe_sqrt(9.0) - 3.0).abs() < 1e-12);
563    }
564
565    #[test]
566    fn test_safe_sqrt_negative() {
567        assert!((safe_sqrt(-1.0)).abs() < 1e-12);
568    }
569
570    #[test]
571    fn test_wrap_angle_in_range() {
572        let wrapped = wrap_angle(3.0 * PI);
573        assert!(wrapped.abs() <= PI + 1e-12, "wrapped = {wrapped}");
574    }
575
576    // ── Vector math tests ────────────────────────────────────────────────
577
578    #[test]
579    fn test_dot3() {
580        let a = [1.0, 2.0, 3.0];
581        let b = [4.0, 5.0, 6.0];
582        assert!((dot3(a, b) - 32.0).abs() < 1e-12);
583    }
584
585    #[test]
586    fn test_cross3() {
587        let i = [1.0, 0.0, 0.0];
588        let j = [0.0, 1.0, 0.0];
589        let k = cross3(i, j);
590        assert!((k[0]).abs() < 1e-12);
591        assert!((k[1]).abs() < 1e-12);
592        assert!((k[2] - 1.0).abs() < 1e-12);
593    }
594
595    #[test]
596    fn test_length3() {
597        let v = [3.0, 4.0, 0.0];
598        assert!((length3(v) - 5.0).abs() < 1e-12);
599    }
600
601    #[test]
602    fn test_normalize3() {
603        let v = [0.0, 0.0, 5.0];
604        let n = normalize3(v);
605        assert!((length3(n) - 1.0).abs() < 1e-12);
606        assert!((n[2] - 1.0).abs() < 1e-12);
607    }
608
609    #[test]
610    fn test_normalize3_zero_vec() {
611        let n = normalize3([0.0; 3]);
612        assert_eq!(n, [0.0; 3]);
613    }
614
615    #[test]
616    fn test_reflect3() {
617        // Reflect (1,0,0) about (0,1,0) => still (1,0,0) but inverted y
618        let d = [0.0, -1.0, 0.0]; // pointing down
619        let n = [0.0, 1.0, 0.0]; // surface normal up
620        let r = reflect3(d, n);
621        // r = d - 2*(d·n)*n = [0,-1,0] - 2*(-1)*[0,1,0] = [0,1,0]
622        assert!((r[1] - 1.0).abs() < 1e-12);
623    }
624}
625pub mod collision_gpu;
626pub mod deformable_gpu;
627pub mod fluid_gpu;
628pub mod fluid_sim_gpu;
629pub mod gpu_cloth;
630pub mod gpu_collision_detection;
631pub mod gpu_collision_ext;
632pub mod gpu_fem_assembly;
633pub mod gpu_fluid;
634pub mod gpu_fluid_euler;
635pub mod gpu_lbm;
636pub mod gpu_md_solver;
637pub mod gpu_mesh_processing;
638pub mod gpu_neural_solver;
639pub mod gpu_nn;
640pub mod gpu_particle_system;
641pub mod gpu_particles;
642pub mod gpu_ray_tracing;
643pub mod gpu_reduction;
644pub mod gpu_rigid;
645pub mod gpu_sdf;
646pub mod gpu_sort;
647pub mod gpu_sparse_solver;
648pub mod gpu_sph_density;
649pub mod gpu_sph_pressure;
650pub mod gpu_sph_solver;
651pub mod gpu_thermal;
652pub mod gpu_voxel;
653pub mod memory;
654pub mod neural_physics;
655pub mod path_tracer;
656pub mod ray_marching;
657pub mod ray_tracing_gpu;
658pub mod raytracing;
659pub mod scheduler;