1#![allow(clippy::missing_const_for_fn, clippy::manual_div_ceil)]
3
4use crate::simd::{detect_compute_backend, ComputeBackend};
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
38pub enum ComputeTier {
39 Tier1Gpu,
41 Tier2Simd,
43 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#[derive(Debug, Clone)]
59pub struct ComputeCapability {
60 pub tier: ComputeTier,
62 pub backend: ComputeBackend,
64 pub gpu_available: bool,
66 pub simd_available: bool,
68 pub max_recommended_particles: u32,
70 pub max_recommended_bodies: u32,
72 pub optimal_batch_size: u32,
74}
75
76impl ComputeCapability {
77 #[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 #[must_use]
101 pub fn supports_large_scale_physics(&self) -> bool {
102 self.tier <= ComputeTier::Tier2Simd
103 }
104
105 #[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#[must_use]
124pub fn detect_compute_capability() -> ComputeCapability {
125 let backend = detect_compute_backend();
126 ComputeCapability::from_backend(backend)
127}
128
129#[derive(Debug, Clone)]
134pub struct ComputeDemo {
135 capability: ComputeCapability,
137 state: ComputeDemoState,
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143pub enum ComputeDemoState {
144 Idle,
146 Running,
148 Completed,
150}
151
152impl Default for ComputeDemo {
153 fn default() -> Self {
154 Self::new()
155 }
156}
157
158impl ComputeDemo {
159 #[must_use]
161 pub fn new() -> Self {
162 Self {
163 capability: detect_compute_capability(),
164 state: ComputeDemoState::Idle,
165 }
166 }
167
168 #[must_use]
170 pub const fn capability(&self) -> &ComputeCapability {
171 &self.capability
172 }
173
174 #[must_use]
176 pub const fn state(&self) -> ComputeDemoState {
177 self.state
178 }
179
180 pub fn start(&mut self) {
182 self.state = ComputeDemoState::Running;
183 }
184
185 #[must_use]
190 pub fn run_benchmark(&mut self, particle_count: usize) -> ComputeBenchmarkResult {
191 self.state = ComputeDemoState::Running;
192
193 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 crate::simd::batch_particle_update(
201 &mut positions_x,
202 &mut positions_y,
203 &velocities_x,
204 &mut velocities_y,
205 100.0, 0.016, );
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 pub fn stop(&mut self) {
222 self.state = ComputeDemoState::Idle;
223 }
224}
225
226#[derive(Debug, Clone)]
228pub struct ComputeBenchmarkResult {
229 pub particle_count: usize,
231 pub backend: ComputeBackend,
233 pub tier: ComputeTier,
235 pub positions_updated: bool,
237 pub velocities_updated: bool,
239}
240
241impl ComputeBenchmarkResult {
242 #[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#[derive(Debug, Clone)]
254pub struct GpuShaderInfo {
255 pub name: String,
257 pub shader_type: ShaderType,
259 pub workgroup_x: u32,
261 pub workgroup_y: u32,
263 pub workgroup_z: u32,
265}
266
267#[derive(Debug, Clone, Copy, PartialEq, Eq)]
269pub enum ShaderType {
270 ParticlePhysics,
272 CollisionBroadPhase,
274 CollisionNarrowPhase,
276 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 #[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 #[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 #[must_use]
318 pub const fn workgroup_size(&self) -> u32 {
319 self.workgroup_x * self.workgroup_y * self.workgroup_z
320 }
321
322 #[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
333pub 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 #[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 assert!(ComputeTier::Tier1Gpu < ComputeTier::Tier2Simd);
398 assert!(ComputeTier::Tier2Simd < ComputeTier::Tier3Scalar);
399 }
400
401 #[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 assert!(caps.max_recommended_particles > 0);
475 assert!(caps.max_recommended_bodies > 0);
476 assert!(caps.optimal_batch_size > 0);
477 }
478
479 #[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 #[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 #[test]
590 fn test_particle_physics_wgsl_is_valid() {
591 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}