1#![allow(clippy::type_complexity)]
3use proc_macro::TokenStream;
70use proc_macro2::Span;
71use quote::quote;
72use syn::{parse_macro_input, Data, DeriveInput, Fields, Ident, Type};
73
74#[proc_macro_derive(ParticleType)]
132pub fn derive_particle_type(input: TokenStream) -> TokenStream {
133 let input = parse_macro_input!(input as DeriveInput);
134 let name = &input.ident;
135
136 let variants = match &input.data {
137 Data::Enum(data) => &data.variants,
138 _ => panic!("ParticleType derive only supports enums"),
139 };
140
141 for variant in variants.iter() {
143 if !matches!(variant.fields, Fields::Unit) {
144 panic!(
145 "ParticleType enum variants must be unit variants (no fields). \
146 Found fields on variant '{}'",
147 variant.ident
148 );
149 }
150 }
151
152 let into_arms: Vec<_> = variants
154 .iter()
155 .enumerate()
156 .map(|(i, variant)| {
157 let variant_name = &variant.ident;
158 let idx = i as u32;
159 quote! { #name::#variant_name => #idx }
160 })
161 .collect();
162
163 let from_arms: Vec<_> = variants
165 .iter()
166 .enumerate()
167 .map(|(i, variant)| {
168 let variant_name = &variant.ident;
169 let idx = i as u32;
170 quote! { #idx => #name::#variant_name }
171 })
172 .collect();
173
174 let first_variant = &variants.first().expect("Enum must have at least one variant").ident;
175 let variant_count = variants.len() as u32;
176
177 let expanded = quote! {
178 impl From<#name> for u32 {
179 fn from(value: #name) -> u32 {
180 match value {
181 #(#into_arms),*
182 }
183 }
184 }
185
186 impl From<u32> for #name {
187 fn from(value: u32) -> #name {
188 match value {
189 #(#from_arms,)*
190 _ => #name::#first_variant, }
192 }
193 }
194
195 impl #name {
196 pub const fn count() -> u32 {
198 #variant_count
199 }
200 }
201 };
202
203 TokenStream::from(expanded)
204}
205
206#[proc_macro_derive(Particle, attributes(color))]
323pub fn derive_particle(input: TokenStream) -> TokenStream {
324 let input = parse_macro_input!(input as DeriveInput);
325 let name = &input.ident;
326 let gpu_name = Ident::new(&format!("{}Gpu", name), Span::call_site());
327
328 let fields = match &input.data {
329 Data::Struct(data) => match &data.fields {
330 Fields::Named(fields) => &fields.named,
331 _ => panic!("Particle derive only supports structs with named fields"),
332 },
333 _ => panic!("Particle derive only supports structs"),
334 };
335
336 let mut wgsl_fields = Vec::new();
337 let mut gpu_struct_fields = Vec::new();
338 let mut to_gpu_conversions = Vec::new();
339 let mut from_gpu_conversions = Vec::new();
340 let mut inspect_field_entries = Vec::new();
341 let mut editable_field_widgets = Vec::new();
342 let mut field_offset = 0u32;
343 let mut padding_count = 0u32;
344 let mut color_field: Option<String> = None;
345 let mut color_offset: Option<u32> = None;
346 let mut has_particle_type = false;
347
348 for field in fields.iter() {
349 let field_name = field.ident.as_ref().unwrap();
350 if field_name == "particle_type" {
351 has_particle_type = true;
352 }
353 }
354
355 for field in fields.iter() {
356 let field_name = field.ident.as_ref().unwrap();
357 let field_name_str = field_name.to_string();
358 let field_type = &field.ty;
359 let type_info = rust_type_info(field_type);
360
361 let mut is_color_field = false;
363 for attr in &field.attrs {
364 if attr.path().is_ident("color") {
365 color_field = Some(field_name_str.clone());
366 is_color_field = true;
367 }
368 }
369
370 let padding_needed = (type_info.align - (field_offset % type_info.align)) % type_info.align;
372 if padding_needed > 0 {
373 let pad_name = Ident::new(&format!("_pad{}", padding_count), Span::call_site());
374 let pad_name_str = format!("_pad{}", padding_count);
375 padding_count += 1;
376
377 if padding_needed == 4 {
378 wgsl_fields.push(format!(" {}: f32,", pad_name_str));
379 gpu_struct_fields.push(quote! { #pad_name: f32 });
380 to_gpu_conversions.push(quote! { #pad_name: 0.0 });
381 } else {
382 let count = (padding_needed / 4) as usize;
383 wgsl_fields.push(format!(" {}: array<f32, {}>,", pad_name_str, count));
384 gpu_struct_fields.push(quote! { #pad_name: [f32; #count] });
385 to_gpu_conversions.push(quote! { #pad_name: [0.0; #count] });
386 }
387 field_offset += padding_needed;
388 }
389
390 if is_color_field {
392 color_offset = Some(field_offset);
393 }
394
395 wgsl_fields.push(format!(" {}: {},", field_name_str, type_info.wgsl_type));
397
398 let gpu_field_type = type_info.gpu_type;
399 gpu_struct_fields.push(quote! { #field_name: #gpu_field_type });
400
401 let conversion = generate_conversion(field_name, field_type);
402 to_gpu_conversions.push(quote! { #field_name: #conversion });
403
404 let reverse_conversion = generate_reverse_conversion(field_name, field_type);
405 from_gpu_conversions.push(quote! { #field_name: #reverse_conversion });
406
407 let inspect_format = generate_inspect_format(field_name, field_type);
409 inspect_field_entries.push(quote! { (#field_name_str, #inspect_format) });
410
411 let editable_widget = generate_editable_widget(field_name, &field_name_str, field_type);
413 editable_field_widgets.push(editable_widget);
414
415 field_offset += type_info.size;
416 }
417
418 if !has_particle_type {
420 let padding_needed = (4 - (field_offset % 4)) % 4;
422 if padding_needed > 0 {
423 let pad_name = Ident::new(&format!("_pad{}", padding_count), Span::call_site());
424 let pad_name_str = format!("_pad{}", padding_count);
425 padding_count += 1;
426
427 if padding_needed == 4 {
428 wgsl_fields.push(format!(" {}: f32,", pad_name_str));
429 gpu_struct_fields.push(quote! { #pad_name: f32 });
430 to_gpu_conversions.push(quote! { #pad_name: 0.0 });
431 } else {
432 let count = (padding_needed / 4) as usize;
433 wgsl_fields.push(format!(" {}: array<f32, {}>,", pad_name_str, count));
434 gpu_struct_fields.push(quote! { #pad_name: [f32; #count] });
435 to_gpu_conversions.push(quote! { #pad_name: [0.0; #count] });
436 }
437 field_offset += padding_needed;
438 }
439
440 wgsl_fields.push(" particle_type: u32,".to_string());
441 gpu_struct_fields.push(quote! { particle_type: u32 });
442 to_gpu_conversions.push(quote! { particle_type: 0 });
443 field_offset += 4;
444 }
445
446 let alive_offset: u32;
450 let scale_offset: u32;
451 {
452 let padding_needed = (4 - (field_offset % 4)) % 4;
454 if padding_needed > 0 {
455 let pad_name = Ident::new(&format!("_pad{}", padding_count), Span::call_site());
456 let pad_name_str = format!("_pad{}", padding_count);
457 padding_count += 1;
458 let count = (padding_needed / 4) as usize;
459 if count == 1 {
460 wgsl_fields.push(format!(" {}: f32,", pad_name_str));
461 gpu_struct_fields.push(quote! { #pad_name: f32 });
462 to_gpu_conversions.push(quote! { #pad_name: 0.0 });
463 } else {
464 wgsl_fields.push(format!(" {}: array<f32, {}>,", pad_name_str, count));
465 gpu_struct_fields.push(quote! { #pad_name: [f32; #count] });
466 to_gpu_conversions.push(quote! { #pad_name: [0.0; #count] });
467 }
468 field_offset += padding_needed;
469 }
470
471 wgsl_fields.push(" age: f32,".to_string());
472 gpu_struct_fields.push(quote! { age: f32 });
473 to_gpu_conversions.push(quote! { age: 0.0 });
474 field_offset += 4;
475
476 alive_offset = field_offset;
479 wgsl_fields.push(" alive: u32,".to_string());
480 gpu_struct_fields.push(quote! { alive: u32 });
481 to_gpu_conversions.push(quote! { alive: 1u32 }); field_offset += 4;
483
484 scale_offset = field_offset;
487 wgsl_fields.push(" scale: f32,".to_string());
488 gpu_struct_fields.push(quote! { scale: f32 });
489 to_gpu_conversions.push(quote! { scale: 1.0 }); field_offset += 4;
491 }
492
493 let final_padding = (16 - (field_offset % 16)) % 16;
495 if final_padding > 0 {
496 let pad_name = Ident::new(&format!("_pad{}", padding_count), Span::call_site());
497 let pad_name_str = format!("_pad{}", padding_count);
498
499 if final_padding == 4 {
500 wgsl_fields.push(format!(" {}: f32,", pad_name_str));
501 gpu_struct_fields.push(quote! { #pad_name: f32 });
502 to_gpu_conversions.push(quote! { #pad_name: 0.0 });
503 } else {
504 let count = (final_padding / 4) as usize;
505 wgsl_fields.push(format!(" {}: array<f32, {}>,", pad_name_str, count));
506 gpu_struct_fields.push(quote! { #pad_name: [f32; #count] });
507 to_gpu_conversions.push(quote! { #pad_name: [0.0; #count] });
508 }
509 }
510
511 let wgsl_struct = format!("struct Particle {{\n{}\n}}", wgsl_fields.join("\n"));
512
513 let color_field_expr = match color_field {
514 Some(ref name) => quote! { Some(#name) },
515 None => quote! { None },
516 };
517
518 let color_offset_expr = match color_offset {
519 Some(offset) => quote! { Some(#offset) },
520 None => quote! { None },
521 };
522
523 let expanded = quote! {
524 #[repr(C)]
525 #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
526 pub struct #gpu_name {
527 #(pub #gpu_struct_fields),*
528 }
529
530 impl rdpe::ParticleTrait for #name {
531 type Gpu = #gpu_name;
532
533 const WGSL_STRUCT: &'static str = #wgsl_struct;
534 const COLOR_FIELD: Option<&'static str> = #color_field_expr;
535 const COLOR_OFFSET: Option<u32> = #color_offset_expr;
536 const ALIVE_OFFSET: u32 = #alive_offset;
537 const SCALE_OFFSET: u32 = #scale_offset;
538
539 fn to_gpu(&self) -> Self::Gpu {
540 #gpu_name {
541 #(#to_gpu_conversions),*
542 }
543 }
544
545 fn from_gpu(gpu: &Self::Gpu) -> Self {
546 Self {
547 #(#from_gpu_conversions),*
548 }
549 }
550
551 fn inspect_fields(&self) -> Vec<(&'static str, String)> {
552 vec![
553 #(#inspect_field_entries),*
554 ]
555 }
556
557 #[cfg(feature = "egui")]
558 fn render_editable_fields(&mut self, ui: &mut egui::Ui) -> bool {
559 let mut modified = false;
560 egui::Grid::new("editable_fields")
561 .num_columns(2)
562 .spacing([20.0, 4.0])
563 .show(ui, |ui| {
564 #(#editable_field_widgets)*
565 });
566 modified
567 }
568 }
569 };
570
571 TokenStream::from(expanded)
572}
573
574struct TypeInfo {
576 wgsl_type: &'static str,
578 gpu_type: proc_macro2::TokenStream,
580 size: u32,
582 align: u32,
584}
585
586fn rust_type_info(ty: &Type) -> TypeInfo {
590 let type_str = quote!(#ty).to_string().replace(" ", "");
591
592 match type_str.as_str() {
593 "Vec3" | "glam::Vec3" => TypeInfo {
594 wgsl_type: "vec3<f32>",
595 gpu_type: quote! { [f32; 3] },
596 size: 12,
597 align: 16, },
599 "Vec2" | "glam::Vec2" => TypeInfo {
600 wgsl_type: "vec2<f32>",
601 gpu_type: quote! { [f32; 2] },
602 size: 8,
603 align: 8,
604 },
605 "Vec4" | "glam::Vec4" => TypeInfo {
606 wgsl_type: "vec4<f32>",
607 gpu_type: quote! { [f32; 4] },
608 size: 16,
609 align: 16,
610 },
611 "f32" => TypeInfo {
612 wgsl_type: "f32",
613 gpu_type: quote! { f32 },
614 size: 4,
615 align: 4,
616 },
617 "u32" => TypeInfo {
618 wgsl_type: "u32",
619 gpu_type: quote! { u32 },
620 size: 4,
621 align: 4,
622 },
623 "i32" => TypeInfo {
624 wgsl_type: "i32",
625 gpu_type: quote! { i32 },
626 size: 4,
627 align: 4,
628 },
629 _ => panic!("Unsupported type in Particle struct: {}", type_str),
630 }
631}
632
633fn generate_conversion(field_name: &Ident, ty: &Type) -> proc_macro2::TokenStream {
637 let type_str = quote!(#ty).to_string().replace(" ", "");
638
639 match type_str.as_str() {
640 "Vec3" | "glam::Vec3" | "Vec2" | "glam::Vec2" | "Vec4" | "glam::Vec4" => {
641 quote! { self.#field_name.to_array() }
642 }
643 _ => {
644 quote! { self.#field_name }
645 }
646 }
647}
648
649fn generate_reverse_conversion(field_name: &Ident, ty: &Type) -> proc_macro2::TokenStream {
653 let type_str = quote!(#ty).to_string().replace(" ", "");
654
655 match type_str.as_str() {
656 "Vec3" | "glam::Vec3" => {
657 quote! { glam::Vec3::from_array(gpu.#field_name) }
658 }
659 "Vec2" | "glam::Vec2" => {
660 quote! { glam::Vec2::from_array(gpu.#field_name) }
661 }
662 "Vec4" | "glam::Vec4" => {
663 quote! { glam::Vec4::from_array(gpu.#field_name) }
664 }
665 _ => {
666 quote! { gpu.#field_name }
667 }
668 }
669}
670
671fn generate_inspect_format(field_name: &Ident, ty: &Type) -> proc_macro2::TokenStream {
675 let type_str = quote!(#ty).to_string().replace(" ", "");
676
677 match type_str.as_str() {
678 "Vec3" | "glam::Vec3" => {
679 quote! {
680 format!("({:.3}, {:.3}, {:.3})", self.#field_name.x, self.#field_name.y, self.#field_name.z)
681 }
682 }
683 "Vec2" | "glam::Vec2" => {
684 quote! {
685 format!("({:.3}, {:.3})", self.#field_name.x, self.#field_name.y)
686 }
687 }
688 "Vec4" | "glam::Vec4" => {
689 quote! {
690 format!("({:.3}, {:.3}, {:.3}, {:.3})", self.#field_name.x, self.#field_name.y, self.#field_name.z, self.#field_name.w)
691 }
692 }
693 "f32" => {
694 quote! { format!("{:.3}", self.#field_name) }
695 }
696 "u32" | "i32" => {
697 quote! { format!("{}", self.#field_name) }
698 }
699 _ => {
700 quote! { format!("{:?}", self.#field_name) }
701 }
702 }
703}
704
705fn generate_editable_widget(field_name: &Ident, field_name_str: &str, ty: &Type) -> proc_macro2::TokenStream {
709 let type_str = quote!(#ty).to_string().replace(" ", "");
710
711 match type_str.as_str() {
712 "Vec3" | "glam::Vec3" => {
713 quote! {
714 ui.label(#field_name_str);
715 ui.horizontal(|ui| {
716 if ui.add(egui::DragValue::new(&mut self.#field_name.x).speed(0.01).prefix("x: ")).changed() {
717 modified = true;
718 }
719 if ui.add(egui::DragValue::new(&mut self.#field_name.y).speed(0.01).prefix("y: ")).changed() {
720 modified = true;
721 }
722 if ui.add(egui::DragValue::new(&mut self.#field_name.z).speed(0.01).prefix("z: ")).changed() {
723 modified = true;
724 }
725 });
726 ui.end_row();
727 }
728 }
729 "Vec2" | "glam::Vec2" => {
730 quote! {
731 ui.label(#field_name_str);
732 ui.horizontal(|ui| {
733 if ui.add(egui::DragValue::new(&mut self.#field_name.x).speed(0.01).prefix("x: ")).changed() {
734 modified = true;
735 }
736 if ui.add(egui::DragValue::new(&mut self.#field_name.y).speed(0.01).prefix("y: ")).changed() {
737 modified = true;
738 }
739 });
740 ui.end_row();
741 }
742 }
743 "Vec4" | "glam::Vec4" => {
744 quote! {
745 ui.label(#field_name_str);
746 ui.horizontal(|ui| {
747 if ui.add(egui::DragValue::new(&mut self.#field_name.x).speed(0.01).prefix("x: ")).changed() {
748 modified = true;
749 }
750 if ui.add(egui::DragValue::new(&mut self.#field_name.y).speed(0.01).prefix("y: ")).changed() {
751 modified = true;
752 }
753 if ui.add(egui::DragValue::new(&mut self.#field_name.z).speed(0.01).prefix("z: ")).changed() {
754 modified = true;
755 }
756 if ui.add(egui::DragValue::new(&mut self.#field_name.w).speed(0.01).prefix("w: ")).changed() {
757 modified = true;
758 }
759 });
760 ui.end_row();
761 }
762 }
763 "f32" => {
764 quote! {
765 ui.label(#field_name_str);
766 if ui.add(egui::DragValue::new(&mut self.#field_name).speed(0.01)).changed() {
767 modified = true;
768 }
769 ui.end_row();
770 }
771 }
772 "u32" => {
773 quote! {
774 ui.label(#field_name_str);
775 let mut val = self.#field_name as i64;
776 if ui.add(egui::DragValue::new(&mut val).speed(1.0)).changed() {
777 self.#field_name = val.max(0) as u32;
778 modified = true;
779 }
780 ui.end_row();
781 }
782 }
783 "i32" => {
784 quote! {
785 ui.label(#field_name_str);
786 if ui.add(egui::DragValue::new(&mut self.#field_name).speed(1.0)).changed() {
787 modified = true;
788 }
789 ui.end_row();
790 }
791 }
792 _ => {
793 quote! {
795 ui.label(#field_name_str);
796 ui.label(format!("{:?}", self.#field_name));
797 ui.end_row();
798 }
799 }
800 }
801}
802
803fn generate_editable_widget_from_string(field_name: &Ident, field_name_str: &str, type_str: &str) -> proc_macro2::TokenStream {
807 match type_str {
808 "Vec3" => {
809 quote! {
810 ui.label(#field_name_str);
811 ui.horizontal(|ui| {
812 if ui.add(egui::DragValue::new(&mut self.#field_name.x).speed(0.01).prefix("x: ")).changed() {
813 modified = true;
814 }
815 if ui.add(egui::DragValue::new(&mut self.#field_name.y).speed(0.01).prefix("y: ")).changed() {
816 modified = true;
817 }
818 if ui.add(egui::DragValue::new(&mut self.#field_name.z).speed(0.01).prefix("z: ")).changed() {
819 modified = true;
820 }
821 });
822 ui.end_row();
823 }
824 }
825 "Vec2" => {
826 quote! {
827 ui.label(#field_name_str);
828 ui.horizontal(|ui| {
829 if ui.add(egui::DragValue::new(&mut self.#field_name.x).speed(0.01).prefix("x: ")).changed() {
830 modified = true;
831 }
832 if ui.add(egui::DragValue::new(&mut self.#field_name.y).speed(0.01).prefix("y: ")).changed() {
833 modified = true;
834 }
835 });
836 ui.end_row();
837 }
838 }
839 "Vec4" => {
840 quote! {
841 ui.label(#field_name_str);
842 ui.horizontal(|ui| {
843 if ui.add(egui::DragValue::new(&mut self.#field_name.x).speed(0.01).prefix("x: ")).changed() {
844 modified = true;
845 }
846 if ui.add(egui::DragValue::new(&mut self.#field_name.y).speed(0.01).prefix("y: ")).changed() {
847 modified = true;
848 }
849 if ui.add(egui::DragValue::new(&mut self.#field_name.z).speed(0.01).prefix("z: ")).changed() {
850 modified = true;
851 }
852 if ui.add(egui::DragValue::new(&mut self.#field_name.w).speed(0.01).prefix("w: ")).changed() {
853 modified = true;
854 }
855 });
856 ui.end_row();
857 }
858 }
859 "f32" => {
860 quote! {
861 ui.label(#field_name_str);
862 if ui.add(egui::DragValue::new(&mut self.#field_name).speed(0.01)).changed() {
863 modified = true;
864 }
865 ui.end_row();
866 }
867 }
868 "u32" => {
869 quote! {
870 ui.label(#field_name_str);
871 let mut val = self.#field_name as i64;
872 if ui.add(egui::DragValue::new(&mut val).speed(1.0)).changed() {
873 self.#field_name = val.max(0) as u32;
874 modified = true;
875 }
876 ui.end_row();
877 }
878 }
879 "i32" => {
880 quote! {
881 ui.label(#field_name_str);
882 if ui.add(egui::DragValue::new(&mut self.#field_name).speed(1.0)).changed() {
883 modified = true;
884 }
885 ui.end_row();
886 }
887 }
888 _ => {
889 quote! {
890 ui.label(#field_name_str);
891 ui.label(format!("{:?}", self.#field_name));
892 ui.end_row();
893 }
894 }
895 }
896}
897
898#[proc_macro_derive(MultiParticle, attributes(color))]
993pub fn derive_multi_particle(input: TokenStream) -> TokenStream {
994 let input = parse_macro_input!(input as DeriveInput);
995 let enum_name = &input.ident;
996 let enum_gpu_name = Ident::new(&format!("{}Gpu", enum_name), Span::call_site());
997 let visibility = &input.vis;
998
999 let variants = match &input.data {
1001 Data::Enum(data) => &data.variants,
1002 _ => panic!("MultiParticle derive only supports enums"),
1003 };
1004
1005 let mut variant_info: Vec<(Ident, Vec<(Ident, String, bool)>)> = Vec::new();
1007
1008 for variant in variants.iter() {
1009 let variant_name = variant.ident.clone();
1010
1011 let fields = match &variant.fields {
1012 Fields::Named(named) => {
1013 named.named.iter().map(|f| {
1014 let field_name = f.ident.clone().unwrap();
1015 let ty = &f.ty;
1016 let type_str = quote!(#ty).to_string().replace(" ", "");
1017 let is_color = f.attrs.iter().any(|a| a.path().is_ident("color"));
1018 (field_name, type_str, is_color)
1019 }).collect::<Vec<_>>()
1020 }
1021 _ => panic!(
1022 "MultiParticle variant '{}' must have named fields (struct-like syntax)",
1023 variant_name
1024 ),
1025 };
1026
1027 let has_position = fields.iter().any(|(n, t, _)| n == "position" && (t == "Vec3" || t == "glam::Vec3"));
1029 let has_velocity = fields.iter().any(|(n, t, _)| n == "velocity" && (t == "Vec3" || t == "glam::Vec3"));
1030
1031 if !has_position {
1032 panic!("MultiParticle variant '{}' must have 'position: Vec3' field", variant_name);
1033 }
1034 if !has_velocity {
1035 panic!("MultiParticle variant '{}' must have 'velocity: Vec3' field", variant_name);
1036 }
1037
1038 variant_info.push((variant_name, fields));
1039 }
1040
1041 let mut standalone_structs = Vec::new();
1045
1046 for (variant_name, fields) in &variant_info {
1047 let struct_gpu_name = Ident::new(&format!("{}Gpu", variant_name), Span::call_site());
1048
1049 let struct_fields: Vec<_> = fields.iter().map(|(name, type_str, _)| {
1051 let ty = rust_type_from_string(type_str);
1052 quote! { pub #name: #ty }
1053 }).collect();
1054
1055 let (gpu_fields, wgsl_struct, color_field, color_offset, alive_offset, scale_offset) =
1057 generate_particle_gpu_struct(fields, false); let gpu_field_tokens: Vec<_> = gpu_fields.iter().map(|(name, ty)| {
1060 let name_ident = Ident::new(name, Span::call_site());
1061 quote! { pub #name_ident: #ty }
1062 }).collect();
1063
1064 let to_gpu_conversions: Vec<_> = gpu_fields.iter().map(|(name, _ty)| {
1066 let name_ident = Ident::new(name, Span::call_site());
1067 if name.starts_with("_pad") {
1068 quote! { #name_ident: Default::default() }
1070 } else if name == "particle_type" {
1071 quote! { #name_ident: 0 }
1072 } else if name == "age" {
1073 quote! { #name_ident: 0.0 }
1074 } else if name == "alive" {
1075 quote! { #name_ident: 1u32 }
1076 } else if name == "scale" {
1077 quote! { #name_ident: 1.0 }
1078 } else {
1079 #[allow(clippy::cmp_owned)]
1081 let field_info = fields.iter().find(|(n, _, _)| n.to_string() == *name);
1082 if let Some((_, type_str, _)) = field_info {
1083 if type_str == "Vec3" || type_str == "Vec2" || type_str == "Vec4" ||
1084 type_str == "glam::Vec3" || type_str == "glam::Vec2" || type_str == "glam::Vec4" {
1085 quote! { #name_ident: self.#name_ident.to_array() }
1086 } else {
1087 quote! { #name_ident: self.#name_ident }
1088 }
1089 } else {
1090 quote! { #name_ident: Default::default() }
1091 }
1092 }
1093 }).collect();
1094
1095 let from_gpu_conversions: Vec<_> = fields.iter().map(|(name, type_str, _)| {
1097 let name_ident = name;
1098 let type_normalized = type_str.replace("glam::", "");
1099 match type_normalized.as_str() {
1100 "Vec3" => quote! { #name_ident: rdpe::Vec3::from_array(gpu.#name_ident) },
1101 "Vec2" => quote! { #name_ident: rdpe::Vec2::from_array(gpu.#name_ident) },
1102 "Vec4" => quote! { #name_ident: rdpe::Vec4::from_array(gpu.#name_ident) },
1103 _ => quote! { #name_ident: gpu.#name_ident },
1104 }
1105 }).collect();
1106
1107 let inspect_entries: Vec<_> = fields.iter().map(|(name, type_str, _)| {
1109 let name_str = name.to_string();
1110 let type_normalized = type_str.replace("glam::", "");
1111 match type_normalized.as_str() {
1112 "Vec3" => quote! { (#name_str, format!("({:.3}, {:.3}, {:.3})", self.#name.x, self.#name.y, self.#name.z)) },
1113 "Vec2" => quote! { (#name_str, format!("({:.3}, {:.3})", self.#name.x, self.#name.y)) },
1114 "Vec4" => quote! { (#name_str, format!("({:.3}, {:.3}, {:.3}, {:.3})", self.#name.x, self.#name.y, self.#name.z, self.#name.w)) },
1115 "f32" => quote! { (#name_str, format!("{:.3}", self.#name)) },
1116 "u32" | "i32" => quote! { (#name_str, format!("{}", self.#name)) },
1117 _ => quote! { (#name_str, format!("{:?}", self.#name)) },
1118 }
1119 }).collect();
1120
1121 let editable_entries: Vec<_> = fields.iter().map(|(name, type_str, _)| {
1123 let name_str = name.to_string();
1124 let type_normalized = type_str.replace("glam::", "");
1125 generate_editable_widget_from_string(name, &name_str, &type_normalized)
1126 }).collect();
1127
1128 let color_field_expr = match &color_field {
1129 Some(name) => quote! { Some(#name) },
1130 None => quote! { None },
1131 };
1132
1133 let color_offset_expr = match color_offset {
1134 Some(offset) => quote! { Some(#offset) },
1135 None => quote! { None },
1136 };
1137
1138 standalone_structs.push(quote! {
1139 #[derive(Clone)]
1141 #visibility struct #variant_name {
1142 #(#struct_fields),*
1143 }
1144
1145 #[repr(C)]
1146 #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
1147 #visibility struct #struct_gpu_name {
1148 #(#gpu_field_tokens),*
1149 }
1150
1151 impl rdpe::ParticleTrait for #variant_name {
1152 type Gpu = #struct_gpu_name;
1153
1154 const WGSL_STRUCT: &'static str = #wgsl_struct;
1155 const COLOR_FIELD: Option<&'static str> = #color_field_expr;
1156 const COLOR_OFFSET: Option<u32> = #color_offset_expr;
1157 const ALIVE_OFFSET: u32 = #alive_offset;
1158 const SCALE_OFFSET: u32 = #scale_offset;
1159
1160 fn to_gpu(&self) -> Self::Gpu {
1161 #struct_gpu_name {
1162 #(#to_gpu_conversions),*
1163 }
1164 }
1165
1166 fn from_gpu(gpu: &Self::Gpu) -> Self {
1167 Self {
1168 #(#from_gpu_conversions),*
1169 }
1170 }
1171
1172 fn inspect_fields(&self) -> Vec<(&'static str, String)> {
1173 vec![
1174 #(#inspect_entries),*
1175 ]
1176 }
1177
1178 #[cfg(feature = "egui")]
1179 fn render_editable_fields(&mut self, ui: &mut egui::Ui) -> bool {
1180 let mut modified = false;
1181 egui::Grid::new("editable_fields")
1182 .num_columns(2)
1183 .spacing([20.0, 4.0])
1184 .show(ui, |ui| {
1185 #(#editable_entries)*
1186 });
1187 modified
1188 }
1189 }
1190 });
1191 }
1192
1193 let mut all_fields: Vec<(String, String, bool)> = vec![
1197 ("position".to_string(), "Vec3".to_string(), false),
1198 ("velocity".to_string(), "Vec3".to_string(), false),
1199 ];
1200
1201 let mut seen_fields: std::collections::HashMap<String, String> = std::collections::HashMap::new();
1202 seen_fields.insert("position".to_string(), "Vec3".to_string());
1203 seen_fields.insert("velocity".to_string(), "Vec3".to_string());
1204
1205 for (variant_name, fields) in &variant_info {
1206 for (fname, ftype, is_color) in fields {
1207 let fname_str = fname.to_string();
1208 let ftype_normalized = ftype.replace("glam::", "");
1209 if fname_str == "position" || fname_str == "velocity" {
1210 continue; }
1212 if let Some(existing_type) = seen_fields.get(&fname_str) {
1213 if existing_type != &ftype_normalized {
1214 panic!(
1215 "Field '{}' has conflicting types: '{}' in one variant, '{}' in '{}'",
1216 fname_str, existing_type, ftype_normalized, variant_name
1217 );
1218 }
1219 } else {
1220 seen_fields.insert(fname_str.clone(), ftype_normalized.clone());
1221 all_fields.push((fname_str, ftype_normalized, *is_color));
1222 }
1223 }
1224 }
1225
1226 let (enum_gpu_fields, enum_wgsl_struct, _, _, enum_alive_offset, enum_scale_offset) =
1230 generate_unified_gpu_struct(&all_fields);
1231
1232 let enum_gpu_field_tokens: Vec<_> = enum_gpu_fields.iter().map(|(name, ty)| {
1233 let name_ident = Ident::new(name, Span::call_site());
1234 quote! { pub #name_ident: #ty }
1235 }).collect();
1236
1237 let mut extra_wgsl_lines = Vec::new();
1239 extra_wgsl_lines.push("// MultiParticle type constants".to_string());
1240
1241 for (i, (variant_name, _)) in variant_info.iter().enumerate() {
1242 let const_name = variant_name.to_string().to_uppercase();
1243 extra_wgsl_lines.push(format!("const {}: u32 = {}u;", const_name, i));
1244 }
1245
1246 extra_wgsl_lines.push("".to_string());
1247 extra_wgsl_lines.push("// MultiParticle type helpers".to_string());
1248
1249 for (i, (variant_name, _)) in variant_info.iter().enumerate() {
1250 let fn_name = format!("is_{}", variant_name.to_string().to_lowercase());
1251 extra_wgsl_lines.push(format!(
1252 "fn {}(p: Particle) -> bool {{ return p.particle_type == {}u; }}",
1253 fn_name, i
1254 ));
1255 }
1256
1257 let extra_wgsl = extra_wgsl_lines.join("\n");
1258
1259 let to_gpu_arms: Vec<_> = variant_info
1261 .iter()
1262 .enumerate()
1263 .map(|(idx, (variant_name, variant_fields))| {
1264 let idx_u32 = idx as u32;
1265
1266 let field_bindings: Vec<_> = variant_fields.iter().map(|(fname, _, _)| {
1268 quote! { #fname }
1269 }).collect();
1270
1271 let mut field_assignments = Vec::new();
1273
1274 for (fname, ftype, _) in &all_fields {
1275 let field_ident = Ident::new(fname, Span::call_site());
1276
1277 #[allow(clippy::cmp_owned)]
1279 let has_field = variant_fields.iter().any(|(vf, _, _)| vf.to_string() == *fname);
1280
1281 if has_field {
1282 let type_info = type_info_from_string(ftype);
1283 if type_info.needs_to_array {
1284 field_assignments.push(quote! { #field_ident: #field_ident.to_array() });
1285 } else {
1286 field_assignments.push(quote! { #field_ident: *#field_ident });
1287 }
1288 } else {
1289 let zero_val = zero_value_for_type(ftype);
1290 field_assignments.push(quote! { #field_ident: #zero_val });
1291 }
1292 }
1293
1294 field_assignments.push(quote! { particle_type: #idx_u32 });
1296 field_assignments.push(quote! { age: 0.0 });
1297 field_assignments.push(quote! { alive: 1u32 });
1298 field_assignments.push(quote! { scale: 1.0 });
1299
1300 quote! {
1301 #enum_name::#variant_name { #(#field_bindings),* } => {
1302 #enum_gpu_name {
1303 #(#field_assignments,)*
1304 ..bytemuck::Zeroable::zeroed()
1305 }
1306 }
1307 }
1308 })
1309 .collect();
1310
1311 let from_gpu_arms: Vec<_> = variant_info
1313 .iter()
1314 .enumerate()
1315 .map(|(idx, (variant_name, variant_fields))| {
1316 let idx_u32 = idx as u32;
1317
1318 let field_assignments: Vec<_> = variant_fields.iter().map(|(fname, ftype, _)| {
1320 let type_normalized = ftype.replace("glam::", "");
1321 match type_normalized.as_str() {
1322 "Vec3" => quote! { #fname: rdpe::Vec3::from_array(gpu.#fname) },
1323 "Vec2" => quote! { #fname: rdpe::Vec2::from_array(gpu.#fname) },
1324 "Vec4" => quote! { #fname: rdpe::Vec4::from_array(gpu.#fname) },
1325 _ => quote! { #fname: gpu.#fname },
1326 }
1327 }).collect();
1328
1329 if idx == variant_info.len() - 1 {
1331 quote! {
1332 _ => #enum_name::#variant_name { #(#field_assignments),* }
1333 }
1334 } else {
1335 quote! {
1336 #idx_u32 => #enum_name::#variant_name { #(#field_assignments),* },
1337 }
1338 }
1339 })
1340 .collect();
1341
1342 let inspect_arms: Vec<_> = variant_info
1344 .iter()
1345 .map(|(variant_name, variant_fields)| {
1346 let field_bindings: Vec<_> = variant_fields.iter().map(|(fname, _, _)| {
1348 quote! { #fname }
1349 }).collect();
1350
1351 let inspect_entries: Vec<_> = variant_fields.iter().map(|(fname, ftype, _)| {
1353 let fname_str = fname.to_string();
1354 let type_normalized = ftype.replace("glam::", "");
1355 match type_normalized.as_str() {
1356 "Vec3" => quote! { (#fname_str, format!("({:.3}, {:.3}, {:.3})", #fname.x, #fname.y, #fname.z)) },
1357 "Vec2" => quote! { (#fname_str, format!("({:.3}, {:.3})", #fname.x, #fname.y)) },
1358 "Vec4" => quote! { (#fname_str, format!("({:.3}, {:.3}, {:.3}, {:.3})", #fname.x, #fname.y, #fname.z, #fname.w)) },
1359 "f32" => quote! { (#fname_str, format!("{:.3}", #fname)) },
1360 "u32" | "i32" => quote! { (#fname_str, format!("{}", #fname)) },
1361 _ => quote! { (#fname_str, format!("{:?}", #fname)) },
1362 }
1363 }).collect();
1364
1365 quote! {
1366 #enum_name::#variant_name { #(#field_bindings),* } => {
1367 vec![#(#inspect_entries),*]
1368 }
1369 }
1370 })
1371 .collect();
1372
1373 let editable_arms: Vec<_> = variant_info
1375 .iter()
1376 .map(|(variant_name, variant_fields)| {
1377 let field_bindings: Vec<_> = variant_fields.iter().map(|(fname, _, _)| {
1379 quote! { #fname }
1380 }).collect();
1381
1382 let editable_widgets: Vec<_> = variant_fields.iter().map(|(fname, ftype, _)| {
1384 let fname_str = fname.to_string();
1385 let type_normalized = ftype.replace("glam::", "");
1386 match type_normalized.as_str() {
1387 "Vec3" => quote! {
1388 ui.label(#fname_str);
1389 ui.horizontal(|ui| {
1390 if ui.add(egui::DragValue::new(&mut #fname.x).speed(0.01).prefix("x: ")).changed() {
1391 modified = true;
1392 }
1393 if ui.add(egui::DragValue::new(&mut #fname.y).speed(0.01).prefix("y: ")).changed() {
1394 modified = true;
1395 }
1396 if ui.add(egui::DragValue::new(&mut #fname.z).speed(0.01).prefix("z: ")).changed() {
1397 modified = true;
1398 }
1399 });
1400 ui.end_row();
1401 },
1402 "Vec2" => quote! {
1403 ui.label(#fname_str);
1404 ui.horizontal(|ui| {
1405 if ui.add(egui::DragValue::new(&mut #fname.x).speed(0.01).prefix("x: ")).changed() {
1406 modified = true;
1407 }
1408 if ui.add(egui::DragValue::new(&mut #fname.y).speed(0.01).prefix("y: ")).changed() {
1409 modified = true;
1410 }
1411 });
1412 ui.end_row();
1413 },
1414 "Vec4" => quote! {
1415 ui.label(#fname_str);
1416 ui.horizontal(|ui| {
1417 if ui.add(egui::DragValue::new(&mut #fname.x).speed(0.01).prefix("x: ")).changed() {
1418 modified = true;
1419 }
1420 if ui.add(egui::DragValue::new(&mut #fname.y).speed(0.01).prefix("y: ")).changed() {
1421 modified = true;
1422 }
1423 if ui.add(egui::DragValue::new(&mut #fname.z).speed(0.01).prefix("z: ")).changed() {
1424 modified = true;
1425 }
1426 if ui.add(egui::DragValue::new(&mut #fname.w).speed(0.01).prefix("w: ")).changed() {
1427 modified = true;
1428 }
1429 });
1430 ui.end_row();
1431 },
1432 "f32" => quote! {
1433 ui.label(#fname_str);
1434 if ui.add(egui::DragValue::new(#fname).speed(0.01)).changed() {
1435 modified = true;
1436 }
1437 ui.end_row();
1438 },
1439 "u32" => quote! {
1440 ui.label(#fname_str);
1441 let mut val = *#fname as i64;
1442 if ui.add(egui::DragValue::new(&mut val).speed(1.0)).changed() {
1443 *#fname = val.max(0) as u32;
1444 modified = true;
1445 }
1446 ui.end_row();
1447 },
1448 "i32" => quote! {
1449 ui.label(#fname_str);
1450 if ui.add(egui::DragValue::new(#fname).speed(1.0)).changed() {
1451 modified = true;
1452 }
1453 ui.end_row();
1454 },
1455 _ => quote! {
1456 ui.label(#fname_str);
1457 ui.label(format!("{:?}", #fname));
1458 ui.end_row();
1459 },
1460 }
1461 }).collect();
1462
1463 quote! {
1464 #enum_name::#variant_name { #(#field_bindings),* } => {
1465 egui::Grid::new("editable_fields")
1466 .num_columns(2)
1467 .spacing([20.0, 4.0])
1468 .show(ui, |ui| {
1469 #(#editable_widgets)*
1470 });
1471 }
1472 }
1473 })
1474 .collect();
1475
1476 let type_constants: Vec<_> = variant_info
1478 .iter()
1479 .enumerate()
1480 .map(|(idx, (variant_name, _))| {
1481 let const_name = Ident::new(
1482 &variant_name.to_string().to_uppercase(),
1483 Span::call_site(),
1484 );
1485 let idx_u32 = idx as u32;
1486 quote! {
1487 pub const #const_name: u32 = #idx_u32;
1489 }
1490 })
1491 .collect();
1492
1493 let expanded = quote! {
1494 #(#standalone_structs)*
1496
1497 impl #enum_name {
1499 #(#type_constants)*
1500 }
1501
1502 #[repr(C)]
1504 #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
1505 #visibility struct #enum_gpu_name {
1506 #(#enum_gpu_field_tokens),*
1507 }
1508
1509 impl rdpe::ParticleTrait for #enum_name {
1510 type Gpu = #enum_gpu_name;
1511
1512 const WGSL_STRUCT: &'static str = #enum_wgsl_struct;
1513 const COLOR_FIELD: Option<&'static str> = None;
1514 const COLOR_OFFSET: Option<u32> = None;
1515 const ALIVE_OFFSET: u32 = #enum_alive_offset;
1516 const SCALE_OFFSET: u32 = #enum_scale_offset;
1517 const EXTRA_WGSL: &'static str = #extra_wgsl;
1518
1519 fn to_gpu(&self) -> Self::Gpu {
1520 match self {
1521 #(#to_gpu_arms)*
1522 }
1523 }
1524
1525 fn from_gpu(gpu: &Self::Gpu) -> Self {
1526 match gpu.particle_type {
1527 #(#from_gpu_arms)*
1528 }
1529 }
1530
1531 fn inspect_fields(&self) -> Vec<(&'static str, String)> {
1532 match self {
1533 #(#inspect_arms)*
1534 }
1535 }
1536
1537 #[cfg(feature = "egui")]
1538 fn render_editable_fields(&mut self, ui: &mut egui::Ui) -> bool {
1539 let mut modified = false;
1540 match self {
1541 #(#editable_arms)*
1542 }
1543 modified
1544 }
1545 }
1546 };
1547
1548 TokenStream::from(expanded)
1549}
1550
1551fn rust_type_from_string(ty: &str) -> proc_macro2::TokenStream {
1553 match ty.replace("glam::", "").as_str() {
1554 "Vec3" => quote! { rdpe::Vec3 },
1555 "Vec2" => quote! { rdpe::Vec2 },
1556 "Vec4" => quote! { rdpe::Vec4 },
1557 "f32" => quote! { f32 },
1558 "u32" => quote! { u32 },
1559 "i32" => quote! { i32 },
1560 _ => panic!("Unsupported type: {}", ty),
1561 }
1562}
1563
1564fn generate_particle_gpu_struct(
1566 fields: &[(Ident, String, bool)],
1567 _is_standalone: bool,
1568) -> (Vec<(String, proc_macro2::TokenStream)>, String, Option<String>, Option<u32>, u32, u32) {
1569 let mut gpu_fields: Vec<(String, proc_macro2::TokenStream)> = Vec::new();
1570 let mut wgsl_lines = Vec::new();
1571 let mut field_offset = 0u32;
1572 let mut padding_count = 0u32;
1573 let mut color_field: Option<String> = None;
1574 let mut color_offset: Option<u32> = None;
1575
1576 for (field_name, type_str, is_color) in fields {
1577 let type_info = type_info_from_string(&type_str.replace("glam::", ""));
1578
1579 let padding_needed = (type_info.align - (field_offset % type_info.align)) % type_info.align;
1581 if padding_needed > 0 {
1582 let pad_name = format!("_pad{}", padding_count);
1583 padding_count += 1;
1584
1585 if padding_needed == 4 {
1586 wgsl_lines.push(format!(" {}: f32,", pad_name));
1587 gpu_fields.push((pad_name, quote! { f32 }));
1588 } else {
1589 let count = (padding_needed / 4) as usize;
1590 wgsl_lines.push(format!(" {}: array<f32, {}>,", pad_name, count));
1591 gpu_fields.push((pad_name, quote! { [f32; #count] }));
1592 }
1593 field_offset += padding_needed;
1594 }
1595
1596 if *is_color {
1597 color_field = Some(field_name.to_string());
1598 color_offset = Some(field_offset);
1599 }
1600
1601 wgsl_lines.push(format!(" {}: {},", field_name, type_info.wgsl_type));
1602 gpu_fields.push((field_name.to_string(), type_info.gpu_type.clone()));
1603 field_offset += type_info.size;
1604 }
1605
1606 {
1608 let padding_needed = (4 - (field_offset % 4)) % 4;
1609 if padding_needed > 0 {
1610 let pad_name = format!("_pad{}", padding_count);
1611 padding_count += 1;
1612 let count = (padding_needed / 4) as usize;
1613 if count == 1 {
1614 wgsl_lines.push(format!(" {}: f32,", pad_name));
1615 gpu_fields.push((pad_name, quote! { f32 }));
1616 } else {
1617 wgsl_lines.push(format!(" {}: array<f32, {}>,", pad_name, count));
1618 gpu_fields.push((pad_name, quote! { [f32; #count] }));
1619 }
1620 field_offset += padding_needed;
1621 }
1622
1623 wgsl_lines.push(" particle_type: u32,".to_string());
1624 gpu_fields.push(("particle_type".to_string(), quote! { u32 }));
1625 field_offset += 4;
1626 }
1627
1628 wgsl_lines.push(" age: f32,".to_string());
1630 gpu_fields.push(("age".to_string(), quote! { f32 }));
1631 field_offset += 4;
1632
1633 let alive_offset = field_offset;
1634 wgsl_lines.push(" alive: u32,".to_string());
1635 gpu_fields.push(("alive".to_string(), quote! { u32 }));
1636 field_offset += 4;
1637
1638 let scale_offset = field_offset;
1639 wgsl_lines.push(" scale: f32,".to_string());
1640 gpu_fields.push(("scale".to_string(), quote! { f32 }));
1641 field_offset += 4;
1642
1643 let final_padding = (16 - (field_offset % 16)) % 16;
1645 if final_padding > 0 {
1646 let pad_name = format!("_pad{}", padding_count);
1647 if final_padding == 4 {
1648 wgsl_lines.push(format!(" {}: f32,", pad_name));
1649 gpu_fields.push((pad_name, quote! { f32 }));
1650 } else {
1651 let count = (final_padding / 4) as usize;
1652 wgsl_lines.push(format!(" {}: array<f32, {}>,", pad_name, count));
1653 gpu_fields.push((pad_name, quote! { [f32; #count] }));
1654 }
1655 }
1656
1657 let wgsl_struct = format!("struct Particle {{\n{}\n}}", wgsl_lines.join("\n"));
1658
1659 (gpu_fields, wgsl_struct, color_field, color_offset, alive_offset, scale_offset)
1660}
1661
1662fn generate_unified_gpu_struct(
1664 all_fields: &[(String, String, bool)],
1665) -> (Vec<(String, proc_macro2::TokenStream)>, String, Option<String>, Option<u32>, u32, u32) {
1666 let mut gpu_fields: Vec<(String, proc_macro2::TokenStream)> = Vec::new();
1667 let mut wgsl_lines = Vec::new();
1668 let mut field_offset = 0u32;
1669 let mut padding_count = 0u32;
1670
1671 for (field_name, type_str, _) in all_fields {
1672 let type_info = type_info_from_string(type_str);
1673
1674 let padding_needed = (type_info.align - (field_offset % type_info.align)) % type_info.align;
1676 if padding_needed > 0 {
1677 let pad_name = format!("_pad{}", padding_count);
1678 padding_count += 1;
1679
1680 if padding_needed == 4 {
1681 wgsl_lines.push(format!(" {}: f32,", pad_name));
1682 gpu_fields.push((pad_name, quote! { f32 }));
1683 } else {
1684 let count = (padding_needed / 4) as usize;
1685 wgsl_lines.push(format!(" {}: array<f32, {}>,", pad_name, count));
1686 gpu_fields.push((pad_name, quote! { [f32; #count] }));
1687 }
1688 field_offset += padding_needed;
1689 }
1690
1691 wgsl_lines.push(format!(" {}: {},", field_name, type_info.wgsl_type));
1692 gpu_fields.push((field_name.clone(), type_info.gpu_type.clone()));
1693 field_offset += type_info.size;
1694 }
1695
1696 {
1698 let padding_needed = (4 - (field_offset % 4)) % 4;
1699 if padding_needed > 0 {
1700 let pad_name = format!("_pad{}", padding_count);
1701 padding_count += 1;
1702 let count = (padding_needed / 4) as usize;
1703 if count == 1 {
1704 wgsl_lines.push(format!(" {}: f32,", pad_name));
1705 gpu_fields.push((pad_name, quote! { f32 }));
1706 } else {
1707 wgsl_lines.push(format!(" {}: array<f32, {}>,", pad_name, count));
1708 gpu_fields.push((pad_name, quote! { [f32; #count] }));
1709 }
1710 field_offset += padding_needed;
1711 }
1712
1713 wgsl_lines.push(" particle_type: u32,".to_string());
1714 gpu_fields.push(("particle_type".to_string(), quote! { u32 }));
1715 field_offset += 4;
1716 }
1717
1718 wgsl_lines.push(" age: f32,".to_string());
1720 gpu_fields.push(("age".to_string(), quote! { f32 }));
1721 field_offset += 4;
1722
1723 let alive_offset = field_offset;
1724 wgsl_lines.push(" alive: u32,".to_string());
1725 gpu_fields.push(("alive".to_string(), quote! { u32 }));
1726 field_offset += 4;
1727
1728 let scale_offset = field_offset;
1729 wgsl_lines.push(" scale: f32,".to_string());
1730 gpu_fields.push(("scale".to_string(), quote! { f32 }));
1731 field_offset += 4;
1732
1733 let final_padding = (16 - (field_offset % 16)) % 16;
1735 if final_padding > 0 {
1736 let pad_name = format!("_pad{}", padding_count);
1737 if final_padding == 4 {
1738 wgsl_lines.push(format!(" {}: f32,", pad_name));
1739 gpu_fields.push((pad_name, quote! { f32 }));
1740 } else {
1741 let count = (final_padding / 4) as usize;
1742 wgsl_lines.push(format!(" {}: array<f32, {}>,", pad_name, count));
1743 gpu_fields.push((pad_name, quote! { [f32; #count] }));
1744 }
1745 }
1746
1747 let wgsl_struct = format!("struct Particle {{\n{}\n}}", wgsl_lines.join("\n"));
1748
1749 (gpu_fields, wgsl_struct, None, None, alive_offset, scale_offset)
1750}
1751
1752struct MultiTypeInfo {
1754 wgsl_type: &'static str,
1755 gpu_type: proc_macro2::TokenStream,
1756 size: u32,
1757 align: u32,
1758 needs_to_array: bool,
1759}
1760
1761fn type_info_from_string(ty: &str) -> MultiTypeInfo {
1762 match ty {
1763 "Vec3" => MultiTypeInfo {
1764 wgsl_type: "vec3<f32>",
1765 gpu_type: quote! { [f32; 3] },
1766 size: 12,
1767 align: 16,
1768 needs_to_array: true,
1769 },
1770 "Vec2" => MultiTypeInfo {
1771 wgsl_type: "vec2<f32>",
1772 gpu_type: quote! { [f32; 2] },
1773 size: 8,
1774 align: 8,
1775 needs_to_array: true,
1776 },
1777 "Vec4" => MultiTypeInfo {
1778 wgsl_type: "vec4<f32>",
1779 gpu_type: quote! { [f32; 4] },
1780 size: 16,
1781 align: 16,
1782 needs_to_array: true,
1783 },
1784 "f32" => MultiTypeInfo {
1785 wgsl_type: "f32",
1786 gpu_type: quote! { f32 },
1787 size: 4,
1788 align: 4,
1789 needs_to_array: false,
1790 },
1791 "u32" => MultiTypeInfo {
1792 wgsl_type: "u32",
1793 gpu_type: quote! { u32 },
1794 size: 4,
1795 align: 4,
1796 needs_to_array: false,
1797 },
1798 "i32" => MultiTypeInfo {
1799 wgsl_type: "i32",
1800 gpu_type: quote! { i32 },
1801 size: 4,
1802 align: 4,
1803 needs_to_array: false,
1804 },
1805 _ => panic!("Unsupported type in MultiParticle: {}", ty),
1806 }
1807}
1808
1809fn zero_value_for_type(ty: &str) -> proc_macro2::TokenStream {
1810 match ty {
1811 "Vec3" => quote! { [0.0, 0.0, 0.0] },
1812 "Vec2" => quote! { [0.0, 0.0] },
1813 "Vec4" => quote! { [0.0, 0.0, 0.0, 0.0] },
1814 "f32" => quote! { 0.0 },
1815 "u32" => quote! { 0 },
1816 "i32" => quote! { 0 },
1817 _ => quote! { Default::default() },
1818 }
1819}