Skip to main content

jugar_web/
compute.rs

1// const fn and div_ceil are clearer as they are
2#![allow(clippy::missing_const_for_fn, clippy::manual_div_ceil)]
3
4//! WebGPU compute shader demonstration for physics calculations.
5//!
6//! This module provides a compute capability detection and demonstration layer
7//! showing the engine's GPU compute potential. The actual WebGPU bindings are
8//! handled by trueno, but this module exposes the game-engine-level API.
9//!
10//! ## Compute Tiers
11//!
12//! ```text
13//! ┌─────────────────────────────────────────────────────────────┐
14//! │                     Compute Backend Tiers                    │
15//! ├───────────┬──────────────────┬──────────────────────────────┤
16//! │   Tier    │     Backend      │         Capability           │
17//! ├───────────┼──────────────────┼──────────────────────────────┤
18//! │  Tier 1   │ WebGPU Compute   │ 10,000+ rigid bodies         │
19//! │  Tier 2   │ WASM SIMD128     │ 1,000+ rigid bodies          │
20//! │  Tier 3   │ Scalar           │ ~100 rigid bodies            │
21//! └───────────┴──────────────────┴──────────────────────────────┘
22//! ```
23//!
24//! ## Usage
25//!
26//! ```rust,ignore
27//! use jugar_web::compute::{ComputeCapability, detect_compute_capability};
28//!
29//! let caps = detect_compute_capability();
30//! println!("Compute tier: {}", caps.tier);
31//! println!("Max particles: {}", caps.max_recommended_particles);
32//! ```
33
34use crate::simd::{detect_compute_backend, ComputeBackend};
35
36/// Compute capability tier classification.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
38pub enum ComputeTier {
39    /// Tier 1: WebGPU compute shaders available
40    Tier1Gpu,
41    /// Tier 2: SIMD acceleration available (AVX2/NEON/WASM SIMD)
42    Tier2Simd,
43    /// Tier 3: Scalar fallback only
44    Tier3Scalar,
45}
46
47impl core::fmt::Display for ComputeTier {
48    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
49        match self {
50            Self::Tier1Gpu => write!(f, "Tier 1 (GPU)"),
51            Self::Tier2Simd => write!(f, "Tier 2 (SIMD)"),
52            Self::Tier3Scalar => write!(f, "Tier 3 (Scalar)"),
53        }
54    }
55}
56
57/// Detected compute capabilities for the current platform.
58#[derive(Debug, Clone)]
59pub struct ComputeCapability {
60    /// Compute tier classification
61    pub tier: ComputeTier,
62    /// Underlying backend being used
63    pub backend: ComputeBackend,
64    /// Whether GPU compute is available
65    pub gpu_available: bool,
66    /// Whether SIMD is available
67    pub simd_available: bool,
68    /// Maximum recommended particle count for 60 FPS
69    pub max_recommended_particles: u32,
70    /// Maximum recommended rigid body count for 60 FPS
71    pub max_recommended_bodies: u32,
72    /// Recommended batch size for physics updates
73    pub optimal_batch_size: u32,
74}
75
76impl ComputeCapability {
77    /// Creates capability info from detected backend.
78    #[must_use]
79    pub fn from_backend(backend: ComputeBackend) -> Self {
80        let (tier, gpu_available, simd_available, particles, bodies, batch) = match backend {
81            ComputeBackend::Gpu => (ComputeTier::Tier1Gpu, true, true, 100_000, 10_000, 1024),
82            ComputeBackend::CpuSimd | ComputeBackend::WasmSimd => {
83                (ComputeTier::Tier2Simd, false, true, 10_000, 1_000, 256)
84            }
85            ComputeBackend::CpuScalar => (ComputeTier::Tier3Scalar, false, false, 1_000, 100, 64),
86        };
87
88        Self {
89            tier,
90            backend,
91            gpu_available,
92            simd_available,
93            max_recommended_particles: particles,
94            max_recommended_bodies: bodies,
95            optimal_batch_size: batch,
96        }
97    }
98
99    /// Returns true if the system can handle large-scale physics.
100    #[must_use]
101    pub fn supports_large_scale_physics(&self) -> bool {
102        self.tier <= ComputeTier::Tier2Simd
103    }
104
105    /// Returns the recommended physics substep count for stable simulation.
106    #[must_use]
107    pub const fn recommended_substeps(&self) -> u32 {
108        match self.tier {
109            ComputeTier::Tier1Gpu => 1,
110            ComputeTier::Tier2Simd => 2,
111            ComputeTier::Tier3Scalar => 4,
112        }
113    }
114}
115
116impl Default for ComputeCapability {
117    fn default() -> Self {
118        detect_compute_capability()
119    }
120}
121
122/// Detects the compute capabilities of the current platform.
123#[must_use]
124pub fn detect_compute_capability() -> ComputeCapability {
125    let backend = detect_compute_backend();
126    ComputeCapability::from_backend(backend)
127}
128
129/// WebGPU compute demonstration module.
130///
131/// This struct demonstrates WebGPU compute shader concepts for physics.
132/// In a full implementation, this would interface with actual WebGPU bindings.
133#[derive(Debug, Clone)]
134pub struct ComputeDemo {
135    /// Current capability
136    capability: ComputeCapability,
137    /// Demo state
138    state: ComputeDemoState,
139}
140
141/// State for the compute demo.
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143pub enum ComputeDemoState {
144    /// Not started
145    Idle,
146    /// Demo is running
147    Running,
148    /// Demo completed
149    Completed,
150}
151
152impl Default for ComputeDemo {
153    fn default() -> Self {
154        Self::new()
155    }
156}
157
158impl ComputeDemo {
159    /// Creates a new compute demonstration.
160    #[must_use]
161    pub fn new() -> Self {
162        Self {
163            capability: detect_compute_capability(),
164            state: ComputeDemoState::Idle,
165        }
166    }
167
168    /// Returns the detected compute capability.
169    #[must_use]
170    pub const fn capability(&self) -> &ComputeCapability {
171        &self.capability
172    }
173
174    /// Returns the current demo state.
175    #[must_use]
176    pub const fn state(&self) -> ComputeDemoState {
177        self.state
178    }
179
180    /// Starts the compute demo.
181    pub fn start(&mut self) {
182        self.state = ComputeDemoState::Running;
183    }
184
185    /// Runs a compute benchmark and returns the result.
186    ///
187    /// This demonstrates the physics compute capability by running
188    /// a batch particle update operation.
189    #[must_use]
190    pub fn run_benchmark(&mut self, particle_count: usize) -> ComputeBenchmarkResult {
191        self.state = ComputeDemoState::Running;
192
193        // Allocate test data
194        let mut positions_x: Vec<f32> = (0..particle_count).map(|i| i as f32).collect();
195        let mut positions_y: Vec<f32> = (0..particle_count).map(|i| (i * 2) as f32).collect();
196        let velocities_x: Vec<f32> = (0..particle_count).map(|i| (i % 100) as f32).collect();
197        let mut velocities_y: Vec<f32> = (0..particle_count).map(|i| -((i % 50) as f32)).collect();
198
199        // Run physics update (uses SIMD when available)
200        crate::simd::batch_particle_update(
201            &mut positions_x,
202            &mut positions_y,
203            &velocities_x,
204            &mut velocities_y,
205            100.0, // gravity
206            0.016, // dt (~60 FPS)
207        );
208
209        self.state = ComputeDemoState::Completed;
210
211        ComputeBenchmarkResult {
212            particle_count,
213            backend: self.capability.backend,
214            tier: self.capability.tier,
215            positions_updated: !positions_x.is_empty(),
216            velocities_updated: !velocities_y.is_empty(),
217        }
218    }
219
220    /// Stops the demo and resets state.
221    pub fn stop(&mut self) {
222        self.state = ComputeDemoState::Idle;
223    }
224}
225
226/// Result of a compute benchmark run.
227#[derive(Debug, Clone)]
228pub struct ComputeBenchmarkResult {
229    /// Number of particles processed
230    pub particle_count: usize,
231    /// Backend used
232    pub backend: ComputeBackend,
233    /// Tier classification
234    pub tier: ComputeTier,
235    /// Whether positions were updated
236    pub positions_updated: bool,
237    /// Whether velocities were updated
238    pub velocities_updated: bool,
239}
240
241impl ComputeBenchmarkResult {
242    /// Returns a summary string for display.
243    #[must_use]
244    pub fn summary(&self) -> String {
245        format!(
246            "Processed {} particles using {} ({})",
247            self.particle_count, self.backend, self.tier
248        )
249    }
250}
251
252/// GPU compute shader information (for future WebGPU integration).
253#[derive(Debug, Clone)]
254pub struct GpuShaderInfo {
255    /// Shader name
256    pub name: String,
257    /// Shader type
258    pub shader_type: ShaderType,
259    /// Workgroup size X
260    pub workgroup_x: u32,
261    /// Workgroup size Y
262    pub workgroup_y: u32,
263    /// Workgroup size Z
264    pub workgroup_z: u32,
265}
266
267/// Type of compute shader.
268#[derive(Debug, Clone, Copy, PartialEq, Eq)]
269pub enum ShaderType {
270    /// Particle physics update
271    ParticlePhysics,
272    /// Collision detection broad phase
273    CollisionBroadPhase,
274    /// Collision detection narrow phase
275    CollisionNarrowPhase,
276    /// Constraint solver
277    ConstraintSolver,
278}
279
280impl core::fmt::Display for ShaderType {
281    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
282        match self {
283            Self::ParticlePhysics => write!(f, "Particle Physics"),
284            Self::CollisionBroadPhase => write!(f, "Collision Broad Phase"),
285            Self::CollisionNarrowPhase => write!(f, "Collision Narrow Phase"),
286            Self::ConstraintSolver => write!(f, "Constraint Solver"),
287        }
288    }
289}
290
291impl GpuShaderInfo {
292    /// Creates info for a particle physics shader.
293    #[must_use]
294    pub fn particle_physics() -> Self {
295        Self {
296            name: "jugar_particle_physics".to_string(),
297            shader_type: ShaderType::ParticlePhysics,
298            workgroup_x: 256,
299            workgroup_y: 1,
300            workgroup_z: 1,
301        }
302    }
303
304    /// Creates info for a collision broad phase shader.
305    #[must_use]
306    pub fn collision_broad_phase() -> Self {
307        Self {
308            name: "jugar_collision_broad".to_string(),
309            shader_type: ShaderType::CollisionBroadPhase,
310            workgroup_x: 64,
311            workgroup_y: 1,
312            workgroup_z: 1,
313        }
314    }
315
316    /// Returns total workgroup size.
317    #[must_use]
318    pub const fn workgroup_size(&self) -> u32 {
319        self.workgroup_x * self.workgroup_y * self.workgroup_z
320    }
321
322    /// Returns number of workgroups needed for given element count.
323    #[must_use]
324    pub const fn workgroups_for(&self, element_count: u32) -> u32 {
325        let size = self.workgroup_size();
326        if size == 0 {
327            return 0;
328        }
329        (element_count + size - 1) / size
330    }
331}
332
333/// WGSL compute shader source for particle physics (demonstration).
334///
335/// This is the WGSL shader that would be used when WebGPU is available.
336/// Currently shown for documentation purposes.
337pub const PARTICLE_PHYSICS_WGSL: &str = r"
338// Jugar Particle Physics Compute Shader
339// Processes particle positions and velocities in parallel on GPU
340
341struct Particle {
342    pos_x: f32,
343    pos_y: f32,
344    vel_x: f32,
345    vel_y: f32,
346}
347
348struct Params {
349    dt: f32,
350    gravity: f32,
351    particle_count: u32,
352    _padding: u32,
353}
354
355@group(0) @binding(0) var<storage, read_write> particles: array<Particle>;
356@group(0) @binding(1) var<uniform> params: Params;
357
358@compute @workgroup_size(256)
359fn main(@builtin(global_invocation_id) id: vec3<u32>) {
360    let idx = id.x;
361    if (idx >= params.particle_count) {
362        return;
363    }
364
365    var p = particles[idx];
366
367    // Apply velocity
368    p.pos_x += p.vel_x * params.dt;
369    p.pos_y += p.vel_y * params.dt;
370
371    // Apply gravity
372    p.vel_y += params.gravity * params.dt;
373
374    particles[idx] = p;
375}
376";
377
378#[cfg(test)]
379#[allow(clippy::unwrap_used, clippy::expect_used)]
380mod tests {
381    use super::*;
382
383    // =========================================================================
384    // ComputeTier Tests
385    // =========================================================================
386
387    #[test]
388    fn test_compute_tier_display() {
389        assert_eq!(format!("{}", ComputeTier::Tier1Gpu), "Tier 1 (GPU)");
390        assert_eq!(format!("{}", ComputeTier::Tier2Simd), "Tier 2 (SIMD)");
391        assert_eq!(format!("{}", ComputeTier::Tier3Scalar), "Tier 3 (Scalar)");
392    }
393
394    #[test]
395    fn test_compute_tier_ordering() {
396        // Lower tier = better
397        assert!(ComputeTier::Tier1Gpu < ComputeTier::Tier2Simd);
398        assert!(ComputeTier::Tier2Simd < ComputeTier::Tier3Scalar);
399    }
400
401    // =========================================================================
402    // ComputeCapability Tests
403    // =========================================================================
404
405    #[test]
406    fn test_compute_capability_from_gpu_backend() {
407        let caps = ComputeCapability::from_backend(ComputeBackend::Gpu);
408
409        assert_eq!(caps.tier, ComputeTier::Tier1Gpu);
410        assert!(caps.gpu_available);
411        assert!(caps.simd_available);
412        assert_eq!(caps.max_recommended_particles, 100_000);
413        assert_eq!(caps.max_recommended_bodies, 10_000);
414    }
415
416    #[test]
417    fn test_compute_capability_from_simd_backend() {
418        let caps = ComputeCapability::from_backend(ComputeBackend::CpuSimd);
419
420        assert_eq!(caps.tier, ComputeTier::Tier2Simd);
421        assert!(!caps.gpu_available);
422        assert!(caps.simd_available);
423        assert_eq!(caps.max_recommended_particles, 10_000);
424        assert_eq!(caps.max_recommended_bodies, 1_000);
425    }
426
427    #[test]
428    fn test_compute_capability_from_wasm_simd_backend() {
429        let caps = ComputeCapability::from_backend(ComputeBackend::WasmSimd);
430
431        assert_eq!(caps.tier, ComputeTier::Tier2Simd);
432        assert!(!caps.gpu_available);
433        assert!(caps.simd_available);
434    }
435
436    #[test]
437    fn test_compute_capability_from_scalar_backend() {
438        let caps = ComputeCapability::from_backend(ComputeBackend::CpuScalar);
439
440        assert_eq!(caps.tier, ComputeTier::Tier3Scalar);
441        assert!(!caps.gpu_available);
442        assert!(!caps.simd_available);
443        assert_eq!(caps.max_recommended_particles, 1_000);
444        assert_eq!(caps.max_recommended_bodies, 100);
445    }
446
447    #[test]
448    fn test_compute_capability_supports_large_scale_physics() {
449        let gpu_caps = ComputeCapability::from_backend(ComputeBackend::Gpu);
450        let simd_caps = ComputeCapability::from_backend(ComputeBackend::CpuSimd);
451        let scalar_caps = ComputeCapability::from_backend(ComputeBackend::CpuScalar);
452
453        assert!(gpu_caps.supports_large_scale_physics());
454        assert!(simd_caps.supports_large_scale_physics());
455        assert!(!scalar_caps.supports_large_scale_physics());
456    }
457
458    #[test]
459    fn test_compute_capability_recommended_substeps() {
460        let gpu_caps = ComputeCapability::from_backend(ComputeBackend::Gpu);
461        let simd_caps = ComputeCapability::from_backend(ComputeBackend::CpuSimd);
462        let scalar_caps = ComputeCapability::from_backend(ComputeBackend::CpuScalar);
463
464        assert_eq!(gpu_caps.recommended_substeps(), 1);
465        assert_eq!(simd_caps.recommended_substeps(), 2);
466        assert_eq!(scalar_caps.recommended_substeps(), 4);
467    }
468
469    #[test]
470    fn test_detect_compute_capability() {
471        let caps = detect_compute_capability();
472
473        // Should return valid capability
474        assert!(caps.max_recommended_particles > 0);
475        assert!(caps.max_recommended_bodies > 0);
476        assert!(caps.optimal_batch_size > 0);
477    }
478
479    // =========================================================================
480    // ComputeDemo Tests
481    // =========================================================================
482
483    #[test]
484    fn test_compute_demo_new() {
485        let demo = ComputeDemo::new();
486
487        assert_eq!(demo.state(), ComputeDemoState::Idle);
488    }
489
490    #[test]
491    fn test_compute_demo_start() {
492        let mut demo = ComputeDemo::new();
493        demo.start();
494
495        assert_eq!(demo.state(), ComputeDemoState::Running);
496    }
497
498    #[test]
499    fn test_compute_demo_stop() {
500        let mut demo = ComputeDemo::new();
501        demo.start();
502        demo.stop();
503
504        assert_eq!(demo.state(), ComputeDemoState::Idle);
505    }
506
507    #[test]
508    fn test_compute_demo_run_benchmark() {
509        let mut demo = ComputeDemo::new();
510        let result = demo.run_benchmark(100);
511
512        assert_eq!(demo.state(), ComputeDemoState::Completed);
513        assert_eq!(result.particle_count, 100);
514        assert!(result.positions_updated);
515        assert!(result.velocities_updated);
516    }
517
518    #[test]
519    fn test_compute_benchmark_result_summary() {
520        let result = ComputeBenchmarkResult {
521            particle_count: 1000,
522            backend: ComputeBackend::CpuSimd,
523            tier: ComputeTier::Tier2Simd,
524            positions_updated: true,
525            velocities_updated: true,
526        };
527
528        let summary = result.summary();
529        assert!(summary.contains("1000"));
530        assert!(summary.contains("CPU SIMD"));
531    }
532
533    // =========================================================================
534    // GpuShaderInfo Tests
535    // =========================================================================
536
537    #[test]
538    fn test_gpu_shader_info_particle_physics() {
539        let info = GpuShaderInfo::particle_physics();
540
541        assert_eq!(info.name, "jugar_particle_physics");
542        assert_eq!(info.shader_type, ShaderType::ParticlePhysics);
543        assert_eq!(info.workgroup_size(), 256);
544    }
545
546    #[test]
547    fn test_gpu_shader_info_collision_broad_phase() {
548        let info = GpuShaderInfo::collision_broad_phase();
549
550        assert_eq!(info.name, "jugar_collision_broad");
551        assert_eq!(info.shader_type, ShaderType::CollisionBroadPhase);
552        assert_eq!(info.workgroup_size(), 64);
553    }
554
555    #[test]
556    fn test_gpu_shader_info_workgroups_for() {
557        let info = GpuShaderInfo::particle_physics();
558
559        assert_eq!(info.workgroups_for(256), 1);
560        assert_eq!(info.workgroups_for(257), 2);
561        assert_eq!(info.workgroups_for(512), 2);
562        assert_eq!(info.workgroups_for(1000), 4);
563    }
564
565    #[test]
566    fn test_shader_type_display() {
567        assert_eq!(
568            format!("{}", ShaderType::ParticlePhysics),
569            "Particle Physics"
570        );
571        assert_eq!(
572            format!("{}", ShaderType::CollisionBroadPhase),
573            "Collision Broad Phase"
574        );
575        assert_eq!(
576            format!("{}", ShaderType::CollisionNarrowPhase),
577            "Collision Narrow Phase"
578        );
579        assert_eq!(
580            format!("{}", ShaderType::ConstraintSolver),
581            "Constraint Solver"
582        );
583    }
584
585    // =========================================================================
586    // WGSL Shader Tests
587    // =========================================================================
588
589    #[test]
590    fn test_particle_physics_wgsl_is_valid() {
591        // Verify the WGSL shader source contains expected elements
592        assert!(PARTICLE_PHYSICS_WGSL.contains("@compute"));
593        assert!(PARTICLE_PHYSICS_WGSL.contains("@workgroup_size(256)"));
594        assert!(PARTICLE_PHYSICS_WGSL.contains("struct Particle"));
595        assert!(PARTICLE_PHYSICS_WGSL.contains("pos_x"));
596        assert!(PARTICLE_PHYSICS_WGSL.contains("vel_y"));
597        assert!(PARTICLE_PHYSICS_WGSL.contains("gravity"));
598    }
599}