1use glam::{Vec3, Vec4};
20use std::sync::atomic::{AtomicU32, Ordering};
21
22#[repr(C, align(16))]
28#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
29pub struct GpuParticle {
30 pub position: [f32; 3],
32 pub _pad0: f32,
34 pub velocity: [f32; 3],
36 pub _pad1: f32,
38 pub color: [f32; 4],
40 pub life: f32,
42 pub max_life: f32,
44 pub size: f32,
46 pub engine_type: u32,
50 pub seed: f32,
52 pub flags: u32,
54 pub _reserved: [f32; 2],
56}
57
58const _: () = assert!(std::mem::size_of::<GpuParticle>() == 80);
60
61impl GpuParticle {
62 pub fn dead() -> Self {
63 Self {
64 position: [0.0; 3],
65 _pad0: 0.0,
66 velocity: [0.0; 3],
67 _pad1: 0.0,
68 color: [0.0; 4],
69 life: 0.0,
70 max_life: 0.0,
71 size: 0.0,
72 engine_type: 0,
73 seed: 0.0,
74 flags: 0,
75 _reserved: [0.0; 2],
76 }
77 }
78}
79
80#[repr(C)]
86#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
87pub struct GpuForceField {
88 pub position: [f32; 3],
90 pub strength: f32,
92 pub field_type: u32,
94 pub radius: f32,
96 pub falloff: f32,
98 pub _pad: f32,
100}
101
102pub const MAX_GPU_FORCE_FIELDS: usize = 16;
104
105#[derive(Clone, Debug)]
109pub struct GpuEmitterConfig {
110 pub emit_count: u32,
112 pub origin: Vec3,
114 pub radius: f32,
116 pub speed_min: f32,
118 pub speed_max: f32,
119 pub life_min: f32,
121 pub life_max: f32,
122 pub engine_type: u32,
124 pub size_min: f32,
126 pub size_max: f32,
127 pub color: Vec4,
129 pub flags: u32,
131}
132
133impl Default for GpuEmitterConfig {
134 fn default() -> Self {
135 Self {
136 emit_count: 0,
137 origin: Vec3::ZERO,
138 radius: 1.0,
139 speed_min: 0.5,
140 speed_max: 2.0,
141 life_min: 2.0,
142 life_max: 5.0,
143 engine_type: 0,
144 size_min: 0.5,
145 size_max: 1.5,
146 color: Vec4::ONE,
147 flags: 1, }
149 }
150}
151
152#[derive(Clone, Debug)]
156pub struct EngineDistribution {
157 pub counts: [u32; 10],
159 pub total: u32,
161}
162
163impl EngineDistribution {
164 pub fn even(total: u32) -> Self {
166 let per = total / 10;
167 let remainder = total % 10;
168 let mut counts = [per; 10];
169 for i in 0..remainder as usize {
170 counts[i] += 1;
171 }
172 Self { counts, total }
173 }
174
175 pub fn weighted(total: u32, weights: &[f32; 10]) -> Self {
177 let sum: f32 = weights.iter().sum();
178 let mut counts = [0u32; 10];
179 let mut assigned = 0u32;
180 for i in 0..10 {
181 counts[i] = ((weights[i] / sum) * total as f32).round() as u32;
182 assigned += counts[i];
183 }
184 if assigned < total {
186 counts[0] += total - assigned;
187 }
188 Self { counts, total }
189 }
190
191 pub fn chaos_field(total: u32) -> Self {
193 Self::weighted(total, &[
194 0.05, 0.20, 0.10, 0.10, 0.15, 0.10, 0.08, 0.08, 0.07, 0.07, ])
205 }
206}
207
208pub struct GpuParticleSystem {
216 pub max_particles: u32,
218 pub alive_count_approx: u32,
220 pub current_read: u32,
222 pub corruption: f32,
224 pub force_fields: Vec<GpuForceField>,
226 pub pending_emits: Vec<GpuEmitterConfig>,
228 pub initialized: bool,
230 pub distribution: EngineDistribution,
232 pub depth_layers: Vec<f32>,
234 pub damping: f32,
236 pub gravity: Vec3,
238 pub wind: Vec3,
240 pub turbulence: f32,
242}
243
244impl GpuParticleSystem {
245 pub fn new(max_particles: u32) -> Self {
247 Self {
248 max_particles,
249 alive_count_approx: 0,
250 current_read: 0,
251 corruption: 0.0,
252 force_fields: Vec::with_capacity(MAX_GPU_FORCE_FIELDS),
253 pending_emits: Vec::new(),
254 initialized: false,
255 distribution: EngineDistribution::chaos_field(max_particles),
256 depth_layers: vec![-5.0, 0.0, 5.0],
257 damping: 0.99,
258 gravity: Vec3::ZERO,
259 wind: Vec3::ZERO,
260 turbulence: 0.0,
261 }
262 }
263
264 pub fn chaos_field() -> Self {
266 let mut sys = Self::new(50_000);
267 sys.depth_layers = vec![-8.0, 0.0, 8.0];
268 sys.damping = 0.995;
269 sys.turbulence = 0.3;
270 sys
271 }
272
273 pub fn chaos_field_large() -> Self {
275 let mut sys = Self::new(131_072);
276 sys.depth_layers = vec![-12.0, -4.0, 4.0, 12.0];
277 sys.damping = 0.997;
278 sys.turbulence = 0.2;
279 sys
280 }
281
282 pub fn generate_initial_particles(&self, bounds: Vec3) -> Vec<GpuParticle> {
287 let mut particles = Vec::with_capacity(self.max_particles as usize);
288 let mut rng_state = 12345u32;
289
290 let mut rng = || -> f32 {
292 rng_state = rng_state.wrapping_mul(1664525).wrapping_add(1013904223);
293 (rng_state as f32) / (u32::MAX as f32)
294 };
295
296 let num_layers = self.depth_layers.len().max(1);
297
298 for engine_type in 0..10u32 {
299 let count = self.distribution.counts[engine_type as usize];
300 for i in 0..count {
301 let layer_idx = (i as usize) % num_layers;
302 let z = if layer_idx < self.depth_layers.len() {
303 self.depth_layers[layer_idx]
304 } else {
305 0.0
306 };
307
308 let x = (rng() - 0.5) * bounds.x * 2.0;
309 let y = (rng() - 0.5) * bounds.y * 2.0;
310 let z = z + (rng() - 0.5) * 2.0;
311
312 let vx = (rng() - 0.5) * 0.5;
313 let vy = (rng() - 0.5) * 0.5;
314 let vz = (rng() - 0.5) * 0.1;
315
316 let life = 5.0 + rng() * 10.0;
317 let size = 0.3 + rng() * 0.7;
318
319 let color = engine_color(engine_type, rng());
321
322 particles.push(GpuParticle {
323 position: [x, y, z],
324 _pad0: 0.0,
325 velocity: [vx, vy, vz],
326 _pad1: 0.0,
327 color: color.to_array(),
328 life,
329 max_life: life,
330 size,
331 engine_type,
332 seed: rng(),
333 flags: 1, _reserved: [0.0; 2],
335 });
336 }
337 }
338
339 while particles.len() < self.max_particles as usize {
341 particles.push(GpuParticle::dead());
342 }
343
344 particles
345 }
346
347 pub fn add_force_field(&mut self, field: GpuForceField) {
349 if self.force_fields.len() < MAX_GPU_FORCE_FIELDS {
350 self.force_fields.push(field);
351 }
352 }
353
354 pub fn add_impact_field(&mut self, position: Vec3, strength: f32, radius: f32) {
356 self.add_force_field(GpuForceField {
357 position: position.to_array(),
358 strength,
359 field_type: 0, radius,
361 falloff: 2.0, _pad: 0.0,
363 });
364 }
365
366 pub fn add_vortex_field(&mut self, position: Vec3, strength: f32, radius: f32) {
368 self.add_force_field(GpuForceField {
369 position: position.to_array(),
370 strength,
371 field_type: 1, radius,
373 falloff: 1.0,
374 _pad: 0.0,
375 });
376 }
377
378 pub fn add_repulsion_field(&mut self, position: Vec3, strength: f32, radius: f32) {
380 self.add_force_field(GpuForceField {
381 position: position.to_array(),
382 strength: -strength.abs(),
383 field_type: 2, radius,
385 falloff: 2.0,
386 _pad: 0.0,
387 });
388 }
389
390 pub fn emit(&mut self, config: GpuEmitterConfig) {
392 self.pending_emits.push(config);
393 }
394
395 pub fn emit_burst(&mut self, origin: Vec3, count: u32, engine_type: u32, color: Vec4) {
397 self.emit(GpuEmitterConfig {
398 emit_count: count,
399 origin,
400 engine_type,
401 color,
402 ..GpuEmitterConfig::default()
403 });
404 }
405
406 pub fn clear_frame_state(&mut self) {
408 self.force_fields.clear();
409 self.pending_emits.clear();
410 }
411
412 pub fn swap_buffers(&mut self) {
414 self.current_read = 1 - self.current_read;
415 }
416
417 pub fn update_dispatch_params(&self) -> GpuParticleDispatchParams {
419 GpuParticleDispatchParams {
420 particle_count: self.max_particles,
421 workgroup_size: 256,
422 corruption: self.corruption,
423 damping: self.damping,
424 gravity: self.gravity.to_array(),
425 wind: self.wind.to_array(),
426 turbulence: self.turbulence,
427 force_field_count: self.force_fields.len() as u32,
428 }
429 }
430
431 pub fn indirect_draw_params(&self) -> GpuIndirectDrawParams {
433 GpuIndirectDrawParams {
434 vertex_count: 6, instance_count: self.alive_count_approx,
436 first_vertex: 0,
437 first_instance: 0,
438 }
439 }
440}
441
442impl Default for GpuParticleSystem {
443 fn default() -> Self {
444 Self::new(50_000)
445 }
446}
447
448#[derive(Clone, Debug)]
452pub struct GpuParticleDispatchParams {
453 pub particle_count: u32,
454 pub workgroup_size: u32,
455 pub corruption: f32,
456 pub damping: f32,
457 pub gravity: [f32; 3],
458 pub wind: [f32; 3],
459 pub turbulence: f32,
460 pub force_field_count: u32,
461}
462
463impl GpuParticleDispatchParams {
464 pub fn num_workgroups(&self) -> u32 {
466 (self.particle_count + self.workgroup_size - 1) / self.workgroup_size
467 }
468}
469
470#[repr(C)]
472#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
473pub struct GpuIndirectDrawParams {
474 pub vertex_count: u32,
475 pub instance_count: u32,
476 pub first_vertex: u32,
477 pub first_instance: u32,
478}
479
480#[derive(Clone, Debug)]
484pub struct TemporalForceField {
485 pub field: GpuForceField,
486 pub life: f32,
487 pub max_life: f32,
488 pub fade_start: f32, }
490
491impl TemporalForceField {
492 pub fn new(field: GpuForceField, duration: f32) -> Self {
493 Self {
494 field,
495 life: duration,
496 max_life: duration,
497 fade_start: 0.3,
498 }
499 }
500
501 pub fn tick(&mut self, dt: f32) -> bool {
503 self.life -= dt;
504 if self.life <= 0.0 {
505 return false;
506 }
507 let life_frac = self.life / self.max_life;
509 if life_frac < self.fade_start {
510 let fade = life_frac / self.fade_start;
511 self.field.strength *= fade;
512 }
513 true
514 }
515
516 pub fn is_alive(&self) -> bool {
517 self.life > 0.0
518 }
519}
520
521pub struct TemporalFieldManager {
523 fields: Vec<TemporalForceField>,
524}
525
526impl TemporalFieldManager {
527 pub fn new() -> Self {
528 Self { fields: Vec::with_capacity(32) }
529 }
530
531 pub fn add(&mut self, field: TemporalForceField) {
533 self.fields.push(field);
534 }
535
536 pub fn add_impact(&mut self, position: Vec3, strength: f32, radius: f32, duration: f32) {
538 self.add(TemporalForceField::new(
539 GpuForceField {
540 position: position.to_array(),
541 strength,
542 field_type: 0,
543 radius,
544 falloff: 2.0,
545 _pad: 0.0,
546 },
547 duration,
548 ));
549 }
550
551 pub fn add_vortex(&mut self, position: Vec3, strength: f32, radius: f32, duration: f32) {
553 self.add(TemporalForceField::new(
554 GpuForceField {
555 position: position.to_array(),
556 strength,
557 field_type: 1,
558 radius,
559 falloff: 1.0,
560 _pad: 0.0,
561 },
562 duration,
563 ));
564 }
565
566 pub fn add_shockwave(&mut self, position: Vec3, strength: f32, radius: f32, duration: f32) {
568 self.add(TemporalForceField::new(
569 GpuForceField {
570 position: position.to_array(),
571 strength: -strength.abs(),
572 field_type: 2,
573 radius,
574 falloff: 2.0,
575 _pad: 0.0,
576 },
577 duration,
578 ));
579 }
580
581 pub fn tick_and_apply(&mut self, dt: f32, gpu_sys: &mut GpuParticleSystem) {
583 self.fields.retain_mut(|f| {
584 if f.tick(dt) {
585 gpu_sys.add_force_field(f.field);
586 true
587 } else {
588 false
589 }
590 });
591 }
592
593 pub fn count(&self) -> usize {
595 self.fields.len()
596 }
597
598 pub fn clear(&mut self) {
600 self.fields.clear();
601 }
602}
603
604impl Default for TemporalFieldManager {
605 fn default() -> Self {
606 Self::new()
607 }
608}
609
610fn engine_color(engine_type: u32, variation: f32) -> Vec4 {
614 let v = variation * 0.2; match engine_type {
616 0 => Vec4::new(0.5 + v, 0.5 + v, 0.5 + v, 0.8), 1 => Vec4::new(0.2 + v, 0.4, 1.0, 0.9), 2 => Vec4::new(0.8, 0.2 + v, 0.8, 0.85), 3 => Vec4::new(0.1, 0.8 + v, 0.8, 0.85), 4 => Vec4::new(1.0, 0.4 + v, 0.1, 0.9), 5 => Vec4::new(0.3, 0.9 + v, 0.3, 0.85), 6 => Vec4::new(0.9, 0.9 + v, 0.2, 0.85), 7 => Vec4::new(1.0, 0.2, 0.3 + v, 0.9), 8 => Vec4::new(0.6, 0.3 + v, 1.0, 0.85), 9 => Vec4::new(0.9, 0.7 + v, 0.5, 0.85), _ => Vec4::new(1.0, 1.0, 1.0, 0.8),
627 }
628}
629
630pub struct ChaosFieldPresets;
634
635impl ChaosFieldPresets {
636 pub fn exploration() -> GpuParticleSystem {
638 let mut sys = GpuParticleSystem::new(30_000);
639 sys.damping = 0.998;
640 sys.turbulence = 0.1;
641 sys.corruption = 0.0;
642 sys
643 }
644
645 pub fn combat() -> GpuParticleSystem {
647 let mut sys = GpuParticleSystem::new(50_000);
648 sys.damping = 0.995;
649 sys.turbulence = 0.3;
650 sys.corruption = 0.1;
651 sys
652 }
653
654 pub fn boss_fight() -> GpuParticleSystem {
656 let mut sys = GpuParticleSystem::new(80_000);
657 sys.damping = 0.99;
658 sys.turbulence = 0.5;
659 sys.corruption = 0.3;
660 sys
661 }
662
663 pub fn corruption_zone(corruption_level: f32) -> GpuParticleSystem {
665 let mut sys = GpuParticleSystem::new(65_000);
666 sys.damping = 0.985;
667 sys.turbulence = 0.7;
668 sys.corruption = corruption_level.clamp(0.0, 1.0);
669 sys
670 }
671
672 pub fn menu_background() -> GpuParticleSystem {
674 let mut sys = GpuParticleSystem::new(15_000);
675 sys.damping = 0.999;
676 sys.turbulence = 0.05;
677 sys.corruption = 0.0;
678 sys.depth_layers = vec![-3.0, 0.0, 3.0];
679 sys
680 }
681}
682
683#[cfg(test)]
686mod tests {
687 use super::*;
688
689 #[test]
690 fn gpu_particle_size() {
691 assert_eq!(std::mem::size_of::<GpuParticle>(), 80);
692 }
693
694 #[test]
695 fn indirect_draw_params_size() {
696 assert_eq!(std::mem::size_of::<GpuIndirectDrawParams>(), 16);
697 }
698
699 #[test]
700 fn even_distribution() {
701 let dist = EngineDistribution::even(1000);
702 assert_eq!(dist.counts.iter().sum::<u32>(), 1000);
703 }
704
705 #[test]
706 fn chaos_field_distribution() {
707 let dist = EngineDistribution::chaos_field(50_000);
708 assert_eq!(dist.total, 50_000);
709 assert!(dist.counts[1] > dist.counts[0]);
711 }
712
713 #[test]
714 fn generate_initial_fills_to_max() {
715 let sys = GpuParticleSystem::new(100);
716 let particles = sys.generate_initial_particles(Vec3::new(10.0, 10.0, 5.0));
717 assert_eq!(particles.len(), 100);
718 }
719
720 #[test]
721 fn temporal_field_fades() {
722 let mut field = TemporalForceField::new(
723 GpuForceField {
724 position: [0.0; 3],
725 strength: 10.0,
726 field_type: 0,
727 radius: 5.0,
728 falloff: 2.0,
729 _pad: 0.0,
730 },
731 1.0,
732 );
733 assert!(field.tick(0.5));
734 assert!(field.tick(0.4));
735 assert!(!field.tick(0.2)); }
737
738 #[test]
739 fn temporal_manager_removes_dead() {
740 let mut mgr = TemporalFieldManager::new();
741 let mut sys = GpuParticleSystem::new(100);
742 mgr.add_impact(Vec3::ZERO, 10.0, 5.0, 0.1);
743 mgr.add_impact(Vec3::ONE, 5.0, 3.0, 1.0);
744 mgr.tick_and_apply(0.2, &mut sys);
745 assert_eq!(mgr.count(), 1); assert_eq!(sys.force_fields.len(), 1); }
748
749 #[test]
750 fn swap_buffers() {
751 let mut sys = GpuParticleSystem::new(100);
752 assert_eq!(sys.current_read, 0);
753 sys.swap_buffers();
754 assert_eq!(sys.current_read, 1);
755 sys.swap_buffers();
756 assert_eq!(sys.current_read, 0);
757 }
758
759 #[test]
760 fn dispatch_params_workgroups() {
761 let sys = GpuParticleSystem::new(1000);
762 let params = sys.update_dispatch_params();
763 assert_eq!(params.num_workgroups(), 4); }
765
766 #[test]
767 fn force_field_limit() {
768 let mut sys = GpuParticleSystem::new(100);
769 for _ in 0..20 {
770 sys.add_impact_field(Vec3::ZERO, 1.0, 1.0);
771 }
772 assert_eq!(sys.force_fields.len(), MAX_GPU_FORCE_FIELDS);
773 }
774}