From c31b76198cccdb8a1d406d2b889fff1fce91d089 Mon Sep 17 00:00:00 2001 From: Mitchell Mosure Date: Tue, 13 Feb 2024 11:07:53 -0800 Subject: [PATCH 01/10] refactor: separate macro impl and interface (#1) --- crates/bevy_interleave_macros/src/lib.rs | 136 +------------------ crates/bevy_interleave_macros/src/planar.rs | 140 ++++++++++++++++++++ 2 files changed, 143 insertions(+), 133 deletions(-) create mode 100644 crates/bevy_interleave_macros/src/planar.rs diff --git a/crates/bevy_interleave_macros/src/lib.rs b/crates/bevy_interleave_macros/src/lib.rs index 89307cc..37a33ae 100644 --- a/crates/bevy_interleave_macros/src/lib.rs +++ b/crates/bevy_interleave_macros/src/lib.rs @@ -1,18 +1,14 @@ extern crate proc_macro; use proc_macro::TokenStream; -use quote::quote; use syn::{ - Data, DeriveInput, - Error, - Fields, - FieldsNamed, - Ident, - Result, parse_macro_input, }; +mod planar; +use planar::generate_planar_struct; + #[proc_macro_derive(Planar)] pub fn planar_macro_derive(input: TokenStream) -> TokenStream { @@ -25,129 +21,3 @@ pub fn planar_macro_derive(input: TokenStream) -> TokenStream { TokenStream::from(output) } - -fn generate_planar_struct(input: &DeriveInput) -> Result { - let name = &input.ident; - let planar_name = Ident::new(&format!("Planar{}", name), name.span()); - - let fields_struct = if let Data::Struct(ref data_struct) = input.data { - match data_struct.fields { - Fields::Named(ref fields) => fields, - _ => return Err(Error::new_spanned(input, "Unsupported struct type")), - } - } else { - return Err(Error::new_spanned(input, "Planar macro only supports structs")); - }; - - let field_names = fields_struct.named.iter().map(|f| f.ident.as_ref().unwrap()); - let field_types = fields_struct.named.iter().map(|_| quote! { Vec }); - - let conversion_methods = generate_conversion_methods(name, fields_struct); - let get_set_methods = generate_accessor_setter_methods(name, fields_struct); - let len_method = generate_len_method(fields_struct); - - let expanded = quote! { - pub struct #planar_name { - #(pub #field_names: #field_types,)* - } - - impl Planar<#name> for #planar_name { - #conversion_methods - #get_set_methods - #len_method - } - }; - - Ok(expanded) -} - - -fn generate_len_method(fields_named: &FieldsNamed) -> quote::__private::TokenStream { - if let Some(first_field) = fields_named.named.first() { - let first_field_name = first_field.ident.as_ref().unwrap(); - quote! { - fn is_empty(&self) -> bool { - self.#first_field_name.is_empty() - } - - fn len(&self) -> usize { - self.#first_field_name.len() - } - } - } else { - quote! { - fn is_empty(&self) -> bool { - true - } - - fn len(&self) -> usize { - 0 - } - } - } -} - - -fn generate_accessor_setter_methods(struct_name: &Ident, fields_named: &FieldsNamed) -> quote::__private::TokenStream { - let packed_assignments = fields_named.named.iter().map(|field| { - let name = field.ident.as_ref().unwrap(); - quote! { #name: self.#name[index].clone() } - }); - - let set_assignments = fields_named.named.iter().map(|field| { - let name = field.ident.as_ref().unwrap(); - quote! { self.#name[index] = value.#name.clone(); } - }); - - quote! { - fn get(&self, index: usize) -> #struct_name { - #struct_name { - #(#packed_assignments),* - } - } - - fn set(&mut self, index: usize, value: #struct_name) { - #(#set_assignments);* - } - } -} - - -fn generate_conversion_methods(struct_name: &Ident, fields_named: &FieldsNamed) -> quote::__private::TokenStream { - let ( - from_interleaved_fields, - to_interleaved_fields_templates - ): (Vec<_>, Vec<_>) = fields_named.named.iter().map(|field| { - let name = field.ident.as_ref().unwrap(); - - let from_interleaved_field = quote! { - #name: packed.iter().map(|x| x.#name.clone()).collect() - }; - let to_interleaved_field_template = quote! { - #name: self.#name[index].clone() - }; - - (from_interleaved_field, to_interleaved_field_template) - }).unzip(); - - let to_interleaved_method = quote! { - fn to_interleaved(&self) -> Vec<#struct_name> { - (0..self.len()) - .map(|index| #struct_name { - #(#to_interleaved_fields_templates),* - }) - .collect() - } - }; - - let conversion_methods = quote! { - fn from_interleaved(packed: Vec<#struct_name>) -> Self { - Self { - #(#from_interleaved_fields),* - } - } - #to_interleaved_method - }; - - conversion_methods -} diff --git a/crates/bevy_interleave_macros/src/planar.rs b/crates/bevy_interleave_macros/src/planar.rs new file mode 100644 index 0000000..b6f01a2 --- /dev/null +++ b/crates/bevy_interleave_macros/src/planar.rs @@ -0,0 +1,140 @@ +extern crate proc_macro; + +use quote::quote; +use syn::{ + Data, + DeriveInput, + Error, + Fields, + FieldsNamed, + Ident, + Result, +}; + + +pub fn generate_planar_struct(input: &DeriveInput) -> Result { + let name = &input.ident; + let planar_name = Ident::new(&format!("Planar{}", name), name.span()); + + let fields_struct = if let Data::Struct(ref data_struct) = input.data { + match data_struct.fields { + Fields::Named(ref fields) => fields, + _ => return Err(Error::new_spanned(input, "Unsupported struct type")), + } + } else { + return Err(Error::new_spanned(input, "Planar macro only supports structs")); + }; + + let field_names = fields_struct.named.iter().map(|f| f.ident.as_ref().unwrap()); + let field_types = fields_struct.named.iter().map(|_| quote! { Vec }); + + let conversion_methods = generate_conversion_methods(name, fields_struct); + let get_set_methods = generate_accessor_setter_methods(name, fields_struct); + let len_method = generate_len_method(fields_struct); + + let expanded = quote! { + pub struct #planar_name { + #(pub #field_names: #field_types,)* + } + + impl Planar<#name> for #planar_name { + #conversion_methods + #get_set_methods + #len_method + } + }; + + Ok(expanded) +} + + +pub fn generate_len_method(fields_named: &FieldsNamed) -> quote::__private::TokenStream { + if let Some(first_field) = fields_named.named.first() { + let first_field_name = first_field.ident.as_ref().unwrap(); + quote! { + fn is_empty(&self) -> bool { + self.#first_field_name.is_empty() + } + + fn len(&self) -> usize { + self.#first_field_name.len() + } + } + } else { + quote! { + fn is_empty(&self) -> bool { + true + } + + fn len(&self) -> usize { + 0 + } + } + } +} + + +pub fn generate_accessor_setter_methods(struct_name: &Ident, fields_named: &FieldsNamed) -> quote::__private::TokenStream { + let packed_assignments = fields_named.named.iter().map(|field| { + let name = field.ident.as_ref().unwrap(); + quote! { #name: self.#name[index].clone() } + }); + + let set_assignments = fields_named.named.iter().map(|field| { + let name = field.ident.as_ref().unwrap(); + quote! { self.#name[index] = value.#name.clone(); } + }); + + quote! { + fn get(&self, index: usize) -> #struct_name { + #struct_name { + #(#packed_assignments),* + } + } + + fn set(&mut self, index: usize, value: #struct_name) { + #(#set_assignments);* + } + } +} + + +pub fn generate_conversion_methods(struct_name: &Ident, fields_named: &FieldsNamed) -> quote::__private::TokenStream { + let ( + from_interleaved_fields, + to_interleaved_fields_templates + ): (Vec<_>, Vec<_>) = fields_named.named.iter().map(|field| { + let name = field.ident.as_ref().unwrap(); + + let from_interleaved_field = quote! { + #name: packed.iter().map(|x| x.#name.clone()).collect() + }; + let to_interleaved_field_template = quote! { + #name: self.#name[index].clone() + }; + + (from_interleaved_field, to_interleaved_field_template) + }).unzip(); + + let to_interleaved_method = quote! { + fn to_interleaved(&self) -> Vec<#struct_name> { + (0..self.len()) + .map(|index| #struct_name { + #(#to_interleaved_fields_templates),* + }) + .collect() + } + }; + + let conversion_methods = quote! { + fn from_interleaved(packed: Vec<#struct_name>) -> Self { + Self { + #(#from_interleaved_fields),* + } + } + #to_interleaved_method + }; + + conversion_methods +} + From 357b4690848a79515b2a6787055a27cdf5c8661b Mon Sep 17 00:00:00 2001 From: Mitchell Mosure Date: Sat, 17 Feb 2024 15:37:41 -0800 Subject: [PATCH 02/10] feat: automatic gpu storage planar bind groups (#3) * feat: derived planar storage buffer bind groups * chore: clean texture bind group scaffolding * fix: lint --- Cargo.toml | 8 +- crates/bevy_interleave_interface/Cargo.toml | 6 + crates/bevy_interleave_interface/src/lib.rs | 44 +++- crates/bevy_interleave_macros/Cargo.toml | 12 +- .../src/bindings/mod.rs | 2 + .../src/bindings/storage.rs | 202 ++++++++++++++++++ .../src/bindings/texture.rs | 0 crates/bevy_interleave_macros/src/lib.rs | 34 ++- crates/bevy_interleave_macros/src/packed.rs | 53 +++++ crates/bevy_interleave_macros/src/planar.rs | 45 +++- examples/minimal.rs | 14 +- src/prelude.rs | 13 +- 12 files changed, 411 insertions(+), 22 deletions(-) create mode 100644 crates/bevy_interleave_macros/src/bindings/mod.rs create mode 100644 crates/bevy_interleave_macros/src/bindings/storage.rs create mode 100644 crates/bevy_interleave_macros/src/bindings/texture.rs create mode 100644 crates/bevy_interleave_macros/src/packed.rs diff --git a/Cargo.toml b/Cargo.toml index 70372a7..d07b763 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,10 +31,12 @@ members = [ [dependencies] bevy_interleave_interface = { path = "crates/bevy_interleave_interface", version = "0.1.0" } bevy_interleave_macros = { path = "crates/bevy_interleave_macros", version = "0.1.0" } +bytemuck = "1.14" +serde = "1.0" -# [dependencies.bevy] -# version = "0.12" -# default-features = false +[dependencies.bevy] +version = "0.12" +default-features = false [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/crates/bevy_interleave_interface/Cargo.toml b/crates/bevy_interleave_interface/Cargo.toml index b0319ee..1e1a054 100644 --- a/crates/bevy_interleave_interface/Cargo.toml +++ b/crates/bevy_interleave_interface/Cargo.toml @@ -10,3 +10,9 @@ keywords = [ "bevy", "shader-types", ] + + +[dependencies.bevy] +version = "0.12" +default-features = false +features = ["bevy_render"] diff --git a/crates/bevy_interleave_interface/src/lib.rs b/crates/bevy_interleave_interface/src/lib.rs index 1074b0a..4a904c2 100644 --- a/crates/bevy_interleave_interface/src/lib.rs +++ b/crates/bevy_interleave_interface/src/lib.rs @@ -1,10 +1,44 @@ -pub trait Planar { - fn get(&self, index: usize) -> T; +pub trait GpuStoragePlanar { + type PackedType; + type PlanarType; + + fn bind_group( + &self, + render_device: &bevy::render::renderer::RenderDevice, + layout: &bevy::render::render_resource::BindGroupLayout, + ) -> bevy::render::render_resource::BindGroup; + + fn bind_group_layout( + &self, + render_device: &bevy::render::renderer::RenderDevice, + read_only: bool, + ) -> bevy::render::render_resource::BindGroupLayout; + + fn ordered_field_names(&self) -> &'static [&'static str]; + + fn prepare( + render_device: &bevy::render::renderer::RenderDevice, + planar: &Self::PlanarType, + ) -> Self; +} + + +pub trait MinBindingSize { + type PackedType; + + fn min_binding_sizes() -> &'static [usize]; +} + + +pub trait Planar { + type PackedType; + + fn get(&self, index: usize) -> Self::PackedType; fn is_empty(&self) -> bool; fn len(&self) -> usize; - fn set(&mut self, index: usize, value: T); - fn to_interleaved(&self) -> Vec; + fn set(&mut self, index: usize, value: Self::PackedType); + fn to_interleaved(&self) -> Vec; - fn from_interleaved(packed: Vec) -> Self where Self: Sized; + fn from_interleaved(packed: Vec) -> Self where Self: Sized; } diff --git a/crates/bevy_interleave_macros/Cargo.toml b/crates/bevy_interleave_macros/Cargo.toml index b65a8e3..b0e36cb 100644 --- a/crates/bevy_interleave_macros/Cargo.toml +++ b/crates/bevy_interleave_macros/Cargo.toml @@ -14,9 +14,17 @@ keywords = [ [dependencies] bevy_interleave_interface = { path = "../bevy_interleave_interface", version = "0.1.0" } -syn = "2.0" -quote = "1.0" +bytemuck = "1.14" +convert_case = "0.6" proc-macro2 = "1.0" +quote = "1.0" +sha1 = "0.10" +syn = "2.0" + +[dependencies.bevy] +version = "0.12" +default-features = false +features = ["bevy_render"] [lib] diff --git a/crates/bevy_interleave_macros/src/bindings/mod.rs b/crates/bevy_interleave_macros/src/bindings/mod.rs new file mode 100644 index 0000000..03394c1 --- /dev/null +++ b/crates/bevy_interleave_macros/src/bindings/mod.rs @@ -0,0 +1,2 @@ +pub mod storage; +pub mod texture; diff --git a/crates/bevy_interleave_macros/src/bindings/storage.rs b/crates/bevy_interleave_macros/src/bindings/storage.rs new file mode 100644 index 0000000..2b52c29 --- /dev/null +++ b/crates/bevy_interleave_macros/src/bindings/storage.rs @@ -0,0 +1,202 @@ +use convert_case::{ + Case, + Casing, +}; +use quote::quote; +use syn::{ + Data, + DeriveInput, + Error, + Fields, + FieldsNamed, + Ident, + Result, +}; + + +pub fn storage_bindings(input: &DeriveInput) -> Result { + let name = &input.ident; + + let planar_name = Ident::new(&format!("Planar{}", name), name.span()); + let gpu_planar_name = Ident::new(&format!("GpuPlanar{}", name), name.span()); + + let fields_struct = if let Data::Struct(ref data_struct) = input.data { + match data_struct.fields { + Fields::Named(ref fields) => fields, + _ => return Err(Error::new_spanned(input, "Unsupported struct type")), + } + } else { + return Err(Error::new_spanned(input, "Planar macro only supports structs")); + }; + + let field_names = fields_struct.named.iter().map(|f| f.ident.as_ref().unwrap()); + let field_types = fields_struct.named.iter().map(|_| { + quote! { bevy::render::render_resource::Buffer } + }); + + let bind_group = generate_bind_group_method(name, fields_struct); + let bind_group_layout = generate_bind_group_layout_method(name, fields_struct); + let prepare = generate_prepare_method(fields_struct); + let ordered_field_names = generate_ordered_field_names_method(fields_struct); + + let expanded = quote! { + #[derive(Debug, Clone)] + pub struct #gpu_planar_name { + #(pub #field_names: #field_types,)* + } + + impl GpuStoragePlanar for #gpu_planar_name { + type PackedType = #name; + type PlanarType = #planar_name; + + #bind_group + #bind_group_layout + #prepare + #ordered_field_names + } + }; + + Ok(expanded) +} + + +pub fn generate_bind_group_method(struct_name: &Ident, fields_named: &FieldsNamed) -> quote::__private::TokenStream { + let struct_name_snake = struct_name.to_string().to_case(Case::Snake); + let bind_group_name = format!("{}_bind_group", struct_name_snake); + + let bind_group_entries = fields_named.named + .iter() + .enumerate() + .map(|(idx, field)| { + let name = field.ident.as_ref().unwrap(); + quote! { + bevy::render::render_resource::BindGroupEntry { + binding: #idx as u32, + resource: bevy::render::render_resource::BindingResource::Buffer( + bevy::render::render_resource::BufferBinding { + buffer: &self.#name, + offset: 0, + size: bevy::render::render_resource::BufferSize::new(self.#name.size()), + } + ), + }, + } + }); + + quote! { + fn bind_group( + &self, + render_device: &bevy::render::renderer::RenderDevice, + layout: &bevy::render::render_resource::BindGroupLayout, + ) -> bevy::render::render_resource::BindGroup { + render_device.create_bind_group( + #bind_group_name, + &layout, + &[ + #(#bind_group_entries)* + ] + ) + } + } +} + + +pub fn generate_bind_group_layout_method(struct_name: &Ident, fields_named: &FieldsNamed) -> quote::__private::TokenStream { + let struct_name_snake = struct_name.to_string().to_case(Case::Snake); + let bind_group_layout_name = format!("{}_bind_group_layout", struct_name_snake); + + let bind_group_layout_entries = fields_named.named + .iter() + .enumerate() + .map(|(idx, _)| { + quote! { + bevy::render::render_resource::BindGroupLayoutEntry { + binding: #idx as u32, + visibility: bevy::render::render_resource::ShaderStages::all(), + ty: bevy::render::render_resource::BindingType::Buffer { + ty: bevy::render::render_resource::BufferBindingType::Storage { read_only }, + has_dynamic_offset: false, + min_binding_size: bevy::render::render_resource::BufferSize::new(Self::PackedType::min_binding_sizes()[#idx] as u64), + }, + count: None, + }, + } + }); + + quote! { + fn bind_group_layout( + &self, + render_device: &bevy::render::renderer::RenderDevice, + read_only: bool, + ) -> bevy::render::render_resource::BindGroupLayout { + render_device.create_bind_group_layout( + &bevy::render::render_resource::BindGroupLayoutDescriptor { + label: Some(#bind_group_layout_name), + entries: &[ + #(#bind_group_layout_entries)* + ], + } + ) + } + } +} + + +pub fn generate_prepare_method(fields_named: &FieldsNamed) -> quote::__private::TokenStream { + let buffers = fields_named.named + .iter() + .map(|field| { + let name = field.ident.as_ref().unwrap(); + let buffer_name_string = format!("{}_buffer", name); + + quote! { + let #name = render_device.create_buffer_with_data( + &bevy::render::render_resource::BufferInitDescriptor { + label: Some(#buffer_name_string), + contents: bytemuck::cast_slice(planar.#name.as_slice()), + usage: bevy::render::render_resource::BufferUsages::COPY_DST + | bevy::render::render_resource::BufferUsages::STORAGE, + } + ); + } + }); + + let buffer_names = fields_named.named + .iter() + .map(|field| { + let name = field.ident.as_ref().unwrap(); + quote! { #name } + }); + + quote! { + fn prepare( + render_device: &bevy::render::renderer::RenderDevice, + planar: &Self::PlanarType, + ) -> Self { + #(#buffers)* + + Self { + #(#buffer_names),* + } + } + } +} + + +pub fn generate_ordered_field_names_method(fields_named: &FieldsNamed) -> quote::__private::TokenStream { + let string_field_names = fields_named.named + .iter() + .map(|field| { + let name = field.ident.as_ref().unwrap(); + let name_str = name.to_string(); + quote! { #name_str } + }); + + quote! { + fn ordered_field_names(&self) -> &'static [&'static str] { + &[ + #(#string_field_names),* + ] + } + } +} diff --git a/crates/bevy_interleave_macros/src/bindings/texture.rs b/crates/bevy_interleave_macros/src/bindings/texture.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/bevy_interleave_macros/src/lib.rs b/crates/bevy_interleave_macros/src/lib.rs index 37a33ae..9925bf7 100644 --- a/crates/bevy_interleave_macros/src/lib.rs +++ b/crates/bevy_interleave_macros/src/lib.rs @@ -6,10 +6,10 @@ use syn::{ parse_macro_input, }; + mod planar; use planar::generate_planar_struct; - #[proc_macro_derive(Planar)] pub fn planar_macro_derive(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); @@ -21,3 +21,35 @@ pub fn planar_macro_derive(input: TokenStream) -> TokenStream { TokenStream::from(output) } + + +mod packed; +use packed::generate_min_binding_sizes; + +#[proc_macro_derive(MinBindingSize)] +pub fn min_binding_size_macro_derive(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + let output = match generate_min_binding_sizes(&input) { + Ok(quote) => quote, + Err(e) => return e.to_compile_error().into(), + }; + + TokenStream::from(output) +} + + +mod bindings; +use bindings::storage::storage_bindings; + +#[proc_macro_derive(StorageBindings)] +pub fn storage_bindings_macro_derive(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + let output = match storage_bindings(&input) { + Ok(quote) => quote, + Err(e) => return e.to_compile_error().into(), + }; + + TokenStream::from(output) +} diff --git a/crates/bevy_interleave_macros/src/packed.rs b/crates/bevy_interleave_macros/src/packed.rs new file mode 100644 index 0000000..441ced5 --- /dev/null +++ b/crates/bevy_interleave_macros/src/packed.rs @@ -0,0 +1,53 @@ +use quote::quote; +use syn::{ + Data, + DeriveInput, + Error, + Fields, + FieldsNamed, + Result, +}; + + +pub fn generate_min_binding_sizes(input: &DeriveInput) -> Result { + let name = &input.ident; + + let fields_struct = if let Data::Struct(ref data_struct) = input.data { + match data_struct.fields { + Fields::Named(ref fields) => fields, + _ => return Err(Error::new_spanned(input, "Unsupported struct type")), + } + } else { + return Err(Error::new_spanned(input, "Planar macro only supports structs")); + }; + + let min_binding_size_method = generate_min_binding_size_method(fields_struct); + + let expanded = quote! { + impl MinBindingSize for #name { + type PackedType = #name; + + #min_binding_size_method + } + }; + + Ok(expanded) +} + + +pub fn generate_min_binding_size_method(fields_named: &FieldsNamed) -> quote::__private::TokenStream { + let min_binding_sizes = fields_named.named + .iter() + .map(|f| { + let field_type = &f.ty; + quote! { + std::mem::size_of::<#field_type>() + } + }); + + quote! { + fn min_binding_sizes() -> &'static [usize] { + &[#(#min_binding_sizes),*] + } + } +} diff --git a/crates/bevy_interleave_macros/src/planar.rs b/crates/bevy_interleave_macros/src/planar.rs index b6f01a2..1f9c649 100644 --- a/crates/bevy_interleave_macros/src/planar.rs +++ b/crates/bevy_interleave_macros/src/planar.rs @@ -1,5 +1,3 @@ -extern crate proc_macro; - use quote::quote; use syn::{ Data, @@ -16,6 +14,10 @@ pub fn generate_planar_struct(input: &DeriveInput) -> Result fields, @@ -26,18 +28,34 @@ pub fn generate_planar_struct(input: &DeriveInput) -> Result }); + let field_types = fields_struct.named.iter().map(|og| { + let ty = &og.ty; + quote! { Vec<#ty> } + }); let conversion_methods = generate_conversion_methods(name, fields_struct); let get_set_methods = generate_accessor_setter_methods(name, fields_struct); let len_method = generate_len_method(fields_struct); let expanded = quote! { + #[derive( + Clone, + Debug, + Default, + PartialEq, + bevy::reflect::Reflect, + bevy::reflect::TypeUuid, + serde::Serialize, + serde::Deserialize, + )] + #[uuid = #uuid] pub struct #planar_name { #(pub #field_names: #field_types,)* } - impl Planar<#name> for #planar_name { + impl Planar for #planar_name { + type PackedType = #name; + #conversion_methods #get_set_methods #len_method @@ -138,3 +156,22 @@ pub fn generate_conversion_methods(struct_name: &Ident, fields_named: &FieldsNam conversion_methods } + +fn create_unique_identifier(name: &str) -> String { + use sha1::{Sha1, Digest}; + let mut hasher = Sha1::new(); + hasher.update(name.as_bytes()); + let result = hasher.finalize(); + format!("{:x}", result) +} + +fn format_unique_identifier_as_uuid(identifier: &str) -> String { + format!( + "{}-{}-{}-{}-{}", + &identifier[0..8], + &identifier[8..12], + &identifier[12..16], + &identifier[16..20], + &identifier[20..32] + ) +} diff --git a/examples/minimal.rs b/examples/minimal.rs index d8adf80..8775423 100644 --- a/examples/minimal.rs +++ b/examples/minimal.rs @@ -1,18 +1,22 @@ use bevy_interleave::prelude::*; -#[derive(Planar)] +#[derive( + Planar, + MinBindingSize, + StorageBindings, +)] pub struct MyStruct { pub field: i32, - pub field2: i32, + pub field2: u32, } fn main() { let interleaved = vec![ - MyStruct { field: 0, field2: 1 }, - MyStruct { field: 2, field2: 3 }, - MyStruct { field: 4, field2: 5 }, + MyStruct { field: 0, field2: 1_u32 }, + MyStruct { field: 2, field2: 3_u32 }, + MyStruct { field: 4, field2: 5_u32 }, ]; let planar = PlanarMyStruct::from_interleaved(interleaved); diff --git a/src/prelude.rs b/src/prelude.rs index 6c63883..778af84 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1,2 +1,11 @@ -pub use crate::interface::Planar; -pub use crate::macros::Planar; +pub use crate::interface::{ + GpuStoragePlanar, + MinBindingSize, + Planar, +}; + +pub use crate::macros::{ + StorageBindings, + MinBindingSize, + Planar, +}; From 2c3458f2f435346a79774641eb1552e1e0287b88 Mon Sep 17 00:00:00 2001 From: mosure Date: Sat, 17 Feb 2024 17:55:01 -0600 Subject: [PATCH 03/10] refactor: reflect interleaved trait --- crates/bevy_interleave_interface/src/lib.rs | 5 ++-- .../src/bindings/storage.rs | 21 ---------------- crates/bevy_interleave_macros/src/lib.rs | 8 +++--- crates/bevy_interleave_macros/src/packed.rs | 25 +++++++++++++++++-- examples/minimal.rs | 22 +++++++++++++--- src/prelude.rs | 4 +-- 6 files changed, 49 insertions(+), 36 deletions(-) diff --git a/crates/bevy_interleave_interface/src/lib.rs b/crates/bevy_interleave_interface/src/lib.rs index 4a904c2..b37a702 100644 --- a/crates/bevy_interleave_interface/src/lib.rs +++ b/crates/bevy_interleave_interface/src/lib.rs @@ -15,8 +15,6 @@ pub trait GpuStoragePlanar { read_only: bool, ) -> bevy::render::render_resource::BindGroupLayout; - fn ordered_field_names(&self) -> &'static [&'static str]; - fn prepare( render_device: &bevy::render::renderer::RenderDevice, planar: &Self::PlanarType, @@ -24,10 +22,11 @@ pub trait GpuStoragePlanar { } -pub trait MinBindingSize { +pub trait ReflectInterleaved { type PackedType; fn min_binding_sizes() -> &'static [usize]; + fn ordered_field_names() -> &'static [&'static str]; } diff --git a/crates/bevy_interleave_macros/src/bindings/storage.rs b/crates/bevy_interleave_macros/src/bindings/storage.rs index 2b52c29..013a9cb 100644 --- a/crates/bevy_interleave_macros/src/bindings/storage.rs +++ b/crates/bevy_interleave_macros/src/bindings/storage.rs @@ -37,7 +37,6 @@ pub fn storage_bindings(input: &DeriveInput) -> Result Result quote::__private:: } } } - - -pub fn generate_ordered_field_names_method(fields_named: &FieldsNamed) -> quote::__private::TokenStream { - let string_field_names = fields_named.named - .iter() - .map(|field| { - let name = field.ident.as_ref().unwrap(); - let name_str = name.to_string(); - quote! { #name_str } - }); - - quote! { - fn ordered_field_names(&self) -> &'static [&'static str] { - &[ - #(#string_field_names),* - ] - } - } -} diff --git a/crates/bevy_interleave_macros/src/lib.rs b/crates/bevy_interleave_macros/src/lib.rs index 9925bf7..7bf4c44 100644 --- a/crates/bevy_interleave_macros/src/lib.rs +++ b/crates/bevy_interleave_macros/src/lib.rs @@ -24,13 +24,13 @@ pub fn planar_macro_derive(input: TokenStream) -> TokenStream { mod packed; -use packed::generate_min_binding_sizes; +use packed::generate_reflect_interleaved; -#[proc_macro_derive(MinBindingSize)] -pub fn min_binding_size_macro_derive(input: TokenStream) -> TokenStream { +#[proc_macro_derive(ReflectInterleaved)] +pub fn reflect_interleaved_macro_derive(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - let output = match generate_min_binding_sizes(&input) { + let output = match generate_reflect_interleaved(&input) { Ok(quote) => quote, Err(e) => return e.to_compile_error().into(), }; diff --git a/crates/bevy_interleave_macros/src/packed.rs b/crates/bevy_interleave_macros/src/packed.rs index 441ced5..2fdbcff 100644 --- a/crates/bevy_interleave_macros/src/packed.rs +++ b/crates/bevy_interleave_macros/src/packed.rs @@ -9,7 +9,7 @@ use syn::{ }; -pub fn generate_min_binding_sizes(input: &DeriveInput) -> Result { +pub fn generate_reflect_interleaved(input: &DeriveInput) -> Result { let name = &input.ident; let fields_struct = if let Data::Struct(ref data_struct) = input.data { @@ -22,12 +22,14 @@ pub fn generate_min_binding_sizes(input: &DeriveInput) -> Result quote::__ } } } + + +pub fn generate_ordered_field_names_method(fields_named: &FieldsNamed) -> quote::__private::TokenStream { + let string_field_names = fields_named.named + .iter() + .map(|field| { + let name = field.ident.as_ref().unwrap(); + let name_str = name.to_string(); + quote! { #name_str } + }); + + quote! { + fn ordered_field_names() -> &'static [&'static str] { + &[ + #(#string_field_names),* + ] + } + } +} diff --git a/examples/minimal.rs b/examples/minimal.rs index 8775423..b85b320 100644 --- a/examples/minimal.rs +++ b/examples/minimal.rs @@ -2,29 +2,43 @@ use bevy_interleave::prelude::*; #[derive( + Debug, Planar, - MinBindingSize, + ReflectInterleaved, StorageBindings, )] pub struct MyStruct { pub field: i32, pub field2: u32, + pub bool_field: bool, + pub array: [u32; 4], } fn main() { let interleaved = vec![ - MyStruct { field: 0, field2: 1_u32 }, - MyStruct { field: 2, field2: 3_u32 }, - MyStruct { field: 4, field2: 5_u32 }, + MyStruct { field: 0, field2: 1_u32, bool_field: true, array: [0, 1, 2, 3] }, + MyStruct { field: 2, field2: 3_u32, bool_field: false, array: [4, 5, 6, 7] }, + MyStruct { field: 4, field2: 5_u32, bool_field: true, array: [8, 9, 10, 11] }, ]; let planar = PlanarMyStruct::from_interleaved(interleaved); println!("{:?}", planar.field); println!("{:?}", planar.field2); + println!("{:?}", planar.bool_field); + println!("{:?}", planar.array); // Prints: // [0, 2, 4] // [1, 3, 5] + // [true, false, true] + // [[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]] + + println!("\n\n{:?}", MyStruct::min_binding_sizes()); + println!("{:?}", MyStruct::ordered_field_names()); + + // Prints: + // [4, 4, 1, 16] + // ["field", "field2", "bool_field", "array"] } diff --git a/src/prelude.rs b/src/prelude.rs index 778af84..1f747ad 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1,11 +1,11 @@ pub use crate::interface::{ GpuStoragePlanar, - MinBindingSize, + ReflectInterleaved, Planar, }; pub use crate::macros::{ StorageBindings, - MinBindingSize, + ReflectInterleaved, Planar, }; From b8659ef2f94e3352464d5706b709096979840fd2 Mon Sep 17 00:00:00 2001 From: Mitchell Mosure Date: Mon, 19 Feb 2024 14:24:57 -0800 Subject: [PATCH 04/10] feat: bevy 0.13 & planar texture (#4) * feat: bevy 0.13 & planar texture outline * feat: planar texture bind group and prepare * feat: bind_group_layout scaffold * feat: functional planar texture bindings * docs: 3d texture support * feat: planar and texture plugins /w example app * feat: e2e planar texture bind group * feat: e2e test --- .github/workflows/test.yml | 5 +- Cargo.toml | 5 +- README.md | 38 ++- crates/bevy_interleave_interface/Cargo.toml | 2 +- crates/bevy_interleave_interface/src/lib.rs | 30 +- .../bevy_interleave_interface/src/planar.rs | 31 ++ .../bevy_interleave_interface/src/texture.rs | 160 ++++++++++ crates/bevy_interleave_macros/Cargo.toml | 4 +- .../src/bindings/storage.rs | 19 +- .../src/bindings/texture.rs | 280 ++++++++++++++++++ crates/bevy_interleave_macros/src/lib.rs | 15 + crates/bevy_interleave_macros/src/planar.rs | 27 +- examples/app.rs | 39 +++ examples/minimal.rs | 8 + src/lib.rs | 106 +++++++ src/prelude.rs | 22 +- 16 files changed, 737 insertions(+), 54 deletions(-) create mode 100644 crates/bevy_interleave_interface/src/planar.rs create mode 100644 crates/bevy_interleave_interface/src/texture.rs create mode 100644 examples/app.rs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d9b99f0..705363a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,7 +34,10 @@ jobs: run: cargo clippy -- -Dwarnings - name: build - run: cargo build --example=minimal + run: cargo build + + - name: test + run: cargo test # - name: build (web) # run: cargo build --example=minimal --target wasm32-unknown-unknown --release diff --git a/Cargo.toml b/Cargo.toml index d07b763..e63648d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "bevy_interleave" description = "bevy support for e2e packed to planar bind groups" -version = "0.1.1" +version = "0.2.0" edition = "2021" authors = ["mosure "] license = "MIT" @@ -35,8 +35,9 @@ bytemuck = "1.14" serde = "1.0" [dependencies.bevy] -version = "0.12" +version = "0.13" default-features = false +features = ["bevy_asset", "bevy_render"] [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/README.md b/README.md index fe1d103..50dc1e8 100644 --- a/README.md +++ b/README.md @@ -13,28 +13,55 @@ bevy support for e2e packed to planar bind groups ```rust use bevy_interleave::prelude::*; -#[derive(Planar)] +#[derive( + Debug, + Planar, + ReflectInterleaved, + StorageBindings, + TextureBindings, +)] pub struct MyStruct { + #[texture_format(TextureFormat::R32Sint)] pub field: i32, - pub field2: i32, + + #[texture_format(TextureFormat::R32Uint)] + pub field2: u32, + + #[texture_format(TextureFormat::R8Unorm)] + pub bool_field: bool, + + #[texture_format(TextureFormat::Rgba32Uint)] + pub array: [u32; 4], } fn main() { let interleaved = vec![ - MyStruct { field: 0, field2: 1 }, - MyStruct { field: 2, field2: 3 }, - MyStruct { field: 4, field2: 5 }, + MyStruct { field: 0, field2: 1_u32, bool_field: true, array: [0, 1, 2, 3] }, + MyStruct { field: 2, field2: 3_u32, bool_field: false, array: [4, 5, 6, 7] }, + MyStruct { field: 4, field2: 5_u32, bool_field: true, array: [8, 9, 10, 11] }, ]; let planar = PlanarMyStruct::from_interleaved(interleaved); println!("{:?}", planar.field); println!("{:?}", planar.field2); + println!("{:?}", planar.bool_field); + println!("{:?}", planar.array); // Prints: // [0, 2, 4] // [1, 3, 5] + // [true, false, true] + // [[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]] + + println!("\n\n{:?}", MyStruct::min_binding_sizes()); + println!("{:?}", MyStruct::ordered_field_names()); + + // Prints: + // [4, 4, 1, 16] + // ["field", "field2", "bool_field", "array"] } + ``` @@ -47,4 +74,5 @@ fn main() { | `bevy_interleave` | `bevy` | | :-- | :-- | +| `0.2` | `0.13` | | `0.1` | `0.12` | diff --git a/crates/bevy_interleave_interface/Cargo.toml b/crates/bevy_interleave_interface/Cargo.toml index 1e1a054..835a713 100644 --- a/crates/bevy_interleave_interface/Cargo.toml +++ b/crates/bevy_interleave_interface/Cargo.toml @@ -13,6 +13,6 @@ keywords = [ [dependencies.bevy] -version = "0.12" +version = "0.13" default-features = false features = ["bevy_render"] diff --git a/crates/bevy_interleave_interface/src/lib.rs b/crates/bevy_interleave_interface/src/lib.rs index b37a702..02565cd 100644 --- a/crates/bevy_interleave_interface/src/lib.rs +++ b/crates/bevy_interleave_interface/src/lib.rs @@ -1,5 +1,8 @@ +pub mod planar; +pub mod texture; -pub trait GpuStoragePlanar { + +pub trait PlanarStorage { type PackedType; type PlanarType; @@ -10,7 +13,6 @@ pub trait GpuStoragePlanar { ) -> bevy::render::render_resource::BindGroup; fn bind_group_layout( - &self, render_device: &bevy::render::renderer::RenderDevice, read_only: bool, ) -> bevy::render::render_resource::BindGroupLayout; @@ -22,6 +24,30 @@ pub trait GpuStoragePlanar { } +pub trait PlanarTexture { + type PackedType; + type PlanarType; + + fn bind_group( + &self, + render_device: &bevy::render::renderer::RenderDevice, + gpu_images: &bevy::render::render_asset::RenderAssets, + layout: &bevy::render::render_resource::BindGroupLayout, + ) -> bevy::render::render_resource::BindGroup; + + fn bind_group_layout( + render_device: &bevy::render::renderer::RenderDevice, + ) -> bevy::render::render_resource::BindGroupLayout; + + fn prepare( + images: &mut bevy::asset::Assets, + planar: &Self::PlanarType, + ) -> Self; + + fn get_asset_handles(&self) -> Vec>; +} + + pub trait ReflectInterleaved { type PackedType; diff --git a/crates/bevy_interleave_interface/src/planar.rs b/crates/bevy_interleave_interface/src/planar.rs new file mode 100644 index 0000000..f3f1c45 --- /dev/null +++ b/crates/bevy_interleave_interface/src/planar.rs @@ -0,0 +1,31 @@ +use std::marker::PhantomData; + +use bevy::{ + prelude::*, + reflect::GetTypeRegistration, +}; + +use crate::Planar; + + +pub struct PlanarPlugin { + phantom: PhantomData R>, +} +impl Default for PlanarPlugin { + fn default() -> Self { + Self { + phantom: PhantomData, + } + } +} + +impl Plugin for PlanarPlugin +where + R: Planar + Default + Asset + GetTypeRegistration + Clone + Reflect + FromReflect, +{ + fn build(&self, app: &mut App) { + app.register_type::(); + app.init_asset::(); + app.register_asset_reflect::(); + } +} diff --git a/crates/bevy_interleave_interface/src/texture.rs b/crates/bevy_interleave_interface/src/texture.rs new file mode 100644 index 0000000..2b5299b --- /dev/null +++ b/crates/bevy_interleave_interface/src/texture.rs @@ -0,0 +1,160 @@ +use std::marker::PhantomData; + +use bevy::{ + prelude::*, + reflect::GetTypeRegistration, + render::extract_component::{ + ExtractComponent, + ExtractComponentPlugin, + }, +}; + +use crate::PlanarTexture; + + +pub struct PlanarTexturePlugin { + phantom: PhantomData R>, +} +impl Default for PlanarTexturePlugin { + fn default() -> Self { + Self { + phantom: PhantomData, + } + } +} + +impl Plugin for PlanarTexturePlugin +where + R: PlanarTexture + Default + Component + ExtractComponent + GetTypeRegistration + Clone + Reflect, + R::PlanarType: Asset, +{ + fn build(&self, app: &mut App) { + app.register_type::(); + + app.add_plugins(ExtractComponentPlugin::::default()); + + app.add_systems(Update, prepare_textures::); + + let render_app = app.sub_app_mut(bevy::render::RenderApp); + render_app.add_systems( + bevy::render::Render, + queue_gpu_texture_buffers::.in_set(bevy::render::RenderSet::PrepareAssets), + ); + } + + fn finish(&self, app: &mut App) { + if let Ok(render_app) = app.get_sub_app_mut(bevy::render::RenderApp) { + render_app.init_resource::>(); + } + } +} + + +#[derive(bevy::prelude::Resource)] +pub struct PlanarTextureLayouts { + pub bind_group_layout: bevy::render::render_resource::BindGroupLayout, + pub phantom: PhantomData R>, +} + +impl +FromWorld for PlanarTextureLayouts { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + + let bind_group_layout = R::bind_group_layout( + render_device, + ); + + PlanarTextureLayouts:: { + bind_group_layout, + phantom: PhantomData, + } + } +} + +fn prepare_textures( + mut commands: Commands, + asset_server: Res, + cloud_res: Res>, + mut images: ResMut>, + clouds: Query< + ( + Entity, + &Handle, + ), + Without, + >, +) +where + R: PlanarTexture + Default + Component + ExtractComponent + GetTypeRegistration + Clone + Reflect, + R::PlanarType: Asset, +{ + for (entity, cloud_handle) in clouds.iter() { + if Some(bevy::asset::LoadState::Loading) == asset_server.get_load_state(cloud_handle){ + continue; + } + + if cloud_res.get(cloud_handle).is_none() { + continue; + } + + let cloud = cloud_res.get(cloud_handle).unwrap(); + + let buffers = R::prepare( + &mut images, + cloud, + ); + + commands.entity(entity).insert(buffers); + } +} + + +#[derive(bevy::prelude::Component, Clone, Debug)] +pub struct PlanarTextureBindGroup { + pub bind_group: bevy::render::render_resource::BindGroup, + pub phantom: PhantomData R>, +} + + +fn queue_gpu_texture_buffers( + mut commands: Commands, + render_device: ResMut, + gpu_images: Res>, + bind_group_layout: Res>, + clouds: Query< + ( + Entity, + &R, + ), + Without>, + >, +) +where + R: PlanarTexture + Default + Component + ExtractComponent + GetTypeRegistration + Clone + Reflect, + R::PlanarType: Asset, +{ + let layout = &bind_group_layout.bind_group_layout; + + for (entity, texture_buffers,) in clouds.iter() { + let not_ready = texture_buffers.get_asset_handles().iter() + .map(|handle| gpu_images.get(handle).is_none()) + .reduce(|a, b| a || b) + .unwrap_or(true); + + if not_ready { + continue; + } + + let bind_group = texture_buffers.bind_group( + &render_device, + &gpu_images, + layout, + ); + + commands.entity(entity).insert(PlanarTextureBindGroup:: { + bind_group, + phantom: PhantomData, + }); + } +} diff --git a/crates/bevy_interleave_macros/Cargo.toml b/crates/bevy_interleave_macros/Cargo.toml index b0e36cb..04a265b 100644 --- a/crates/bevy_interleave_macros/Cargo.toml +++ b/crates/bevy_interleave_macros/Cargo.toml @@ -22,9 +22,9 @@ sha1 = "0.10" syn = "2.0" [dependencies.bevy] -version = "0.12" +version = "0.13" default-features = false -features = ["bevy_render"] +features = ["bevy_asset", "bevy_render"] [lib] diff --git a/crates/bevy_interleave_macros/src/bindings/storage.rs b/crates/bevy_interleave_macros/src/bindings/storage.rs index 013a9cb..ae1dd91 100644 --- a/crates/bevy_interleave_macros/src/bindings/storage.rs +++ b/crates/bevy_interleave_macros/src/bindings/storage.rs @@ -18,7 +18,7 @@ pub fn storage_bindings(input: &DeriveInput) -> Result Result Result quote::__private::TokenStream { let struct_name_snake = struct_name.to_string().to_case(Case::Snake); - let bind_group_name = format!("{}_bind_group", struct_name_snake); + let bind_group_name = format!("storage_{}_bind_group", struct_name_snake); let bind_group_entries = fields_named.named .iter() @@ -101,7 +101,7 @@ pub fn generate_bind_group_method(struct_name: &Ident, fields_named: &FieldsName pub fn generate_bind_group_layout_method(struct_name: &Ident, fields_named: &FieldsNamed) -> quote::__private::TokenStream { let struct_name_snake = struct_name.to_string().to_case(Case::Snake); - let bind_group_layout_name = format!("{}_bind_group_layout", struct_name_snake); + let bind_group_layout_name = format!("storage_{}_bind_group_layout", struct_name_snake); let bind_group_layout_entries = fields_named.named .iter() @@ -123,17 +123,14 @@ pub fn generate_bind_group_layout_method(struct_name: &Ident, fields_named: &Fie quote! { fn bind_group_layout( - &self, render_device: &bevy::render::renderer::RenderDevice, read_only: bool, ) -> bevy::render::render_resource::BindGroupLayout { render_device.create_bind_group_layout( - &bevy::render::render_resource::BindGroupLayoutDescriptor { - label: Some(#bind_group_layout_name), - entries: &[ - #(#bind_group_layout_entries)* - ], - } + Some(#bind_group_layout_name), + &[ + #(#bind_group_layout_entries)* + ], ) } } diff --git a/crates/bevy_interleave_macros/src/bindings/texture.rs b/crates/bevy_interleave_macros/src/bindings/texture.rs index e69de29..e95e6ea 100644 --- a/crates/bevy_interleave_macros/src/bindings/texture.rs +++ b/crates/bevy_interleave_macros/src/bindings/texture.rs @@ -0,0 +1,280 @@ +use convert_case::{ + Case, + Casing, +}; +use proc_macro2::TokenStream; +use quote::quote; +use syn::{ + Attribute, + Data, + DeriveInput, + Error, + Fields, + FieldsNamed, + Ident, + parse::{ + Parse, + ParseStream, + }, + Path, + Result, +}; + + +pub fn texture_bindings(input: &DeriveInput) -> Result { + let name = &input.ident; + + let planar_name = Ident::new(&format!("Planar{}", name), name.span()); + let gpu_planar_name = Ident::new(&format!("PlanarTexture{}", name), name.span()); + + let fields_struct = if let Data::Struct(ref data_struct) = input.data { + match data_struct.fields { + Fields::Named(ref fields) => fields, + _ => return Err(Error::new_spanned(input, "Unsupported struct type")), + } + } else { + return Err(Error::new_spanned(input, "Planar macro only supports structs")); + }; + + let field_names = fields_struct.named.iter().map(|f| f.ident.as_ref().unwrap()); + let field_types = fields_struct.named.iter().map(|_| { + quote! { bevy::asset::Handle } + }); + + let bind_group = generate_bind_group_method(name, fields_struct); + let bind_group_layout = generate_bind_group_layout_method(name, fields_struct); + let prepare = generate_prepare_method(fields_struct); + let get_asset_handles = generate_get_asset_handles_method(fields_struct); + + let expanded = quote! { + #[derive(bevy::prelude::Component, Default, Clone, Debug, bevy::reflect::Reflect)] + pub struct #gpu_planar_name { + #(pub #field_names: #field_types,)* + } + + impl bevy::render::extract_component::ExtractComponent for #gpu_planar_name { + type QueryData = &'static Self; + + type QueryFilter = (); + type Out = Self; + + fn extract_component(texture_buffers: bevy::ecs::query::QueryItem<'_, Self::QueryData>) -> Option { + texture_buffers.clone().into() + } + } + + impl PlanarTexture for #gpu_planar_name { + type PackedType = #name; + type PlanarType = #planar_name; + + #bind_group + #bind_group_layout + #prepare + #get_asset_handles + } + }; + + Ok(expanded) +} + + +pub fn generate_bind_group_method(struct_name: &Ident, fields_named: &FieldsNamed) -> quote::__private::TokenStream { + let struct_name_snake = struct_name.to_string().to_case(Case::Snake); + let bind_group_name = format!("texture_{}_bind_group", struct_name_snake); + + let bind_group_entries = fields_named.named + .iter() + .enumerate() + .map(|(idx, field)| { + let name = field.ident.as_ref().unwrap(); + quote! { + bevy::render::render_resource::BindGroupEntry { + binding: #idx as u32, + resource: bevy::render::render_resource::BindingResource::TextureView( + &gpu_images.get(&self.#name).unwrap().texture_view + ), + }, + } + }); + + quote! { + fn bind_group( + &self, + render_device: &bevy::render::renderer::RenderDevice, + gpu_images: &bevy::render::render_asset::RenderAssets, + layout: &bevy::render::render_resource::BindGroupLayout, + ) -> bevy::render::render_resource::BindGroup { + render_device.create_bind_group( + #bind_group_name, + &layout, + &[ + #(#bind_group_entries)* + ] + ) + } + } +} + + +pub fn generate_bind_group_layout_method(struct_name: &Ident, fields_named: &FieldsNamed) -> quote::__private::TokenStream { + let struct_name_snake = struct_name.to_string().to_case(Case::Snake); + let bind_group_layout_name = format!("texture_{}_bind_group_layout", struct_name_snake); + + let bind_group_layout_entries = fields_named.named + .iter() + .enumerate() + .map(|(idx, field)| { + let name = field.ident.as_ref().unwrap(); + let format = extract_texture_format(&field.attrs); + + let field_type = &field.ty; + + quote! { + let sample_type = #format.sample_type(None, None).unwrap(); + + let size = std::mem::size_of::<#field_type>(); + let format_bpp = #format.pixel_size(); + let depth = (size as f32 / format_bpp as f32).ceil() as u32; + + let view_dimension = if depth == 1 { + bevy::render::render_resource::TextureViewDimension::D2 // TODO: support 3D texture sampling + } else { + bevy::render::render_resource::TextureViewDimension::D2Array + }; + + let #name = bevy::render::render_resource::BindGroupLayoutEntry { + binding: #idx as u32, + visibility: bevy::render::render_resource::ShaderStages::all(), + ty: bevy::render::render_resource::BindingType::Texture { + view_dimension, + sample_type, + multisampled: false, + }, + count: None, + }; + } + }); + + let layout_names = fields_named.named + .iter() + .map(|field| { + let name = field.ident.as_ref().unwrap(); + quote! { #name } + }); + + quote! { + fn bind_group_layout( + render_device: &bevy::render::renderer::RenderDevice, + ) -> bevy::render::render_resource::BindGroupLayout { + #(#bind_group_layout_entries)* + + render_device.create_bind_group_layout( + Some(#bind_group_layout_name), + &[ + #(#layout_names),* + ], + ) + } + } +} + + +pub fn generate_prepare_method(fields_named: &FieldsNamed) -> quote::__private::TokenStream { + let buffers = fields_named.named + .iter() + .map(|field| { + let name = field.ident.as_ref().unwrap(); + let format = extract_texture_format(&field.attrs); + + let field_type = &field.ty; + + quote! { + let square = (planar.#name.len() as f32).sqrt().ceil() as u32; + + let size = std::mem::size_of::<#field_type>(); + let format_bpp = #format.pixel_size(); + let depth = (size as f32 / format_bpp as f32).ceil() as u32; + + let mut data = bytemuck::cast_slice(planar.#name.as_slice()).to_vec(); + + let padded_size = (square * square * depth * format_bpp as u32) as usize; + data.resize(padded_size, 0); + + let mut #name = bevy::render::texture::Image::new( + bevy::render::render_resource::Extent3d { + width: square, + height: square, + depth_or_array_layers: depth, + }, + bevy::render::render_resource::TextureDimension::D2, + data, + #format, + bevy::render::render_asset::RenderAssetUsages::default(), // TODO: if there are no CPU image derived features, set to render only + ); + let #name = images.add(#name); + } + }); + + let buffer_names = fields_named.named + .iter() + .map(|field| { + let name = field.ident.as_ref().unwrap(); + quote! { #name } + }); + + quote! { + fn prepare( + images: &mut bevy::asset::Assets, + planar: &Self::PlanarType, + ) -> Self { + #(#buffers)* + + Self { + #(#buffer_names),* + } + } + } +} + + +pub fn generate_get_asset_handles_method(fields_named: &FieldsNamed) -> quote::__private::TokenStream { + let buffer_names = fields_named.named + .iter() + .map(|field| { + let name = field.ident.as_ref().unwrap(); + quote! { self.#name.clone() } + }); + + quote! { + fn get_asset_handles(&self) -> Vec> { + vec![ + #(#buffer_names),* + ] + } + } +} + + +struct TextureFormatAttr(Path); + +impl Parse for TextureFormatAttr { + fn parse(input: ParseStream) -> Result { + let format: Path = input.parse()?; + Ok(TextureFormatAttr(format)) + } +} + +fn extract_texture_format(attributes: &[Attribute]) -> TokenStream { + for attr in attributes { + if attr.path().is_ident("texture_format") { + if let Ok(parsed) = attr.parse_args::() { + let TextureFormatAttr(format) = parsed; + return quote! { #format }; + } else { + panic!("error parsing texture_format attribute"); + } + } + } + + panic!("no texture_format attribute found, add `#[texture_format(Ident)]` to the field declarations"); +} diff --git a/crates/bevy_interleave_macros/src/lib.rs b/crates/bevy_interleave_macros/src/lib.rs index 7bf4c44..00c920e 100644 --- a/crates/bevy_interleave_macros/src/lib.rs +++ b/crates/bevy_interleave_macros/src/lib.rs @@ -53,3 +53,18 @@ pub fn storage_bindings_macro_derive(input: TokenStream) -> TokenStream { TokenStream::from(output) } + + +use bindings::texture::texture_bindings; + +#[proc_macro_derive(TextureBindings, attributes(texture_format))] +pub fn texture_bindings_macro_derive(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + let output = match texture_bindings(&input) { + Ok(quote) => quote, + Err(e) => return e.to_compile_error().into(), + }; + + TokenStream::from(output) +} diff --git a/crates/bevy_interleave_macros/src/planar.rs b/crates/bevy_interleave_macros/src/planar.rs index 1f9c649..96c85d9 100644 --- a/crates/bevy_interleave_macros/src/planar.rs +++ b/crates/bevy_interleave_macros/src/planar.rs @@ -14,10 +14,6 @@ pub fn generate_planar_struct(input: &DeriveInput) -> Result fields, @@ -39,16 +35,15 @@ pub fn generate_planar_struct(input: &DeriveInput) -> Result String { - use sha1::{Sha1, Digest}; - let mut hasher = Sha1::new(); - hasher.update(name.as_bytes()); - let result = hasher.finalize(); - format!("{:x}", result) -} - -fn format_unique_identifier_as_uuid(identifier: &str) -> String { - format!( - "{}-{}-{}-{}-{}", - &identifier[0..8], - &identifier[8..12], - &identifier[12..16], - &identifier[16..20], - &identifier[20..32] - ) -} diff --git a/examples/app.rs b/examples/app.rs new file mode 100644 index 0000000..7edf4b0 --- /dev/null +++ b/examples/app.rs @@ -0,0 +1,39 @@ +use bevy::prelude::*; + +use bevy_interleave::prelude::*; + + +#[derive( + Debug, + Planar, + ReflectInterleaved, + StorageBindings, + TextureBindings, +)] +pub struct MyStruct { + #[texture_format(TextureFormat::R32Sint)] + pub field: i32, + + #[texture_format(TextureFormat::R32Uint)] + pub field2: u32, + + #[texture_format(TextureFormat::R8Unorm)] + pub bool_field: bool, + + #[texture_format(TextureFormat::Rgba32Uint)] + pub array: [u32; 4], +} + + +fn main() { + let mut app = App::new(); + + app.add_plugins(( + DefaultPlugins, + PlanarPlugin::::default(), + PlanarTexturePlugin::::default(), + // TODO: PlanarStoragePlugin::::default(), + )); + + app.run(); +} diff --git a/examples/minimal.rs b/examples/minimal.rs index b85b320..6a88d96 100644 --- a/examples/minimal.rs +++ b/examples/minimal.rs @@ -6,11 +6,19 @@ use bevy_interleave::prelude::*; Planar, ReflectInterleaved, StorageBindings, + TextureBindings, )] pub struct MyStruct { + #[texture_format(TextureFormat::R32Sint)] pub field: i32, + + #[texture_format(TextureFormat::R32Uint)] pub field2: u32, + + #[texture_format(TextureFormat::R8Unorm)] pub bool_field: bool, + + #[texture_format(TextureFormat::Rgba32Uint)] pub array: [u32; 4], } diff --git a/src/lib.rs b/src/lib.rs index 5817e50..229dd01 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,3 +7,109 @@ pub mod interface { pub mod macros { pub use bevy_interleave_macros::*; } + + + +mod tests { + use std::sync::{Arc, Mutex}; + + use bevy::prelude::*; + use crate::prelude::*; + + #[derive( + Debug, + Planar, + ReflectInterleaved, + StorageBindings, + TextureBindings, + )] + pub struct MyStruct { + #[texture_format(TextureFormat::R32Sint)] + pub field: i32, + + #[texture_format(TextureFormat::R32Uint)] + pub field2: u32, + + #[texture_format(TextureFormat::R8Unorm)] + pub bool_field: bool, + + #[texture_format(TextureFormat::Rgba32Uint)] + pub array: [u32; 4], + } + + #[derive(Resource, Default)] + struct TestSuccess(Arc>); + + #[allow(dead_code)] + fn setup_planar( + mut commands: Commands, + mut gaussian_assets: ResMut>, + ) { + let planar = PlanarMyStruct::from_interleaved(vec![ + MyStruct { field: 0, field2: 1_u32, bool_field: true, array: [0, 1, 2, 3] }, + MyStruct { field: 2, field2: 3_u32, bool_field: false, array: [4, 5, 6, 7] }, + MyStruct { field: 4, field2: 5_u32, bool_field: true, array: [8, 9, 10, 11] }, + ]); + + let planar_handle = gaussian_assets.add(planar); + + commands.spawn(planar_handle); + } + + #[allow(dead_code)] + fn check_bind_group( + bind_group: Query<&PlanarTextureBindGroup::>, + success: Res, + ) { + if bind_group.iter().count() > 0 { + *success.0.lock().unwrap() = true; + } + } + + #[allow(dead_code)] + fn test_timeout( + mut exit: EventWriter, + mut frame_count: Local, + ) { + *frame_count += 1; + + if *frame_count > 5 { + exit.send(bevy::app::AppExit); + } + } + + + #[test] + fn texture_bind_group() { + let mut app = App::new(); + + app.add_plugins(( + DefaultPlugins, + bevy::app::ScheduleRunnerPlugin::run_loop( + std::time::Duration::from_millis(50) + ), + + PlanarPlugin::::default(), + PlanarTexturePlugin::::default(), + )); + + app.add_systems(Startup, setup_planar); + + let render_app = app.sub_app_mut(bevy::render::RenderApp); + render_app.add_systems( + bevy::render::Render, + check_bind_group.in_set(bevy::render::RenderSet::QueueMeshes), + ); + + render_app.init_resource::(); + let success = render_app.world.resource::(); + let success = success.0.clone(); + + app.add_systems(Update, test_timeout); + app.run(); + + if !*success.lock().unwrap() { + panic!("app exit without success flag set - bind group was not found"); + } + } +} diff --git a/src/prelude.rs b/src/prelude.rs index 1f747ad..34bab6e 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1,11 +1,25 @@ +pub use bevy::render::{ + render_resource::TextureFormat, + texture::TextureFormatPixelInfo, +}; + + pub use crate::interface::{ - GpuStoragePlanar, - ReflectInterleaved, Planar, + PlanarStorage, + PlanarTexture, + planar::PlanarPlugin, + texture::{ + PlanarTextureBindGroup, + PlanarTextureLayouts, + PlanarTexturePlugin, + }, + ReflectInterleaved, }; pub use crate::macros::{ - StorageBindings, - ReflectInterleaved, Planar, + ReflectInterleaved, + StorageBindings, + TextureBindings, }; From d7ca93977d68d619ae3658e230d49aa79e74cba6 Mon Sep 17 00:00:00 2001 From: mosure Date: Mon, 19 Feb 2024 16:25:51 -0600 Subject: [PATCH 05/10] docs: update code example --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index 50dc1e8..04bc04a 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,27 @@ fn main() { // Prints: // [4, 4, 1, 16] // ["field", "field2", "bool_field", "array"] + + let mut app = App::new() + .add_plugins(( + DefaultPlugins, + PlanarPlugin::::default(), + PlanarTexturePlugin::::default(), + )); + + app.sub_app_mut(bevy::render::RenderApp) + .add_systems( + bevy::render::Render, + check_bind_group.in_set(bevy::render::RenderSet::QueueMeshes), + ); + + app.run(); +} + +fn check_bind_group( + bind_group: Query<&PlanarTextureBindGroup::>, +) { + // attach bind group to render pipeline } ``` From f90b886fe26b115747d86937fa0c6d206333d40e Mon Sep 17 00:00:00 2001 From: mosure Date: Mon, 19 Feb 2024 17:08:44 -0600 Subject: [PATCH 06/10] docs: bind group usage --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 04bc04a..3a96b58 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ fn main() { // [4, 4, 1, 16] // ["field", "field2", "bool_field", "array"] + let mut app = App::new() .add_plugins(( DefaultPlugins, @@ -77,6 +78,21 @@ fn main() { app.run(); } +fn setup_planar_asset( + mut commands: Commands, + mut planar_assets: ResMut>, +) { + let interleaved = vec![ + MyStruct { field: 0, field2: 1_u32, bool_field: true, array: [0, 1, 2, 3] }, + MyStruct { field: 2, field2: 3_u32, bool_field: false, array: [4, 5, 6, 7] }, + MyStruct { field: 4, field2: 5_u32, bool_field: true, array: [8, 9, 10, 11] }, + ]; + + let planar = PlanarMyStruct::from_interleaved(interleaved); + + commands.spawn(planar_assets.add(planar)); +} + fn check_bind_group( bind_group: Query<&PlanarTextureBindGroup::>, ) { From 6bcf8555f382a781e7cf71f4a3fb1cb20bbe88c1 Mon Sep 17 00:00:00 2001 From: mosure Date: Mon, 19 Feb 2024 17:10:02 -0600 Subject: [PATCH 07/10] chore: bump crate versions --- crates/bevy_interleave_interface/Cargo.toml | 2 +- crates/bevy_interleave_macros/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_interleave_interface/Cargo.toml b/crates/bevy_interleave_interface/Cargo.toml index 835a713..c4b42d7 100644 --- a/crates/bevy_interleave_interface/Cargo.toml +++ b/crates/bevy_interleave_interface/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_interleave_interface" -version = "0.1.1" +version = "0.2.0" edition = "2021" description = "interface for e2e packed to planar bind groups" homepage = "https://github.com/mosure/bevy_interleave" diff --git a/crates/bevy_interleave_macros/Cargo.toml b/crates/bevy_interleave_macros/Cargo.toml index 04a265b..f81f3d5 100644 --- a/crates/bevy_interleave_macros/Cargo.toml +++ b/crates/bevy_interleave_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_interleave_macros" -version = "0.1.1" +version = "0.2.0" edition = "2021" description = "macros for e2e packed to planar bind groups" homepage = "https://github.com/mosure/bevy_interleave" From 751dfc143d672ae06d81bf6712b57fadc8483a8b Mon Sep 17 00:00:00 2001 From: mosure Date: Mon, 19 Feb 2024 17:15:37 -0600 Subject: [PATCH 08/10] fix: interface depends on bevy_asset --- crates/bevy_interleave_interface/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_interleave_interface/Cargo.toml b/crates/bevy_interleave_interface/Cargo.toml index c4b42d7..b9a726c 100644 --- a/crates/bevy_interleave_interface/Cargo.toml +++ b/crates/bevy_interleave_interface/Cargo.toml @@ -15,4 +15,4 @@ keywords = [ [dependencies.bevy] version = "0.13" default-features = false -features = ["bevy_render"] +features = ["bevy_asset", "bevy_render"] From b822d8127b2ed5484b72178c8be77dee3fffee1e Mon Sep 17 00:00:00 2001 From: mosure Date: Mon, 19 Feb 2024 17:20:56 -0600 Subject: [PATCH 09/10] chore: update child crate versions --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e63648d..efc3a67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,8 +29,8 @@ members = [ [dependencies] -bevy_interleave_interface = { path = "crates/bevy_interleave_interface", version = "0.1.0" } -bevy_interleave_macros = { path = "crates/bevy_interleave_macros", version = "0.1.0" } +bevy_interleave_interface = { path = "crates/bevy_interleave_interface", version = "0.2.0" } +bevy_interleave_macros = { path = "crates/bevy_interleave_macros", version = "0.2.0" } bytemuck = "1.14" serde = "1.0" From 74adbd44e7f4a2d761a2d5149716fbdb6458ce1c Mon Sep 17 00:00:00 2001 From: mosure Date: Mon, 19 Feb 2024 17:26:30 -0600 Subject: [PATCH 10/10] docs: simplify example --- README.md | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 3a96b58..24b0ba0 100644 --- a/README.md +++ b/README.md @@ -34,14 +34,16 @@ pub struct MyStruct { pub array: [u32; 4], } -fn main() { - let interleaved = vec![ +fn interleaved() -> Vec { + vec![ MyStruct { field: 0, field2: 1_u32, bool_field: true, array: [0, 1, 2, 3] }, MyStruct { field: 2, field2: 3_u32, bool_field: false, array: [4, 5, 6, 7] }, MyStruct { field: 4, field2: 5_u32, bool_field: true, array: [8, 9, 10, 11] }, ]; +} - let planar = PlanarMyStruct::from_interleaved(interleaved); +fn main() { + let planar = PlanarMyStruct::from_interleaved(interleaved()); println!("{:?}", planar.field); println!("{:?}", planar.field2); @@ -82,13 +84,7 @@ fn setup_planar_asset( mut commands: Commands, mut planar_assets: ResMut>, ) { - let interleaved = vec![ - MyStruct { field: 0, field2: 1_u32, bool_field: true, array: [0, 1, 2, 3] }, - MyStruct { field: 2, field2: 3_u32, bool_field: false, array: [4, 5, 6, 7] }, - MyStruct { field: 4, field2: 5_u32, bool_field: true, array: [8, 9, 10, 11] }, - ]; - - let planar = PlanarMyStruct::from_interleaved(interleaved); + let planar = PlanarMyStruct::from_interleaved(interleaved()); commands.spawn(planar_assets.add(planar)); } @@ -97,6 +93,13 @@ fn check_bind_group( bind_group: Query<&PlanarTextureBindGroup::>, ) { // attach bind group to render pipeline + // size: + // 2 x 2 x bpp + // format: + // binding: 0 - texture - R32Sint - depth 1 + // binding: 1 - texture - R32Uint - depth 1 + // binding: 2 - texture - R8Unorm - depth 1 + // binding: 3 - texture - Rgba32Uint - depth 1 } ```