diff --git a/Cargo.toml b/Cargo.toml index 7dc0d8d6e5..6b5267be70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,14 +13,18 @@ repository = "https://github.com/bevyengine/bevy" [workspace] exclude = ["benches", "crates/bevy_ecs_compile_fail_tests"] -members = ["crates/*", "examples/ios", "tools/ci", "errors"] +members = ["crates/*", "pipelined/*", "examples/ios", "tools/ci", "errors"] [features] default = [ "bevy_audio", + "bevy_core_pipeline", "bevy_gilrs", - "bevy_gltf", + "bevy_gltf2", "bevy_wgpu", + "bevy_sprite2", + "bevy_render2", + "bevy_pbr2", "bevy_winit", "render", "png", @@ -50,6 +54,12 @@ bevy_gltf = ["bevy_internal/bevy_gltf"] bevy_wgpu = ["bevy_internal/bevy_wgpu"] bevy_winit = ["bevy_internal/bevy_winit"] +bevy_core_pipeline = ["bevy_internal/bevy_core_pipeline"] +bevy_render2 = ["bevy_internal/bevy_render2"] +bevy_sprite2 = ["bevy_internal/bevy_sprite2"] +bevy_pbr2 = ["bevy_internal/bevy_pbr2"] +bevy_gltf2 = ["bevy_internal/bevy_gltf2"] + trace_chrome = ["bevy_internal/trace_chrome"] trace_tracy = ["bevy_internal/trace_tracy"] trace = ["bevy_internal/trace"] @@ -95,6 +105,7 @@ ron = "0.6.2" serde = { version = "1", features = ["derive"] } # Needed to poll Task examples futures-lite = "1.11.3" +crevice = { path = "crates/crevice", version = "0.8.0", features = ["glam"] } [[example]] name = "hello_world" @@ -133,23 +144,51 @@ path = "examples/2d/text2d.rs" name = "texture_atlas" path = "examples/2d/texture_atlas.rs" +[[example]] +name = "pipelined_texture_atlas" +path = "examples/2d/pipelined_texture_atlas.rs" + # 3D Rendering [[example]] name = "3d_scene" path = "examples/3d/3d_scene.rs" +[[example]] +name = "3d_scene_pipelined" +path = "examples/3d/3d_scene_pipelined.rs" + +[[example]] +name = "many_cubes_pipelined" +path = "examples/3d/many_cubes_pipelined.rs" + +[[example]] +name = "cornell_box_pipelined" +path = "examples/3d/cornell_box_pipelined.rs" + [[example]] name = "load_gltf" path = "examples/3d/load_gltf.rs" +[[example]] +name = "load_gltf_pipelined" +path = "examples/3d/load_gltf_pipelined.rs" + [[example]] name = "msaa" path = "examples/3d/msaa.rs" +[[example]] +name = "msaa_pipelined" +path = "examples/3d/msaa_pipelined.rs" + [[example]] name = "orthographic" path = "examples/3d/orthographic.rs" +[[example]] +name = "orthographic_pipelined" +path = "examples/3d/orthographic_pipelined.rs" + [[example]] name = "parenting" path = "examples/3d/parenting.rs" @@ -158,10 +197,22 @@ path = "examples/3d/parenting.rs" name = "pbr" path = "examples/3d/pbr.rs" +[[example]] +name = "pbr_pipelined" +path = "examples/3d/pbr_pipelined.rs" + [[example]] name = "render_to_texture" path = "examples/3d/render_to_texture.rs" +[[example]] +name = "shadow_biases_pipelined" +path = "examples/3d/shadow_biases_pipelined.rs" + +[[example]] +name = "shadow_caster_receiver_pipelined" +path = "examples/3d/shadow_caster_receiver_pipelined.rs" + [[example]] name = "spawner" path = "examples/3d/spawner.rs" @@ -170,6 +221,10 @@ path = "examples/3d/spawner.rs" name = "texture" path = "examples/3d/texture.rs" +[[example]] +name = "texture_pipelined" +path = "examples/3d/texture_pipelined.rs" + [[example]] name = "update_gltf_scene" path = "examples/3d/update_gltf_scene.rs" @@ -413,11 +468,23 @@ path = "examples/shader/shader_custom_material.rs" name = "shader_defs" path = "examples/shader/shader_defs.rs" +[[example]] +name = "custom_shader_pipelined" +path = "examples/shader/custom_shader_pipelined.rs" + +[[example]] +name = "shader_defs_pipelined" +path = "examples/shader/shader_defs_pipelined.rs" + # Tools [[example]] name = "bevymark" path = "examples/tools/bevymark.rs" +[[example]] +name = "bevymark_pipelined" +path = "examples/tools/bevymark_pipelined.rs" + # UI (User Interface) [[example]] name = "button" @@ -444,6 +511,10 @@ path = "examples/ui/ui.rs" name = "clear_color" path = "examples/window/clear_color.rs" +[[example]] +name = "clear_color_pipelined" +path = "examples/window/clear_color_pipelined.rs" + [[example]] name = "multiple_windows" path = "examples/window/multiple_windows.rs" diff --git a/assets/shaders/custom_material.wgsl b/assets/shaders/custom_material.wgsl new file mode 100644 index 0000000000..9dfaa6f568 --- /dev/null +++ b/assets/shaders/custom_material.wgsl @@ -0,0 +1,11 @@ +[[block]] +struct CustomMaterial { + color: vec4; +}; +[[group(1), binding(0)]] +var material: CustomMaterial; + +[[stage(fragment)]] +fn fragment() -> [[location(0)]] vec4 { + return material.color; +} diff --git a/assets/shaders/shader_defs.wgsl b/assets/shaders/shader_defs.wgsl new file mode 100644 index 0000000000..0d1c93d37e --- /dev/null +++ b/assets/shaders/shader_defs.wgsl @@ -0,0 +1,33 @@ +#import bevy_pbr::mesh_view_bind_group +#import bevy_pbr::mesh_struct + +[[group(1), binding(0)]] +var mesh: Mesh; + +struct Vertex { + [[location(0)]] position: vec3; + [[location(1)]] normal: vec3; + [[location(2)]] uv: vec2; +}; + +struct VertexOutput { + [[builtin(position)]] clip_position: vec4; +}; + +[[stage(vertex)]] +fn vertex(vertex: Vertex) -> VertexOutput { + let world_position = mesh.model * vec4(vertex.position, 1.0); + + var out: VertexOutput; + out.clip_position = view.view_proj * world_position; + return out; +} + +[[stage(fragment)]] +fn fragment() -> [[location(0)]] vec4 { + var color = vec4(0.0, 0.0, 1.0, 1.0); +# ifdef IS_RED + color = vec4(1.0, 0.0, 0.0, 1.0); +# endif + return color; +} diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index 4887bb5917..a14baf2ae2 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -1,4 +1,5 @@ use crate::{CoreStage, Events, Plugin, PluginGroup, PluginGroupBuilder, StartupStage}; +pub use bevy_derive::AppLabel; use bevy_ecs::{ prelude::{FromWorld, IntoExclusiveSystem}, schedule::{ @@ -8,12 +9,14 @@ use bevy_ecs::{ system::Resource, world::World, }; -use bevy_utils::tracing::debug; +use bevy_utils::{tracing::debug, HashMap}; use std::fmt::Debug; #[cfg(feature = "trace")] use bevy_utils::tracing::info_span; +bevy_utils::define_label!(AppLabel); + #[allow(clippy::needless_doctest_main)] /// Containers of app logic and data /// @@ -42,6 +45,12 @@ pub struct App { pub world: World, pub runner: Box, pub schedule: Schedule, + sub_apps: HashMap, SubApp>, +} + +struct SubApp { + app: App, + runner: Box, } impl Default for App { @@ -73,6 +82,7 @@ impl App { world: Default::default(), schedule: Default::default(), runner: Box::new(run_once), + sub_apps: HashMap::default(), } } @@ -85,6 +95,9 @@ impl App { #[cfg(feature = "trace")] let _bevy_frame_update_guard = bevy_frame_update_span.enter(); self.schedule.run(&mut self.world); + for sub_app in self.sub_apps.values_mut() { + (sub_app.runner)(&mut self.world, &mut sub_app.app); + } } /// Starts the application by calling the app's [runner function](Self::set_runner). @@ -823,6 +836,39 @@ impl App { } self } + + pub fn add_sub_app( + &mut self, + label: impl AppLabel, + app: App, + f: impl Fn(&mut World, &mut App) + 'static, + ) -> &mut Self { + self.sub_apps.insert( + Box::new(label), + SubApp { + app, + runner: Box::new(f), + }, + ); + self + } + + /// Retrieves a "sub app" stored inside this [App]. This will panic if the sub app does not exist. + pub fn sub_app(&mut self, label: impl AppLabel) -> &mut App { + match self.get_sub_app(label) { + Ok(app) => app, + Err(label) => panic!("Sub-App with label '{:?}' does not exist", label), + } + } + + /// Retrieves a "sub app" inside this [App] with the given label, if it exists. Otherwise returns + /// an [Err] containing the given label. + pub fn get_sub_app(&mut self, label: impl AppLabel) -> Result<&mut App, impl AppLabel> { + self.sub_apps + .get_mut((&label) as &dyn AppLabel) + .map(|sub_app| &mut sub_app.app) + .ok_or(label) + } } fn run_once(mut app: App) { diff --git a/crates/bevy_app/src/ci_testing.rs b/crates/bevy_app/src/ci_testing.rs index 5c8bc87960..a61ee728dd 100644 --- a/crates/bevy_app/src/ci_testing.rs +++ b/crates/bevy_app/src/ci_testing.rs @@ -22,16 +22,15 @@ fn ci_testing_exit_after( *current_frame += 1; } -pub(crate) fn setup_app(app_builder: &mut App) -> &mut App { +pub(crate) fn setup_app(app: &mut App) -> &mut App { let filename = std::env::var("CI_TESTING_CONFIG").unwrap_or_else(|_| "ci_testing_config.ron".to_string()); let config: CiTestingConfig = ron::from_str( &std::fs::read_to_string(filename).expect("error reading CI testing configuration file"), ) .expect("error deserializing CI testing configuration file"); - app_builder - .insert_resource(config) + app.insert_resource(config) .add_system(ci_testing_exit_after); - app_builder + app } diff --git a/crates/bevy_asset/src/handle.rs b/crates/bevy_asset/src/handle.rs index 1a606c1a62..c140f6247e 100644 --- a/crates/bevy_asset/src/handle.rs +++ b/crates/bevy_asset/src/handle.rs @@ -96,6 +96,7 @@ impl Handle { } } + #[inline] pub fn weak(id: HandleId) -> Self { Self { id, @@ -129,6 +130,7 @@ impl Handle { self.handle_type = HandleType::Strong(sender); } + #[inline] pub fn clone_weak(&self) -> Self { Handle::weak(self.id) } diff --git a/crates/bevy_asset/src/loader.rs b/crates/bevy_asset/src/loader.rs index 960208c77b..aa12dbddac 100644 --- a/crates/bevy_asset/src/loader.rs +++ b/crates/bevy_asset/src/loader.rs @@ -43,13 +43,19 @@ impl LoadedAsset { } } - pub fn with_dependency(mut self, asset_path: AssetPath) -> Self { + pub fn add_dependency(&mut self, asset_path: AssetPath) { self.dependencies.push(asset_path.to_owned()); + } + + pub fn with_dependency(mut self, asset_path: AssetPath) -> Self { + self.add_dependency(asset_path); self } - pub fn with_dependencies(mut self, asset_paths: Vec>) -> Self { - self.dependencies.extend(asset_paths); + pub fn with_dependencies(mut self, mut asset_paths: Vec>) -> Self { + for asset_path in asset_paths.drain(..) { + self.add_dependency(asset_path); + } self } } diff --git a/crates/bevy_derive/src/lib.rs b/crates/bevy_derive/src/lib.rs index edbbf6f1ab..9036b1b602 100644 --- a/crates/bevy_derive/src/lib.rs +++ b/crates/bevy_derive/src/lib.rs @@ -9,7 +9,9 @@ mod render_resource; mod render_resources; mod shader_defs; +use bevy_macro_utils::{derive_label, BevyManifest}; use proc_macro::TokenStream; +use quote::format_ident; /// Derives the Bytes trait. Each field must also implements Bytes or this will fail. #[proc_macro_derive(Bytes)] @@ -52,3 +54,11 @@ pub fn bevy_main(attr: TokenStream, item: TokenStream) -> TokenStream { pub fn derive_enum_variant_meta(input: TokenStream) -> TokenStream { enum_variant_meta::derive_enum_variant_meta(input) } + +#[proc_macro_derive(AppLabel)] +pub fn derive_app_label(input: TokenStream) -> TokenStream { + let input = syn::parse_macro_input!(input as syn::DeriveInput); + let mut trait_path = BevyManifest::default().get_path("bevy_app"); + trait_path.segments.push(format_ident!("AppLabel").into()); + derive_label(input, trait_path) +} diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index 5c5df5b7fc..ed6fd79f95 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -2,16 +2,16 @@ extern crate proc_macro; mod component; -use bevy_macro_utils::BevyManifest; +use bevy_macro_utils::{derive_label, BevyManifest}; use proc_macro::TokenStream; -use proc_macro2::{Span, TokenStream as TokenStream2}; +use proc_macro2::Span; use quote::{format_ident, quote}; use syn::{ parse::{Parse, ParseStream}, parse_macro_input, punctuated::Punctuated, token::Comma, - Data, DataStruct, DeriveInput, Field, Fields, GenericParam, Ident, Index, LitInt, Path, Result, + Data, DataStruct, DeriveInput, Field, Fields, GenericParam, Ident, Index, LitInt, Result, Token, }; @@ -429,46 +429,43 @@ pub fn derive_system_param(input: TokenStream) -> TokenStream { #[proc_macro_derive(SystemLabel)] pub fn derive_system_label(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - - derive_label(input, Ident::new("SystemLabel", Span::call_site())).into() + let mut trait_path = bevy_ecs_path(); + trait_path.segments.push(format_ident!("schedule").into()); + trait_path + .segments + .push(format_ident!("SystemLabel").into()); + derive_label(input, trait_path) } #[proc_macro_derive(StageLabel)] pub fn derive_stage_label(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - derive_label(input, Ident::new("StageLabel", Span::call_site())).into() + let mut trait_path = bevy_ecs_path(); + trait_path.segments.push(format_ident!("schedule").into()); + trait_path.segments.push(format_ident!("StageLabel").into()); + derive_label(input, trait_path) } #[proc_macro_derive(AmbiguitySetLabel)] pub fn derive_ambiguity_set_label(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - derive_label(input, Ident::new("AmbiguitySetLabel", Span::call_site())).into() + let mut trait_path = bevy_ecs_path(); + trait_path.segments.push(format_ident!("schedule").into()); + trait_path + .segments + .push(format_ident!("AmbiguitySetLabel").into()); + derive_label(input, trait_path) } #[proc_macro_derive(RunCriteriaLabel)] pub fn derive_run_criteria_label(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - derive_label(input, Ident::new("RunCriteriaLabel", Span::call_site())).into() -} - -fn derive_label(input: DeriveInput, label_type: Ident) -> TokenStream2 { - let ident = input.ident; - let ecs_path: Path = bevy_ecs_path(); - - let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); - let mut where_clause = where_clause.cloned().unwrap_or_else(|| syn::WhereClause { - where_token: Default::default(), - predicates: Default::default(), - }); - where_clause.predicates.push(syn::parse2(quote! { Self: Eq + ::std::fmt::Debug + ::std::hash::Hash + Clone + Send + Sync + 'static }).unwrap()); - - quote! { - impl #impl_generics #ecs_path::schedule::#label_type for #ident #ty_generics #where_clause { - fn dyn_clone(&self) -> Box { - Box::new(Clone::clone(self)) - } - } - } + let mut trait_path = bevy_ecs_path(); + trait_path.segments.push(format_ident!("schedule").into()); + trait_path + .segments + .push(format_ident!("RunCriteriaLabel").into()); + derive_label(input, trait_path) } pub(crate) fn bevy_ecs_path() -> syn::Path { diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 6fb347a2a7..6020a9d68e 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -38,6 +38,8 @@ pub mod prelude { }; } +pub use bevy_ecs_macros::all_tuples; + #[cfg(test)] mod tests { use crate as bevy_ecs; diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 9572a797f1..215b151790 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -45,6 +45,8 @@ pub trait WorldQuery { type State: FetchState; } +pub type QueryItem<'w, 's, Q> = <::Fetch as Fetch<'w, 's>>::Item; + pub trait Fetch<'world, 'state>: Sized { type Item; type State: FetchState; diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index 6ecca3b870..3dd9a7c6a7 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -66,7 +66,7 @@ where matched_archetypes: Default::default(), archetype_component_access: Default::default(), }; - state.validate_world_and_update_archetypes(world); + state.update_archetypes(world); state } @@ -87,11 +87,8 @@ where /// # Panics /// /// Panics if the `world.id()` does not equal the current [`QueryState`] internal id. - pub fn validate_world_and_update_archetypes(&mut self, world: &World) { - if world.id() != self.world_id { - panic!("Attempted to use {} with a mismatched World. QueryStates can only be used with the World they were created from.", - std::any::type_name::()); - } + pub fn update_archetypes(&mut self, world: &World) { + self.validate_world(world); let archetypes = world.archetypes(); let new_generation = archetypes.generation(); let old_generation = std::mem::replace(&mut self.archetype_generation, new_generation); @@ -102,6 +99,14 @@ where } } + #[inline] + pub fn validate_world(&self, world: &World) { + if world.id() != self.world_id { + panic!("Attempted to use {} with a mismatched World. QueryStates can only be used with the World they were created from.", + std::any::type_name::()); + } + } + /// Creates a new [`Archetype`]. pub fn new_archetype(&mut self, archetype: &Archetype) { if self.fetch_state.matches_archetype(archetype) @@ -153,6 +158,27 @@ where unsafe { self.get_unchecked(world, entity) } } + #[inline] + pub fn get_manual<'w, 's>( + &'s self, + world: &'w World, + entity: Entity, + ) -> Result<>::Item, QueryEntityError> + where + Q::Fetch: ReadOnlyFetch, + { + self.validate_world(world); + // SAFETY: query is read only and world is validated + unsafe { + self.get_unchecked_manual( + world, + entity, + world.last_change_tick(), + world.read_change_tick(), + ) + } + } + /// Gets the query result for the given [`World`] and [`Entity`]. /// /// # Safety @@ -165,7 +191,7 @@ where world: &'w World, entity: Entity, ) -> Result<>::Item, QueryEntityError> { - self.validate_world_and_update_archetypes(world); + self.update_archetypes(world); self.get_unchecked_manual( world, entity, @@ -232,6 +258,28 @@ where unsafe { self.iter_unchecked(world) } } + /// Returns an [`Iterator`] over all possible combinations of `K` query results without repetition. + /// This can only be called for read-only queries. + /// + /// For permutations of size K of query returning N results, you will get: + /// - if K == N: one permutation of all query results + /// - if K < N: all possible K-sized combinations of query results, without repetition + /// - if K > N: empty set (no K-sized combinations exist) + /// + /// This can only be called for read-only queries, see [`Self::iter_combinations_mut`] for + /// write-queries. + #[inline] + pub fn iter_manual<'w, 's>(&'s self, world: &'w World) -> QueryIter<'w, 's, Q, F> + where + Q::Fetch: ReadOnlyFetch, + { + self.validate_world(world); + // SAFETY: query is read only and world is validated + unsafe { + self.iter_unchecked_manual(world, world.last_change_tick(), world.read_change_tick()) + } + } + /// Returns an [`Iterator`] over all possible combinations of `K` query results without repetition. /// This can only be called for read-only queries. /// @@ -281,7 +329,7 @@ where &'s mut self, world: &'w World, ) -> QueryIter<'w, 's, Q, F> { - self.validate_world_and_update_archetypes(world); + self.update_archetypes(world); self.iter_unchecked_manual(world, world.last_change_tick(), world.read_change_tick()) } @@ -298,7 +346,7 @@ where &'s mut self, world: &'w World, ) -> QueryCombinationIter<'w, 's, Q, F, K> { - self.validate_world_and_update_archetypes(world); + self.update_archetypes(world); self.iter_combinations_unchecked_manual( world, world.last_change_tick(), @@ -392,7 +440,7 @@ where world: &'w World, func: impl FnMut(>::Item), ) { - self.validate_world_and_update_archetypes(world); + self.update_archetypes(world); self.for_each_unchecked_manual( world, func, @@ -452,7 +500,7 @@ where batch_size: usize, func: impl Fn(>::Item) + Send + Sync + Clone, ) { - self.validate_world_and_update_archetypes(world); + self.update_archetypes(world); self.par_for_each_unchecked_manual( world, task_pool, diff --git a/crates/bevy_ecs/src/schedule/label.rs b/crates/bevy_ecs/src/schedule/label.rs index a7f3538741..5235dc5837 100644 --- a/crates/bevy_ecs/src/schedule/label.rs +++ b/crates/bevy_ecs/src/schedule/label.rs @@ -1,115 +1,12 @@ pub use bevy_ecs_macros::{AmbiguitySetLabel, RunCriteriaLabel, StageLabel, SystemLabel}; +use bevy_utils::define_label; -use std::{ - any::Any, - borrow::Cow, - fmt::Debug, - hash::{Hash, Hasher}, -}; +define_label!(StageLabel); +define_label!(SystemLabel); +define_label!(AmbiguitySetLabel); +define_label!(RunCriteriaLabel); -pub trait DynEq: Any { - fn as_any(&self) -> &dyn Any; - - fn dyn_eq(&self, other: &dyn DynEq) -> bool; -} - -impl DynEq for T -where - T: Any + Eq, -{ - fn as_any(&self) -> &dyn Any { - self - } - - fn dyn_eq(&self, other: &dyn DynEq) -> bool { - if let Some(other) = other.as_any().downcast_ref::() { - return self == other; - } - false - } -} - -pub trait DynHash: DynEq { - fn as_dyn_eq(&self) -> &dyn DynEq; - - fn dyn_hash(&self, state: &mut dyn Hasher); -} - -impl DynHash for T -where - T: DynEq + Hash, -{ - fn as_dyn_eq(&self) -> &dyn DynEq { - self - } - - fn dyn_hash(&self, mut state: &mut dyn Hasher) { - T::hash(self, &mut state); - self.type_id().hash(&mut state); - } -} - -pub trait StageLabel: DynHash + Debug + Send + Sync + 'static { - #[doc(hidden)] - fn dyn_clone(&self) -> Box; -} pub(crate) type BoxedStageLabel = Box; - -pub trait SystemLabel: DynHash + Debug + Send + Sync + 'static { - #[doc(hidden)] - fn dyn_clone(&self) -> Box; -} pub(crate) type BoxedSystemLabel = Box; - -pub trait AmbiguitySetLabel: DynHash + Debug + Send + Sync + 'static { - #[doc(hidden)] - fn dyn_clone(&self) -> Box; -} pub(crate) type BoxedAmbiguitySetLabel = Box; - -pub trait RunCriteriaLabel: DynHash + Debug + Send + Sync + 'static { - #[doc(hidden)] - fn dyn_clone(&self) -> Box; -} pub(crate) type BoxedRunCriteriaLabel = Box; - -macro_rules! impl_label { - ($trait_name:ident) => { - impl PartialEq for dyn $trait_name { - fn eq(&self, other: &Self) -> bool { - self.dyn_eq(other.as_dyn_eq()) - } - } - - impl Eq for dyn $trait_name {} - - impl Hash for dyn $trait_name { - fn hash(&self, state: &mut H) { - self.dyn_hash(state); - } - } - - impl Clone for Box { - fn clone(&self) -> Self { - self.dyn_clone() - } - } - - impl $trait_name for Cow<'static, str> { - fn dyn_clone(&self) -> Box { - Box::new(self.clone()) - } - } - - impl $trait_name for &'static str { - fn dyn_clone(&self) -> Box { - Box::new(<&str>::clone(self)) - } - } - }; -} - -impl_label!(StageLabel); -impl_label!(SystemLabel); -impl_label!(AmbiguitySetLabel); -impl_label!(RunCriteriaLabel); diff --git a/crates/bevy_ecs/src/schedule/stage.rs b/crates/bevy_ecs/src/schedule/stage.rs index 11cf3ea0c0..39880a3035 100644 --- a/crates/bevy_ecs/src/schedule/stage.rs +++ b/crates/bevy_ecs/src/schedule/stage.rs @@ -84,6 +84,8 @@ pub struct SystemStage { uninitialized_parallel: Vec, /// Saves the value of the World change_tick during the last tick check last_tick_check: u32, + /// If true, buffers will be automatically applied at the end of the stage. If false, buffers must be manually applied. + apply_buffers: bool, } impl SystemStage { @@ -105,6 +107,7 @@ impl SystemStage { uninitialized_before_commands: vec![], uninitialized_at_end: vec![], last_tick_check: Default::default(), + apply_buffers: true, } } @@ -204,6 +207,21 @@ impl SystemStage { } } + pub fn apply_buffers(&mut self, world: &mut World) { + for container in self.parallel.iter_mut() { + let system = container.system_mut(); + #[cfg(feature = "trace")] + let span = bevy_utils::tracing::info_span!("system_commands", name = &*system.name()); + #[cfg(feature = "trace")] + let _guard = span.enter(); + system.apply_buffers(world); + } + } + + pub fn set_apply_buffers(&mut self, apply_buffers: bool) { + self.apply_buffers = apply_buffers; + } + /// Topologically sorted parallel systems. /// /// Note that systems won't be fully-formed until the stage has been run at least once. @@ -832,9 +850,11 @@ impl Stage for SystemStage { } // Apply parallel systems' buffers. - for container in &mut self.parallel { - if container.should_run { - container.system_mut().apply_buffers(world); + if self.apply_buffers { + for container in &mut self.parallel { + if container.should_run { + container.system_mut().apply_buffers(world); + } } } diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index 90b36671e8..15f5b8a952 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -4,7 +4,7 @@ use crate::{ query::{Access, FilteredAccessSet}, system::{ check_system_change_tick, ReadOnlySystemParamFetch, System, SystemParam, SystemParamFetch, - SystemParamState, + SystemParamItem, SystemParamState, }, world::{World, WorldId}, }; @@ -46,6 +46,11 @@ impl SystemMeta { pub fn set_non_send(&mut self) { self.is_send = false; } + + #[inline] + pub(crate) fn check_change_tick(&mut self, change_tick: u32) { + check_system_change_tick(&mut self.last_change_tick, change_tick, self.name.as_ref()); + } } // TODO: Actually use this in FunctionSystem. We should probably only do this once Systems are constructed using a World reference @@ -121,6 +126,10 @@ impl SystemState { self.world_id == world.id() } + pub(crate) fn new_archetype(&mut self, archetype: &Archetype) { + self.param_state.new_archetype(archetype, &mut self.meta); + } + fn validate_world_and_update_archetypes(&mut self, world: &World) { assert!(self.matches_world(world), "Encountered a mismatched World. A SystemState cannot be used with Worlds other than the one it was created with."); let archetypes = world.archetypes(); @@ -159,6 +168,74 @@ impl SystemState { } } +/// A trait for defining systems with a [`SystemParam`] associated type. +/// +/// This facilitates the creation of systems that are generic over some trait +/// and that use that trait's associated types as `SystemParam`s. +pub trait RunSystem: Send + Sync + 'static { + /// The `SystemParam` type passed to the system when it runs. + type Param: SystemParam; + + /// Runs the system. + fn run(param: SystemParamItem); + + /// Creates a concrete instance of the system for the specified `World`. + fn system(world: &mut World) -> ParamSystem { + ParamSystem { + run: Self::run, + state: SystemState::new(world), + } + } +} + +pub struct ParamSystem { + state: SystemState

, + run: fn(SystemParamItem

), +} + +impl System for ParamSystem

{ + type In = (); + + type Out = (); + + fn name(&self) -> Cow<'static, str> { + self.state.meta().name.clone() + } + + fn new_archetype(&mut self, archetype: &Archetype) { + self.state.new_archetype(archetype); + } + + fn component_access(&self) -> &Access { + self.state.meta().component_access_set.combined_access() + } + + fn archetype_component_access(&self) -> &Access { + &self.state.meta().archetype_component_access + } + + fn is_send(&self) -> bool { + self.state.meta().is_send() + } + + unsafe fn run_unsafe(&mut self, _input: Self::In, world: &World) -> Self::Out { + let param = self.state.get_unchecked_manual(world); + (self.run)(param); + } + + fn apply_buffers(&mut self, world: &mut World) { + self.state.apply(world); + } + + fn initialize(&mut self, _world: &mut World) { + // already initialized by nature of the SystemState being constructed + } + + fn check_change_tick(&mut self, change_tick: u32) { + self.state.meta.check_change_tick(change_tick); + } +} + /// Conversion trait to turn something into a [`System`]. /// /// Use this to get a system from a function. Also note that every system implements this trait as diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index b544ed5a6c..af52e24d7b 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -48,6 +48,8 @@ pub trait SystemParam: Sized { type Fetch: for<'w, 's> SystemParamFetch<'w, 's>; } +pub type SystemParamItem<'w, 's, P> = <

::Fetch as SystemParamFetch<'w, 's>>::Item; + /// The state of a [`SystemParam`]. /// /// # Safety @@ -1230,3 +1232,12 @@ macro_rules! impl_system_param_tuple { } all_tuples!(impl_system_param_tuple, 0, 16, P); + +pub mod lifetimeless { + pub type SQuery = super::Query<'static, 'static, Q, F>; + pub type Read = &'static T; + pub type Write = &'static mut T; + pub type SRes = super::Res<'static, T>; + pub type SResMut = super::ResMut<'static, T>; + pub type SCommands = crate::system::Commands<'static, 'static>; +} diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 1a052cdc88..9f06295817 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -11,17 +11,17 @@ categories = ["game-engines", "graphics", "gui", "rendering"] [features] wgpu_trace = ["bevy_wgpu/trace"] -trace = [ "bevy_app/trace", "bevy_ecs/trace" ] +trace = [ "bevy_app/trace", "bevy_ecs/trace", "bevy_render2/trace" ] trace_chrome = [ "bevy_log/tracing-chrome" ] trace_tracy = [ "bevy_log/tracing-tracy" ] # Image format support for texture loading (PNG and HDR are enabled by default) -hdr = ["bevy_render/hdr"] -png = ["bevy_render/png"] -dds = ["bevy_render/dds"] -tga = ["bevy_render/tga"] -jpeg = ["bevy_render/jpeg"] -bmp = ["bevy_render/bmp"] +hdr = ["bevy_render/hdr", "bevy_render2/hdr" ] +png = ["bevy_render/png", "bevy_render2/png" ] +dds = ["bevy_render/dds", "bevy_render2/dds" ] +tga = ["bevy_render/tga", "bevy_render2/tga" ] +jpeg = ["bevy_render/jpeg", "bevy_render2/jpeg" ] +bmp = ["bevy_render/bmp", "bevy_render2/bmp" ] # Audio format support (MP3 is enabled by default) flac = ["bevy_audio/flac"] @@ -63,11 +63,16 @@ bevy_window = { path = "../bevy_window", version = "0.5.0" } bevy_tasks = { path = "../bevy_tasks", version = "0.5.0" } # bevy (optional) bevy_audio = { path = "../bevy_audio", optional = true, version = "0.5.0" } +bevy_core_pipeline = { path = "../../pipelined/bevy_core_pipeline", optional = true, version = "0.5.0" } bevy_gltf = { path = "../bevy_gltf", optional = true, version = "0.5.0" } +bevy_gltf2 = { path = "../../pipelined/bevy_gltf2", optional = true, version = "0.5.0" } bevy_pbr = { path = "../bevy_pbr", optional = true, version = "0.5.0" } +bevy_pbr2 = { path = "../../pipelined/bevy_pbr2", optional = true, version = "0.5.0" } bevy_render = { path = "../bevy_render", optional = true, version = "0.5.0" } +bevy_render2 = { path = "../../pipelined/bevy_render2", optional = true, version = "0.5.0" } bevy_dynamic_plugin = { path = "../bevy_dynamic_plugin", optional = true, version = "0.5.0" } bevy_sprite = { path = "../bevy_sprite", optional = true, version = "0.5.0" } +bevy_sprite2 = { path = "../../pipelined/bevy_sprite2", optional = true, version = "0.5.0" } bevy_text = { path = "../bevy_text", optional = true, version = "0.5.0" } bevy_ui = { path = "../bevy_ui", optional = true, version = "0.5.0" } bevy_wgpu = { path = "../bevy_wgpu", optional = true, version = "0.5.0" } diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index 0cc8f49ca2..733fd2d419 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -109,3 +109,40 @@ impl PluginGroup for MinimalPlugins { group.add(ScheduleRunnerPlugin::default()); } } + +pub struct PipelinedDefaultPlugins; + +impl PluginGroup for PipelinedDefaultPlugins { + fn build(&mut self, group: &mut PluginGroupBuilder) { + group.add(bevy_log::LogPlugin::default()); + group.add(bevy_core::CorePlugin::default()); + group.add(bevy_transform::TransformPlugin::default()); + group.add(bevy_diagnostic::DiagnosticsPlugin::default()); + group.add(bevy_input::InputPlugin::default()); + group.add(bevy_window::WindowPlugin::default()); + group.add(bevy_asset::AssetPlugin::default()); + group.add(bevy_scene::ScenePlugin::default()); + + #[cfg(feature = "bevy_winit")] + group.add(bevy_winit::WinitPlugin::default()); + + #[cfg(feature = "bevy_render2")] + { + group.add(bevy_render2::RenderPlugin::default()); + } + + #[cfg(feature = "bevy_core_pipeline")] + { + group.add(bevy_core_pipeline::CorePipelinePlugin::default()); + + #[cfg(feature = "bevy_sprite2")] + group.add(bevy_sprite2::SpritePlugin::default()); + + #[cfg(feature = "bevy_pbr2")] + group.add(bevy_pbr2::PbrPlugin::default()); + + #[cfg(feature = "bevy_gltf2")] + group.add(bevy_gltf2::GltfPlugin::default()); + } + } +} diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index 87dc85dd3c..932971b6f1 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -82,6 +82,12 @@ pub mod audio { pub use bevy_audio::*; } +#[cfg(feature = "bevy_core_pipeline")] +pub mod core_pipeline { + //! Core render pipeline. + pub use bevy_core_pipeline::*; +} + #[cfg(feature = "bevy_gilrs")] pub mod gilrs { pub use bevy_gilrs::*; @@ -93,24 +99,48 @@ pub mod gltf { pub use bevy_gltf::*; } +#[cfg(feature = "bevy_gltf2")] +pub mod gltf2 { + //! Support for GLTF file loading. + pub use bevy_gltf2::*; +} + #[cfg(feature = "bevy_pbr")] pub mod pbr { //! Physically based rendering. pub use bevy_pbr::*; } +#[cfg(feature = "bevy_pbr2")] +pub mod pbr2 { + //! Physically based rendering. + pub use bevy_pbr2::*; +} + #[cfg(feature = "bevy_render")] pub mod render { //! Cameras, meshes, textures, shaders, and pipelines. pub use bevy_render::*; } +#[cfg(feature = "bevy_render2")] +pub mod render2 { + //! Cameras, meshes, textures, shaders, and pipelines. + pub use bevy_render2::*; +} + #[cfg(feature = "bevy_sprite")] pub mod sprite { //! Items for sprites, rects, texture atlases, etc. pub use bevy_sprite::*; } +#[cfg(feature = "bevy_sprite2")] +pub mod sprite2 { + //! Items for sprites, rects, texture atlases, etc. + pub use bevy_sprite2::*; +} + #[cfg(feature = "bevy_text")] pub mod text { //! Text drawing, styling, and font assets. diff --git a/crates/bevy_log/Cargo.toml b/crates/bevy_log/Cargo.toml index 4b8027adb9..d7f45cf6ec 100644 --- a/crates/bevy_log/Cargo.toml +++ b/crates/bevy_log/Cargo.toml @@ -15,6 +15,7 @@ bevy_utils = { path = "../bevy_utils", version = "0.5.0" } tracing-subscriber = {version = "0.3.1", features = ["registry", "env-filter"]} tracing-chrome = { version = "0.4.0", optional = true } tracing-tracy = { version = "0.8.0", optional = true } +tracing-log = "0.1.2" [target.'cfg(target_os = "android")'.dependencies] android_log-sys = "0.2.0" diff --git a/crates/bevy_log/src/lib.rs b/crates/bevy_log/src/lib.rs index b4c1760b5c..c769a652ec 100644 --- a/crates/bevy_log/src/lib.rs +++ b/crates/bevy_log/src/lib.rs @@ -13,6 +13,7 @@ pub use bevy_utils::tracing::{ }; use bevy_app::{App, Plugin}; +use tracing_log::LogTracer; #[cfg(feature = "tracing-chrome")] use tracing_subscriber::fmt::{format::DefaultFields, FormattedFields}; use tracing_subscriber::{prelude::*, registry::Registry, EnvFilter}; @@ -88,7 +89,7 @@ impl Plugin for LogPlugin { let settings = app.world.get_resource_or_insert_with(LogSettings::default); format!("{},{}", settings.level, settings.filter) }; - + LogTracer::init().unwrap(); let filter_layer = EnvFilter::try_from_default_env() .or_else(|_| EnvFilter::try_new(&default_filter)) .unwrap(); diff --git a/crates/bevy_macro_utils/Cargo.toml b/crates/bevy_macro_utils/Cargo.toml index 2845405e2f..f541e2555b 100644 --- a/crates/bevy_macro_utils/Cargo.toml +++ b/crates/bevy_macro_utils/Cargo.toml @@ -11,3 +11,4 @@ keywords = ["bevy"] [dependencies] cargo-manifest = "0.2.6" syn = "1.0" +quote = "1.0" diff --git a/crates/bevy_macro_utils/src/lib.rs b/crates/bevy_macro_utils/src/lib.rs index 98e72e6a89..0dd6a57037 100644 --- a/crates/bevy_macro_utils/src/lib.rs +++ b/crates/bevy_macro_utils/src/lib.rs @@ -8,6 +8,7 @@ pub use symbol::*; use cargo_manifest::{DepsSet, Manifest}; use proc_macro::TokenStream; +use quote::quote; use std::{env, path::PathBuf}; pub struct BevyManifest { @@ -65,3 +66,29 @@ fn get_path(path: &str) -> syn::Path { fn parse_str(path: &str) -> T { syn::parse(path.parse::().unwrap()).unwrap() } + +/// Derive a label trait +/// +/// # Args +/// +/// - `input`: The [`syn::DeriveInput`] for struct that is deriving the label trait +/// - `trait_path`: The path [`syn::Path`] to the label trait +pub fn derive_label(input: syn::DeriveInput, trait_path: syn::Path) -> TokenStream { + let ident = input.ident; + + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + let mut where_clause = where_clause.cloned().unwrap_or_else(|| syn::WhereClause { + where_token: Default::default(), + predicates: Default::default(), + }); + where_clause.predicates.push(syn::parse2(quote! { Self: Eq + ::std::fmt::Debug + ::std::hash::Hash + Clone + Send + Sync + 'static }).unwrap()); + + (quote! { + impl #impl_generics #trait_path for #ident #ty_generics #where_clause { + fn dyn_clone(&self) -> Box { + Box::new(Clone::clone(self)) + } + } + }) + .into() +} diff --git a/crates/bevy_math/Cargo.toml b/crates/bevy_math/Cargo.toml index f998039ba1..f3cc0b02d3 100644 --- a/crates/bevy_math/Cargo.toml +++ b/crates/bevy_math/Cargo.toml @@ -9,5 +9,5 @@ license = "MIT OR Apache-2.0" keywords = ["bevy"] [dependencies] -glam = { version = "0.18.0", features = ["serde", "bytemuck"] } +glam = { version = "0.20.0", features = ["serde", "bytemuck"] } bevy_reflect = { path = "../bevy_reflect", version = "0.5.0", features = ["bevy"] } diff --git a/crates/bevy_reflect/Cargo.toml b/crates/bevy_reflect/Cargo.toml index 2e9af8f073..ab9b8cbd23 100644 --- a/crates/bevy_reflect/Cargo.toml +++ b/crates/bevy_reflect/Cargo.toml @@ -24,7 +24,7 @@ parking_lot = "0.11.0" thiserror = "1.0" serde = "1" smallvec = { version = "1.6", features = ["serde", "union", "const_generics"], optional = true } -glam = { version = "0.18.0", features = ["serde"], optional = true } +glam = { version = "0.20.0", features = ["serde"], optional = true } [dev-dependencies] ron = "0.6.2" diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index 6dbad550a9..c3ac5c6167 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -32,7 +32,7 @@ downcast-rs = "1.2.0" thiserror = "1.0" anyhow = "1.0.4" hex = "0.4.2" -hexasphere = "5.0.0" +hexasphere = "6.0.0" parking_lot = "0.11.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/crates/bevy_render/src/render_graph/base.rs b/crates/bevy_render/src/render_graph/base.rs index 160039a8b0..1e526ba8da 100644 --- a/crates/bevy_render/src/render_graph/base.rs +++ b/crates/bevy_render/src/render_graph/base.rs @@ -7,7 +7,7 @@ use crate::{ LoadOp, Operations, PassDescriptor, RenderPassColorAttachment, RenderPassDepthStencilAttachment, TextureAttachment, }, - texture::{Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsage}, + texture::{Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages}, Color, }; use bevy_ecs::{component::Component, reflect::ReflectComponent, world::World}; @@ -126,7 +126,7 @@ pub(crate) fn add_base_graph(config: &BaseRenderGraphConfig, world: &mut World) dimension: TextureDimension::D2, format: TextureFormat::Depth32Float, /* PERF: vulkan docs recommend using 24 * bit depth for better performance */ - usage: TextureUsage::OUTPUT_ATTACHMENT, + usage: TextureUsages::OUTPUT_ATTACHMENT, }, ), ); @@ -220,7 +220,7 @@ pub(crate) fn add_base_graph(config: &BaseRenderGraphConfig, world: &mut World) sample_count: msaa.samples, dimension: TextureDimension::D2, format: TextureFormat::default(), - usage: TextureUsage::OUTPUT_ATTACHMENT, + usage: TextureUsages::OUTPUT_ATTACHMENT, }, ), ); diff --git a/crates/bevy_render/src/render_graph/nodes/window_swapchain_node.rs b/crates/bevy_render/src/render_graph/nodes/window_swapchain_node.rs index 50df2ea883..52c7904e42 100644 --- a/crates/bevy_render/src/render_graph/nodes/window_swapchain_node.rs +++ b/crates/bevy_render/src/render_graph/nodes/window_swapchain_node.rs @@ -52,7 +52,7 @@ impl Node for WindowSwapChainNode { let render_resource_context = render_context.resources_mut(); - // create window swapchain when window is resized or created + // reconfigure surface window is resized or created if self .window_created_event_reader .iter(window_created_events) @@ -62,10 +62,10 @@ impl Node for WindowSwapChainNode { .iter(window_resized_events) .any(|e| e.id == window.id()) { - render_resource_context.create_swap_chain(window); + render_resource_context.configure_surface(window); } - let swap_chain_texture = render_resource_context.next_swap_chain_texture(window); + let swap_chain_texture = render_resource_context.next_surface_frame(window); output.set( WINDOW_TEXTURE, RenderResourceId::Texture(swap_chain_texture), diff --git a/crates/bevy_render/src/renderer/headless_render_resource_context.rs b/crates/bevy_render/src/renderer/headless_render_resource_context.rs index 738c065966..89f6c2e726 100644 --- a/crates/bevy_render/src/renderer/headless_render_resource_context.rs +++ b/crates/bevy_render/src/renderer/headless_render_resource_context.rs @@ -31,15 +31,15 @@ impl HeadlessRenderResourceContext { } impl RenderResourceContext for HeadlessRenderResourceContext { - fn create_swap_chain(&self, _window: &Window) {} + fn configure_surface(&self, _window: &Window) {} - fn next_swap_chain_texture(&self, _window: &Window) -> TextureId { + fn next_surface_frame(&self, _window: &Window) -> TextureId { TextureId::new() } - fn drop_swap_chain_texture(&self, _render_resource: TextureId) {} + fn drop_surface_frame(&self, _render_resource: TextureId) {} - fn drop_all_swap_chain_textures(&self) {} + fn drop_all_surface_frames(&self) {} fn create_sampler(&self, _sampler_descriptor: &SamplerDescriptor) -> SamplerId { SamplerId::new() diff --git a/crates/bevy_render/src/renderer/render_resource_context.rs b/crates/bevy_render/src/renderer/render_resource_context.rs index 9afa958aa8..9d16197e76 100644 --- a/crates/bevy_render/src/renderer/render_resource_context.rs +++ b/crates/bevy_render/src/renderer/render_resource_context.rs @@ -12,10 +12,10 @@ use downcast_rs::{impl_downcast, Downcast}; use std::ops::Range; pub trait RenderResourceContext: Downcast + Send + Sync + 'static { - fn create_swap_chain(&self, window: &Window); - fn next_swap_chain_texture(&self, window: &Window) -> TextureId; - fn drop_swap_chain_texture(&self, resource: TextureId); - fn drop_all_swap_chain_textures(&self); + fn configure_surface(&self, window: &Window); + fn next_surface_frame(&self, window: &Window) -> TextureId; + fn drop_surface_frame(&self, resource: TextureId); + fn drop_all_surface_frames(&self); fn create_sampler(&self, sampler_descriptor: &SamplerDescriptor) -> SamplerId; fn create_texture(&self, texture_descriptor: TextureDescriptor) -> TextureId; fn create_buffer(&self, buffer_info: BufferInfo) -> BufferId; diff --git a/crates/bevy_render/src/texture/image_texture_conversion.rs b/crates/bevy_render/src/texture/image_texture_conversion.rs index dc31be26d5..d355860f26 100644 --- a/crates/bevy_render/src/texture/image_texture_conversion.rs +++ b/crates/bevy_render/src/texture/image_texture_conversion.rs @@ -1,4 +1,3 @@ -use std::convert::TryFrom; use thiserror::Error; use super::{Extent3d, Texture, TextureDimension, TextureFormat}; @@ -87,7 +86,8 @@ impl From for Texture { Vec::with_capacity(width as usize * height as usize * format.pixel_size()); for pixel in image.into_raw().chunks_exact(3) { - // TODO unsafe_get in release builds? + // TODO use the array_chunks method once stabilised + // https://github.com/rust-lang/rust/issues/74985 let r = pixel[0]; let g = pixel[1]; let b = pixel[2]; diff --git a/crates/bevy_render/src/texture/texture_descriptor.rs b/crates/bevy_render/src/texture/texture_descriptor.rs index 7df3bc4070..9f83c86457 100644 --- a/crates/bevy_render/src/texture/texture_descriptor.rs +++ b/crates/bevy_render/src/texture/texture_descriptor.rs @@ -1,4 +1,4 @@ -use super::{Extent3d, Texture, TextureDimension, TextureFormat, TextureUsage}; +use super::{Extent3d, Texture, TextureDimension, TextureFormat, TextureUsages}; /// Describes a texture #[derive(Debug, Copy, Clone, Eq, PartialEq)] @@ -8,7 +8,7 @@ pub struct TextureDescriptor { pub sample_count: u32, pub dimension: TextureDimension, pub format: TextureFormat, - pub usage: TextureUsage, + pub usage: TextureUsages, } impl From<&Texture> for TextureDescriptor { @@ -19,7 +19,7 @@ impl From<&Texture> for TextureDescriptor { sample_count: 1, dimension: texture.dimension, format: texture.format, - usage: TextureUsage::SAMPLED | TextureUsage::COPY_DST, + usage: TextureUsages::SAMPLED | TextureUsages::COPY_DST, } } } @@ -36,7 +36,7 @@ impl Default for TextureDescriptor { sample_count: 1, dimension: TextureDimension::D2, format: TextureFormat::Rgba8UnormSrgb, - usage: TextureUsage::SAMPLED | TextureUsage::COPY_DST, + usage: TextureUsages::SAMPLED | TextureUsages::COPY_DST, } } } diff --git a/crates/bevy_render/src/texture/texture_dimension.rs b/crates/bevy_render/src/texture/texture_dimension.rs index 5791863591..e1cdda77eb 100644 --- a/crates/bevy_render/src/texture/texture_dimension.rs +++ b/crates/bevy_render/src/texture/texture_dimension.rs @@ -276,7 +276,7 @@ impl Default for TextureFormat { bitflags::bitflags! { #[repr(transparent)] - pub struct TextureUsage: u32 { + pub struct TextureUsages: u32 { const COPY_SRC = 1; const COPY_DST = 2; const SAMPLED = 4; diff --git a/crates/bevy_sprite/src/render/sprite_sheet.vert b/crates/bevy_sprite/src/render/sprite_sheet.vert index 62ecf33237..8380a12455 100644 --- a/crates/bevy_sprite/src/render/sprite_sheet.vert +++ b/crates/bevy_sprite/src/render/sprite_sheet.vert @@ -21,7 +21,7 @@ struct Rect { vec2 end; }; -layout(set = 1, binding = 1) buffer TextureAtlas_textures { +layout(set = 1, binding = 1) readonly buffer TextureAtlas_textures { Rect[] Textures; }; diff --git a/crates/bevy_sprite/src/texture_atlas_builder.rs b/crates/bevy_sprite/src/texture_atlas_builder.rs index c09b819037..e26ac27c87 100644 --- a/crates/bevy_sprite/src/texture_atlas_builder.rs +++ b/crates/bevy_sprite/src/texture_atlas_builder.rs @@ -168,10 +168,11 @@ impl TextureAtlasBuilder { &contains_smallest_box, ) { Ok(rect_placements) => { - atlas_texture = Texture::new_fill( - Extent3d::new(current_width, current_height, 1), + let size = Extent3d::new(current_width, current_height, 1); + atlas_texture = Texture::new( + size, TextureDimension::D2, - &[0, 0, 0, 0], + vec![0; self.format.pixel_size() * size.volume()], self.format, ); Some(rect_placements) diff --git a/crates/bevy_transform/src/components/global_transform.rs b/crates/bevy_transform/src/components/global_transform.rs index 930f23a48a..bf01169353 100644 --- a/crates/bevy_transform/src/components/global_transform.rs +++ b/crates/bevy_transform/src/components/global_transform.rs @@ -190,7 +190,7 @@ impl GlobalTransform { #[doc(hidden)] #[inline] pub fn rotate(&mut self, rotation: Quat) { - self.rotation *= rotation; + self.rotation = rotation * self.rotation; } /// Multiplies `self` with `transform` component by component, returning the diff --git a/crates/bevy_transform/src/components/transform.rs b/crates/bevy_transform/src/components/transform.rs index cea0fc32cb..e97beaea20 100644 --- a/crates/bevy_transform/src/components/transform.rs +++ b/crates/bevy_transform/src/components/transform.rs @@ -202,7 +202,7 @@ impl Transform { /// Rotates the transform by the given rotation. #[inline] pub fn rotate(&mut self, rotation: Quat) { - self.rotation *= rotation; + self.rotation = rotation * self.rotation; } /// Multiplies `self` with `transform` component by component, returning the diff --git a/crates/bevy_utils/src/label.rs b/crates/bevy_utils/src/label.rs new file mode 100644 index 0000000000..ed759f6a59 --- /dev/null +++ b/crates/bevy_utils/src/label.rs @@ -0,0 +1,100 @@ +//! Traits used by label implementations + +use std::{ + any::Any, + hash::{Hash, Hasher}, +}; + +pub trait DynEq: Any { + fn as_any(&self) -> &dyn Any; + + fn dyn_eq(&self, other: &dyn DynEq) -> bool; +} + +impl DynEq for T +where + T: Any + Eq, +{ + fn as_any(&self) -> &dyn Any { + self + } + + fn dyn_eq(&self, other: &dyn DynEq) -> bool { + if let Some(other) = other.as_any().downcast_ref::() { + return self == other; + } + false + } +} + +pub trait DynHash: DynEq { + fn as_dyn_eq(&self) -> &dyn DynEq; + + fn dyn_hash(&self, state: &mut dyn Hasher); +} + +impl DynHash for T +where + T: DynEq + Hash, +{ + fn as_dyn_eq(&self) -> &dyn DynEq { + self + } + + fn dyn_hash(&self, mut state: &mut dyn Hasher) { + T::hash(self, &mut state); + self.type_id().hash(&mut state); + } +} + +/// Macro to define a new label trait +/// +/// # Example +/// +/// ``` +/// # use bevy_utils::define_label; +/// define_label!(MyNewLabelTrait); +/// ``` +#[macro_export] +macro_rules! define_label { + ($label_trait_name:ident) => { + pub trait $label_trait_name: + ::bevy_utils::label::DynHash + ::std::fmt::Debug + Send + Sync + 'static + { + #[doc(hidden)] + fn dyn_clone(&self) -> Box; + } + + impl PartialEq for dyn $label_trait_name { + fn eq(&self, other: &Self) -> bool { + self.dyn_eq(other.as_dyn_eq()) + } + } + + impl Eq for dyn $label_trait_name {} + + impl ::std::hash::Hash for dyn $label_trait_name { + fn hash(&self, state: &mut H) { + self.dyn_hash(state); + } + } + + impl Clone for Box { + fn clone(&self) -> Self { + self.dyn_clone() + } + } + + impl $label_trait_name for ::std::borrow::Cow<'static, str> { + fn dyn_clone(&self) -> Box { + Box::new(self.clone()) + } + } + + impl $label_trait_name for &'static str { + fn dyn_clone(&self) -> Box { + Box::new(<&str>::clone(self)) + } + } + }; +} diff --git a/crates/bevy_utils/src/lib.rs b/crates/bevy_utils/src/lib.rs index 4b810f0a21..175152e84f 100644 --- a/crates/bevy_utils/src/lib.rs +++ b/crates/bevy_utils/src/lib.rs @@ -1,7 +1,8 @@ mod enum_variant_meta; -pub use enum_variant_meta::*; +pub mod label; pub use ahash::AHasher; +pub use enum_variant_meta::*; pub use instant::{Duration, Instant}; pub use tracing; pub use uuid::Uuid; diff --git a/crates/bevy_wgpu/Cargo.toml b/crates/bevy_wgpu/Cargo.toml index ad9dc72019..e1bc40f8d2 100644 --- a/crates/bevy_wgpu/Cargo.toml +++ b/crates/bevy_wgpu/Cargo.toml @@ -25,7 +25,7 @@ bevy_winit = { path = "../bevy_winit", optional = true, version = "0.5.0" } bevy_utils = { path = "../bevy_utils", version = "0.5.0" } # other -wgpu = "0.9" +wgpu = { version = "0.11.0", features = ["spirv"] } futures-lite = "1.4.0" crossbeam-channel = "0.5.0" crossbeam-utils = "0.8.1" diff --git a/crates/bevy_wgpu/src/diagnostic/wgpu_resource_diagnostics_plugin.rs b/crates/bevy_wgpu/src/diagnostic/wgpu_resource_diagnostics_plugin.rs index b696b7bbc0..d4eb37a0af 100644 --- a/crates/bevy_wgpu/src/diagnostic/wgpu_resource_diagnostics_plugin.rs +++ b/crates/bevy_wgpu/src/diagnostic/wgpu_resource_diagnostics_plugin.rs @@ -29,9 +29,7 @@ impl WgpuResourceDiagnosticsPlugin { DiagnosticId::from_u128(305855369913076220671125671543184691267); pub const SHADER_MODULES: DiagnosticId = DiagnosticId::from_u128(287681470908132753275843248383768232237); - pub const SWAP_CHAINS: DiagnosticId = - DiagnosticId::from_u128(199253035828743332241465305105689014605); - pub const SWAP_CHAIN_OUTPUTS: DiagnosticId = + pub const SURFACE_FRAMES: DiagnosticId = DiagnosticId::from_u128(112048874168736161226721327099863374234); pub const TEXTURES: DiagnosticId = DiagnosticId::from_u128(305955424195390184883220102469231911115); @@ -47,10 +45,8 @@ impl WgpuResourceDiagnosticsPlugin { 10, )); - diagnostics.add(Diagnostic::new(Self::SWAP_CHAINS, "swap_chains", 10)); - diagnostics.add(Diagnostic::new( - Self::SWAP_CHAIN_OUTPUTS, + Self::SURFACE_FRAMES, "swap_chain_outputs", 10, )); @@ -99,19 +95,10 @@ impl WgpuResourceDiagnosticsPlugin { ); diagnostics.add_measurement( - Self::SWAP_CHAINS, + Self::SURFACE_FRAMES, render_resource_context .resources - .window_swap_chains - .read() - .len() as f64, - ); - - diagnostics.add_measurement( - Self::SWAP_CHAIN_OUTPUTS, - render_resource_context - .resources - .swap_chain_frames + .surface_textures .read() .len() as f64, ); diff --git a/crates/bevy_wgpu/src/lib.rs b/crates/bevy_wgpu/src/lib.rs index 5722f649d1..b5c730735e 100644 --- a/crates/bevy_wgpu/src/lib.rs +++ b/crates/bevy_wgpu/src/lib.rs @@ -26,15 +26,12 @@ pub enum WgpuFeature { TimestampQuery, PipelineStatisticsQuery, MappablePrimaryBuffers, - SampledTextureBindingArray, - SampledTextureArrayDynamicIndexing, - SampledTextureArrayNonUniformIndexing, UnsizedBindingArray, MultiDrawIndirect, MultiDrawIndirectCount, PushConstants, AddressModeClampToBorder, - NonFillPolygonMode, + PolygonModeLine, TextureCompressionEtc2, TextureCompressionAstcLdr, TextureAdapterSpecificFormatFeatures, @@ -67,6 +64,8 @@ pub struct WgpuLimits { pub max_vertex_buffers: u32, pub max_vertex_attributes: u32, pub max_vertex_buffer_array_stride: u32, + pub min_storage_buffer_offset_alignment: u32, + pub min_uniform_buffer_offset_alignment: u32, } impl Default for WgpuLimits { @@ -93,6 +92,8 @@ impl Default for WgpuLimits { max_vertex_buffers: default.max_vertex_buffers, max_vertex_attributes: default.max_vertex_attributes, max_vertex_buffer_array_stride: default.max_vertex_buffer_array_stride, + min_storage_buffer_offset_alignment: default.min_storage_buffer_offset_alignment, + min_uniform_buffer_offset_alignment: default.min_uniform_buffer_offset_alignment, } } } diff --git a/crates/bevy_wgpu/src/renderer/wgpu_render_context.rs b/crates/bevy_wgpu/src/renderer/wgpu_render_context.rs index d2aba29fa7..65f09b5fb1 100644 --- a/crates/bevy_wgpu/src/renderer/wgpu_render_context.rs +++ b/crates/bevy_wgpu/src/renderer/wgpu_render_context.rs @@ -231,8 +231,15 @@ fn get_texture_view<'a>( panic!("Color attachment {} does not exist.", name); } }, - TextureAttachment::Id(render_resource) => refs.textures.get(render_resource).unwrap_or_else(|| &refs.swap_chain_frames.get(render_resource).unwrap().output.view), - TextureAttachment::Input(_) => panic!("Encountered unset `TextureAttachment::Input`. The `RenderGraph` executor should always set `TextureAttachment::Inputs` to `TextureAttachment::RenderResource` before running. This is a bug, please report it!"), + TextureAttachment::Id(render_resource) => refs + .textures + .get(render_resource) + .unwrap_or_else(|| &refs.surface_textures.get(render_resource).unwrap().0), + TextureAttachment::Input(_) => panic!( + "Encountered unset `TextureAttachment::Input`. The `RenderGraph` executor should \ + always set `TextureAttachment::Inputs` to `TextureAttachment::RenderResource` before \ + running. This is a bug, please report it!" + ), } } diff --git a/crates/bevy_wgpu/src/renderer/wgpu_render_resource_context.rs b/crates/bevy_wgpu/src/renderer/wgpu_render_resource_context.rs index 8e53d3c292..3031e7ca6e 100644 --- a/crates/bevy_wgpu/src/renderer/wgpu_render_resource_context.rs +++ b/crates/bevy_wgpu/src/renderer/wgpu_render_resource_context.rs @@ -31,7 +31,8 @@ pub struct WgpuRenderResourceContext { } pub const COPY_BYTES_PER_ROW_ALIGNMENT: usize = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize; -pub const BIND_BUFFER_ALIGNMENT: usize = wgpu::BIND_BUFFER_ALIGNMENT as usize; +// TODO: fix this? +pub const BIND_BUFFER_ALIGNMENT: usize = 256; pub const COPY_BUFFER_ALIGNMENT: usize = wgpu::COPY_BUFFER_ALIGNMENT as usize; pub const PUSH_CONSTANT_ALIGNMENT: u32 = wgpu::PUSH_CONSTANT_ALIGNMENT; @@ -94,6 +95,7 @@ impl WgpuRenderResourceContext { y: source_origin[1], z: source_origin[2], }, + aspect: wgpu::TextureAspect::All, }, wgpu::ImageCopyTexture { texture: destination, @@ -103,6 +105,7 @@ impl WgpuRenderResourceContext { y: destination_origin[1], z: destination_origin[2], }, + aspect: wgpu::TextureAspect::All, }, size.wgpu_into(), ) @@ -134,6 +137,7 @@ impl WgpuRenderResourceContext { y: source_origin[1], z: source_origin[2], }, + aspect: wgpu::TextureAspect::All, }, wgpu::ImageCopyBuffer { buffer: destination, @@ -181,6 +185,7 @@ impl WgpuRenderResourceContext { y: destination_origin[1], z: destination_origin[2], }, + aspect: wgpu::TextureAspect::All, }, size.wgpu_into(), ); @@ -206,11 +211,11 @@ impl WgpuRenderResourceContext { let shader_stage = if binding.shader_stage == BindingShaderStage::VERTEX | BindingShaderStage::FRAGMENT { - wgpu::ShaderStage::VERTEX | wgpu::ShaderStage::FRAGMENT + wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT } else if binding.shader_stage == BindingShaderStage::VERTEX { - wgpu::ShaderStage::VERTEX + wgpu::ShaderStages::VERTEX } else if binding.shader_stage == BindingShaderStage::FRAGMENT { - wgpu::ShaderStage::FRAGMENT + wgpu::ShaderStages::FRAGMENT } else { panic!("Invalid binding shader stage.") }; @@ -230,14 +235,15 @@ impl WgpuRenderResourceContext { bind_group_layouts.insert(descriptor.id, bind_group_layout); } - fn try_next_swap_chain_texture(&self, window_id: bevy_window::WindowId) -> Option { - let mut window_swap_chains = self.resources.window_swap_chains.write(); - let mut swap_chain_outputs = self.resources.swap_chain_frames.write(); + fn try_next_surface_frame(&self, window_id: bevy_window::WindowId) -> Option { + let mut window_surfaces = self.resources.window_surfaces.write(); + let mut surface_frames = self.resources.surface_textures.write(); - let window_swap_chain = window_swap_chains.get_mut(&window_id).unwrap(); - let next_texture = window_swap_chain.get_current_frame().ok()?; + let window_surface = window_surfaces.get_mut(&window_id).unwrap(); + let next_texture = window_surface.get_current_texture().ok()?; + let view = next_texture.texture.create_view(&Default::default()); let id = TextureId::new(); - swap_chain_outputs.insert(id, next_texture); + surface_frames.insert(id, (view, next_texture)); Some(id) } } @@ -339,7 +345,6 @@ impl RenderResourceContext for WgpuRenderResourceContext { .create_shader_module(&wgpu::ShaderModuleDescriptor { label: None, source: wgpu::ShaderSource::SpirV(spirv), - flags: Default::default(), }); shader_modules.insert(shader_handle.clone_weak(), shader_module); } @@ -358,43 +363,39 @@ impl RenderResourceContext for WgpuRenderResourceContext { self.create_shader_module_from_source(shader_handle, shader); } - fn create_swap_chain(&self, window: &Window) { + fn configure_surface(&self, window: &Window) { let surfaces = self.resources.window_surfaces.read(); - let mut window_swap_chains = self.resources.window_swap_chains.write(); - let swap_chain_descriptor: wgpu::SwapChainDescriptor = window.wgpu_into(); + let surface_configuration: wgpu::SurfaceConfiguration = window.wgpu_into(); let surface = surfaces .get(&window.id()) .expect("No surface found for window."); - let swap_chain = self - .device - .create_swap_chain(surface, &swap_chain_descriptor); - - window_swap_chains.insert(window.id(), swap_chain); + surface.configure(&self.device, &surface_configuration); } - fn next_swap_chain_texture(&self, window: &bevy_window::Window) -> TextureId { - if let Some(texture_id) = self.try_next_swap_chain_texture(window.id()) { + fn next_surface_frame(&self, window: &bevy_window::Window) -> TextureId { + if let Some(texture_id) = self.try_next_surface_frame(window.id()) { texture_id } else { - self.resources - .window_swap_chains - .write() - .remove(&window.id()); - self.create_swap_chain(window); - self.try_next_swap_chain_texture(window.id()) + self.resources.window_surfaces.write().remove(&window.id()); + self.configure_surface(window); + self.try_next_surface_frame(window.id()) .expect("Failed to acquire next swap chain texture!") } } - fn drop_swap_chain_texture(&self, texture: TextureId) { - let mut swap_chain_outputs = self.resources.swap_chain_frames.write(); - swap_chain_outputs.remove(&texture); + fn drop_surface_frame(&self, texture: TextureId) { + let mut surface_frames = self.resources.surface_textures.write(); + surface_frames.remove(&texture); } - fn drop_all_swap_chain_textures(&self) { - let mut swap_chain_outputs = self.resources.swap_chain_frames.write(); - swap_chain_outputs.clear(); + fn drop_all_surface_frames(&self) { + let mut surface_frames = self.resources.surface_textures.write(); + for (_, (_, texture)) in surface_frames.drain() { + texture.present(); + } + + surface_frames.clear(); } fn set_asset_resource_untyped( diff --git a/crates/bevy_wgpu/src/wgpu_renderer.rs b/crates/bevy_wgpu/src/wgpu_renderer.rs index 4ed697cd33..1dcebadd39 100644 --- a/crates/bevy_wgpu/src/wgpu_renderer.rs +++ b/crates/bevy_wgpu/src/wgpu_renderer.rs @@ -24,13 +24,13 @@ pub struct WgpuRenderer { impl WgpuRenderer { pub async fn new(options: WgpuOptions) -> Self { let backend = match options.backend { - WgpuBackend::Auto => wgpu::BackendBit::PRIMARY, - WgpuBackend::Vulkan => wgpu::BackendBit::VULKAN, - WgpuBackend::Metal => wgpu::BackendBit::METAL, - WgpuBackend::Dx12 => wgpu::BackendBit::DX12, - WgpuBackend::Dx11 => wgpu::BackendBit::DX11, - WgpuBackend::Gl => wgpu::BackendBit::GL, - WgpuBackend::BrowserWgpu => wgpu::BackendBit::BROWSER_WEBGPU, + WgpuBackend::Auto => wgpu::Backends::PRIMARY, + WgpuBackend::Vulkan => wgpu::Backends::VULKAN, + WgpuBackend::Metal => wgpu::Backends::METAL, + WgpuBackend::Dx12 => wgpu::Backends::DX12, + WgpuBackend::Dx11 => wgpu::Backends::DX11, + WgpuBackend::Gl => wgpu::Backends::GL, + WgpuBackend::BrowserWgpu => wgpu::Backends::BROWSER_WEBGPU, }; let instance = wgpu::Instance::new(backend); @@ -42,6 +42,7 @@ impl WgpuRenderer { WgpuPowerOptions::LowPower => wgpu::PowerPreference::LowPower, }, compatible_surface: None, + ..Default::default() }) .await .expect("Unable to find a GPU! Make sure you have installed required drivers!"); @@ -56,12 +57,14 @@ impl WgpuRenderer { #[cfg(not(feature = "trace"))] let trace_path = None; + let adapter_limits = adapter.limits(); + let (device, queue) = adapter .request_device( &wgpu::DeviceDescriptor { label: options.device_label.as_ref().map(|a| a.as_ref()), features: options.features.wgpu_into(), - limits: options.limits.wgpu_into(), + limits: adapter_limits, }, trace_path, ) @@ -129,7 +132,7 @@ impl WgpuRenderer { let render_resource_context = world .get_resource::>() .unwrap(); - render_resource_context.drop_all_swap_chain_textures(); + render_resource_context.drop_all_surface_frames(); render_resource_context.remove_stale_bind_groups(); } } diff --git a/crates/bevy_wgpu/src/wgpu_resources.rs b/crates/bevy_wgpu/src/wgpu_resources.rs index e3e918a97a..a1ec6878ca 100644 --- a/crates/bevy_wgpu/src/wgpu_resources.rs +++ b/crates/bevy_wgpu/src/wgpu_resources.rs @@ -47,7 +47,8 @@ pub struct WgpuBindGroupInfo { pub struct WgpuResourcesReadLock<'a> { pub buffers: RwLockReadGuard<'a, HashMap>>, pub textures: RwLockReadGuard<'a, HashMap>, - pub swap_chain_frames: RwLockReadGuard<'a, HashMap>, + pub surface_textures: + RwLockReadGuard<'a, HashMap>, pub render_pipelines: RwLockReadGuard<'a, HashMap, wgpu::RenderPipeline>>, pub bind_groups: RwLockReadGuard<'a, HashMap>, @@ -59,7 +60,7 @@ impl<'a> WgpuResourcesReadLock<'a> { WgpuResourceRefs { buffers: &self.buffers, textures: &self.textures, - swap_chain_frames: &self.swap_chain_frames, + surface_textures: &self.surface_textures, render_pipelines: &self.render_pipelines, bind_groups: &self.bind_groups, used_bind_group_sender: &self.used_bind_group_sender, @@ -73,7 +74,7 @@ impl<'a> WgpuResourcesReadLock<'a> { pub struct WgpuResourceRefs<'a> { pub buffers: &'a HashMap>, pub textures: &'a HashMap, - pub swap_chain_frames: &'a HashMap, + pub surface_textures: &'a HashMap, pub render_pipelines: &'a HashMap, wgpu::RenderPipeline>, pub bind_groups: &'a HashMap, pub used_bind_group_sender: &'a Sender, @@ -84,8 +85,8 @@ pub struct WgpuResources { pub buffer_infos: Arc>>, pub texture_descriptors: Arc>>, pub window_surfaces: Arc>>, - pub window_swap_chains: Arc>>, - pub swap_chain_frames: Arc>>, + pub surface_textures: + Arc>>, pub buffers: Arc>>>, pub texture_views: Arc>>, pub textures: Arc>>, @@ -103,7 +104,7 @@ impl WgpuResources { WgpuResourcesReadLock { buffers: self.buffers.read(), textures: self.texture_views.read(), - swap_chain_frames: self.swap_chain_frames.read(), + surface_textures: self.surface_textures.read(), render_pipelines: self.render_pipelines.read(), bind_groups: self.bind_groups.read(), used_bind_group_sender: self.bind_group_counter.used_bind_group_sender.clone(), diff --git a/crates/bevy_wgpu/src/wgpu_type_converter.rs b/crates/bevy_wgpu/src/wgpu_type_converter.rs index e84f76febb..a1e72e36f6 100644 --- a/crates/bevy_wgpu/src/wgpu_type_converter.rs +++ b/crates/bevy_wgpu/src/wgpu_type_converter.rs @@ -13,7 +13,7 @@ use bevy_render::{ texture::{ AddressMode, Extent3d, FilterMode, SamplerBorderColor, SamplerDescriptor, StorageTextureAccess, TextureDescriptor, TextureDimension, TextureFormat, - TextureSampleType, TextureUsage, TextureViewDimension, + TextureSampleType, TextureUsages, TextureViewDimension, }, }; use bevy_window::Window; @@ -83,11 +83,11 @@ impl WgpuFrom<&VertexAttribute> for wgpu::VertexAttribute { } } -impl WgpuFrom for wgpu::InputStepMode { +impl WgpuFrom for wgpu::VertexStepMode { fn from(val: InputStepMode) -> Self { match val { - InputStepMode::Vertex => wgpu::InputStepMode::Vertex, - InputStepMode::Instance => wgpu::InputStepMode::Instance, + InputStepMode::Vertex => wgpu::VertexStepMode::Vertex, + InputStepMode::Instance => wgpu::VertexStepMode::Instance, } } } @@ -95,7 +95,7 @@ impl WgpuFrom for wgpu::InputStepMode { #[derive(Clone, Debug)] pub struct OwnedWgpuVertexBufferLayout { pub array_stride: wgpu::BufferAddress, - pub step_mode: wgpu::InputStepMode, + pub step_mode: wgpu::VertexStepMode, pub attributes: Vec, } @@ -137,9 +137,9 @@ impl WgpuFrom for wgpu::Color { } } -impl WgpuFrom for wgpu::BufferUsage { +impl WgpuFrom for wgpu::BufferUsages { fn from(val: BufferUsage) -> Self { - wgpu::BufferUsage::from_bits(val.bits()).unwrap() + wgpu::BufferUsages::from_bits(val.bits()).unwrap() } } @@ -190,7 +190,9 @@ impl WgpuFrom<&BindType> for wgpu::BindingType { } => wgpu::BindingType::Buffer { ty: BufferBindingType::Uniform, has_dynamic_offset: *has_dynamic_offset, - min_binding_size: bind_type.get_uniform_size().and_then(wgpu::BufferSize::new), + // FIXME: The line below cause a validation error + // min_binding_size: bind_type.get_uniform_size().and_then(wgpu::BufferSize::new), + min_binding_size: None, }, BindType::StorageBuffer { has_dynamic_offset, @@ -200,7 +202,9 @@ impl WgpuFrom<&BindType> for wgpu::BindingType { read_only: *readonly, }, has_dynamic_offset: *has_dynamic_offset, - min_binding_size: bind_type.get_uniform_size().and_then(wgpu::BufferSize::new), + // FIXME: The line below cause a validation error + // min_binding_size: bind_type.get_uniform_size().and_then(wgpu::BufferSize::new), + min_binding_size: None, }, BindType::Texture { view_dimension, @@ -346,9 +350,9 @@ impl WgpuFrom for wgpu::TextureFormat { } } -impl WgpuFrom for wgpu::TextureUsage { - fn from(val: TextureUsage) -> Self { - wgpu::TextureUsage::from_bits(val.bits()).unwrap() +impl WgpuFrom for wgpu::TextureUsages { + fn from(val: TextureUsages) -> Self { + wgpu::TextureUsages::from_bits(val.bits()).unwrap() } } @@ -526,9 +530,9 @@ impl WgpuFrom for wgpu::PrimitiveState { } } -impl WgpuFrom for wgpu::ColorWrite { +impl WgpuFrom for wgpu::ColorWrites { fn from(val: ColorWrite) -> Self { - wgpu::ColorWrite::from_bits(val.bits()).unwrap() + wgpu::ColorWrites::from_bits(val.bits()).unwrap() } } @@ -640,10 +644,10 @@ impl WgpuFrom for wgpu::SamplerBorderColor { } } -impl WgpuFrom<&Window> for wgpu::SwapChainDescriptor { +impl WgpuFrom<&Window> for wgpu::SurfaceConfiguration { fn from(window: &Window) -> Self { - wgpu::SwapChainDescriptor { - usage: wgpu::TextureUsage::RENDER_ATTACHMENT, + wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, format: TextureFormat::default().wgpu_into(), width: window.physical_width().max(1), height: window.physical_height().max(1), @@ -664,21 +668,12 @@ impl WgpuFrom for wgpu::Features { WgpuFeature::TimestampQuery => wgpu::Features::TIMESTAMP_QUERY, WgpuFeature::PipelineStatisticsQuery => wgpu::Features::PIPELINE_STATISTICS_QUERY, WgpuFeature::MappablePrimaryBuffers => wgpu::Features::MAPPABLE_PRIMARY_BUFFERS, - WgpuFeature::SampledTextureBindingArray => { - wgpu::Features::SAMPLED_TEXTURE_BINDING_ARRAY - } - WgpuFeature::SampledTextureArrayDynamicIndexing => { - wgpu::Features::SAMPLED_TEXTURE_ARRAY_DYNAMIC_INDEXING - } - WgpuFeature::SampledTextureArrayNonUniformIndexing => { - wgpu::Features::SAMPLED_TEXTURE_ARRAY_NON_UNIFORM_INDEXING - } WgpuFeature::UnsizedBindingArray => wgpu::Features::UNSIZED_BINDING_ARRAY, WgpuFeature::MultiDrawIndirect => wgpu::Features::MULTI_DRAW_INDIRECT, WgpuFeature::MultiDrawIndirectCount => wgpu::Features::MULTI_DRAW_INDIRECT_COUNT, WgpuFeature::PushConstants => wgpu::Features::PUSH_CONSTANTS, WgpuFeature::AddressModeClampToBorder => wgpu::Features::ADDRESS_MODE_CLAMP_TO_BORDER, - WgpuFeature::NonFillPolygonMode => wgpu::Features::NON_FILL_POLYGON_MODE, + WgpuFeature::PolygonModeLine => wgpu::Features::POLYGON_MODE_LINE, WgpuFeature::TextureCompressionEtc2 => wgpu::Features::TEXTURE_COMPRESSION_ETC2, WgpuFeature::TextureCompressionAstcLdr => wgpu::Features::TEXTURE_COMPRESSION_ASTC_LDR, WgpuFeature::TextureAdapterSpecificFormatFeatures => { @@ -724,6 +719,8 @@ impl WgpuFrom for wgpu::Limits { max_vertex_buffers: val.max_vertex_buffers, max_vertex_attributes: val.max_vertex_attributes, max_vertex_buffer_array_stride: val.max_vertex_buffer_array_stride, + min_storage_buffer_offset_alignment: val.min_storage_buffer_offset_alignment, + min_uniform_buffer_offset_alignment: val.min_uniform_buffer_offset_alignment, } } } diff --git a/crates/bevy_window/Cargo.toml b/crates/bevy_window/Cargo.toml index bbb8f5880e..d4a164549c 100644 --- a/crates/bevy_window/Cargo.toml +++ b/crates/bevy_window/Cargo.toml @@ -13,6 +13,7 @@ keywords = ["bevy"] bevy_app = { path = "../bevy_app", version = "0.5.0" } bevy_math = { path = "../bevy_math", version = "0.5.0" } bevy_utils = { path = "../bevy_utils", version = "0.5.0" } +raw-window-handle = "0.3.0" # other diff --git a/crates/bevy_window/src/lib.rs b/crates/bevy_window/src/lib.rs index 00b26de361..c07eecf598 100644 --- a/crates/bevy_window/src/lib.rs +++ b/crates/bevy_window/src/lib.rs @@ -1,8 +1,10 @@ mod event; +mod raw_window_handle; mod system; mod window; mod windows; +pub use crate::raw_window_handle::*; pub use event::*; pub use system::*; pub use window::*; diff --git a/crates/bevy_window/src/raw_window_handle.rs b/crates/bevy_window/src/raw_window_handle.rs new file mode 100644 index 0000000000..76f1411261 --- /dev/null +++ b/crates/bevy_window/src/raw_window_handle.rs @@ -0,0 +1,37 @@ +use raw_window_handle::{HasRawWindowHandle, RawWindowHandle}; + +/// This wrapper exist to enable safely passing a [`RawWindowHandle`] across threads. Extracting the handle +/// is still an unsafe operation, so the caller must still validate that using the raw handle is safe for a given context. +#[derive(Debug, Clone)] +pub struct RawWindowHandleWrapper(RawWindowHandle); + +impl RawWindowHandleWrapper { + pub(crate) fn new(handle: RawWindowHandle) -> Self { + Self(handle) + } + + /// # Safety + /// This returns a [`HasRawWindowHandle`] impl, which exposes [`RawWindowHandle`]. Some platforms + /// have constraints on where/how this handle can be used. For example, some platforms don't support doing window + /// operations off of the main thread. The caller must ensure the [`RawWindowHandle`] is only used in valid contexts. + pub unsafe fn get_handle(&self) -> HasRawWindowHandleWrapper { + HasRawWindowHandleWrapper(self.0) + } +} + +// SAFE: RawWindowHandle is just a normal "raw pointer", which doesn't impl Send/Sync. However the pointer is only +// exposed via an unsafe method that forces the user to make a call for a given platform. (ex: some platforms don't +// support doing window operations off of the main thread). +// A recommendation for this pattern (and more context) is available here: +// https://github.com/rust-windowing/raw-window-handle/issues/59 +unsafe impl Send for RawWindowHandleWrapper {} +unsafe impl Sync for RawWindowHandleWrapper {} + +pub struct HasRawWindowHandleWrapper(RawWindowHandle); + +// SAFE: the caller has validated that this is a valid context to get RawWindowHandle +unsafe impl HasRawWindowHandle for HasRawWindowHandleWrapper { + fn raw_window_handle(&self) -> RawWindowHandle { + self.0 + } +} diff --git a/crates/bevy_window/src/window.rs b/crates/bevy_window/src/window.rs index 94b0e1edda..a7a497ff65 100644 --- a/crates/bevy_window/src/window.rs +++ b/crates/bevy_window/src/window.rs @@ -1,5 +1,6 @@ use bevy_math::{DVec2, IVec2, Vec2}; use bevy_utils::{tracing::warn, Uuid}; +use raw_window_handle::RawWindowHandle; #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub struct WindowId(Uuid); @@ -20,6 +21,8 @@ impl WindowId { use std::fmt; +use crate::raw_window_handle::RawWindowHandleWrapper; + impl fmt::Display for WindowId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.to_simple().fmt(f) @@ -123,6 +126,7 @@ pub struct Window { cursor_visible: bool, cursor_locked: bool, physical_cursor_position: Option, + raw_window_handle: RawWindowHandleWrapper, focused: bool, mode: WindowMode, #[cfg(target_arch = "wasm32")] @@ -198,6 +202,7 @@ impl Window { physical_height: u32, scale_factor: f64, position: Option, + raw_window_handle: RawWindowHandle, ) -> Self { Window { id, @@ -216,6 +221,7 @@ impl Window { cursor_visible: window_descriptor.cursor_visible, cursor_locked: window_descriptor.cursor_locked, physical_cursor_position: None, + raw_window_handle: RawWindowHandleWrapper::new(raw_window_handle), focused: true, mode: window_descriptor.mode, #[cfg(target_arch = "wasm32")] @@ -519,6 +525,10 @@ impl Window { pub fn is_focused(&self) -> bool { self.focused } + + pub fn raw_window_handle(&self) -> RawWindowHandleWrapper { + self.raw_window_handle.clone() + } } #[derive(Debug, Clone)] diff --git a/crates/bevy_winit/Cargo.toml b/crates/bevy_winit/Cargo.toml index a531e41680..ad3e58c0d6 100644 --- a/crates/bevy_winit/Cargo.toml +++ b/crates/bevy_winit/Cargo.toml @@ -24,6 +24,7 @@ bevy_utils = { path = "../bevy_utils", version = "0.5.0" } # other winit = { version = "0.25.0", default-features = false } approx = { version = "0.5.0", default-features = false } +raw-window-handle = "0.3.0" [target.'cfg(target_arch = "wasm32")'.dependencies] winit = { version = "0.25.0", features = ["web-sys"], default-features = false } diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 97b79131ca..7e47c00222 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -26,14 +26,6 @@ use winit::{ }; use winit::dpi::LogicalSize; -#[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" -))] -use winit::platform::unix::EventLoopExtUnix; #[derive(Default)] pub struct WinitPlugin; @@ -43,6 +35,9 @@ impl Plugin for WinitPlugin { app.init_resource::() .set_runner(winit_runner) .add_system_to_stage(CoreStage::PostUpdate, change_window.exclusive_system()); + let event_loop = EventLoop::new(); + handle_initial_window_events(&mut app.world, &event_loop); + app.insert_non_send_resource(event_loop); } } @@ -207,21 +202,22 @@ where } pub fn winit_runner(app: App) { - winit_runner_with(app, EventLoop::new()); + winit_runner_with(app); } -#[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" -))] -pub fn winit_runner_any_thread(app: App) { - winit_runner_with(app, EventLoop::new_any_thread()); -} +// #[cfg(any( +// target_os = "linux", +// target_os = "dragonfly", +// target_os = "freebsd", +// target_os = "netbsd", +// target_os = "openbsd" +// ))] +// pub fn winit_runner_any_thread(app: App) { +// winit_runner_with(app, EventLoop::new_any_thread()); +// } -pub fn winit_runner_with(mut app: App, mut event_loop: EventLoop<()>) { +pub fn winit_runner_with(mut app: App) { + let mut event_loop = app.world.remove_non_send::>().unwrap(); let mut create_window_event_reader = ManualEventReader::::default(); let mut app_exit_event_reader = ManualEventReader::::default(); app.world.insert_non_send(event_loop.create_proxy()); @@ -534,3 +530,22 @@ fn handle_create_window_events( }); } } + +fn handle_initial_window_events(world: &mut World, event_loop: &EventLoop<()>) { + let world = world.cell(); + let mut winit_windows = world.get_resource_mut::().unwrap(); + let mut windows = world.get_resource_mut::().unwrap(); + let mut create_window_events = world.get_resource_mut::>().unwrap(); + let mut window_created_events = world.get_resource_mut::>().unwrap(); + for create_window_event in create_window_events.drain() { + let window = winit_windows.create_window( + event_loop, + create_window_event.id, + &create_window_event.descriptor, + ); + windows.add(window); + window_created_events.send(WindowCreated { + id: create_window_event.id, + }); + } +} diff --git a/crates/bevy_winit/src/winit_windows.rs b/crates/bevy_winit/src/winit_windows.rs index 8194ddb702..86ca6d3d43 100644 --- a/crates/bevy_winit/src/winit_windows.rs +++ b/crates/bevy_winit/src/winit_windows.rs @@ -1,6 +1,7 @@ use bevy_math::IVec2; use bevy_utils::HashMap; use bevy_window::{Window, WindowDescriptor, WindowId, WindowMode}; +use raw_window_handle::HasRawWindowHandle; use winit::dpi::LogicalSize; #[derive(Debug, Default)] @@ -156,6 +157,7 @@ impl WinitWindows { .map(|position| IVec2::new(position.x, position.y)); let inner_size = winit_window.inner_size(); let scale_factor = winit_window.scale_factor(); + let raw_window_handle = winit_window.raw_window_handle(); self.windows.insert(winit_window.id(), winit_window); Window::new( window_id, @@ -164,6 +166,7 @@ impl WinitWindows { inner_size.height, scale_factor, position, + raw_window_handle, ) } diff --git a/crates/crevice/Cargo.toml b/crates/crevice/Cargo.toml new file mode 100644 index 0000000000..76d473d0c5 --- /dev/null +++ b/crates/crevice/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "crevice" +description = "Create GLSL-compatible versions of structs with explicitly-initialized padding" +version = "0.8.0" +edition = "2021" +authors = ["Lucien Greathouse "] +documentation = "https://docs.rs/crevice" +homepage = "https://github.com/LPGhatguy/crevice" +repository = "https://github.com/LPGhatguy/crevice" +readme = "README.md" +keywords = ["glsl", "std140", "std430"] +license = "MIT OR Apache-2.0" +# resolver = "2" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +default = ["std"] +std = [] + +# [workspace] +# members = ["crevice-derive", "crevice-tests"] +# default-members = ["crevice-derive", "crevice-tests"] + +[dependencies] +crevice-derive = { version = "0.8.0", path = "crevice-derive" } + +bytemuck = "1.4.1" +mint = "0.5.8" + +cgmath = { version = "0.18.0", optional = true } +glam = { version = "0.20.0", features = ["mint"], optional = true } +nalgebra = { version = "0.29.0", features = ["mint"], optional = true } + +[dev-dependencies] +insta = "0.16.1" diff --git a/crates/crevice/LICENSE-APACHE b/crates/crevice/LICENSE-APACHE new file mode 100644 index 0000000000..d42ecff58b --- /dev/null +++ b/crates/crevice/LICENSE-APACHE @@ -0,0 +1,201 @@ +i Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/crates/crevice/LICENSE-MIT b/crates/crevice/LICENSE-MIT new file mode 100644 index 0000000000..32c2307fc6 --- /dev/null +++ b/crates/crevice/LICENSE-MIT @@ -0,0 +1,19 @@ +Copyright (c) 2020 Lucien Greathouse + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/crates/crevice/README.md b/crates/crevice/README.md new file mode 100644 index 0000000000..7e7070178e --- /dev/null +++ b/crates/crevice/README.md @@ -0,0 +1,170 @@ +# Crevice + +[![GitHub CI Status](https://github.com/LPGhatguy/crevice/workflows/CI/badge.svg)](https://github.com/LPGhatguy/crevice/actions) +[![crevice on crates.io](https://img.shields.io/crates/v/crevice.svg)](https://crates.io/crates/crevice) +[![crevice docs](https://img.shields.io/badge/docs-docs.rs-orange.svg)](https://docs.rs/crevice) + +Crevice creates GLSL-compatible versions of types through the power of derive +macros. Generated structures provide an [`as_bytes`][std140::Std140::as_bytes] +method to allow safely packing data into buffers for uploading. + +Generated structs also implement [`bytemuck::Zeroable`] and +[`bytemuck::Pod`] for use with other libraries. + +Crevice is similar to [`glsl-layout`][glsl-layout], but supports types from many +math crates, can generate GLSL source from structs, and explicitly initializes +padding to remove one source of undefined behavior. + +Crevice has support for many Rust math libraries via feature flags, and most +other math libraries by use of the mint crate. Crevice currently supports: + +* mint 0.5, enabled by default +* cgmath 0.18, using the `cgmath` feature +* nalgebra 0.29, using the `nalgebra` feature +* glam 0.19, using the `glam` feature + +PRs are welcome to add or update math libraries to Crevice. + +If your math library is not supported, it's possible to define structs using the +types from mint and convert your math library's types into mint types. This is +supported by most Rust math libraries. + +Your math library may require you to turn on a feature flag to get mint support. +For example, cgmath requires the "mint" feature to be enabled to allow +conversions to and from mint types. + +## Examples + +### Single Value + +Uploading many types can be done by deriving [`AsStd140`][std140::AsStd140] and +using [`as_std140`][std140::AsStd140::as_std140] and +[`as_bytes`][std140::Std140::as_bytes] to turn the result into bytes. + +```glsl +uniform MAIN { + mat3 orientation; + vec3 position; + float scale; +} main; +``` + +```rust +use crevice::std140::{AsStd140, Std140}; + +#[derive(AsStd140)] +struct MainUniform { + orientation: mint::ColumnMatrix3, + position: mint::Vector3, + scale: f32, +} + +let value = MainUniform { + orientation: [ + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + ].into(), + position: [1.0, 2.0, 3.0].into(), + scale: 4.0, +}; + +let value_std140 = value.as_std140(); + +upload_data_to_gpu(value_std140.as_bytes()); +``` + +### Sequential Types + +More complicated data can be uploaded using the std140 +[`Writer`][std140::Writer] type. + +```glsl +struct PointLight { + vec3 position; + vec3 color; + float brightness; +}; + +buffer POINT_LIGHTS { + uint len; + PointLight[] lights; +} point_lights; +``` + +```rust +use crevice::std140::{self, AsStd140}; + +#[derive(AsStd140)] +struct PointLight { + position: mint::Vector3, + color: mint::Vector3, + brightness: f32, +} + +let lights = vec![ + PointLight { + position: [0.0, 1.0, 0.0].into(), + color: [1.0, 0.0, 0.0].into(), + brightness: 0.6, + }, + PointLight { + position: [0.0, 4.0, 3.0].into(), + color: [1.0, 1.0, 1.0].into(), + brightness: 1.0, + }, +]; + +let target_buffer = map_gpu_buffer_for_write(); +let mut writer = std140::Writer::new(target_buffer); + +let light_count = lights.len() as u32; +writer.write(&light_count)?; + +// Crevice will automatically insert the required padding to align the +// PointLight structure correctly. In this case, there will be 12 bytes of +// padding between the length field and the light list. + +writer.write(lights.as_slice())?; + +unmap_gpu_buffer(); + +``` + +## Features + +* `std` (default): Enables [`std::io::Write`]-based structs. +* `cgmath`: Enables support for types from cgmath. +* `nalgebra`: Enables support for types from nalgebra. +* `glam`: Enables support for types from glam. + +## Minimum Supported Rust Version (MSRV) + +Crevice supports Rust 1.52.1 and newer due to use of new `const fn` features. + +[glsl-layout]: https://github.com/rustgd/glsl-layout + +[std140::AsStd140]: https://docs.rs/crevice/latest/crevice/std140/trait.AsStd140.html +[std140::AsStd140::as_std140]: https://docs.rs/crevice/latest/crevice/std140/trait.AsStd140.html#method.as_std140 +[std140::Std140::as_bytes]: https://docs.rs/crevice/latest/crevice/std140/trait.Std140.html#method.as_bytes +[std140::Writer]: https://docs.rs/crevice/latest/crevice/std140/struct.Writer.html + +[`std::io::Write`]: https://doc.rust-lang.org/stable/std/io/trait.Write.html + +[`bytemuck::Pod`]: https://docs.rs/bytemuck/latest/bytemuck/trait.Pod.html +[`bytemuck::Zeroable`]: https://docs.rs/bytemuck/latest/bytemuck/trait.Zeroable.html + +## License + +Licensed under either of + +* Apache License, Version 2.0, ([LICENSE-APACHE](http://www.apache.org/licenses/LICENSE-2.0)) +* MIT license ([LICENSE-MIT](http://opensource.org/licenses/MIT)) + +at your option. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you, as defined in the Apache-2.0 license, shall +be dual licensed as above, without any additional terms or conditions. diff --git a/crates/crevice/README.tpl b/crates/crevice/README.tpl new file mode 100644 index 0000000000..42ca95cfb8 --- /dev/null +++ b/crates/crevice/README.tpl @@ -0,0 +1,25 @@ +# Crevice + +{{readme}} + +[std140::AsStd140]: https://docs.rs/crevice/latest/crevice/std140/trait.AsStd140.html +[std140::AsStd140::as_std140]: https://docs.rs/crevice/latest/crevice/std140/trait.AsStd140.html#method.as_std140 +[std140::Std140::as_bytes]: https://docs.rs/crevice/latest/crevice/std140/trait.Std140.html#method.as_bytes +[std140::Writer]: https://docs.rs/crevice/latest/crevice/std140/struct.Writer.html + +[`std::io::Write`]: https://doc.rust-lang.org/stable/std/io/trait.Write.html + +[`bytemuck::Pod`]: https://docs.rs/bytemuck/latest/bytemuck/trait.Pod.html +[`bytemuck::Zeroable`]: https://docs.rs/bytemuck/latest/bytemuck/trait.Zeroable.html + +## License + +Licensed under either of + + * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) + * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. + +### Contribution +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. \ No newline at end of file diff --git a/crates/crevice/crevice-derive/Cargo.toml b/crates/crevice/crevice-derive/Cargo.toml new file mode 100644 index 0000000000..1d64fbb673 --- /dev/null +++ b/crates/crevice/crevice-derive/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "crevice-derive" +description = "Derive crate for the 'crevice' crate" +version = "0.8.0" +edition = "2018" +authors = ["Lucien Greathouse "] +documentation = "https://docs.rs/crevice-derive" +homepage = "https://github.com/LPGhatguy/crevice" +repository = "https://github.com/LPGhatguy/crevice" +license = "MIT OR Apache-2.0" + +[features] +default = [] + +# Enable methods that let you introspect into the generated structs. +debug-methods = [] + +[lib] +proc-macro = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +syn = "1.0.40" +quote = "1.0.7" +proc-macro2 = "1.0.21" diff --git a/crates/crevice/crevice-derive/src/glsl.rs b/crates/crevice/crevice-derive/src/glsl.rs new file mode 100644 index 0000000000..fd74f0cd91 --- /dev/null +++ b/crates/crevice/crevice-derive/src/glsl.rs @@ -0,0 +1,49 @@ +use proc_macro2::{Literal, TokenStream}; +use quote::quote; +use syn::{parse_quote, Data, DeriveInput, Fields, Path}; + +pub fn emit(input: DeriveInput) -> TokenStream { + let fields = match &input.data { + Data::Struct(data) => match &data.fields { + Fields::Named(fields) => fields, + Fields::Unnamed(_) => panic!("Tuple structs are not supported"), + Fields::Unit => panic!("Unit structs are not supported"), + }, + Data::Enum(_) | Data::Union(_) => panic!("Only structs are supported"), + }; + + let base_trait_path: Path = parse_quote!(::crevice::glsl::Glsl); + let struct_trait_path: Path = parse_quote!(::crevice::glsl::GlslStruct); + + let name = input.ident; + let name_str = Literal::string(&name.to_string()); + + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + + let glsl_fields = fields.named.iter().map(|field| { + let field_ty = &field.ty; + let field_name_str = Literal::string(&field.ident.as_ref().unwrap().to_string()); + let field_as = quote! {<#field_ty as ::crevice::glsl::GlslArray>}; + + quote! { + s.push_str("\t"); + s.push_str(#field_as::NAME); + s.push_str(" "); + s.push_str(#field_name_str); + <#field_as::ArraySize as ::crevice::glsl::DimensionList>::push_to_string(s); + s.push_str(";\n"); + } + }); + + quote! { + unsafe impl #impl_generics #base_trait_path for #name #ty_generics #where_clause { + const NAME: &'static str = #name_str; + } + + unsafe impl #impl_generics #struct_trait_path for #name #ty_generics #where_clause { + fn enumerate_fields(s: &mut String) { + #( #glsl_fields )* + } + } + } +} diff --git a/crates/crevice/crevice-derive/src/layout.rs b/crates/crevice/crevice-derive/src/layout.rs new file mode 100644 index 0000000000..0819a01cf2 --- /dev/null +++ b/crates/crevice/crevice-derive/src/layout.rs @@ -0,0 +1,288 @@ +use proc_macro2::{Span, TokenStream}; +use quote::{format_ident, quote}; +use syn::{parse_quote, Data, DeriveInput, Fields, Ident, Path, Type}; + +pub fn emit( + input: DeriveInput, + trait_name: &'static str, + mod_name: &'static str, + min_struct_alignment: usize, +) -> TokenStream { + let mod_name = Ident::new(mod_name, Span::call_site()); + let trait_name = Ident::new(trait_name, Span::call_site()); + + let mod_path: Path = parse_quote!(::crevice::#mod_name); + let trait_path: Path = parse_quote!(#mod_path::#trait_name); + + let as_trait_name = format_ident!("As{}", trait_name); + let as_trait_path: Path = parse_quote!(#mod_path::#as_trait_name); + let as_trait_method = format_ident!("as_{}", mod_name); + let from_trait_method = format_ident!("from_{}", mod_name); + + let padded_name = format_ident!("{}Padded", trait_name); + let padded_path: Path = parse_quote!(#mod_path::#padded_name); + + let visibility = input.vis; + let input_name = input.ident; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + + let generated_name = format_ident!("{}{}", trait_name, input_name); + + // Crevice's derive only works on regular structs. We could potentially + // support transparent tuple structs in the future. + let fields: Vec<_> = match &input.data { + Data::Struct(data) => match &data.fields { + Fields::Named(fields) => fields.named.iter().collect(), + Fields::Unnamed(_) => panic!("Tuple structs are not supported"), + Fields::Unit => panic!("Unit structs are not supported"), + }, + Data::Enum(_) | Data::Union(_) => panic!("Only structs are supported"), + }; + + // Gives the layout-specific version of the given type. + let layout_version_of_ty = |ty: &Type| { + quote! { + <#ty as #as_trait_path>::Output + } + }; + + // Gives an expression returning the layout-specific alignment for the type. + let layout_alignment_of_ty = |ty: &Type| { + quote! { + <<#ty as #as_trait_path>::Output as #trait_path>::ALIGNMENT + } + }; + + // Gives an expression telling whether the type should have trailing padding + // at least equal to its alignment. + let layout_pad_at_end_of_ty = |ty: &Type| { + quote! { + <<#ty as #as_trait_path>::Output as #trait_path>::PAD_AT_END + } + }; + + let field_alignments = fields.iter().map(|field| layout_alignment_of_ty(&field.ty)); + let struct_alignment = quote! { + ::crevice::internal::max_arr([ + #min_struct_alignment, + #(#field_alignments,)* + ]) + }; + + // Generate names for each padding calculation function. + let pad_fns: Vec<_> = (0..fields.len()) + .map(|index| format_ident!("_{}__{}Pad{}", input_name, trait_name, index)) + .collect(); + + // Computes the offset immediately AFTER the field with the given index. + // + // This function depends on the generated padding calculation functions to + // do correct alignment. Be careful not to cause recursion! + let offset_after_field = |target: usize| { + let mut output = vec![quote!(0usize)]; + + for index in 0..=target { + let field_ty = &fields[index].ty; + let layout_ty = layout_version_of_ty(field_ty); + + output.push(quote! { + + ::core::mem::size_of::<#layout_ty>() + }); + + // For every field except our target field, also add the generated + // padding. Padding occurs after each field, so it isn't included in + // this value. + if index < target { + let pad_fn = &pad_fns[index]; + output.push(quote! { + + #pad_fn() + }); + } + } + + output.into_iter().collect::() + }; + + let pad_fn_impls: TokenStream = fields + .iter() + .enumerate() + .map(|(index, prev_field)| { + let pad_fn = &pad_fns[index]; + + let starting_offset = offset_after_field(index); + let prev_field_has_end_padding = layout_pad_at_end_of_ty(&prev_field.ty); + let prev_field_alignment = layout_alignment_of_ty(&prev_field.ty); + + let next_field_or_self_alignment = fields + .get(index + 1) + .map(|next_field| layout_alignment_of_ty(&next_field.ty)) + .unwrap_or(quote!(#struct_alignment)); + + quote! { + /// Tells how many bytes of padding have to be inserted after + /// the field with index #index. + #[allow(non_snake_case)] + const fn #pad_fn() -> usize { + // First up, calculate our offset into the struct so far. + // We'll use this value to figure out how far out of + // alignment we are. + let starting_offset = #starting_offset; + + // If the previous field is a struct or array, we must align + // the next field to at least THAT field's alignment. + let min_alignment = if #prev_field_has_end_padding { + #prev_field_alignment + } else { + 0 + }; + + // We set our target alignment to the larger of the + // alignment due to the previous field and the alignment + // requirement of the next field. + let alignment = ::crevice::internal::max( + #next_field_or_self_alignment, + min_alignment, + ); + + // Using everything we've got, compute our padding amount. + ::crevice::internal::align_offset(starting_offset, alignment) + } + } + }) + .collect(); + + let generated_struct_fields: TokenStream = fields + .iter() + .enumerate() + .map(|(index, field)| { + let field_name = field.ident.as_ref().unwrap(); + let field_ty = layout_version_of_ty(&field.ty); + let pad_field_name = format_ident!("_pad{}", index); + let pad_fn = &pad_fns[index]; + + quote! { + #field_name: #field_ty, + #pad_field_name: [u8; #pad_fn()], + } + }) + .collect(); + + let generated_struct_field_init: TokenStream = fields + .iter() + .map(|field| { + let field_name = field.ident.as_ref().unwrap(); + + quote! { + #field_name: self.#field_name.#as_trait_method(), + } + }) + .collect(); + + let input_struct_field_init: TokenStream = fields + .iter() + .map(|field| { + let field_name = field.ident.as_ref().unwrap(); + + quote! { + #field_name: #as_trait_path::#from_trait_method(input.#field_name), + } + }) + .collect(); + + let struct_definition = quote! { + #[derive(Debug, Clone, Copy)] + #[repr(C)] + #[allow(non_snake_case)] + #visibility struct #generated_name #ty_generics #where_clause { + #generated_struct_fields + } + }; + + let debug_methods = if cfg!(feature = "debug-methods") { + let debug_fields: TokenStream = fields + .iter() + .map(|field| { + let field_name = field.ident.as_ref().unwrap(); + let field_ty = &field.ty; + + quote! { + fields.push(Field { + name: stringify!(#field_name), + size: ::core::mem::size_of::<#field_ty>(), + offset: (&zeroed.#field_name as *const _ as usize) + - (&zeroed as *const _ as usize), + }); + } + }) + .collect(); + + quote! { + impl #impl_generics #generated_name #ty_generics #where_clause { + fn debug_metrics() -> String { + let size = ::core::mem::size_of::(); + let align = ::ALIGNMENT; + + let zeroed: Self = ::crevice::internal::bytemuck::Zeroable::zeroed(); + + #[derive(Debug)] + struct Field { + name: &'static str, + offset: usize, + size: usize, + } + let mut fields = Vec::new(); + + #debug_fields + + format!("Size {}, Align {}, fields: {:#?}", size, align, fields) + } + + fn debug_definitions() -> &'static str { + stringify!( + #struct_definition + #pad_fn_impls + ) + } + } + } + } else { + quote!() + }; + + quote! { + #pad_fn_impls + #struct_definition + + unsafe impl #impl_generics ::crevice::internal::bytemuck::Zeroable for #generated_name #ty_generics #where_clause {} + unsafe impl #impl_generics ::crevice::internal::bytemuck::Pod for #generated_name #ty_generics #where_clause {} + + unsafe impl #impl_generics #mod_path::#trait_name for #generated_name #ty_generics #where_clause { + const ALIGNMENT: usize = #struct_alignment; + const PAD_AT_END: bool = true; + type Padded = #padded_path(), + #struct_alignment + )}>; + } + + impl #impl_generics #as_trait_path for #input_name #ty_generics #where_clause { + type Output = #generated_name; + + fn #as_trait_method(&self) -> Self::Output { + Self::Output { + #generated_struct_field_init + + ..::crevice::internal::bytemuck::Zeroable::zeroed() + } + } + + fn #from_trait_method(input: Self::Output) -> Self { + Self { + #input_struct_field_init + } + } + } + + #debug_methods + } +} diff --git a/crates/crevice/crevice-derive/src/lib.rs b/crates/crevice/crevice-derive/src/lib.rs new file mode 100644 index 0000000000..ab1012a909 --- /dev/null +++ b/crates/crevice/crevice-derive/src/lib.rs @@ -0,0 +1,30 @@ +mod glsl; +mod layout; + +use proc_macro::TokenStream as CompilerTokenStream; + +use syn::{parse_macro_input, DeriveInput}; + +#[proc_macro_derive(AsStd140)] +pub fn derive_as_std140(input: CompilerTokenStream) -> CompilerTokenStream { + let input = parse_macro_input!(input as DeriveInput); + let expanded = layout::emit(input, "Std140", "std140", 16); + + CompilerTokenStream::from(expanded) +} + +#[proc_macro_derive(AsStd430)] +pub fn derive_as_std430(input: CompilerTokenStream) -> CompilerTokenStream { + let input = parse_macro_input!(input as DeriveInput); + let expanded = layout::emit(input, "Std430", "std430", 0); + + CompilerTokenStream::from(expanded) +} + +#[proc_macro_derive(GlslStruct)] +pub fn derive_glsl_struct(input: CompilerTokenStream) -> CompilerTokenStream { + let input = parse_macro_input!(input as DeriveInput); + let expanded = glsl::emit(input); + + CompilerTokenStream::from(expanded) +} diff --git a/crates/crevice/crevice-tests/Cargo.toml b/crates/crevice/crevice-tests/Cargo.toml new file mode 100644 index 0000000000..a6f8605c5a --- /dev/null +++ b/crates/crevice/crevice-tests/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "crevice-tests" +version = "0.1.0" +edition = "2018" + +[features] +wgpu-validation = ["wgpu", "naga", "futures"] + +[dependencies] +crevice = { path = ".." } +crevice-derive = { path = "../crevice-derive", features = ["debug-methods"] } + +anyhow = "1.0.44" +bytemuck = "1.7.2" +memoffset = "0.6.4" +mint = "0.5.5" + +futures = { version = "0.3.17", features = ["executor"], optional = true } +naga = { version = "0.7.0", features = ["glsl-in", "wgsl-out"], optional = true } +wgpu = { version = "0.11.0", optional = true } diff --git a/crates/crevice/crevice-tests/src/gpu.rs b/crates/crevice/crevice-tests/src/gpu.rs new file mode 100644 index 0000000000..517e8d7a46 --- /dev/null +++ b/crates/crevice/crevice-tests/src/gpu.rs @@ -0,0 +1,268 @@ +use std::borrow::Cow; +use std::fmt::Debug; +use std::marker::PhantomData; + +use crevice::glsl::{Glsl, GlslStruct}; +use crevice::std140::{AsStd140, Std140}; +use crevice::std430::{AsStd430, Std430}; +use futures::executor::block_on; +use wgpu::util::DeviceExt; + +const BASE_SHADER: &str = "#version 450 + +{struct_definition} + +layout({layout}, set = 0, binding = 0) readonly buffer INPUT { + {struct_name} in_data; +}; + +layout({layout}, set = 0, binding = 1) buffer OUTPUT { + {struct_name} out_data; +}; + +void main() { + out_data = in_data; +}"; + +pub fn test_round_trip_struct(value: T) { + let shader_std140 = glsl_shader_for_struct::("std140"); + let shader_std430 = glsl_shader_for_struct::("std430"); + + let context = Context::new(); + context.test_round_trip_std140(&shader_std140, &value); + context.test_round_trip_std430(&shader_std430, &value); +} + +pub fn test_round_trip_primitive(value: T) { + let shader_std140 = glsl_shader_for_primitive::("std140"); + let shader_std430 = glsl_shader_for_primitive::("std430"); + + let context = Context::new(); + context.test_round_trip_std140(&shader_std140, &value); + context.test_round_trip_std430(&shader_std430, &value); +} + +fn glsl_shader_for_struct(layout: &str) -> String { + BASE_SHADER + .replace("{struct_name}", T::NAME) + .replace("{struct_definition}", &T::glsl_definition()) + .replace("{layout}", layout) +} + +fn glsl_shader_for_primitive(layout: &str) -> String { + BASE_SHADER + .replace("{struct_name}", T::NAME) + .replace("{struct_definition}", "") + .replace("{layout}", layout) +} + +fn compile_glsl(glsl: &str) -> String { + match compile(glsl) { + Ok(shader) => shader, + Err(err) => { + eprintln!("Bad shader: {}", glsl); + panic!("{}", err); + } + } +} + +struct Context { + device: wgpu::Device, + queue: wgpu::Queue, + _phantom: PhantomData<*const T>, +} + +impl Context +where + T: Debug + PartialEq + AsStd140 + AsStd430 + Glsl, +{ + fn new() -> Self { + let (device, queue) = setup(); + Self { + device, + queue, + _phantom: PhantomData, + } + } + + fn test_round_trip_std140(&self, glsl_shader: &str, value: &T) { + let mut data = Vec::new(); + data.extend_from_slice(value.as_std140().as_bytes()); + + let wgsl_shader = compile_glsl(glsl_shader); + let bytes = self.round_trip(&wgsl_shader, &data); + + let std140 = bytemuck::from_bytes::<::Output>(&bytes); + let output = T::from_std140(*std140); + + if value != &output { + println!( + "std140 value did not round-trip through wgpu successfully.\n\ + Input: {:?}\n\ + Output: {:?}\n\n\ + GLSL shader:\n{}\n\n\ + WGSL shader:\n{}", + value, output, glsl_shader, wgsl_shader, + ); + + panic!("wgpu round-trip failure for {}", T::NAME); + } + } + + fn test_round_trip_std430(&self, glsl_shader: &str, value: &T) { + let mut data = Vec::new(); + data.extend_from_slice(value.as_std430().as_bytes()); + + let wgsl_shader = compile_glsl(glsl_shader); + let bytes = self.round_trip(&wgsl_shader, &data); + + let std430 = bytemuck::from_bytes::<::Output>(&bytes); + let output = T::from_std430(*std430); + + if value != &output { + println!( + "std430 value did not round-trip through wgpu successfully.\n\ + Input: {:?}\n\ + Output: {:?}\n\n\ + GLSL shader:\n{}\n\n\ + WGSL shader:\n{}", + value, output, glsl_shader, wgsl_shader, + ); + + panic!("wgpu round-trip failure for {}", T::NAME); + } + } + + fn round_trip(&self, shader: &str, data: &[u8]) -> Vec { + let input_buffer = self + .device + .create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Input Buffer"), + contents: &data, + usage: wgpu::BufferUsages::STORAGE + | wgpu::BufferUsages::COPY_DST + | wgpu::BufferUsages::COPY_SRC, + }); + + let output_gpu_buffer = self.device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Output Buffer"), + size: data.len() as wgpu::BufferAddress, + usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC, + mapped_at_creation: false, + }); + + let output_cpu_buffer = self.device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Output Buffer"), + size: data.len() as wgpu::BufferAddress, + usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let cs_module = self + .device + .create_shader_module(&wgpu::ShaderModuleDescriptor { + label: None, + source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(shader)), + }); + + let compute_pipeline = + self.device + .create_compute_pipeline(&wgpu::ComputePipelineDescriptor { + label: None, + layout: None, + module: &cs_module, + entry_point: "main", + }); + + let bind_group_layout = compute_pipeline.get_bind_group_layout(0); + let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor { + label: None, + layout: &bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: input_buffer.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: output_gpu_buffer.as_entire_binding(), + }, + ], + }); + + let mut encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); + + { + let mut cpass = + encoder.begin_compute_pass(&wgpu::ComputePassDescriptor { label: None }); + cpass.set_pipeline(&compute_pipeline); + cpass.set_bind_group(0, &bind_group, &[]); + cpass.dispatch(1, 1, 1); + } + + encoder.copy_buffer_to_buffer( + &output_gpu_buffer, + 0, + &output_cpu_buffer, + 0, + data.len() as wgpu::BufferAddress, + ); + + self.queue.submit(std::iter::once(encoder.finish())); + + let output_slice = output_cpu_buffer.slice(..); + let output_future = output_slice.map_async(wgpu::MapMode::Read); + + self.device.poll(wgpu::Maintain::Wait); + block_on(output_future).unwrap(); + + let output = output_slice.get_mapped_range().to_vec(); + output_cpu_buffer.unmap(); + + output + } +} + +fn setup() -> (wgpu::Device, wgpu::Queue) { + let instance = wgpu::Instance::new(wgpu::Backends::all()); + let adapter = + block_on(instance.request_adapter(&wgpu::RequestAdapterOptions::default())).unwrap(); + + println!("Adapter info: {:#?}", adapter.get_info()); + + block_on(adapter.request_device( + &wgpu::DeviceDescriptor { + label: None, + features: wgpu::Features::empty(), + limits: wgpu::Limits::downlevel_defaults(), + }, + None, + )) + .unwrap() +} + +fn compile(glsl_source: &str) -> anyhow::Result { + let mut parser = naga::front::glsl::Parser::default(); + + let module = parser + .parse( + &naga::front::glsl::Options { + stage: naga::ShaderStage::Compute, + defines: Default::default(), + }, + glsl_source, + ) + .map_err(|err| anyhow::format_err!("{:?}", err))?; + + let info = naga::valid::Validator::new( + naga::valid::ValidationFlags::default(), + naga::valid::Capabilities::all(), + ) + .validate(&module)?; + + let wgsl = naga::back::wgsl::write_string(&module, &info)?; + + Ok(wgsl) +} diff --git a/crates/crevice/crevice-tests/src/lib.rs b/crates/crevice/crevice-tests/src/lib.rs new file mode 100644 index 0000000000..da61b47fb4 --- /dev/null +++ b/crates/crevice/crevice-tests/src/lib.rs @@ -0,0 +1,366 @@ +#![cfg(test)] + +#[cfg(feature = "wgpu-validation")] +mod gpu; + +#[cfg(feature = "wgpu-validation")] +use gpu::{test_round_trip_primitive, test_round_trip_struct}; + +#[cfg(not(feature = "wgpu-validation"))] +fn test_round_trip_struct(_value: T) {} + +#[cfg(not(feature = "wgpu-validation"))] +fn test_round_trip_primitive(_value: T) {} + +#[macro_use] +mod util; + +use crevice::glsl::GlslStruct; +use crevice::std140::AsStd140; +use crevice::std430::AsStd430; +use mint::{ColumnMatrix2, ColumnMatrix3, ColumnMatrix4, Vector2, Vector3, Vector4}; + +#[test] +fn two_f32() { + #[derive(Debug, PartialEq, AsStd140, AsStd430, GlslStruct)] + struct TwoF32 { + x: f32, + y: f32, + } + + assert_std140!((size = 16, align = 16) TwoF32 { + x: 0, + y: 4, + }); + + assert_std430!((size = 8, align = 4) TwoF32 { + x: 0, + y: 4, + }); + + test_round_trip_struct(TwoF32 { x: 5.0, y: 7.0 }); +} + +#[test] +fn vec2() { + #[derive(Debug, PartialEq, AsStd140, AsStd430, GlslStruct)] + struct UseVec2 { + one: Vector2, + } + + assert_std140!((size = 16, align = 16) UseVec2 { + one: 0, + }); + + test_round_trip_struct(UseVec2 { + one: [1.0, 2.0].into(), + }); +} + +#[test] +fn mat2_bare() { + type Mat2 = ColumnMatrix2; + + assert_std140!((size = 32, align = 16) Mat2 { + x: 0, + y: 16, + }); + + assert_std430!((size = 16, align = 8) Mat2 { + x: 0, + y: 8, + }); + + // Naga doesn't work with std140 mat2 values. + // https://github.com/gfx-rs/naga/issues/1400 + + // test_round_trip_primitive(Mat2 { + // x: [1.0, 2.0].into(), + // y: [3.0, 4.0].into(), + // }); +} + +#[test] +fn mat3_bare() { + type Mat3 = ColumnMatrix3; + + assert_std140!((size = 48, align = 16) Mat3 { + x: 0, + y: 16, + z: 32, + }); + + // Naga produces invalid HLSL for mat3 value. + // https://github.com/gfx-rs/naga/issues/1466 + + // test_round_trip_primitive(Mat3 { + // x: [1.0, 2.0, 3.0].into(), + // y: [4.0, 5.0, 6.0].into(), + // z: [7.0, 8.0, 9.0].into(), + // }); +} + +#[test] +fn mat4_bare() { + type Mat4 = ColumnMatrix4; + + assert_std140!((size = 64, align = 16) Mat4 { + x: 0, + y: 16, + z: 32, + w: 48, + }); + + test_round_trip_primitive(Mat4 { + x: [1.0, 2.0, 3.0, 4.0].into(), + y: [5.0, 6.0, 7.0, 8.0].into(), + z: [9.0, 10.0, 11.0, 12.0].into(), + w: [13.0, 14.0, 15.0, 16.0].into(), + }); +} + +#[test] +fn mat3() { + #[derive(Debug, PartialEq, AsStd140, AsStd430, GlslStruct)] + struct TestData { + one: ColumnMatrix3, + } + + // Naga produces invalid HLSL for mat3 value. + // https://github.com/gfx-rs/naga/issues/1466 + + // test_round_trip_struct(TestData { + // one: [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]].into(), + // }); +} + +#[test] +fn dvec4() { + #[derive(Debug, PartialEq, AsStd140, AsStd430, GlslStruct)] + struct UsingDVec4 { + doubles: Vector4, + } + + assert_std140!((size = 32, align = 32) UsingDVec4 { + doubles: 0, + }); + + // Naga does not appear to support doubles. + // https://github.com/gfx-rs/naga/issues/1272 + + // test_round_trip_struct(UsingDVec4 { + // doubles: [1.0, 2.0, 3.0, 4.0].into(), + // }); +} + +#[test] +fn four_f64() { + #[derive(Debug, PartialEq, AsStd140, AsStd430, GlslStruct)] + struct FourF64 { + x: f64, + y: f64, + z: f64, + w: f64, + } + + assert_std140!((size = 32, align = 16) FourF64 { + x: 0, + y: 8, + z: 16, + w: 24, + }); + + // Naga does not appear to support doubles. + // https://github.com/gfx-rs/naga/issues/1272 + + // test_round_trip_struct(FourF64 { + // x: 5.0, + // y: 7.0, + // z: 9.0, + // w: 11.0, + // }); +} + +#[test] +fn two_vec3() { + #[derive(Debug, PartialEq, AsStd140, AsStd430, GlslStruct)] + struct TwoVec3 { + one: Vector3, + two: Vector3, + } + + print_std140!(TwoVec3); + print_std430!(TwoVec3); + + assert_std140!((size = 32, align = 16) TwoVec3 { + one: 0, + two: 16, + }); + + assert_std430!((size = 32, align = 16) TwoVec3 { + one: 0, + two: 16, + }); + + test_round_trip_struct(TwoVec3 { + one: [1.0, 2.0, 3.0].into(), + two: [4.0, 5.0, 6.0].into(), + }); +} + +#[test] +fn two_vec4() { + #[derive(Debug, PartialEq, AsStd140, AsStd430, GlslStruct)] + struct TwoVec4 { + one: Vector4, + two: Vector4, + } + + assert_std140!((size = 32, align = 16) TwoVec4 { + one: 0, + two: 16, + }); + + test_round_trip_struct(TwoVec4 { + one: [1.0, 2.0, 3.0, 4.0].into(), + two: [5.0, 6.0, 7.0, 8.0].into(), + }); +} + +#[test] +fn vec3_then_f32() { + #[derive(Debug, PartialEq, AsStd140, AsStd430, GlslStruct)] + struct Vec3ThenF32 { + one: Vector3, + two: f32, + } + + assert_std140!((size = 16, align = 16) Vec3ThenF32 { + one: 0, + two: 12, + }); + + test_round_trip_struct(Vec3ThenF32 { + one: [1.0, 2.0, 3.0].into(), + two: 4.0, + }); +} + +#[test] +fn mat3_padding() { + #[derive(Debug, PartialEq, AsStd140, AsStd430, GlslStruct)] + struct Mat3Padding { + // Three rows of 16 bytes (3x f32 + 4 bytes padding) + one: mint::ColumnMatrix3, + two: f32, + } + + assert_std140!((size = 64, align = 16) Mat3Padding { + one: 0, + two: 48, + }); + + // Naga produces invalid HLSL for mat3 value. + // https://github.com/gfx-rs/naga/issues/1466 + + // test_round_trip_struct(Mat3Padding { + // one: [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]].into(), + // two: 10.0, + // }); +} + +#[test] +fn padding_after_struct() { + #[derive(AsStd140)] + struct TwoF32 { + x: f32, + } + + #[derive(AsStd140)] + struct PaddingAfterStruct { + base_value: TwoF32, + // There should be 8 bytes of padding inserted here. + small_field: f32, + } + + assert_std140!((size = 32, align = 16) PaddingAfterStruct { + base_value: 0, + small_field: 16, + }); +} + +#[test] +fn proper_offset_calculations_for_differing_member_sizes() { + #[derive(AsStd140)] + struct Foo { + x: f32, + } + + #[derive(AsStd140)] + struct Bar { + first: Foo, + second: Foo, + } + + #[derive(AsStd140)] + struct Outer { + leading: Bar, + trailing: Foo, + } + + // Offset Size Contents + // 0 4 Bar.leading.first.x + // 4 12 [padding] + // 16 4 Bar.leading.second.x + // 20 12 [padding] + // 32 4 Bar.trailing.x + // 36 12 [padding] + // + // Total size is 48. + + assert_std140!((size = 48, align = 16) Outer { + leading: 0, + trailing: 32, + }); +} + +#[test] +fn array_strides_small_value() { + #[derive(Debug, PartialEq, AsStd140, AsStd430)] + struct ArrayOfSmallValues { + inner: [f32; 4], + } + + assert_std140!((size = 64, align = 16) ArrayOfSmallValues { + inner: 0, + }); + + assert_std430!((size = 16, align = 4) ArrayOfSmallValues { + inner: 0, + }); +} + +#[test] +fn array_strides_vec3() { + #[derive(Debug, PartialEq, AsStd140, AsStd430, GlslStruct)] + struct ArrayOfVector3 { + inner: [Vector3; 4], + } + + assert_std140!((size = 64, align = 16) ArrayOfVector3 { + inner: 0, + }); + + assert_std430!((size = 64, align = 16) ArrayOfVector3 { + inner: 0, + }); + + test_round_trip_struct(ArrayOfVector3 { + inner: [ + [0.0, 1.0, 2.0].into(), + [3.0, 4.0, 5.0].into(), + [6.0, 7.0, 8.0].into(), + [9.0, 10.0, 11.0].into(), + ], + }) +} diff --git a/crates/crevice/crevice-tests/src/util.rs b/crates/crevice/crevice-tests/src/util.rs new file mode 100644 index 0000000000..203afced8e --- /dev/null +++ b/crates/crevice/crevice-tests/src/util.rs @@ -0,0 +1,143 @@ +#[macro_export] +macro_rules! print_std140 { + ($type:ty) => { + println!( + "{}", + <$type as crevice::std140::AsStd140>::Output::debug_metrics() + ); + println!(); + println!(); + println!( + "{}", + <$type as crevice::std140::AsStd140>::Output::debug_definitions() + ); + }; +} + +#[macro_export] +macro_rules! print_std430 { + ($type:ty) => { + println!( + "{}", + <$type as crevice::std430::AsStd430>::Output::debug_metrics() + ); + println!(); + println!(); + println!( + "{}", + <$type as crevice::std430::AsStd430>::Output::debug_definitions() + ); + }; +} + +#[macro_export] +macro_rules! assert_std140 { + ((size = $size:literal, align = $align:literal) $struct:ident { + $( $field:ident: $offset:literal, )* + }) => {{ + type Target = <$struct as crevice::std140::AsStd140>::Output; + + let mut fail = false; + + let actual_size = std::mem::size_of::(); + if actual_size != $size { + fail = true; + println!( + "Invalid size for std140 struct {}\n\ + Expected: {}\n\ + Actual: {}\n", + stringify!($struct), + $size, + actual_size, + ); + } + + let actual_alignment = ::ALIGNMENT; + if actual_alignment != $align { + fail = true; + println!( + "Invalid alignment for std140 struct {}\n\ + Expected: {}\n\ + Actual: {}\n", + stringify!($struct), + $align, + actual_alignment, + ); + } + + $({ + let actual_offset = memoffset::offset_of!(Target, $field); + if actual_offset != $offset { + fail = true; + println!( + "Invalid offset for field {}\n\ + Expected: {}\n\ + Actual: {}\n", + stringify!($field), + $offset, + actual_offset, + ); + } + })* + + if fail { + panic!("Invalid std140 result for {}", stringify!($struct)); + } + }}; +} + +#[macro_export] +macro_rules! assert_std430 { + ((size = $size:literal, align = $align:literal) $struct:ident { + $( $field:ident: $offset:literal, )* + }) => {{ + type Target = <$struct as crevice::std430::AsStd430>::Output; + + let mut fail = false; + + let actual_size = std::mem::size_of::(); + if actual_size != $size { + fail = true; + println!( + "Invalid size for std430 struct {}\n\ + Expected: {}\n\ + Actual: {}\n", + stringify!($struct), + $size, + actual_size, + ); + } + + let actual_alignment = ::ALIGNMENT; + if actual_alignment != $align { + fail = true; + println!( + "Invalid alignment for std430 struct {}\n\ + Expected: {}\n\ + Actual: {}\n", + stringify!($struct), + $align, + actual_alignment, + ); + } + + $({ + let actual_offset = memoffset::offset_of!(Target, $field); + if actual_offset != $offset { + fail = true; + println!( + "Invalid offset for std430 field {}\n\ + Expected: {}\n\ + Actual: {}\n", + stringify!($field), + $offset, + actual_offset, + ); + } + })* + + if fail { + panic!("Invalid std430 result for {}", stringify!($struct)); + } + }}; +} diff --git a/crates/crevice/src/glsl.rs b/crates/crevice/src/glsl.rs new file mode 100644 index 0000000000..3f879b6ab1 --- /dev/null +++ b/crates/crevice/src/glsl.rs @@ -0,0 +1,93 @@ +//! Defines traits and types for generating GLSL code from Rust definitions. + +pub use crevice_derive::GlslStruct; +use std::marker::PhantomData; + +/// Type-level linked list of array dimensions +pub struct Dimension { + _marker: PhantomData, +} + +/// Type-level linked list terminator for array dimensions. +pub struct DimensionNil; + +/// Trait for type-level array dimensions. Probably shouldn't be implemented outside this crate. +pub unsafe trait DimensionList { + /// Write dimensions in square brackets to a string, list tail to list head. + fn push_to_string(s: &mut String); +} + +unsafe impl DimensionList for DimensionNil { + fn push_to_string(_: &mut String) {} +} + +unsafe impl DimensionList for Dimension { + fn push_to_string(s: &mut String) { + use std::fmt::Write; + A::push_to_string(s); + write!(s, "[{}]", N).unwrap(); + } +} + +/// Trait for types that have a GLSL equivalent. Useful for generating GLSL code +/// from Rust structs. +pub unsafe trait Glsl { + /// The name of this type in GLSL, like `vec2` or `mat4`. + const NAME: &'static str; +} + +/// Trait for types that can be represented as a struct in GLSL. +/// +/// This trait should not generally be implemented by hand, but can be derived. +pub unsafe trait GlslStruct: Glsl { + /// The fields contained in this struct. + fn enumerate_fields(s: &mut String); + + /// Generates GLSL code that represents this struct and its fields. + fn glsl_definition() -> String { + let mut output = String::new(); + output.push_str("struct "); + output.push_str(Self::NAME); + output.push_str(" {\n"); + + Self::enumerate_fields(&mut output); + + output.push_str("};"); + output + } +} + +/// Trait for types that are expressible as a GLSL type with (possibly zero) array dimensions. +pub unsafe trait GlslArray { + /// Base type name. + const NAME: &'static str; + /// Type-level linked list of array dimensions, ordered outer to inner. + type ArraySize: DimensionList; +} + +unsafe impl GlslArray for T { + const NAME: &'static str = ::NAME; + type ArraySize = DimensionNil; +} + +unsafe impl Glsl for f32 { + const NAME: &'static str = "float"; +} + +unsafe impl Glsl for f64 { + const NAME: &'static str = "double"; +} + +unsafe impl Glsl for i32 { + const NAME: &'static str = "int"; +} + +unsafe impl Glsl for u32 { + const NAME: &'static str = "uint"; +} + +unsafe impl GlslArray for [T; N] { + const NAME: &'static str = T::NAME; + + type ArraySize = Dimension; +} diff --git a/crates/crevice/src/imp.rs b/crates/crevice/src/imp.rs new file mode 100644 index 0000000000..af49bd8915 --- /dev/null +++ b/crates/crevice/src/imp.rs @@ -0,0 +1,10 @@ +mod imp_mint; + +#[cfg(feature = "cgmath")] +mod imp_cgmath; + +#[cfg(feature = "glam")] +mod imp_glam; + +#[cfg(feature = "nalgebra")] +mod imp_nalgebra; diff --git a/crates/crevice/src/imp/imp_cgmath.rs b/crates/crevice/src/imp/imp_cgmath.rs new file mode 100644 index 0000000000..79ee7e071c --- /dev/null +++ b/crates/crevice/src/imp/imp_cgmath.rs @@ -0,0 +1,30 @@ +easy_impl! { + Vec2 cgmath::Vector2 { x, y }, + Vec3 cgmath::Vector3 { x, y, z }, + Vec4 cgmath::Vector4 { x, y, z, w }, + + IVec2 cgmath::Vector2 { x, y }, + IVec3 cgmath::Vector3 { x, y, z }, + IVec4 cgmath::Vector4 { x, y, z, w }, + + UVec2 cgmath::Vector2 { x, y }, + UVec3 cgmath::Vector3 { x, y, z }, + UVec4 cgmath::Vector4 { x, y, z, w }, + + // bool vectors are disabled due to https://github.com/LPGhatguy/crevice/issues/36 + // BVec2 cgmath::Vector2 { x, y }, + // BVec3 cgmath::Vector3 { x, y, z }, + // BVec4 cgmath::Vector4 { x, y, z, w }, + + DVec2 cgmath::Vector2 { x, y }, + DVec3 cgmath::Vector3 { x, y, z }, + DVec4 cgmath::Vector4 { x, y, z, w }, + + Mat2 cgmath::Matrix2 { x, y }, + Mat3 cgmath::Matrix3 { x, y, z }, + Mat4 cgmath::Matrix4 { x, y, z, w }, + + DMat2 cgmath::Matrix2 { x, y }, + DMat3 cgmath::Matrix3 { x, y, z }, + DMat4 cgmath::Matrix4 { x, y, z, w }, +} diff --git a/crates/crevice/src/imp/imp_glam.rs b/crates/crevice/src/imp/imp_glam.rs new file mode 100644 index 0000000000..58ef711c27 --- /dev/null +++ b/crates/crevice/src/imp/imp_glam.rs @@ -0,0 +1,24 @@ +minty_impl! { + mint::Vector2 => glam::Vec2, + mint::Vector3 => glam::Vec3, + mint::Vector4 => glam::Vec4, + mint::Vector2 => glam::IVec2, + mint::Vector3 => glam::IVec3, + mint::Vector4 => glam::IVec4, + mint::Vector2 => glam::UVec2, + mint::Vector3 => glam::UVec3, + mint::Vector4 => glam::UVec4, + // bool vectors are disabled due to https://github.com/LPGhatguy/crevice/issues/36 + // mint::Vector2 => glam::BVec2, + // mint::Vector3 => glam::BVec3, + // mint::Vector4 => glam::BVec4, + mint::Vector2 => glam::DVec2, + mint::Vector3 => glam::DVec3, + mint::Vector4 => glam::DVec4, + mint::ColumnMatrix2 => glam::Mat2, + mint::ColumnMatrix3 => glam::Mat3, + mint::ColumnMatrix4 => glam::Mat4, + mint::ColumnMatrix2 => glam::DMat2, + mint::ColumnMatrix3 => glam::DMat3, + mint::ColumnMatrix4 => glam::DMat4, +} diff --git a/crates/crevice/src/imp/imp_mint.rs b/crates/crevice/src/imp/imp_mint.rs new file mode 100644 index 0000000000..056a181c2c --- /dev/null +++ b/crates/crevice/src/imp/imp_mint.rs @@ -0,0 +1,30 @@ +easy_impl! { + Vec2 mint::Vector2 { x, y }, + Vec3 mint::Vector3 { x, y, z }, + Vec4 mint::Vector4 { x, y, z, w }, + + IVec2 mint::Vector2 { x, y }, + IVec3 mint::Vector3 { x, y, z }, + IVec4 mint::Vector4 { x, y, z, w }, + + UVec2 mint::Vector2 { x, y }, + UVec3 mint::Vector3 { x, y, z }, + UVec4 mint::Vector4 { x, y, z, w }, + + // bool vectors are disabled due to https://github.com/LPGhatguy/crevice/issues/36 + // BVec2 mint::Vector2 { x, y }, + // BVec3 mint::Vector3 { x, y, z }, + // BVec4 mint::Vector4 { x, y, z, w }, + + DVec2 mint::Vector2 { x, y }, + DVec3 mint::Vector3 { x, y, z }, + DVec4 mint::Vector4 { x, y, z, w }, + + Mat2 mint::ColumnMatrix2 { x, y }, + Mat3 mint::ColumnMatrix3 { x, y, z }, + Mat4 mint::ColumnMatrix4 { x, y, z, w }, + + DMat2 mint::ColumnMatrix2 { x, y }, + DMat3 mint::ColumnMatrix3 { x, y, z }, + DMat4 mint::ColumnMatrix4 { x, y, z, w }, +} diff --git a/crates/crevice/src/imp/imp_nalgebra.rs b/crates/crevice/src/imp/imp_nalgebra.rs new file mode 100644 index 0000000000..3d1b89c0d3 --- /dev/null +++ b/crates/crevice/src/imp/imp_nalgebra.rs @@ -0,0 +1,24 @@ +minty_impl! { + mint::Vector2 => nalgebra::Vector2, + mint::Vector3 => nalgebra::Vector3, + mint::Vector4 => nalgebra::Vector4, + mint::Vector2 => nalgebra::Vector2, + mint::Vector3 => nalgebra::Vector3, + mint::Vector4 => nalgebra::Vector4, + mint::Vector2 => nalgebra::Vector2, + mint::Vector3 => nalgebra::Vector3, + mint::Vector4 => nalgebra::Vector4, + // bool vectors are disabled due to https://github.com/LPGhatguy/crevice/issues/36 + // mint::Vector2 => nalgebra::Vector2, + // mint::Vector3 => nalgebra::Vector3, + // mint::Vector4 => nalgebra::Vector4, + mint::Vector2 => nalgebra::Vector2, + mint::Vector3 => nalgebra::Vector3, + mint::Vector4 => nalgebra::Vector4, + mint::ColumnMatrix2 => nalgebra::Matrix2, + mint::ColumnMatrix3 => nalgebra::Matrix3, + mint::ColumnMatrix4 => nalgebra::Matrix4, + mint::ColumnMatrix2 => nalgebra::Matrix2, + mint::ColumnMatrix3 => nalgebra::Matrix3, + mint::ColumnMatrix4 => nalgebra::Matrix4, +} diff --git a/crates/crevice/src/internal.rs b/crates/crevice/src/internal.rs new file mode 100644 index 0000000000..cd22972fb3 --- /dev/null +++ b/crates/crevice/src/internal.rs @@ -0,0 +1,40 @@ +//! This module is internal to crevice but used by its derive macro. No +//! guarantees are made about its contents. + +pub use bytemuck; + +/// Gives the number of bytes needed to make `offset` be aligned to `alignment`. +pub const fn align_offset(offset: usize, alignment: usize) -> usize { + if alignment == 0 || offset % alignment == 0 { + 0 + } else { + alignment - offset % alignment + } +} + +/// Max of two `usize`. Implemented because the `max` method from `Ord` cannot +/// be used in const fns. +pub const fn max(a: usize, b: usize) -> usize { + if a > b { + a + } else { + b + } +} + +/// Max of an array of `usize`. This function's implementation is funky because +/// we have no for loops! +pub const fn max_arr(input: [usize; N]) -> usize { + let mut max = 0; + let mut i = 0; + + while i < N { + if input[i] > max { + max = input[i]; + } + + i += 1; + } + + max +} diff --git a/crates/crevice/src/lib.rs b/crates/crevice/src/lib.rs new file mode 100644 index 0000000000..958d2c118b --- /dev/null +++ b/crates/crevice/src/lib.rs @@ -0,0 +1,172 @@ +#![allow( + clippy::new_without_default, + clippy::needless_update, + clippy::len_without_is_empty, + clippy::needless_range_loop +)] +/*! +[![GitHub CI Status](https://github.com/LPGhatguy/crevice/workflows/CI/badge.svg)](https://github.com/LPGhatguy/crevice/actions) +[![crevice on crates.io](https://img.shields.io/crates/v/crevice.svg)](https://crates.io/crates/crevice) +[![crevice docs](https://img.shields.io/badge/docs-docs.rs-orange.svg)](https://docs.rs/crevice) + +Crevice creates GLSL-compatible versions of types through the power of derive +macros. Generated structures provide an [`as_bytes`][std140::Std140::as_bytes] +method to allow safely packing data into buffers for uploading. + +Generated structs also implement [`bytemuck::Zeroable`] and +[`bytemuck::Pod`] for use with other libraries. + +Crevice is similar to [`glsl-layout`][glsl-layout], but supports types from many +math crates, can generate GLSL source from structs, and explicitly initializes +padding to remove one source of undefined behavior. + +Crevice has support for many Rust math libraries via feature flags, and most +other math libraries by use of the mint crate. Crevice currently supports: + +* mint 0.5, enabled by default +* cgmath 0.18, using the `cgmath` feature +* nalgebra 0.29, using the `nalgebra` feature +* glam 0.19, using the `glam` feature + +PRs are welcome to add or update math libraries to Crevice. + +If your math library is not supported, it's possible to define structs using the +types from mint and convert your math library's types into mint types. This is +supported by most Rust math libraries. + +Your math library may require you to turn on a feature flag to get mint support. +For example, cgmath requires the "mint" feature to be enabled to allow +conversions to and from mint types. + +## Examples + +### Single Value + +Uploading many types can be done by deriving [`AsStd140`][std140::AsStd140] and +using [`as_std140`][std140::AsStd140::as_std140] and +[`as_bytes`][std140::Std140::as_bytes] to turn the result into bytes. + +```glsl +uniform MAIN { + mat3 orientation; + vec3 position; + float scale; +} main; +``` + +```rust +use crevice::std140::{AsStd140, Std140}; + +#[derive(AsStd140)] +struct MainUniform { + orientation: mint::ColumnMatrix3, + position: mint::Vector3, + scale: f32, +} + +let value = MainUniform { + orientation: [ + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + ].into(), + position: [1.0, 2.0, 3.0].into(), + scale: 4.0, +}; + +let value_std140 = value.as_std140(); + +# fn upload_data_to_gpu(_value: &[u8]) {} +upload_data_to_gpu(value_std140.as_bytes()); +``` + +### Sequential Types + +More complicated data can be uploaded using the std140 +[`Writer`][std140::Writer] type. + +```glsl +struct PointLight { + vec3 position; + vec3 color; + float brightness; +}; + +buffer POINT_LIGHTS { + uint len; + PointLight[] lights; +} point_lights; +``` + +```rust +use crevice::std140::{self, AsStd140}; + +#[derive(AsStd140)] +struct PointLight { + position: mint::Vector3, + color: mint::Vector3, + brightness: f32, +} + +let lights = vec![ + PointLight { + position: [0.0, 1.0, 0.0].into(), + color: [1.0, 0.0, 0.0].into(), + brightness: 0.6, + }, + PointLight { + position: [0.0, 4.0, 3.0].into(), + color: [1.0, 1.0, 1.0].into(), + brightness: 1.0, + }, +]; + +# fn map_gpu_buffer_for_write() -> &'static mut [u8] { +# Box::leak(vec![0; 1024].into_boxed_slice()) +# } +let target_buffer = map_gpu_buffer_for_write(); +let mut writer = std140::Writer::new(target_buffer); + +let light_count = lights.len() as u32; +writer.write(&light_count)?; + +// Crevice will automatically insert the required padding to align the +// PointLight structure correctly. In this case, there will be 12 bytes of +// padding between the length field and the light list. + +writer.write(lights.as_slice())?; + +# fn unmap_gpu_buffer() {} +unmap_gpu_buffer(); + +# Ok::<(), std::io::Error>(()) +``` + +## Features + +* `std` (default): Enables [`std::io::Write`]-based structs. +* `cgmath`: Enables support for types from cgmath. +* `nalgebra`: Enables support for types from nalgebra. +* `glam`: Enables support for types from glam. + +## Minimum Supported Rust Version (MSRV) + +Crevice supports Rust 1.52.1 and newer due to use of new `const fn` features. + +[glsl-layout]: https://github.com/rustgd/glsl-layout +*/ + +#![deny(missing_docs)] +#![cfg_attr(not(feature = "std"), no_std)] + +#[macro_use] +mod util; + +pub mod glsl; +pub mod std140; +pub mod std430; + +#[doc(hidden)] +pub mod internal; + +mod imp; diff --git a/crates/crevice/src/std140.rs b/crates/crevice/src/std140.rs new file mode 100644 index 0000000000..184b99a92d --- /dev/null +++ b/crates/crevice/src/std140.rs @@ -0,0 +1,18 @@ +//! Defines traits and types for working with data adhering to GLSL's `std140` +//! layout specification. + +mod dynamic_uniform; +mod primitives; +mod sizer; +mod traits; +#[cfg(feature = "std")] +mod writer; + +pub use self::dynamic_uniform::*; +pub use self::primitives::*; +pub use self::sizer::*; +pub use self::traits::*; +#[cfg(feature = "std")] +pub use self::writer::*; + +pub use crevice_derive::AsStd140; diff --git a/crates/crevice/src/std140/dynamic_uniform.rs b/crates/crevice/src/std140/dynamic_uniform.rs new file mode 100644 index 0000000000..262f8ea449 --- /dev/null +++ b/crates/crevice/src/std140/dynamic_uniform.rs @@ -0,0 +1,68 @@ +use bytemuck::{Pod, Zeroable}; + +#[allow(unused_imports)] +use crate::internal::{align_offset, max}; +use crate::std140::{AsStd140, Std140}; + +/// Wrapper type that aligns the inner type to at least 256 bytes. +/// +/// This type is useful for ensuring correct alignment when creating dynamic +/// uniform buffers in APIs like WebGPU. +pub struct DynamicUniform(pub T); + +impl AsStd140 for DynamicUniform { + type Output = DynamicUniformStd140<::Output>; + + fn as_std140(&self) -> Self::Output { + DynamicUniformStd140(self.0.as_std140()) + } + + fn from_std140(value: Self::Output) -> Self { + DynamicUniform(::from_std140(value.0)) + } +} + +/// std140 variant of [`DynamicUniform`]. +#[derive(Clone, Copy)] +#[repr(transparent)] +pub struct DynamicUniformStd140(T); + +unsafe impl Std140 for DynamicUniformStd140 { + const ALIGNMENT: usize = max(256, T::ALIGNMENT); + #[cfg(const_evaluatable_checked)] + type Padded = crate::std140::Std140Padded< + Self, + { align_offset(core::mem::size_of::(), max(256, T::ALIGNMENT)) }, + >; + #[cfg(not(const_evaluatable_checked))] + type Padded = crate::std140::InvalidPadded; +} + +unsafe impl Zeroable for DynamicUniformStd140 {} +unsafe impl Pod for DynamicUniformStd140 {} + +#[cfg(test)] +mod test { + use super::*; + + use crate::std140::{self, WriteStd140}; + + #[test] + fn size_is_unchanged() { + let dynamic_f32 = DynamicUniform(0.0f32); + + assert_eq!(dynamic_f32.std140_size(), 0.0f32.std140_size()); + } + + #[test] + fn alignment_applies() { + let mut output = Vec::new(); + let mut writer = std140::Writer::new(&mut output); + + writer.write(&DynamicUniform(0.0f32)).unwrap(); + assert_eq!(writer.len(), 4); + + writer.write(&DynamicUniform(1.0f32)).unwrap(); + assert_eq!(writer.len(), 260); + } +} diff --git a/crates/crevice/src/std140/primitives.rs b/crates/crevice/src/std140/primitives.rs new file mode 100644 index 0000000000..34e161e3b7 --- /dev/null +++ b/crates/crevice/src/std140/primitives.rs @@ -0,0 +1,175 @@ +use bytemuck::{Pod, Zeroable}; + +use crate::glsl::Glsl; +use crate::std140::{Std140, Std140Padded}; + +use crate::internal::{align_offset, max}; +use core::mem::size_of; + +unsafe impl Std140 for f32 { + const ALIGNMENT: usize = 4; + type Padded = Std140Padded; +} + +unsafe impl Std140 for f64 { + const ALIGNMENT: usize = 8; + type Padded = Std140Padded; +} + +unsafe impl Std140 for i32 { + const ALIGNMENT: usize = 4; + type Padded = Std140Padded; +} + +unsafe impl Std140 for u32 { + const ALIGNMENT: usize = 4; + type Padded = Std140Padded; +} + +macro_rules! vectors { + ( + $( + #[$doc:meta] align($align:literal) $glsl_name:ident $name:ident <$prim:ident> ($($field:ident),+) + )+ + ) => { + $( + #[$doc] + #[allow(missing_docs)] + #[derive(Debug, Clone, Copy, PartialEq)] + #[repr(C)] + pub struct $name { + $(pub $field: $prim,)+ + } + + unsafe impl Zeroable for $name {} + unsafe impl Pod for $name {} + + unsafe impl Std140 for $name { + const ALIGNMENT: usize = $align; + type Padded = Std140Padded(), max(16, $align))}>; + } + + unsafe impl Glsl for $name { + const NAME: &'static str = stringify!($glsl_name); + } + )+ + }; +} + +vectors! { + #[doc = "Corresponds to a GLSL `vec2` in std140 layout."] align(8) vec2 Vec2(x, y) + #[doc = "Corresponds to a GLSL `vec3` in std140 layout."] align(16) vec3 Vec3(x, y, z) + #[doc = "Corresponds to a GLSL `vec4` in std140 layout."] align(16) vec4 Vec4(x, y, z, w) + + #[doc = "Corresponds to a GLSL `ivec2` in std140 layout."] align(8) ivec2 IVec2(x, y) + #[doc = "Corresponds to a GLSL `ivec3` in std140 layout."] align(16) ivec3 IVec3(x, y, z) + #[doc = "Corresponds to a GLSL `ivec4` in std140 layout."] align(16) ivec4 IVec4(x, y, z, w) + + #[doc = "Corresponds to a GLSL `uvec2` in std140 layout."] align(8) uvec2 UVec2(x, y) + #[doc = "Corresponds to a GLSL `uvec3` in std140 layout."] align(16) uvec3 UVec3(x, y, z) + #[doc = "Corresponds to a GLSL `uvec4` in std140 layout."] align(16) uvec4 UVec4(x, y, z, w) + + // bool vectors are disabled due to https://github.com/LPGhatguy/crevice/issues/36 + + // #[doc = "Corresponds to a GLSL `bvec2` in std140 layout."] align(8) bvec2 BVec2(x, y) + // #[doc = "Corresponds to a GLSL `bvec3` in std140 layout."] align(16) bvec3 BVec3(x, y, z) + // #[doc = "Corresponds to a GLSL `bvec4` in std140 layout."] align(16) bvec4 BVec4(x, y, z, w) + + #[doc = "Corresponds to a GLSL `dvec2` in std140 layout."] align(16) dvec2 DVec2(x, y) + #[doc = "Corresponds to a GLSL `dvec3` in std140 layout."] align(32) dvec3 DVec3(x, y, z) + #[doc = "Corresponds to a GLSL `dvec4` in std140 layout."] align(32) dvec4 DVec4(x, y, z, w) +} + +macro_rules! matrices { + ( + $( + #[$doc:meta] + align($align:literal) + $glsl_name:ident $name:ident { + $($field:ident: $field_ty:ty,)+ + } + )+ + ) => { + $( + #[$doc] + #[allow(missing_docs)] + #[derive(Debug, Clone, Copy)] + #[repr(C)] + pub struct $name { + $(pub $field: $field_ty,)+ + } + + unsafe impl Zeroable for $name {} + unsafe impl Pod for $name {} + + unsafe impl Std140 for $name { + const ALIGNMENT: usize = $align; + /// Matrices are technically arrays of primitives, and as such require pad at end. + const PAD_AT_END: bool = true; + type Padded = Std140Padded(), max(16, $align))}>; + } + + unsafe impl Glsl for $name { + const NAME: &'static str = stringify!($glsl_name); + } + )+ + }; +} + +matrices! { + #[doc = "Corresponds to a GLSL `mat2` in std140 layout."] + align(16) + mat2 Mat2 { + x: Vec2, + _pad_x: [f32; 2], + y: Vec2, + _pad_y: [f32; 2], + } + + #[doc = "Corresponds to a GLSL `mat3` in std140 layout."] + align(16) + mat3 Mat3 { + x: Vec3, + _pad_x: f32, + y: Vec3, + _pad_y: f32, + z: Vec3, + _pad_z: f32, + } + + #[doc = "Corresponds to a GLSL `mat4` in std140 layout."] + align(16) + mat4 Mat4 { + x: Vec4, + y: Vec4, + z: Vec4, + w: Vec4, + } + + #[doc = "Corresponds to a GLSL `dmat2` in std140 layout."] + align(16) + dmat2 DMat2 { + x: DVec2, + y: DVec2, + } + + #[doc = "Corresponds to a GLSL `dmat3` in std140 layout."] + align(32) + dmat3 DMat3 { + x: DVec3, + _pad_x: f64, + y: DVec3, + _pad_y: f64, + z: DVec3, + _pad_z: f64, + } + + #[doc = "Corresponds to a GLSL `dmat3` in std140 layout."] + align(32) + dmat4 DMat4 { + x: DVec4, + y: DVec4, + z: DVec4, + w: DVec4, + } +} diff --git a/crates/crevice/src/std140/sizer.rs b/crates/crevice/src/std140/sizer.rs new file mode 100644 index 0000000000..ee5c134595 --- /dev/null +++ b/crates/crevice/src/std140/sizer.rs @@ -0,0 +1,81 @@ +use core::mem::size_of; + +use crate::internal::align_offset; +use crate::std140::{AsStd140, Std140}; + +/** +Type that computes the buffer size needed by a series of `std140` types laid +out. + +This type works well well when paired with `Writer`, precomputing a buffer's +size to alleviate the need to dynamically re-allocate buffers. + +## Example + +```glsl +struct Frob { + vec3 size; + float frobiness; +} + +buffer FROBS { + uint len; + Frob[] frobs; +} frobs; +``` + +``` +use crevice::std140::{self, AsStd140}; + +#[derive(AsStd140)] +struct Frob { + size: mint::Vector3, + frobiness: f32, +} + +// Many APIs require that buffers contain at least enough space for all +// fixed-size bindiongs to a buffer as well as one element of any arrays, if +// there are any. +let mut sizer = std140::Sizer::new(); +sizer.add::(); +sizer.add::(); + +# fn create_buffer_with_size(size: usize) {} +let buffer = create_buffer_with_size(sizer.len()); +# assert_eq!(sizer.len(), 32); +``` +*/ +pub struct Sizer { + offset: usize, +} + +impl Sizer { + /// Create a new `Sizer`. + pub fn new() -> Self { + Self { offset: 0 } + } + + /// Add a type's necessary padding and size to the `Sizer`. Returns the + /// offset into the buffer where that type would be written. + pub fn add(&mut self) -> usize + where + T: AsStd140, + { + let size = size_of::<::Output>(); + let alignment = ::Output::ALIGNMENT; + let padding = align_offset(self.offset, alignment); + + self.offset += padding; + let write_here = self.offset; + + self.offset += size; + + write_here + } + + /// Returns the number of bytes required to contain all the types added to + /// the `Sizer`. + pub fn len(&self) -> usize { + self.offset + } +} diff --git a/crates/crevice/src/std140/traits.rs b/crates/crevice/src/std140/traits.rs new file mode 100644 index 0000000000..caa8fce6fd --- /dev/null +++ b/crates/crevice/src/std140/traits.rs @@ -0,0 +1,284 @@ +use core::mem::{size_of, MaybeUninit}; +#[cfg(feature = "std")] +use std::io::{self, Write}; + +use bytemuck::{bytes_of, Pod, Zeroable}; + +#[cfg(feature = "std")] +use crate::std140::Writer; + +/// Trait implemented for all `std140` primitives. Generally should not be +/// implemented outside this crate. +pub unsafe trait Std140: Copy + Zeroable + Pod { + /// The required alignment of the type. Must be a power of two. + /// + /// This is distinct from the value returned by `std::mem::align_of` because + /// `AsStd140` structs do not use Rust's alignment. This enables them to + /// control and zero their padding bytes, making converting them to and from + /// slices safe. + const ALIGNMENT: usize; + + /// Whether this type requires a padding at the end (ie, is a struct or an array + /// of primitives). + /// See + /// (rule 4 and 9) + const PAD_AT_END: bool = false; + /// Padded type (Std140Padded specialization) + /// The usual implementation is + /// type Padded = Std140Padded(), max(16, ALIGNMENT))}>; + type Padded: Std140Convertible; + + /// Casts the type to a byte array. Implementors should not override this + /// method. + /// + /// # Safety + /// This is always safe due to the requirements of [`bytemuck::Pod`] being a + /// prerequisite for this trait. + fn as_bytes(&self) -> &[u8] { + bytes_of(self) + } +} + +/// Trait specifically for Std140::Padded, implements conversions between padded type and base type. +pub trait Std140Convertible: Copy { + /// Convert from self to Std140 + fn into_std140(self) -> T; + /// Convert from Std140 to self + fn from_std140(_: T) -> Self; +} + +impl Std140Convertible for T { + fn into_std140(self) -> T { + self + } + fn from_std140(also_self: T) -> Self { + also_self + } +} + +/// Unfortunately, we cannot easily derive padded representation for generic Std140 types. +/// For now, we'll just use this empty enum with no values. +#[derive(Copy, Clone)] +pub enum InvalidPadded {} +impl Std140Convertible for InvalidPadded { + fn into_std140(self) -> T { + unimplemented!() + } + fn from_std140(_: T) -> Self { + unimplemented!() + } +} +/** +Trait implemented for all types that can be turned into `std140` values. +* +This trait can often be `#[derive]`'d instead of manually implementing it. Any +struct which contains only fields that also implement `AsStd140` can derive +`AsStd140`. + +Types from the mint crate implement `AsStd140`, making them convenient for use +in uniform types. Most Rust math crates, like cgmath, nalgebra, and +ultraviolet support mint. + +## Example + +```glsl +uniform CAMERA { + mat4 view; + mat4 projection; +} camera; +``` + +```no_run +use crevice::std140::{AsStd140, Std140}; + +#[derive(AsStd140)] +struct CameraUniform { + view: mint::ColumnMatrix4, + projection: mint::ColumnMatrix4, +} + +let view: mint::ColumnMatrix4 = todo!("your math code here"); +let projection: mint::ColumnMatrix4 = todo!("your math code here"); + +let camera = CameraUniform { + view, + projection, +}; + +# fn write_to_gpu_buffer(bytes: &[u8]) {} +let camera_std140 = camera.as_std140(); +write_to_gpu_buffer(camera_std140.as_bytes()); +``` +*/ +pub trait AsStd140 { + /// The `std140` version of this value. + type Output: Std140; + + /// Convert this value into the `std140` version of itself. + fn as_std140(&self) -> Self::Output; + + /// Returns the size of the `std140` version of this type. Useful for + /// pre-sizing buffers. + fn std140_size_static() -> usize { + size_of::() + } + + /// Converts from `std140` version of self to self. + fn from_std140(val: Self::Output) -> Self; +} + +impl AsStd140 for T +where + T: Std140, +{ + type Output = Self; + + fn as_std140(&self) -> Self { + *self + } + + fn from_std140(x: Self) -> Self { + x + } +} + +#[doc(hidden)] +#[derive(Copy, Clone, Debug)] +pub struct Std140Padded { + inner: T, + _padding: [u8; PAD], +} + +unsafe impl Zeroable for Std140Padded {} +unsafe impl Pod for Std140Padded {} + +impl Std140Convertible for Std140Padded { + fn into_std140(self) -> T { + self.inner + } + + fn from_std140(inner: T) -> Self { + Self { + inner, + _padding: [0u8; PAD], + } + } +} + +#[doc(hidden)] +#[derive(Copy, Clone, Debug)] +#[repr(transparent)] +pub struct Std140Array([T::Padded; N]); + +unsafe impl Zeroable for Std140Array where T::Padded: Zeroable {} +unsafe impl Pod for Std140Array where T::Padded: Pod {} +unsafe impl Std140 for Std140Array +where + T::Padded: Pod, +{ + const ALIGNMENT: usize = crate::internal::max(T::ALIGNMENT, 16); + type Padded = Self; +} + +impl Std140Array { + fn uninit_array() -> [MaybeUninit; N] { + unsafe { MaybeUninit::uninit().assume_init() } + } + + fn from_uninit_array(a: [MaybeUninit; N]) -> Self { + unsafe { core::mem::transmute_copy(&a) } + } +} + +impl AsStd140 for [T; N] +where + ::Padded: Pod, +{ + type Output = Std140Array; + fn as_std140(&self) -> Self::Output { + let mut res = Self::Output::uninit_array(); + + for i in 0..N { + res[i] = MaybeUninit::new(Std140Convertible::from_std140(self[i].as_std140())); + } + + Self::Output::from_uninit_array(res) + } + + fn from_std140(val: Self::Output) -> Self { + let mut res: [MaybeUninit; N] = unsafe { MaybeUninit::uninit().assume_init() }; + for i in 0..N { + res[i] = MaybeUninit::new(T::from_std140(Std140Convertible::into_std140(val.0[i]))); + } + unsafe { core::mem::transmute_copy(&res) } + } +} + +/// Trait implemented for all types that can be written into a buffer as +/// `std140` bytes. This type is more general than [`AsStd140`]: all `AsStd140` +/// types implement `WriteStd140`, but not the other way around. +/// +/// While `AsStd140` requires implementers to return a type that implements the +/// `Std140` trait, `WriteStd140` directly writes bytes using a [`Writer`]. This +/// makes `WriteStd140` usable for writing slices or other DSTs that could not +/// implement `AsStd140` without allocating new memory on the heap. +#[cfg(feature = "std")] +pub trait WriteStd140 { + /// Writes this value into the given [`Writer`] using `std140` layout rules. + /// + /// Should return the offset of the first byte of this type, as returned by + /// the first call to [`Writer::write`]. + fn write_std140(&self, writer: &mut Writer) -> io::Result; + + /// The space required to write this value using `std140` layout rules. This + /// does not include alignment padding that may be needed before or after + /// this type when written as part of a larger buffer. + fn std140_size(&self) -> usize { + let mut writer = Writer::new(io::sink()); + self.write_std140(&mut writer).unwrap(); + writer.len() + } +} + +#[cfg(feature = "std")] +impl WriteStd140 for T +where + T: AsStd140, +{ + fn write_std140(&self, writer: &mut Writer) -> io::Result { + writer.write_std140(&self.as_std140()) + } + + fn std140_size(&self) -> usize { + size_of::<::Output>() + } +} + +#[cfg(feature = "std")] +impl WriteStd140 for [T] +where + T: WriteStd140, +{ + fn write_std140(&self, writer: &mut Writer) -> io::Result { + // if no items are written, offset is current position of the writer + let mut offset = writer.len(); + + let mut iter = self.iter(); + + if let Some(item) = iter.next() { + offset = item.write_std140(writer)?; + } + + for item in iter { + item.write_std140(writer)?; + } + + Ok(offset) + } + + fn std140_size(&self) -> usize { + let mut writer = Writer::new(io::sink()); + self.write_std140(&mut writer).unwrap(); + writer.len() + } +} diff --git a/crates/crevice/src/std140/writer.rs b/crates/crevice/src/std140/writer.rs new file mode 100644 index 0000000000..2a4dbc550f --- /dev/null +++ b/crates/crevice/src/std140/writer.rs @@ -0,0 +1,162 @@ +use std::io::{self, Write}; +use std::mem::size_of; + +use bytemuck::bytes_of; + +use crate::internal::align_offset; +use crate::std140::{AsStd140, Std140, WriteStd140}; + +/** +Type that enables writing correctly aligned `std140` values to a buffer. + +`Writer` is useful when many values need to be laid out in a row that cannot be +represented by a struct alone, like dynamically sized arrays or dynamically +laid-out values. + +## Example +In this example, we'll write a length-prefixed list of lights to a buffer. +`std140::Writer` helps align correctly, even across multiple structs, which can +be tricky and error-prone otherwise. + +```glsl +struct PointLight { + vec3 position; + vec3 color; + float brightness; +}; + +buffer POINT_LIGHTS { + uint len; + PointLight[] lights; +} point_lights; +``` + +``` +use crevice::std140::{self, AsStd140}; + +#[derive(AsStd140)] +struct PointLight { + position: mint::Vector3, + color: mint::Vector3, + brightness: f32, +} + +let lights = vec![ + PointLight { + position: [0.0, 1.0, 0.0].into(), + color: [1.0, 0.0, 0.0].into(), + brightness: 0.6, + }, + PointLight { + position: [0.0, 4.0, 3.0].into(), + color: [1.0, 1.0, 1.0].into(), + brightness: 1.0, + }, +]; + +# fn map_gpu_buffer_for_write() -> &'static mut [u8] { +# Box::leak(vec![0; 1024].into_boxed_slice()) +# } +let target_buffer = map_gpu_buffer_for_write(); +let mut writer = std140::Writer::new(target_buffer); + +let light_count = lights.len() as u32; +writer.write(&light_count)?; + +// Crevice will automatically insert the required padding to align the +// PointLight structure correctly. In this case, there will be 12 bytes of +// padding between the length field and the light list. + +writer.write(lights.as_slice())?; + +# fn unmap_gpu_buffer() {} +unmap_gpu_buffer(); + +# Ok::<(), std::io::Error>(()) +``` +*/ +pub struct Writer { + writer: W, + offset: usize, +} + +impl Writer { + /// Create a new `Writer`, wrapping a buffer, file, or other type that + /// implements [`std::io::Write`]. + pub fn new(writer: W) -> Self { + Self { writer, offset: 0 } + } + + /// Write a new value to the underlying buffer, writing zeroed padding where + /// necessary. + /// + /// Returns the offset into the buffer that the value was written to. + pub fn write(&mut self, value: &T) -> io::Result + where + T: WriteStd140 + ?Sized, + { + value.write_std140(self) + } + + /// Write an iterator of values to the underlying buffer. + /// + /// Returns the offset into the buffer that the first value was written to. + /// If no values were written, returns the `len()`. + pub fn write_iter(&mut self, iter: I) -> io::Result + where + I: IntoIterator, + T: WriteStd140, + { + let mut offset = self.offset; + + let mut iter = iter.into_iter(); + + if let Some(item) = iter.next() { + offset = item.write_std140(self)?; + } + + for item in iter { + item.write_std140(self)?; + } + + Ok(offset) + } + + /// Write an `Std140` type to the underlying buffer. + pub fn write_std140(&mut self, value: &T) -> io::Result + where + T: Std140, + { + let padding = align_offset(self.offset, T::ALIGNMENT); + + for _ in 0..padding { + self.writer.write_all(&[0])?; + } + self.offset += padding; + + let value = value.as_std140(); + self.writer.write_all(bytes_of(&value))?; + + let write_here = self.offset; + self.offset += size_of::(); + + Ok(write_here) + } + + /// Write a slice of values to the underlying buffer. + #[deprecated( + since = "0.6.0", + note = "Use `write` instead -- it now works on slices." + )] + pub fn write_slice(&mut self, slice: &[T]) -> io::Result + where + T: AsStd140, + { + self.write(slice) + } + + /// Returns the amount of data written by this `Writer`. + pub fn len(&self) -> usize { + self.offset + } +} diff --git a/crates/crevice/src/std430.rs b/crates/crevice/src/std430.rs new file mode 100644 index 0000000000..7e1af050ce --- /dev/null +++ b/crates/crevice/src/std430.rs @@ -0,0 +1,16 @@ +//! Defines traits and types for working with data adhering to GLSL's `std140` +//! layout specification. + +mod primitives; +mod sizer; +mod traits; +#[cfg(feature = "std")] +mod writer; + +pub use self::primitives::*; +pub use self::sizer::*; +pub use self::traits::*; +#[cfg(feature = "std")] +pub use self::writer::*; + +pub use crevice_derive::AsStd430; diff --git a/crates/crevice/src/std430/primitives.rs b/crates/crevice/src/std430/primitives.rs new file mode 100644 index 0000000000..3348e82c7b --- /dev/null +++ b/crates/crevice/src/std430/primitives.rs @@ -0,0 +1,173 @@ +use bytemuck::{Pod, Zeroable}; + +use crate::glsl::Glsl; +use crate::std430::{Std430, Std430Padded}; + +use crate::internal::align_offset; +use core::mem::size_of; + +unsafe impl Std430 for f32 { + const ALIGNMENT: usize = 4; + type Padded = Self; +} + +unsafe impl Std430 for f64 { + const ALIGNMENT: usize = 8; + type Padded = Self; +} + +unsafe impl Std430 for i32 { + const ALIGNMENT: usize = 4; + type Padded = Self; +} + +unsafe impl Std430 for u32 { + const ALIGNMENT: usize = 4; + type Padded = Self; +} + +macro_rules! vectors { + ( + $( + #[$doc:meta] align($align:literal) $glsl_name:ident $name:ident <$prim:ident> ($($field:ident),+) + )+ + ) => { + $( + #[$doc] + #[allow(missing_docs)] + #[derive(Debug, Clone, Copy)] + #[repr(C)] + pub struct $name { + $(pub $field: $prim,)+ + } + + unsafe impl Zeroable for $name {} + unsafe impl Pod for $name {} + + unsafe impl Std430 for $name { + const ALIGNMENT: usize = $align; + type Padded = Std430Padded(), $align)}>; + } + + unsafe impl Glsl for $name { + const NAME: &'static str = stringify!($glsl_name); + } + )+ + }; +} + +vectors! { + #[doc = "Corresponds to a GLSL `vec2` in std430 layout."] align(8) vec2 Vec2(x, y) + #[doc = "Corresponds to a GLSL `vec3` in std430 layout."] align(16) vec3 Vec3(x, y, z) + #[doc = "Corresponds to a GLSL `vec4` in std430 layout."] align(16) vec4 Vec4(x, y, z, w) + + #[doc = "Corresponds to a GLSL `ivec2` in std430 layout."] align(8) ivec2 IVec2(x, y) + #[doc = "Corresponds to a GLSL `ivec3` in std430 layout."] align(16) ivec3 IVec3(x, y, z) + #[doc = "Corresponds to a GLSL `ivec4` in std430 layout."] align(16) ivec4 IVec4(x, y, z, w) + + #[doc = "Corresponds to a GLSL `uvec2` in std430 layout."] align(8) uvec2 UVec2(x, y) + #[doc = "Corresponds to a GLSL `uvec3` in std430 layout."] align(16) uvec3 UVec3(x, y, z) + #[doc = "Corresponds to a GLSL `uvec4` in std430 layout."] align(16) uvec4 UVec4(x, y, z, w) + + // bool vectors are disabled due to https://github.com/LPGhatguy/crevice/issues/36 + + // #[doc = "Corresponds to a GLSL `bvec2` in std430 layout."] align(8) bvec2 BVec2(x, y) + // #[doc = "Corresponds to a GLSL `bvec3` in std430 layout."] align(16) bvec3 BVec3(x, y, z) + // #[doc = "Corresponds to a GLSL `bvec4` in std430 layout."] align(16) bvec4 BVec4(x, y, z, w) + + #[doc = "Corresponds to a GLSL `dvec2` in std430 layout."] align(16) dvec2 DVec2(x, y) + #[doc = "Corresponds to a GLSL `dvec3` in std430 layout."] align(32) dvec3 DVec3(x, y, z) + #[doc = "Corresponds to a GLSL `dvec4` in std430 layout."] align(32) dvec4 DVec4(x, y, z, w) +} + +macro_rules! matrices { + ( + $( + #[$doc:meta] + align($align:literal) + $glsl_name:ident $name:ident { + $($field:ident: $field_ty:ty,)+ + } + )+ + ) => { + $( + #[$doc] + #[allow(missing_docs)] + #[derive(Debug, Clone, Copy)] + #[repr(C)] + pub struct $name { + $(pub $field: $field_ty,)+ + } + + unsafe impl Zeroable for $name {} + unsafe impl Pod for $name {} + + unsafe impl Std430 for $name { + const ALIGNMENT: usize = $align; + /// Matrices are technically arrays of primitives, and as such require pad at end. + const PAD_AT_END: bool = true; + type Padded = Std430Padded(), $align)}>; + } + + unsafe impl Glsl for $name { + const NAME: &'static str = stringify!($glsl_name); + } + )+ + }; +} + +matrices! { + #[doc = "Corresponds to a GLSL `mat2` in std430 layout."] + align(8) + mat2 Mat2 { + x: Vec2, + y: Vec2, + } + + #[doc = "Corresponds to a GLSL `mat3` in std430 layout."] + align(16) + mat3 Mat3 { + x: Vec3, + _pad_x: f32, + y: Vec3, + _pad_y: f32, + z: Vec3, + _pad_z: f32, + } + + #[doc = "Corresponds to a GLSL `mat4` in std430 layout."] + align(16) + mat4 Mat4 { + x: Vec4, + y: Vec4, + z: Vec4, + w: Vec4, + } + + #[doc = "Corresponds to a GLSL `dmat2` in std430 layout."] + align(16) + dmat2 DMat2 { + x: DVec2, + y: DVec2, + } + + #[doc = "Corresponds to a GLSL `dmat3` in std430 layout."] + align(32) + dmat3 DMat3 { + x: DVec3, + _pad_x: f64, + y: DVec3, + _pad_y: f64, + z: DVec3, + _pad_z: f64, + } + + #[doc = "Corresponds to a GLSL `dmat3` in std430 layout."] + align(32) + dmat4 DMat4 { + x: DVec4, + y: DVec4, + z: DVec4, + w: DVec4, + } +} diff --git a/crates/crevice/src/std430/sizer.rs b/crates/crevice/src/std430/sizer.rs new file mode 100644 index 0000000000..20b7b29e0b --- /dev/null +++ b/crates/crevice/src/std430/sizer.rs @@ -0,0 +1,81 @@ +use core::mem::size_of; + +use crate::internal::align_offset; +use crate::std430::{AsStd430, Std430}; + +/** +Type that computes the buffer size needed by a series of `std430` types laid +out. + +This type works well well when paired with `Writer`, precomputing a buffer's +size to alleviate the need to dynamically re-allocate buffers. + +## Example + +```glsl +struct Frob { + vec3 size; + float frobiness; +} + +buffer FROBS { + uint len; + Frob[] frobs; +} frobs; +``` + +``` +use crevice::std430::{self, AsStd430}; + +#[derive(AsStd430)] +struct Frob { + size: mint::Vector3, + frobiness: f32, +} + +// Many APIs require that buffers contain at least enough space for all +// fixed-size bindiongs to a buffer as well as one element of any arrays, if +// there are any. +let mut sizer = std430::Sizer::new(); +sizer.add::(); +sizer.add::(); + +# fn create_buffer_with_size(size: usize) {} +let buffer = create_buffer_with_size(sizer.len()); +# assert_eq!(sizer.len(), 32); +``` +*/ +pub struct Sizer { + offset: usize, +} + +impl Sizer { + /// Create a new `Sizer`. + pub fn new() -> Self { + Self { offset: 0 } + } + + /// Add a type's necessary padding and size to the `Sizer`. Returns the + /// offset into the buffer where that type would be written. + pub fn add(&mut self) -> usize + where + T: AsStd430, + { + let size = size_of::<::Output>(); + let alignment = ::Output::ALIGNMENT; + let padding = align_offset(self.offset, alignment); + + self.offset += padding; + let write_here = self.offset; + + self.offset += size; + + write_here + } + + /// Returns the number of bytes required to contain all the types added to + /// the `Sizer`. + pub fn len(&self) -> usize { + self.offset + } +} diff --git a/crates/crevice/src/std430/traits.rs b/crates/crevice/src/std430/traits.rs new file mode 100644 index 0000000000..6206ec7ac6 --- /dev/null +++ b/crates/crevice/src/std430/traits.rs @@ -0,0 +1,283 @@ +use core::mem::{size_of, MaybeUninit}; +#[cfg(feature = "std")] +use std::io::{self, Write}; + +use bytemuck::{bytes_of, Pod, Zeroable}; + +#[cfg(feature = "std")] +use crate::std430::Writer; + +/// Trait implemented for all `std430` primitives. Generally should not be +/// implemented outside this crate. +pub unsafe trait Std430: Copy + Zeroable + Pod { + /// The required alignment of the type. Must be a power of two. + /// + /// This is distinct from the value returned by `std::mem::align_of` because + /// `AsStd430` structs do not use Rust's alignment. This enables them to + /// control and zero their padding bytes, making converting them to and from + /// slices safe. + const ALIGNMENT: usize; + + /// Whether this type requires a padding at the end (ie, is a struct or an array + /// of primitives). + /// See + /// (rule 4 and 9) + const PAD_AT_END: bool = false; + /// Padded type (Std430Padded specialization) + /// The usual implementation is + /// type Padded = Std430Padded(), ALIGNMENT)}>; + type Padded: Std430Convertible; + + /// Casts the type to a byte array. Implementors should not override this + /// method. + /// + /// # Safety + /// This is always safe due to the requirements of [`bytemuck::Pod`] being a + /// prerequisite for this trait. + fn as_bytes(&self) -> &[u8] { + bytes_of(self) + } +} + +/// Trait specifically for Std430::Padded, implements conversions between padded type and base type. +pub trait Std430Convertible: Copy { + /// Convert from self to Std430 + fn into_std430(self) -> T; + /// Convert from Std430 to self + fn from_std430(_: T) -> Self; +} + +impl Std430Convertible for T { + fn into_std430(self) -> T { + self + } + fn from_std430(also_self: T) -> Self { + also_self + } +} + +/// Unfortunately, we cannot easily derive padded representation for generic Std140 types. +/// For now, we'll just use this empty enum with no values. +#[derive(Copy, Clone)] +pub enum InvalidPadded {} +impl Std430Convertible for InvalidPadded { + fn into_std430(self) -> T { + unimplemented!() + } + fn from_std430(_: T) -> Self { + unimplemented!() + } +} +/** +Trait implemented for all types that can be turned into `std430` values. + +This trait can often be `#[derive]`'d instead of manually implementing it. Any +struct which contains only fields that also implement `AsStd430` can derive +`AsStd430`. + +Types from the mint crate implement `AsStd430`, making them convenient for use +in uniform types. Most Rust geometry crates, like cgmath, nalgebra, and +ultraviolet support mint. + +## Example + +```glsl +uniform CAMERA { + mat4 view; + mat4 projection; +} camera; +``` + +```no_run +use crevice::std430::{AsStd430, Std430}; + +#[derive(AsStd430)] +struct CameraUniform { + view: mint::ColumnMatrix4, + projection: mint::ColumnMatrix4, +} + +let view: mint::ColumnMatrix4 = todo!("your math code here"); +let projection: mint::ColumnMatrix4 = todo!("your math code here"); + +let camera = CameraUniform { + view, + projection, +}; + +# fn write_to_gpu_buffer(bytes: &[u8]) {} +let camera_std430 = camera.as_std430(); +write_to_gpu_buffer(camera_std430.as_bytes()); +``` +*/ +pub trait AsStd430 { + /// The `std430` version of this value. + type Output: Std430; + + /// Convert this value into the `std430` version of itself. + fn as_std430(&self) -> Self::Output; + + /// Returns the size of the `std430` version of this type. Useful for + /// pre-sizing buffers. + fn std430_size_static() -> usize { + size_of::() + } + + /// Converts from `std430` version of self to self. + fn from_std430(value: Self::Output) -> Self; +} + +impl AsStd430 for T +where + T: Std430, +{ + type Output = Self; + + fn as_std430(&self) -> Self { + *self + } + + fn from_std430(value: Self) -> Self { + value + } +} + +#[doc(hidden)] +#[derive(Copy, Clone, Debug)] +pub struct Std430Padded { + inner: T, + _padding: [u8; PAD], +} + +unsafe impl Zeroable for Std430Padded {} +unsafe impl Pod for Std430Padded {} + +impl Std430Convertible for Std430Padded { + fn into_std430(self) -> T { + self.inner + } + + fn from_std430(inner: T) -> Self { + Self { + inner, + _padding: [0u8; PAD], + } + } +} + +#[doc(hidden)] +#[derive(Copy, Clone, Debug)] +#[repr(transparent)] +pub struct Std430Array([T::Padded; N]); + +unsafe impl Zeroable for Std430Array where T::Padded: Zeroable {} +unsafe impl Pod for Std430Array where T::Padded: Pod {} +unsafe impl Std430 for Std430Array +where + T::Padded: Pod, +{ + const ALIGNMENT: usize = T::ALIGNMENT; + type Padded = Self; +} + +impl Std430Array { + fn uninit_array() -> [MaybeUninit; N] { + unsafe { MaybeUninit::uninit().assume_init() } + } + + fn from_uninit_array(a: [MaybeUninit; N]) -> Self { + unsafe { core::mem::transmute_copy(&a) } + } +} + +impl AsStd430 for [T; N] +where + ::Padded: Pod, +{ + type Output = Std430Array; + fn as_std430(&self) -> Self::Output { + let mut res = Self::Output::uninit_array(); + + for i in 0..N { + res[i] = MaybeUninit::new(Std430Convertible::from_std430(self[i].as_std430())); + } + + Self::Output::from_uninit_array(res) + } + + fn from_std430(val: Self::Output) -> Self { + let mut res: [MaybeUninit; N] = unsafe { MaybeUninit::uninit().assume_init() }; + for i in 0..N { + res[i] = MaybeUninit::new(T::from_std430(val.0[i].into_std430())); + } + unsafe { core::mem::transmute_copy(&res) } + } +} + +/// Trait implemented for all types that can be written into a buffer as +/// `std430` bytes. This type is more general than [`AsStd430`]: all `AsStd430` +/// types implement `WriteStd430`, but not the other way around. +/// +/// While `AsStd430` requires implementers to return a type that implements the +/// `Std430` trait, `WriteStd430` directly writes bytes using a [`Writer`]. This +/// makes `WriteStd430` usable for writing slices or other DSTs that could not +/// implement `AsStd430` without allocating new memory on the heap. +#[cfg(feature = "std")] +pub trait WriteStd430 { + /// Writes this value into the given [`Writer`] using `std430` layout rules. + /// + /// Should return the offset of the first byte of this type, as returned by + /// the first call to [`Writer::write`]. + fn write_std430(&self, writer: &mut Writer) -> io::Result; + + /// The space required to write this value using `std430` layout rules. This + /// does not include alignment padding that may be needed before or after + /// this type when written as part of a larger buffer. + fn std430_size(&self) -> usize { + let mut writer = Writer::new(io::sink()); + self.write_std430(&mut writer).unwrap(); + writer.len() + } +} + +#[cfg(feature = "std")] +impl WriteStd430 for T +where + T: AsStd430, +{ + fn write_std430(&self, writer: &mut Writer) -> io::Result { + writer.write_std430(&self.as_std430()) + } + + fn std430_size(&self) -> usize { + size_of::<::Output>() + } +} + +#[cfg(feature = "std")] +impl WriteStd430 for [T] +where + T: WriteStd430, +{ + fn write_std430(&self, writer: &mut Writer) -> io::Result { + let mut offset = writer.len(); + + let mut iter = self.iter(); + + if let Some(item) = iter.next() { + offset = item.write_std430(writer)?; + } + + for item in self.iter() { + item.write_std430(writer)?; + } + + Ok(offset) + } + + fn std430_size(&self) -> usize { + let mut writer = Writer::new(io::sink()); + self.write_std430(&mut writer).unwrap(); + writer.len() + } +} diff --git a/crates/crevice/src/std430/writer.rs b/crates/crevice/src/std430/writer.rs new file mode 100644 index 0000000000..5184555435 --- /dev/null +++ b/crates/crevice/src/std430/writer.rs @@ -0,0 +1,150 @@ +use std::io::{self, Write}; +use std::mem::size_of; + +use bytemuck::bytes_of; + +use crate::internal::align_offset; +use crate::std430::{AsStd430, Std430, WriteStd430}; + +/** +Type that enables writing correctly aligned `std430` values to a buffer. + +`Writer` is useful when many values need to be laid out in a row that cannot be +represented by a struct alone, like dynamically sized arrays or dynamically +laid-out values. + +## Example +In this example, we'll write a length-prefixed list of lights to a buffer. +`std430::Writer` helps align correctly, even across multiple structs, which can +be tricky and error-prone otherwise. + +```glsl +struct PointLight { + vec3 position; + vec3 color; + float brightness; +}; + +buffer POINT_LIGHTS { + uint len; + PointLight[] lights; +} point_lights; +``` + +``` +use crevice::std430::{self, AsStd430}; + +#[derive(AsStd430)] +struct PointLight { + position: mint::Vector3, + color: mint::Vector3, + brightness: f32, +} + +let lights = vec![ + PointLight { + position: [0.0, 1.0, 0.0].into(), + color: [1.0, 0.0, 0.0].into(), + brightness: 0.6, + }, + PointLight { + position: [0.0, 4.0, 3.0].into(), + color: [1.0, 1.0, 1.0].into(), + brightness: 1.0, + }, +]; + +# fn map_gpu_buffer_for_write() -> &'static mut [u8] { +# Box::leak(vec![0; 1024].into_boxed_slice()) +# } +let target_buffer = map_gpu_buffer_for_write(); +let mut writer = std430::Writer::new(target_buffer); + +let light_count = lights.len() as u32; +writer.write(&light_count)?; + +// Crevice will automatically insert the required padding to align the +// PointLight structure correctly. In this case, there will be 12 bytes of +// padding between the length field and the light list. + +writer.write(lights.as_slice())?; + +# fn unmap_gpu_buffer() {} +unmap_gpu_buffer(); + +# Ok::<(), std::io::Error>(()) +``` +*/ +pub struct Writer { + writer: W, + offset: usize, +} + +impl Writer { + /// Create a new `Writer`, wrapping a buffer, file, or other type that + /// implements [`std::io::Write`]. + pub fn new(writer: W) -> Self { + Self { writer, offset: 0 } + } + + /// Write a new value to the underlying buffer, writing zeroed padding where + /// necessary. + /// + /// Returns the offset into the buffer that the value was written to. + pub fn write(&mut self, value: &T) -> io::Result + where + T: WriteStd430 + ?Sized, + { + value.write_std430(self) + } + + /// Write an iterator of values to the underlying buffer. + /// + /// Returns the offset into the buffer that the first value was written to. + /// If no values were written, returns the `len()`. + pub fn write_iter(&mut self, iter: I) -> io::Result + where + I: IntoIterator, + T: WriteStd430, + { + let mut offset = self.offset; + + let mut iter = iter.into_iter(); + + if let Some(item) = iter.next() { + offset = item.write_std430(self)?; + } + + for item in iter { + item.write_std430(self)?; + } + + Ok(offset) + } + + /// Write an `Std430` type to the underlying buffer. + pub fn write_std430(&mut self, value: &T) -> io::Result + where + T: Std430, + { + let padding = align_offset(self.offset, T::ALIGNMENT); + + for _ in 0..padding { + self.writer.write_all(&[0])?; + } + self.offset += padding; + + let value = value.as_std430(); + self.writer.write_all(bytes_of(&value))?; + + let write_here = self.offset; + self.offset += size_of::(); + + Ok(write_here) + } + + /// Returns the amount of data written by this `Writer`. + pub fn len(&self) -> usize { + self.offset + } +} diff --git a/crates/crevice/src/util.rs b/crates/crevice/src/util.rs new file mode 100644 index 0000000000..9c6c2a3964 --- /dev/null +++ b/crates/crevice/src/util.rs @@ -0,0 +1,97 @@ +#![allow(unused_macros)] + +macro_rules! easy_impl { + ( $( $std_name:ident $imp_ty:ty { $($field:ident),* }, )* ) => { + $( + impl crate::std140::AsStd140 for $imp_ty { + type Output = crate::std140::$std_name; + + #[inline] + fn as_std140(&self) -> Self::Output { + crate::std140::$std_name { + $( + $field: self.$field.as_std140(), + )* + ..bytemuck::Zeroable::zeroed() + } + } + + #[inline] + fn from_std140(value: Self::Output) -> Self { + Self { + $( + $field: <_ as crate::std140::AsStd140>::from_std140(value.$field), + )* + } + } + } + + impl crate::std430::AsStd430 for $imp_ty { + type Output = crate::std430::$std_name; + + #[inline] + fn as_std430(&self) -> Self::Output { + crate::std430::$std_name { + $( + $field: self.$field.as_std430(), + )* + ..bytemuck::Zeroable::zeroed() + } + } + + #[inline] + fn from_std430(value: Self::Output) -> Self { + Self { + $( + $field: <_ as crate::std430::AsStd430>::from_std430(value.$field), + )* + } + } + } + + unsafe impl crate::glsl::Glsl for $imp_ty { + const NAME: &'static str = crate::std140::$std_name::NAME; + } + )* + }; +} + +macro_rules! minty_impl { + ( $( $mint_ty:ty => $imp_ty:ty, )* ) => { + $( + impl crate::std140::AsStd140 for $imp_ty { + type Output = <$mint_ty as crate::std140::AsStd140>::Output; + + #[inline] + fn as_std140(&self) -> Self::Output { + let mint: $mint_ty = (*self).into(); + mint.as_std140() + } + + #[inline] + fn from_std140(value: Self::Output) -> Self { + <$mint_ty>::from_std140(value).into() + } + } + + impl crate::std430::AsStd430 for $imp_ty { + type Output = <$mint_ty as crate::std430::AsStd430>::Output; + + #[inline] + fn as_std430(&self) -> Self::Output { + let mint: $mint_ty = (*self).into(); + mint.as_std430() + } + + #[inline] + fn from_std430(value: Self::Output) -> Self { + <$mint_ty>::from_std430(value).into() + } + } + + unsafe impl crate::glsl::Glsl for $imp_ty { + const NAME: &'static str = <$mint_ty>::NAME; + } + )* + }; +} diff --git a/crates/crevice/tests/snapshots/test__generate_struct_array_glsl.snap b/crates/crevice/tests/snapshots/test__generate_struct_array_glsl.snap new file mode 100644 index 0000000000..7829bd64ca --- /dev/null +++ b/crates/crevice/tests/snapshots/test__generate_struct_array_glsl.snap @@ -0,0 +1,8 @@ +--- +source: tests/test.rs +expression: "TestGlsl::glsl_definition()" + +--- +struct TestGlsl { + vec3 foo[8][4]; +}; diff --git a/crates/crevice/tests/snapshots/test__generate_struct_glsl.snap b/crates/crevice/tests/snapshots/test__generate_struct_glsl.snap new file mode 100644 index 0000000000..42fc1f4cd7 --- /dev/null +++ b/crates/crevice/tests/snapshots/test__generate_struct_glsl.snap @@ -0,0 +1,9 @@ +--- +source: tests/test.rs +expression: "TestGlsl::glsl_definition()" + +--- +struct TestGlsl { + vec3 foo; + mat2 bar; +}; diff --git a/crates/crevice/tests/test.rs b/crates/crevice/tests/test.rs new file mode 100644 index 0000000000..f07786c827 --- /dev/null +++ b/crates/crevice/tests/test.rs @@ -0,0 +1,61 @@ +use crevice::glsl::GlslStruct; +use crevice::std140::AsStd140; + +#[test] +fn there_and_back_again() { + #[derive(AsStd140, Debug, PartialEq)] + struct ThereAndBackAgain { + view: mint::ColumnMatrix3, + origin: mint::Vector3, + } + + let x = ThereAndBackAgain { + view: mint::ColumnMatrix3 { + x: mint::Vector3 { + x: 1.0, + y: 0.0, + z: 0.0, + }, + y: mint::Vector3 { + x: 0.0, + y: 1.0, + z: 0.0, + }, + z: mint::Vector3 { + x: 0.0, + y: 0.0, + z: 1.0, + }, + }, + origin: mint::Vector3 { + x: 0.0, + y: 1.0, + z: 2.0, + }, + }; + let x_as = x.as_std140(); + assert_eq!(::from_std140(x_as), x); +} + +#[test] +fn generate_struct_glsl() { + #[allow(dead_code)] + #[derive(GlslStruct)] + struct TestGlsl { + foo: mint::Vector3, + bar: mint::ColumnMatrix2, + } + + insta::assert_display_snapshot!(TestGlsl::glsl_definition()); +} + +#[test] +fn generate_struct_array_glsl() { + #[allow(dead_code)] + #[derive(GlslStruct)] + struct TestGlsl { + foo: [[mint::Vector3; 8]; 4], + } + + insta::assert_display_snapshot!(TestGlsl::glsl_definition()); +} diff --git a/examples/2d/pipelined_texture_atlas.rs b/examples/2d/pipelined_texture_atlas.rs new file mode 100644 index 0000000000..ac3ec98584 --- /dev/null +++ b/examples/2d/pipelined_texture_atlas.rs @@ -0,0 +1,94 @@ +use bevy::{ + asset::LoadState, + math::Vec3, + prelude::{ + App, AssetServer, Assets, Commands, HandleUntyped, IntoSystem, Res, ResMut, State, + SystemSet, Transform, + }, + render2::{camera::OrthographicCameraBundle, texture::Image}, + sprite2::{ + PipelinedSpriteBundle, PipelinedSpriteSheetBundle, TextureAtlas, TextureAtlasBuilder, + TextureAtlasSprite, + }, + PipelinedDefaultPlugins, +}; + +/// In this example we generate a new texture atlas (sprite sheet) from a folder containing +/// individual sprites +fn main() { + App::new() + .init_resource::() + .add_plugins(PipelinedDefaultPlugins) + .add_state(AppState::Setup) + .add_system_set(SystemSet::on_enter(AppState::Setup).with_system(load_textures.system())) + .add_system_set(SystemSet::on_update(AppState::Setup).with_system(check_textures.system())) + .add_system_set(SystemSet::on_enter(AppState::Finished).with_system(setup.system())) + .run(); +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +enum AppState { + Setup, + Finished, +} + +#[derive(Default)] +struct RpgSpriteHandles { + handles: Vec, +} + +fn load_textures(mut rpg_sprite_handles: ResMut, asset_server: Res) { + rpg_sprite_handles.handles = asset_server.load_folder("textures/rpg").unwrap(); +} + +fn check_textures( + mut state: ResMut>, + rpg_sprite_handles: ResMut, + asset_server: Res, +) { + if let LoadState::Loaded = + asset_server.get_group_load_state(rpg_sprite_handles.handles.iter().map(|handle| handle.id)) + { + state.set(AppState::Finished).unwrap(); + } +} + +fn setup( + mut commands: Commands, + rpg_sprite_handles: Res, + asset_server: Res, + mut texture_atlases: ResMut>, + mut textures: ResMut>, +) { + let mut texture_atlas_builder = TextureAtlasBuilder::default(); + for handle in rpg_sprite_handles.handles.iter() { + let texture = textures.get(handle).unwrap(); + texture_atlas_builder.add_texture(handle.clone_weak().typed::(), texture); + } + + let texture_atlas = texture_atlas_builder.finish(&mut textures).unwrap(); + let texture_atlas_texture = texture_atlas.texture.clone(); + let vendor_handle = asset_server.get_handle("textures/rpg/chars/vendor/generic-rpg-vendor.png"); + let vendor_index = texture_atlas.get_texture_index(&vendor_handle).unwrap(); + let atlas_handle = texture_atlases.add(texture_atlas); + + // set up a scene to display our texture atlas + commands.spawn_bundle(OrthographicCameraBundle::new_2d()); + // draw a sprite from the atlas + commands.spawn_bundle(PipelinedSpriteSheetBundle { + transform: Transform { + translation: Vec3::new(150.0, 0.0, 0.0), + scale: Vec3::splat(4.0), + ..Default::default() + }, + sprite: TextureAtlasSprite::new(vendor_index), + texture_atlas: atlas_handle, + ..Default::default() + }); + // draw the atlas itself + commands.spawn_bundle(PipelinedSpriteBundle { + texture: texture_atlas_texture, + transform: Transform::from_xyz(-300.0, 0.0, 0.0), + ..Default::default() + }); +} diff --git a/examples/3d/3d_scene_pipelined.rs b/examples/3d/3d_scene_pipelined.rs new file mode 100644 index 0000000000..48e30cfa8c --- /dev/null +++ b/examples/3d/3d_scene_pipelined.rs @@ -0,0 +1,255 @@ +use bevy::{ + core::Time, + diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}, + ecs::prelude::*, + input::Input, + math::{Quat, Vec3}, + pbr2::{ + AmbientLight, DirectionalLight, DirectionalLightBundle, PbrBundle, PointLight, + PointLightBundle, StandardMaterial, + }, + prelude::{App, Assets, BuildChildren, KeyCode, Transform}, + render2::{ + camera::{OrthographicProjection, PerspectiveCameraBundle}, + color::Color, + mesh::{shape, Mesh}, + }, + PipelinedDefaultPlugins, +}; + +fn main() { + App::new() + .add_plugins(PipelinedDefaultPlugins) + .add_plugin(FrameTimeDiagnosticsPlugin::default()) + .add_plugin(LogDiagnosticsPlugin::default()) + .add_startup_system(setup) + .add_system(movement) + .add_system(animate_light_direction) + .run(); +} + +#[derive(Component)] +struct Movable; + +/// set up a simple 3D scene +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + // ground plane + commands.spawn_bundle(PbrBundle { + mesh: meshes.add(Mesh::from(shape::Plane { size: 10.0 })), + material: materials.add(StandardMaterial { + base_color: Color::WHITE, + perceptual_roughness: 1.0, + ..Default::default() + }), + ..Default::default() + }); + + // left wall + let mut transform = Transform::from_xyz(2.5, 2.5, 0.0); + transform.rotate(Quat::from_rotation_z(std::f32::consts::FRAC_PI_2)); + commands.spawn_bundle(PbrBundle { + mesh: meshes.add(Mesh::from(shape::Box::new(5.0, 0.15, 5.0))), + transform, + material: materials.add(StandardMaterial { + base_color: Color::INDIGO, + perceptual_roughness: 1.0, + ..Default::default() + }), + ..Default::default() + }); + // back (right) wall + let mut transform = Transform::from_xyz(0.0, 2.5, -2.5); + transform.rotate(Quat::from_rotation_x(std::f32::consts::FRAC_PI_2)); + commands.spawn_bundle(PbrBundle { + mesh: meshes.add(Mesh::from(shape::Box::new(5.0, 0.15, 5.0))), + transform, + material: materials.add(StandardMaterial { + base_color: Color::INDIGO, + perceptual_roughness: 1.0, + ..Default::default() + }), + ..Default::default() + }); + + // cube + commands + .spawn_bundle(PbrBundle { + mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })), + material: materials.add(StandardMaterial { + base_color: Color::PINK, + ..Default::default() + }), + transform: Transform::from_xyz(0.0, 0.5, 0.0), + ..Default::default() + }) + .insert(Movable); + // sphere + commands + .spawn_bundle(PbrBundle { + mesh: meshes.add(Mesh::from(shape::UVSphere { + radius: 0.5, + ..Default::default() + })), + material: materials.add(StandardMaterial { + base_color: Color::LIME_GREEN, + ..Default::default() + }), + transform: Transform::from_xyz(1.5, 1.0, 1.5), + ..Default::default() + }) + .insert(Movable); + + // ambient light + commands.insert_resource(AmbientLight { + color: Color::ORANGE_RED, + brightness: 0.02, + }); + + // red point light + commands + .spawn_bundle(PointLightBundle { + // transform: Transform::from_xyz(5.0, 8.0, 2.0), + transform: Transform::from_xyz(1.0, 2.0, 0.0), + point_light: PointLight { + intensity: 1600.0, // lumens - roughly a 100W non-halogen incandescent bulb + color: Color::RED, + ..Default::default() + }, + ..Default::default() + }) + .with_children(|builder| { + builder.spawn_bundle(PbrBundle { + mesh: meshes.add(Mesh::from(shape::UVSphere { + radius: 0.1, + ..Default::default() + })), + material: materials.add(StandardMaterial { + base_color: Color::RED, + emissive: Color::rgba_linear(100.0, 0.0, 0.0, 0.0), + ..Default::default() + }), + ..Default::default() + }); + }); + + // green point light + commands + .spawn_bundle(PointLightBundle { + // transform: Transform::from_xyz(5.0, 8.0, 2.0), + transform: Transform::from_xyz(-1.0, 2.0, 0.0), + point_light: PointLight { + intensity: 1600.0, // lumens - roughly a 100W non-halogen incandescent bulb + color: Color::GREEN, + ..Default::default() + }, + ..Default::default() + }) + .with_children(|builder| { + builder.spawn_bundle(PbrBundle { + mesh: meshes.add(Mesh::from(shape::UVSphere { + radius: 0.1, + ..Default::default() + })), + material: materials.add(StandardMaterial { + base_color: Color::GREEN, + emissive: Color::rgba_linear(0.0, 100.0, 0.0, 0.0), + ..Default::default() + }), + ..Default::default() + }); + }); + + // blue point light + commands + .spawn_bundle(PointLightBundle { + // transform: Transform::from_xyz(5.0, 8.0, 2.0), + transform: Transform::from_xyz(0.0, 4.0, 0.0), + point_light: PointLight { + intensity: 1600.0, // lumens - roughly a 100W non-halogen incandescent bulb + color: Color::BLUE, + ..Default::default() + }, + ..Default::default() + }) + .with_children(|builder| { + builder.spawn_bundle(PbrBundle { + mesh: meshes.add(Mesh::from(shape::UVSphere { + radius: 0.1, + ..Default::default() + })), + material: materials.add(StandardMaterial { + base_color: Color::BLUE, + emissive: Color::rgba_linear(0.0, 0.0, 100.0, 0.0), + ..Default::default() + }), + ..Default::default() + }); + }); + + // directional 'sun' light + const HALF_SIZE: f32 = 10.0; + commands.spawn_bundle(DirectionalLightBundle { + directional_light: DirectionalLight { + // Configure the projection to better fit the scene + shadow_projection: OrthographicProjection { + left: -HALF_SIZE, + right: HALF_SIZE, + bottom: -HALF_SIZE, + top: HALF_SIZE, + near: -10.0 * HALF_SIZE, + far: 10.0 * HALF_SIZE, + ..Default::default() + }, + ..Default::default() + }, + transform: Transform { + translation: Vec3::new(0.0, 2.0, 0.0), + rotation: Quat::from_rotation_x(-std::f32::consts::FRAC_PI_4), + ..Default::default() + }, + ..Default::default() + }); + + // camera + commands.spawn_bundle(PerspectiveCameraBundle { + transform: Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), + ..Default::default() + }); +} + +fn animate_light_direction( + time: Res { + fn default() -> Self { + Self(PhantomData) + } +} + +impl Plugin for RenderAssetPlugin { + fn build(&self, app: &mut App) { + let render_app = app.sub_app(RenderApp); + let prepare_asset_system = PrepareAssetSystem::::system(&mut render_app.world); + render_app + .init_resource::>() + .init_resource::>() + .init_resource::>() + .add_system_to_stage(RenderStage::Extract, extract_render_asset::) + .add_system_to_stage(RenderStage::Prepare, prepare_asset_system); + } +} + +/// Temporarily stores the extracted and removed assets of the current frame. +pub struct ExtractedAssets { + extracted: Vec<(Handle, A::ExtractedAsset)>, + removed: Vec>, +} + +impl Default for ExtractedAssets { + fn default() -> Self { + Self { + extracted: Default::default(), + removed: Default::default(), + } + } +} + +/// Stores all GPU representations ([`RenderAsset::PreparedAssets`](RenderAsset::PreparedAsset)) +/// of [`RenderAssets`](RenderAsset) as long as they exist. +pub type RenderAssets = HashMap, ::PreparedAsset>; + +/// This system extracts all crated or modified assets of the corresponding [`RenderAsset`] type +/// into the "render world". +fn extract_render_asset( + mut commands: Commands, + mut events: EventReader>, + assets: Res>, +) { + let mut changed_assets = HashSet::default(); + let mut removed = Vec::new(); + for event in events.iter() { + match event { + AssetEvent::Created { handle } => { + changed_assets.insert(handle); + } + AssetEvent::Modified { handle } => { + changed_assets.insert(handle); + } + AssetEvent::Removed { handle } => { + if !changed_assets.remove(handle) { + removed.push(handle.clone_weak()); + } + } + } + } + + let mut extracted_assets = Vec::new(); + for handle in changed_assets.drain() { + if let Some(asset) = assets.get(handle) { + extracted_assets.push((handle.clone_weak(), asset.extract_asset())); + } + } + + commands.insert_resource(ExtractedAssets { + extracted: extracted_assets, + removed, + }) +} + +/// Specifies all ECS data required by [`PrepareAssetSystem`]. +pub type RenderAssetParams = ( + SResMut>, + SResMut>, + SResMut>, + ::Param, +); + +// TODO: consider storing inside system? +/// All assets that should be prepared next frame. +pub struct PrepareNextFrameAssets { + assets: Vec<(Handle, A::ExtractedAsset)>, +} + +impl Default for PrepareNextFrameAssets { + fn default() -> Self { + Self { + assets: Default::default(), + } + } +} + +/// This system prepares all assets of the corresponding [`RenderAsset`] type +/// which where extracted this frame for the GPU. +pub struct PrepareAssetSystem(PhantomData); + +impl RunSystem for PrepareAssetSystem { + type Param = RenderAssetParams; + + fn run( + (mut extracted_assets, mut render_assets, mut prepare_next_frame, mut param): SystemParamItem, + ) { + let mut queued_assets = std::mem::take(&mut prepare_next_frame.assets); + for (handle, extracted_asset) in queued_assets.drain(..) { + match R::prepare_asset(extracted_asset, &mut param) { + Ok(prepared_asset) => { + render_assets.insert(handle, prepared_asset); + } + Err(PrepareAssetError::RetryNextUpdate(extracted_asset)) => { + prepare_next_frame.assets.push((handle, extracted_asset)); + } + } + } + + for removed in std::mem::take(&mut extracted_assets.removed) { + render_assets.remove(&removed); + } + + for (handle, extracted_asset) in std::mem::take(&mut extracted_assets.extracted) { + match R::prepare_asset(extracted_asset, &mut param) { + Ok(prepared_asset) => { + render_assets.insert(handle, prepared_asset); + } + Err(PrepareAssetError::RetryNextUpdate(extracted_asset)) => { + prepare_next_frame.assets.push((handle, extracted_asset)); + } + } + } + } +} diff --git a/pipelined/bevy_render2/src/render_component.rs b/pipelined/bevy_render2/src/render_component.rs new file mode 100644 index 0000000000..25d4e3f4b7 --- /dev/null +++ b/pipelined/bevy_render2/src/render_component.rs @@ -0,0 +1,187 @@ +use crate::{ + render_resource::DynamicUniformVec, + renderer::{RenderDevice, RenderQueue}, + RenderApp, RenderStage, +}; +use bevy_app::{App, Plugin}; +use bevy_asset::{Asset, Handle}; +use bevy_ecs::{ + component::Component, + prelude::*, + query::{FilterFetch, QueryItem, ReadOnlyFetch, WorldQuery}, + system::{ + lifetimeless::{Read, SCommands, SQuery}, + RunSystem, SystemParamItem, + }, +}; +use crevice::std140::AsStd140; +use std::{marker::PhantomData, ops::Deref}; + +/// Stores the index of a uniform inside of [`ComponentUniforms`]. +#[derive(Component)] +pub struct DynamicUniformIndex { + index: u32, + marker: PhantomData, +} + +impl DynamicUniformIndex { + #[inline] + pub fn index(&self) -> u32 { + self.index + } +} + +/// Describes how a component gets extracted for rendering. +/// +/// Therefore the component is transferred from the "app world" into the "render world" +/// in the [`RenderStage::Extract`](crate::RenderStage::Extract) step. +pub trait ExtractComponent: Component { + /// ECS [`WorldQuery`] to fetch the components to extract. + type Query: WorldQuery; + /// Filters the entities with additional constraints. + type Filter: WorldQuery; + /// Defines how the component is transferred into the "render world". + fn extract_component(item: QueryItem) -> Self; +} + +/// This plugin prepares the components of the corresponding type for the GPU +/// by transforming them into uniforms. +/// +/// They can then be accessed from the [`ComponentUniforms`] resource. +/// For referencing the newly created uniforms a [`DynamicUniformIndex`] is inserted +/// for every processed entity. +/// +/// Therefore it sets up the [`RenderStage::Prepare`](crate::RenderStage::Prepare) step +/// for the specified [`ExtractComponent`]. +pub struct UniformComponentPlugin(PhantomData C>); + +impl Default for UniformComponentPlugin { + fn default() -> Self { + Self(PhantomData) + } +} + +impl Plugin for UniformComponentPlugin { + fn build(&self, app: &mut App) { + app.sub_app(RenderApp) + .insert_resource(ComponentUniforms::::default()) + .add_system_to_stage( + RenderStage::Prepare, + prepare_uniform_components::.system(), + ); + } +} + +/// Stores all uniforms of the component type. +pub struct ComponentUniforms { + uniforms: DynamicUniformVec, +} + +impl Deref for ComponentUniforms { + type Target = DynamicUniformVec; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.uniforms + } +} + +impl ComponentUniforms { + #[inline] + pub fn uniforms(&self) -> &DynamicUniformVec { + &self.uniforms + } +} + +impl Default for ComponentUniforms { + fn default() -> Self { + Self { + uniforms: Default::default(), + } + } +} + +/// This system prepares all components of the corresponding component type. +/// They are transformed into uniforms and stored in the [`ComponentUniforms`] resource. +fn prepare_uniform_components( + mut commands: Commands, + render_device: Res, + render_queue: Res, + mut component_uniforms: ResMut>, + components: Query<(Entity, &C)>, +) where + C: AsStd140 + Clone, +{ + component_uniforms.uniforms.clear(); + for (entity, component) in components.iter() { + commands + .get_or_spawn(entity) + .insert(DynamicUniformIndex:: { + index: component_uniforms.uniforms.push(component.clone()), + marker: PhantomData, + }); + } + + component_uniforms + .uniforms + .write_buffer(&render_device, &render_queue); +} + +/// This plugin extracts the components into the "render world". +/// +/// Therefore it sets up the [`RenderStage::Extract`](crate::RenderStage::Extract) step +/// for the specified [`ExtractComponent`]. +pub struct ExtractComponentPlugin(PhantomData (C, F)>); + +impl Default for ExtractComponentPlugin { + fn default() -> Self { + Self(PhantomData) + } +} + +impl Plugin for ExtractComponentPlugin +where + ::Fetch: ReadOnlyFetch, + ::Fetch: FilterFetch, +{ + fn build(&self, app: &mut App) { + let system = ExtractComponentSystem::::system(&mut app.world); + let render_app = app.sub_app(RenderApp); + render_app.add_system_to_stage(RenderStage::Extract, system); + } +} + +impl ExtractComponent for Handle { + type Query = Read>; + type Filter = (); + + #[inline] + fn extract_component(handle: QueryItem) -> Self { + handle.clone_weak() + } +} + +/// This system extracts all components of the corresponding [`ExtractComponent`] type. +pub struct ExtractComponentSystem(PhantomData); + +impl RunSystem for ExtractComponentSystem +where + ::Fetch: FilterFetch, + ::Fetch: ReadOnlyFetch, +{ + type Param = ( + SCommands, + // the previous amount of extracted components + Local<'static, usize>, + SQuery<(Entity, C::Query), C::Filter>, + ); + + fn run((mut commands, mut previous_len, query): SystemParamItem) { + let mut values = Vec::with_capacity(*previous_len); + for (entity, query_item) in query.iter() { + values.push((entity, (C::extract_component(query_item),))); + } + *previous_len = values.len(); + commands.insert_or_spawn_batch(values); + } +} diff --git a/pipelined/bevy_render2/src/render_graph/context.rs b/pipelined/bevy_render2/src/render_graph/context.rs new file mode 100644 index 0000000000..ec300ac25e --- /dev/null +++ b/pipelined/bevy_render2/src/render_graph/context.rs @@ -0,0 +1,252 @@ +use crate::{ + render_graph::{NodeState, RenderGraph, SlotInfos, SlotLabel, SlotType, SlotValue}, + render_resource::{Buffer, Sampler, TextureView}, +}; +use bevy_ecs::entity::Entity; +use std::borrow::Cow; +use thiserror::Error; + +/// A command that signals the graph runner to run the sub graph corresponding to the `name` +/// with the specified `inputs` next. +pub struct RunSubGraph { + pub name: Cow<'static, str>, + pub inputs: Vec, +} + +/// The context with all graph information required to run a [`Node`](super::Node). +/// This context is created for each node by the `RenderGraphRunner`. +/// +/// The slot input can be read from here and the outputs must be written back to the context for +/// passing them onto the next node. +/// +/// Sub graphs can be queued for running by adding a [`RunSubGraph`] command to the context. +/// After the node has finished running the graph runner is responsible for executing the sub graphs. +pub struct RenderGraphContext<'a> { + graph: &'a RenderGraph, + node: &'a NodeState, + inputs: &'a [SlotValue], + outputs: &'a mut [Option], + run_sub_graphs: Vec, +} + +impl<'a> RenderGraphContext<'a> { + /// Creates a new render graph context for the `node`. + pub fn new( + graph: &'a RenderGraph, + node: &'a NodeState, + inputs: &'a [SlotValue], + outputs: &'a mut [Option], + ) -> Self { + Self { + graph, + node, + inputs, + outputs, + run_sub_graphs: Vec::new(), + } + } + + /// Returns the input slot values for the node. + #[inline] + pub fn inputs(&self) -> &[SlotValue] { + self.inputs + } + + /// Returns the [`SlotInfos`] of the inputs. + pub fn input_info(&self) -> &SlotInfos { + &self.node.input_slots + } + + /// Returns the [`SlotInfos`] of the outputs. + pub fn output_info(&self) -> &SlotInfos { + &self.node.output_slots + } + + /// Retrieves the input slot value referenced by the `label`. + pub fn get_input(&self, label: impl Into) -> Result<&SlotValue, InputSlotError> { + let label = label.into(); + let index = self + .input_info() + .get_slot_index(label.clone()) + .ok_or(InputSlotError::InvalidSlot(label))?; + Ok(&self.inputs[index]) + } + + // TODO: should this return an Arc or a reference? + /// Retrieves the input slot value referenced by the `label` as a [`TextureView`]. + pub fn get_input_texture( + &self, + label: impl Into, + ) -> Result<&TextureView, InputSlotError> { + let label = label.into(); + match self.get_input(label.clone())? { + SlotValue::TextureView(value) => Ok(value), + value => Err(InputSlotError::MismatchedSlotType { + label, + actual: value.slot_type(), + expected: SlotType::TextureView, + }), + } + } + + /// Retrieves the input slot value referenced by the `label` as a [`Sampler`]. + pub fn get_input_sampler( + &self, + label: impl Into, + ) -> Result<&Sampler, InputSlotError> { + let label = label.into(); + match self.get_input(label.clone())? { + SlotValue::Sampler(value) => Ok(value), + value => Err(InputSlotError::MismatchedSlotType { + label, + actual: value.slot_type(), + expected: SlotType::Sampler, + }), + } + } + + /// Retrieves the input slot value referenced by the `label` as a [`Buffer`]. + pub fn get_input_buffer(&self, label: impl Into) -> Result<&Buffer, InputSlotError> { + let label = label.into(); + match self.get_input(label.clone())? { + SlotValue::Buffer(value) => Ok(value), + value => Err(InputSlotError::MismatchedSlotType { + label, + actual: value.slot_type(), + expected: SlotType::Buffer, + }), + } + } + + /// Retrieves the input slot value referenced by the `label` as an [`Entity`]. + pub fn get_input_entity(&self, label: impl Into) -> Result { + let label = label.into(); + match self.get_input(label.clone())? { + SlotValue::Entity(value) => Ok(*value), + value => Err(InputSlotError::MismatchedSlotType { + label, + actual: value.slot_type(), + expected: SlotType::Entity, + }), + } + } + + /// Sets the output slot value referenced by the `label`. + pub fn set_output( + &mut self, + label: impl Into, + value: impl Into, + ) -> Result<(), OutputSlotError> { + let label = label.into(); + let value = value.into(); + let slot_index = self + .output_info() + .get_slot_index(label.clone()) + .ok_or_else(|| OutputSlotError::InvalidSlot(label.clone()))?; + let slot = self + .output_info() + .get_slot(slot_index) + .expect("slot is valid"); + if value.slot_type() != slot.slot_type { + return Err(OutputSlotError::MismatchedSlotType { + label, + actual: slot.slot_type, + expected: value.slot_type(), + }); + } + self.outputs[slot_index] = Some(value); + Ok(()) + } + + /// Queues up a sub graph for execution after the node has finished running. + pub fn run_sub_graph( + &mut self, + name: impl Into>, + inputs: Vec, + ) -> Result<(), RunSubGraphError> { + let name = name.into(); + let sub_graph = self + .graph + .get_sub_graph(&name) + .ok_or_else(|| RunSubGraphError::MissingSubGraph(name.clone()))?; + if let Some(input_node) = sub_graph.input_node() { + for (i, input_slot) in input_node.input_slots.iter().enumerate() { + if let Some(input_value) = inputs.get(i) { + if input_slot.slot_type != input_value.slot_type() { + return Err(RunSubGraphError::MismatchedInputSlotType { + graph_name: name, + slot_index: i, + actual: input_value.slot_type(), + expected: input_slot.slot_type, + label: input_slot.name.clone().into(), + }); + } + } else { + return Err(RunSubGraphError::MissingInput { + slot_index: i, + slot_name: input_slot.name.clone(), + graph_name: name, + }); + } + } + } else if !inputs.is_empty() { + return Err(RunSubGraphError::SubGraphHasNoInputs(name)); + } + + self.run_sub_graphs.push(RunSubGraph { name, inputs }); + + Ok(()) + } + + /// Finishes the context for this [`Node`](super::Node) by + /// returning the sub graphs to run next. + pub fn finish(self) -> Vec { + self.run_sub_graphs + } +} + +#[derive(Error, Debug, Eq, PartialEq)] +pub enum RunSubGraphError { + #[error("tried to run a non-existent sub-graph")] + MissingSubGraph(Cow<'static, str>), + #[error("passed in inputs, but this sub-graph doesn't have any")] + SubGraphHasNoInputs(Cow<'static, str>), + #[error("sub graph (name: '{graph_name:?}') could not be run because slot '{slot_name}' at index {slot_index} has no value")] + MissingInput { + slot_index: usize, + slot_name: Cow<'static, str>, + graph_name: Cow<'static, str>, + }, + #[error("attempted to use the wrong type for input slot")] + MismatchedInputSlotType { + graph_name: Cow<'static, str>, + slot_index: usize, + label: SlotLabel, + expected: SlotType, + actual: SlotType, + }, +} + +#[derive(Error, Debug, Eq, PartialEq)] +pub enum OutputSlotError { + #[error("slot does not exist")] + InvalidSlot(SlotLabel), + #[error("attempted to assign the wrong type to slot")] + MismatchedSlotType { + label: SlotLabel, + expected: SlotType, + actual: SlotType, + }, +} + +#[derive(Error, Debug, Eq, PartialEq)] +pub enum InputSlotError { + #[error("slot does not exist")] + InvalidSlot(SlotLabel), + #[error("attempted to retrieve the wrong type from input slot")] + MismatchedSlotType { + label: SlotLabel, + expected: SlotType, + actual: SlotType, + }, +} diff --git a/pipelined/bevy_render2/src/render_graph/edge.rs b/pipelined/bevy_render2/src/render_graph/edge.rs new file mode 100644 index 0000000000..fce406f53d --- /dev/null +++ b/pipelined/bevy_render2/src/render_graph/edge.rs @@ -0,0 +1,50 @@ +use super::NodeId; + +/// An edge, which connects two [`Nodes`](super::Node) in +/// a [`RenderGraph`](crate::render_graph::RenderGraph). +/// +/// They are used to describe the ordering (which node has to run first) +/// and may be of two kinds: [`NodeEdge`](Self::NodeEdge) and [`SlotEdge`](Self::SlotEdge). +/// +/// Edges are added via the render_graph::add_node_edge(output_node, input_node) and the +/// render_graph::add_slot_edge(output_node, output_slot, input_node, input_slot) methode. +/// +/// The former simply states that the `output_node` has to be run before the `input_node`, +/// while the later connects an output slot of the `output_node` +/// with an input slot of the `input_node` to pass additional data along. +/// For more information see [`SlotType`](super::SlotType). +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Edge { + /// An edge describing to ordering of both nodes (`output_node` before `input_node`) + /// and connecting the output slot at the `output_index` of the output_node + /// with the slot at the `input_index` of the `input_node`. + SlotEdge { + input_node: NodeId, + input_index: usize, + output_node: NodeId, + output_index: usize, + }, + /// An edge describing to ordering of both nodes (`output_node` before `input_node`). + NodeEdge { + input_node: NodeId, + output_node: NodeId, + }, +} + +impl Edge { + /// Returns the id of the 'input_node'. + pub fn get_input_node(&self) -> NodeId { + match self { + Edge::SlotEdge { input_node, .. } => *input_node, + Edge::NodeEdge { input_node, .. } => *input_node, + } + } + + /// Returns the id of the 'output_node'. + pub fn get_output_node(&self) -> NodeId { + match self { + Edge::SlotEdge { output_node, .. } => *output_node, + Edge::NodeEdge { output_node, .. } => *output_node, + } + } +} diff --git a/pipelined/bevy_render2/src/render_graph/graph.rs b/pipelined/bevy_render2/src/render_graph/graph.rs new file mode 100644 index 0000000000..53e8bacb26 --- /dev/null +++ b/pipelined/bevy_render2/src/render_graph/graph.rs @@ -0,0 +1,602 @@ +use crate::{ + render_graph::{ + Edge, Node, NodeId, NodeLabel, NodeRunError, NodeState, RenderGraphContext, + RenderGraphError, SlotInfo, SlotLabel, + }, + renderer::RenderContext, +}; +use bevy_ecs::prelude::World; +use bevy_utils::HashMap; +use std::{borrow::Cow, fmt::Debug}; + +/// The render graph configures the modular, parallel and re-usable render logic. +/// It is a retained and stateless (nodes itself my have their internal state) structure, +/// which can not be modified while it is executed by the graph runner. +/// +/// The `RenderGraphRunner` is responsible for executing the entire graph each frame. +/// +/// It consists of three main components: [`Nodes`](Node), [`Edges`](Edge) +/// and [`Slots`](super::SlotType). +/// +/// Nodes are responsible for generating draw calls and operating on input and output slots. +/// Edges specify the order of execution for nodes and connect input and output slots together. +/// Slots describe the render resources created or used by the nodes. +/// +/// Additionally a render graph can contain multiple sub graphs, which are run by the +/// corresponding nodes. Every render graph can have it’s own optional input node. +/// +/// ## Example +/// Here is a simple render graph example with two nodes connected by a node edge. +/// ``` +/// # use bevy_app::prelude::*; +/// # use bevy_ecs::prelude::World; +/// # use bevy_render2::render_graph::{RenderGraph, Node, RenderGraphContext, NodeRunError}; +/// # use bevy_render2::renderer::RenderContext; +/// # +/// # struct MyNode; +/// # +/// # impl Node for MyNode { +/// # fn run(&self, graph: &mut RenderGraphContext, render_context: &mut RenderContext, world: &World) -> Result<(), NodeRunError> { +/// # unimplemented!() +/// # } +/// # } +/// # +/// let mut graph = RenderGraph::default(); +/// graph.add_node("input_node", MyNode); +/// graph.add_node("output_node", MyNode); +/// graph.add_node_edge("output_node", "input_node").unwrap(); +/// ``` +#[derive(Default)] +pub struct RenderGraph { + nodes: HashMap, + node_names: HashMap, NodeId>, + sub_graphs: HashMap, RenderGraph>, + input_node: Option, +} + +impl RenderGraph { + /// The name of the [`GraphInputNode`] of this graph. Used to connect other nodes to it. + pub const INPUT_NODE_NAME: &'static str = "GraphInputNode"; + + /// Updates all nodes and sub graphs of the render graph. Should be called before executing it. + pub fn update(&mut self, world: &mut World) { + for node in self.nodes.values_mut() { + node.node.update(world); + } + + for sub_graph in self.sub_graphs.values_mut() { + sub_graph.update(world); + } + } + + /// Creates an [`GraphInputNode`] with the specified slots if not already present. + pub fn set_input(&mut self, inputs: Vec) -> NodeId { + if self.input_node.is_some() { + panic!("Graph already has an input node"); + } + + let id = self.add_node("GraphInputNode", GraphInputNode { inputs }); + self.input_node = Some(id); + id + } + + /// Returns the [`NodeState`] of the input node of this graph.. + #[inline] + pub fn input_node(&self) -> Option<&NodeState> { + self.input_node.and_then(|id| self.get_node_state(id).ok()) + } + + /// Adds the `node` with the `name` to the graph. + /// If the name is already present replaces it instead. + pub fn add_node(&mut self, name: impl Into>, node: T) -> NodeId + where + T: Node, + { + let id = NodeId::new(); + let name = name.into(); + let mut node_state = NodeState::new(id, node); + node_state.name = Some(name.clone()); + self.nodes.insert(id, node_state); + self.node_names.insert(name, id); + id + } + + /// Retrieves the [`NodeState`] referenced by the `label`. + pub fn get_node_state( + &self, + label: impl Into, + ) -> Result<&NodeState, RenderGraphError> { + let label = label.into(); + let node_id = self.get_node_id(&label)?; + self.nodes + .get(&node_id) + .ok_or(RenderGraphError::InvalidNode(label)) + } + + /// Retrieves the [`NodeState`] referenced by the `label` mutably. + pub fn get_node_state_mut( + &mut self, + label: impl Into, + ) -> Result<&mut NodeState, RenderGraphError> { + let label = label.into(); + let node_id = self.get_node_id(&label)?; + self.nodes + .get_mut(&node_id) + .ok_or(RenderGraphError::InvalidNode(label)) + } + + /// Retrieves the [`NodeId`] referenced by the `label`. + pub fn get_node_id(&self, label: impl Into) -> Result { + let label = label.into(); + match label { + NodeLabel::Id(id) => Ok(id), + NodeLabel::Name(ref name) => self + .node_names + .get(name) + .cloned() + .ok_or(RenderGraphError::InvalidNode(label)), + } + } + + /// Retrieves the [`Node`] referenced by the `label`. + pub fn get_node(&self, label: impl Into) -> Result<&T, RenderGraphError> + where + T: Node, + { + self.get_node_state(label).and_then(|n| n.node()) + } + + /// Retrieves the [`Node`] referenced by the `label` mutably. + pub fn get_node_mut( + &mut self, + label: impl Into, + ) -> Result<&mut T, RenderGraphError> + where + T: Node, + { + self.get_node_state_mut(label).and_then(|n| n.node_mut()) + } + + /// Adds the [`Edge::SlotEdge`] to the graph. This guarantees that the `output_node` + /// is run before the `input_node` and also connects the `output_slot` to the `input_slot`. + pub fn add_slot_edge( + &mut self, + output_node: impl Into, + output_slot: impl Into, + input_node: impl Into, + input_slot: impl Into, + ) -> Result<(), RenderGraphError> { + let output_slot = output_slot.into(); + let input_slot = input_slot.into(); + let output_node_id = self.get_node_id(output_node)?; + let input_node_id = self.get_node_id(input_node)?; + + let output_index = self + .get_node_state(output_node_id)? + .output_slots + .get_slot_index(output_slot.clone()) + .ok_or(RenderGraphError::InvalidOutputNodeSlot(output_slot))?; + let input_index = self + .get_node_state(input_node_id)? + .input_slots + .get_slot_index(input_slot.clone()) + .ok_or(RenderGraphError::InvalidInputNodeSlot(input_slot))?; + + let edge = Edge::SlotEdge { + output_node: output_node_id, + output_index, + input_node: input_node_id, + input_index, + }; + + self.validate_edge(&edge)?; + + { + let output_node = self.get_node_state_mut(output_node_id)?; + output_node.edges.add_output_edge(edge.clone())?; + } + let input_node = self.get_node_state_mut(input_node_id)?; + input_node.edges.add_input_edge(edge)?; + + Ok(()) + } + + /// Adds the [`Edge::NodeEdge`] to the graph. This guarantees that the `output_node` + /// is run before the `input_node`. + pub fn add_node_edge( + &mut self, + output_node: impl Into, + input_node: impl Into, + ) -> Result<(), RenderGraphError> { + let output_node_id = self.get_node_id(output_node)?; + let input_node_id = self.get_node_id(input_node)?; + + let edge = Edge::NodeEdge { + output_node: output_node_id, + input_node: input_node_id, + }; + + self.validate_edge(&edge)?; + + { + let output_node = self.get_node_state_mut(output_node_id)?; + output_node.edges.add_output_edge(edge.clone())?; + } + let input_node = self.get_node_state_mut(input_node_id)?; + input_node.edges.add_input_edge(edge)?; + + Ok(()) + } + + /// Verifies that the edge is not already existing and + /// checks that slot edges are connected correctly. + pub fn validate_edge(&mut self, edge: &Edge) -> Result<(), RenderGraphError> { + if self.has_edge(edge) { + return Err(RenderGraphError::EdgeAlreadyExists(edge.clone())); + } + + match *edge { + Edge::SlotEdge { + output_node, + output_index, + input_node, + input_index, + } => { + let output_node_state = self.get_node_state(output_node)?; + let input_node_state = self.get_node_state(input_node)?; + + let output_slot = output_node_state + .output_slots + .get_slot(output_index) + .ok_or(RenderGraphError::InvalidOutputNodeSlot(SlotLabel::Index( + output_index, + )))?; + let input_slot = input_node_state.input_slots.get_slot(input_index).ok_or( + RenderGraphError::InvalidInputNodeSlot(SlotLabel::Index(input_index)), + )?; + + if let Some(Edge::SlotEdge { + output_node: current_output_node, + .. + }) = input_node_state.edges.input_edges.iter().find(|e| { + if let Edge::SlotEdge { + input_index: current_input_index, + .. + } = e + { + input_index == *current_input_index + } else { + false + } + }) { + return Err(RenderGraphError::NodeInputSlotAlreadyOccupied { + node: input_node, + input_slot: input_index, + occupied_by_node: *current_output_node, + }); + } + + if output_slot.slot_type != input_slot.slot_type { + return Err(RenderGraphError::MismatchedNodeSlots { + output_node, + output_slot: output_index, + input_node, + input_slot: input_index, + }); + } + } + Edge::NodeEdge { .. } => { /* nothing to validate here */ } + } + + Ok(()) + } + + /// Checks whether the `edge` already exists in the graph. + pub fn has_edge(&self, edge: &Edge) -> bool { + let output_node_state = self.get_node_state(edge.get_output_node()); + let input_node_state = self.get_node_state(edge.get_input_node()); + if let Ok(output_node_state) = output_node_state { + if output_node_state.edges.output_edges.contains(edge) { + if let Ok(input_node_state) = input_node_state { + if input_node_state.edges.input_edges.contains(edge) { + return true; + } + } + } + } + + false + } + + /// Returns an iterator over the [`NodeStates`](NodeState). + pub fn iter_nodes(&self) -> impl Iterator { + self.nodes.values() + } + + /// Returns an iterator over the [`NodeStates`](NodeState), that allows modifying each value. + pub fn iter_nodes_mut(&mut self) -> impl Iterator { + self.nodes.values_mut() + } + + /// Returns an iterator over the sub graphs. + pub fn iter_sub_graphs(&self) -> impl Iterator { + self.sub_graphs + .iter() + .map(|(name, graph)| (name.as_ref(), graph)) + } + + /// Returns an iterator over the sub graphs, that allows modifying each value. + pub fn iter_sub_graphs_mut(&mut self) -> impl Iterator { + self.sub_graphs + .iter_mut() + .map(|(name, graph)| (name.as_ref(), graph)) + } + + /// Returns an iterator over a tuple of the input edges and the corresponding output nodes + /// for the node referenced by the label. + pub fn iter_node_inputs( + &self, + label: impl Into, + ) -> Result, RenderGraphError> { + let node = self.get_node_state(label)?; + Ok(node + .edges + .input_edges + .iter() + .map(|edge| (edge, edge.get_output_node())) + .map(move |(edge, output_node_id)| { + (edge, self.get_node_state(output_node_id).unwrap()) + })) + } + + /// Returns an iterator over a tuple of the ouput edges and the corresponding input nodes + /// for the node referenced by the label. + pub fn iter_node_outputs( + &self, + label: impl Into, + ) -> Result, RenderGraphError> { + let node = self.get_node_state(label)?; + Ok(node + .edges + .output_edges + .iter() + .map(|edge| (edge, edge.get_input_node())) + .map(move |(edge, input_node_id)| (edge, self.get_node_state(input_node_id).unwrap()))) + } + + /// Adds the `sub_graph` with the `name` to the graph. + /// If the name is already present replaces it instead. + pub fn add_sub_graph(&mut self, name: impl Into>, sub_graph: RenderGraph) { + self.sub_graphs.insert(name.into(), sub_graph); + } + + /// Retrieves the sub graph corresponding to the `name`. + pub fn get_sub_graph(&self, name: impl AsRef) -> Option<&RenderGraph> { + self.sub_graphs.get(name.as_ref()) + } + + /// Retrieves the sub graph corresponding to the `name` mutably. + pub fn get_sub_graph_mut(&mut self, name: impl AsRef) -> Option<&mut RenderGraph> { + self.sub_graphs.get_mut(name.as_ref()) + } +} + +impl Debug for RenderGraph { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for node in self.iter_nodes() { + writeln!(f, "{:?}", node.id)?; + writeln!(f, " in: {:?}", node.input_slots)?; + writeln!(f, " out: {:?}", node.output_slots)?; + } + + Ok(()) + } +} + +/// A [`Node`] which acts as an entry point for a [`RenderGraph`] with custom inputs. +/// It has the same input and output slots and simply copies them over when run. +pub struct GraphInputNode { + inputs: Vec, +} + +impl Node for GraphInputNode { + fn input(&self) -> Vec { + self.inputs.clone() + } + + fn output(&self) -> Vec { + self.inputs.clone() + } + + fn run( + &self, + graph: &mut RenderGraphContext, + _render_context: &mut RenderContext, + _world: &World, + ) -> Result<(), NodeRunError> { + for i in 0..graph.inputs().len() { + let input = graph.inputs()[i].clone(); + graph.set_output(i, input)?; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::{ + render_graph::{ + Edge, Node, NodeId, NodeRunError, RenderGraph, RenderGraphContext, RenderGraphError, + SlotInfo, SlotType, + }, + renderer::RenderContext, + }; + use bevy_ecs::world::World; + use bevy_utils::HashSet; + + #[derive(Debug)] + struct TestNode { + inputs: Vec, + outputs: Vec, + } + + impl TestNode { + pub fn new(inputs: usize, outputs: usize) -> Self { + TestNode { + inputs: (0..inputs) + .map(|i| SlotInfo::new(format!("in_{}", i), SlotType::TextureView)) + .collect(), + outputs: (0..outputs) + .map(|i| SlotInfo::new(format!("out_{}", i), SlotType::TextureView)) + .collect(), + } + } + } + + impl Node for TestNode { + fn input(&self) -> Vec { + self.inputs.clone() + } + + fn output(&self) -> Vec { + self.outputs.clone() + } + + fn run( + &self, + _: &mut RenderGraphContext, + _: &mut RenderContext, + _: &World, + ) -> Result<(), NodeRunError> { + Ok(()) + } + } + + #[test] + fn test_graph_edges() { + let mut graph = RenderGraph::default(); + let a_id = graph.add_node("A", TestNode::new(0, 1)); + let b_id = graph.add_node("B", TestNode::new(0, 1)); + let c_id = graph.add_node("C", TestNode::new(1, 1)); + let d_id = graph.add_node("D", TestNode::new(1, 0)); + + graph.add_slot_edge("A", "out_0", "C", "in_0").unwrap(); + graph.add_node_edge("B", "C").unwrap(); + graph.add_slot_edge("C", 0, "D", 0).unwrap(); + + fn input_nodes(name: &'static str, graph: &RenderGraph) -> HashSet { + graph + .iter_node_inputs(name) + .unwrap() + .map(|(_edge, node)| node.id) + .collect::>() + } + + fn output_nodes(name: &'static str, graph: &RenderGraph) -> HashSet { + graph + .iter_node_outputs(name) + .unwrap() + .map(|(_edge, node)| node.id) + .collect::>() + } + + assert!(input_nodes("A", &graph).is_empty(), "A has no inputs"); + assert!( + output_nodes("A", &graph) == HashSet::from_iter(vec![c_id]), + "A outputs to C" + ); + + assert!(input_nodes("B", &graph).is_empty(), "B has no inputs"); + assert!( + output_nodes("B", &graph) == HashSet::from_iter(vec![c_id]), + "B outputs to C" + ); + + assert!( + input_nodes("C", &graph) == HashSet::from_iter(vec![a_id, b_id]), + "A and B input to C" + ); + assert!( + output_nodes("C", &graph) == HashSet::from_iter(vec![d_id]), + "C outputs to D" + ); + + assert!( + input_nodes("D", &graph) == HashSet::from_iter(vec![c_id]), + "C inputs to D" + ); + assert!(output_nodes("D", &graph).is_empty(), "D has no outputs"); + } + + #[test] + fn test_get_node_typed() { + struct MyNode { + value: usize, + } + + impl Node for MyNode { + fn run( + &self, + _: &mut RenderGraphContext, + _: &mut RenderContext, + _: &World, + ) -> Result<(), NodeRunError> { + Ok(()) + } + } + + let mut graph = RenderGraph::default(); + + graph.add_node("A", MyNode { value: 42 }); + + let node: &MyNode = graph.get_node("A").unwrap(); + assert_eq!(node.value, 42, "node value matches"); + + let result: Result<&TestNode, RenderGraphError> = graph.get_node("A"); + assert_eq!( + result.unwrap_err(), + RenderGraphError::WrongNodeType, + "expect a wrong node type error" + ); + } + + #[test] + fn test_slot_already_occupied() { + let mut graph = RenderGraph::default(); + + graph.add_node("A", TestNode::new(0, 1)); + graph.add_node("B", TestNode::new(0, 1)); + graph.add_node("C", TestNode::new(1, 1)); + + graph.add_slot_edge("A", 0, "C", 0).unwrap(); + assert_eq!( + graph.add_slot_edge("B", 0, "C", 0), + Err(RenderGraphError::NodeInputSlotAlreadyOccupied { + node: graph.get_node_id("C").unwrap(), + input_slot: 0, + occupied_by_node: graph.get_node_id("A").unwrap(), + }), + "Adding to a slot that is already occupied should return an error" + ); + } + + #[test] + fn test_edge_already_exists() { + let mut graph = RenderGraph::default(); + + graph.add_node("A", TestNode::new(0, 1)); + graph.add_node("B", TestNode::new(1, 0)); + + graph.add_slot_edge("A", 0, "B", 0).unwrap(); + assert_eq!( + graph.add_slot_edge("A", 0, "B", 0), + Err(RenderGraphError::EdgeAlreadyExists(Edge::SlotEdge { + output_node: graph.get_node_id("A").unwrap(), + output_index: 0, + input_node: graph.get_node_id("B").unwrap(), + input_index: 0, + })), + "Adding to a duplicate edge should return an error" + ); + } +} diff --git a/pipelined/bevy_render2/src/render_graph/mod.rs b/pipelined/bevy_render2/src/render_graph/mod.rs new file mode 100644 index 0000000000..0d204a5162 --- /dev/null +++ b/pipelined/bevy_render2/src/render_graph/mod.rs @@ -0,0 +1,44 @@ +mod context; +mod edge; +mod graph; +mod node; +mod node_slot; + +pub use context::*; +pub use edge::*; +pub use graph::*; +pub use node::*; +pub use node_slot::*; + +use thiserror::Error; + +#[derive(Error, Debug, Eq, PartialEq)] +pub enum RenderGraphError { + #[error("node does not exist")] + InvalidNode(NodeLabel), + #[error("output node slot does not exist")] + InvalidOutputNodeSlot(SlotLabel), + #[error("input node slot does not exist")] + InvalidInputNodeSlot(SlotLabel), + #[error("node does not match the given type")] + WrongNodeType, + #[error("attempted to connect a node output slot to an incompatible input node slot")] + MismatchedNodeSlots { + output_node: NodeId, + output_slot: usize, + input_node: NodeId, + input_slot: usize, + }, + #[error("attempted to add an edge that already exists")] + EdgeAlreadyExists(Edge), + #[error("node has an unconnected input slot")] + UnconnectedNodeInputSlot { node: NodeId, input_slot: usize }, + #[error("node has an unconnected output slot")] + UnconnectedNodeOutputSlot { node: NodeId, output_slot: usize }, + #[error("node input slot already occupied")] + NodeInputSlotAlreadyOccupied { + node: NodeId, + input_slot: usize, + occupied_by_node: NodeId, + }, +} diff --git a/pipelined/bevy_render2/src/render_graph/node.rs b/pipelined/bevy_render2/src/render_graph/node.rs new file mode 100644 index 0000000000..0562cef87b --- /dev/null +++ b/pipelined/bevy_render2/src/render_graph/node.rs @@ -0,0 +1,285 @@ +use crate::{ + render_graph::{ + Edge, InputSlotError, OutputSlotError, RenderGraphContext, RenderGraphError, + RunSubGraphError, SlotInfo, SlotInfos, + }, + renderer::RenderContext, +}; +use bevy_ecs::world::World; +use bevy_utils::Uuid; +use downcast_rs::{impl_downcast, Downcast}; +use std::{borrow::Cow, fmt::Debug}; +use thiserror::Error; + +/// A [`Node`] identifier. +/// It automatically generates its own random uuid. +/// +/// This id is used to reference the node internally (edges, etc). +#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct NodeId(Uuid); + +impl NodeId { + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + NodeId(Uuid::new_v4()) + } + + pub fn uuid(&self) -> &Uuid { + &self.0 + } +} + +/// A render node that can be added to a [`RenderGraph`](super::RenderGraph). +/// +/// Nodes are the fundamental part of the graph and used to extend its functionality, by +/// generating draw calls and/or running subgraphs. +/// They are added via the render_graph::add_node(my_node) methode. +/// +/// To determine their position in the graph and ensure that all required dependencies (inputs) +/// are already executed, [`Edges`](Edge) are used. +/// +/// A node can produce outputs used as dependencies by other nodes. +/// Those inputs and outputs are called slots and are the default way of passing render data +/// inside the graph. For more information see [`SlotType`](super::SlotType). +pub trait Node: Downcast + Send + Sync + 'static { + /// Specifies the required input slots for this node. + /// They will then be available during the run method inside the [`RenderContext`]. + fn input(&self) -> Vec { + Vec::new() + } + + /// Specifies the produced output slots for this node. + /// They can then be passed one inside [`RenderContext`] during the run method. + fn output(&self) -> Vec { + Vec::new() + } + + /// Updates internal node state using the current render [`World`] prior to the run method. + fn update(&mut self, _world: &mut World) {} + + /// Runs the graph node logic, issues draw calls, updates the output slots and + /// optionally queues up subgraphs for execution. The graph data, input and output values are + /// passed via the [`RenderGraphContext`]. + fn run( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + world: &World, + ) -> Result<(), NodeRunError>; +} + +impl_downcast!(Node); + +#[derive(Error, Debug, Eq, PartialEq)] +pub enum NodeRunError { + #[error("encountered an input slot error")] + InputSlotError(#[from] InputSlotError), + #[error("encountered an output slot error")] + OutputSlotError(#[from] OutputSlotError), + #[error("encountered an error when running a sub-graph")] + RunSubGraphError(#[from] RunSubGraphError), +} + +/// A collection of input and output [`Edges`](Edge) for a [`Node`]. +#[derive(Debug)] +pub struct Edges { + pub id: NodeId, + pub input_edges: Vec, + pub output_edges: Vec, +} + +impl Edges { + /// Adds an edge to the `input_edges` if it does not already exist. + pub(crate) fn add_input_edge(&mut self, edge: Edge) -> Result<(), RenderGraphError> { + if self.has_input_edge(&edge) { + return Err(RenderGraphError::EdgeAlreadyExists(edge)); + } + self.input_edges.push(edge); + Ok(()) + } + + /// Adds an edge to the `output_edges` if it does not already exist. + pub(crate) fn add_output_edge(&mut self, edge: Edge) -> Result<(), RenderGraphError> { + if self.has_output_edge(&edge) { + return Err(RenderGraphError::EdgeAlreadyExists(edge)); + } + self.output_edges.push(edge); + Ok(()) + } + + /// Checks whether the input edge already exists. + pub fn has_input_edge(&self, edge: &Edge) -> bool { + self.input_edges.contains(edge) + } + + /// Checks whether the output edge already exists. + pub fn has_output_edge(&self, edge: &Edge) -> bool { + self.output_edges.contains(edge) + } + + /// Searches the `input_edges` for a [`Edge::SlotEdge`], + /// which `input_index` matches the `index`; + pub fn get_input_slot_edge(&self, index: usize) -> Result<&Edge, RenderGraphError> { + self.input_edges + .iter() + .find(|e| { + if let Edge::SlotEdge { input_index, .. } = e { + *input_index == index + } else { + false + } + }) + .ok_or(RenderGraphError::UnconnectedNodeInputSlot { + input_slot: index, + node: self.id, + }) + } + + /// Searches the `output_edges` for a [`Edge::SlotEdge`], + /// which `output_index` matches the `index`; + pub fn get_output_slot_edge(&self, index: usize) -> Result<&Edge, RenderGraphError> { + self.output_edges + .iter() + .find(|e| { + if let Edge::SlotEdge { output_index, .. } = e { + *output_index == index + } else { + false + } + }) + .ok_or(RenderGraphError::UnconnectedNodeOutputSlot { + output_slot: index, + node: self.id, + }) + } +} + +/// The internal representation of a [`Node`], with all data required +/// by the [`RenderGraph`](super::RenderGraph). +/// +/// The `input_slots` and `output_slots` are provided by the `node`. +pub struct NodeState { + pub id: NodeId, + pub name: Option>, + /// The name of the type that implements [`Node`]. + pub type_name: &'static str, + pub node: Box, + pub input_slots: SlotInfos, + pub output_slots: SlotInfos, + pub edges: Edges, +} + +impl Debug for NodeState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "{:?} ({:?})", self.id, self.name) + } +} + +impl NodeState { + /// Creates an [`NodeState`] without edges, but the `input_slots` and `output_slots` + /// are provided by the `node`. + pub fn new(id: NodeId, node: T) -> Self + where + T: Node, + { + NodeState { + id, + name: None, + input_slots: node.input().into(), + output_slots: node.output().into(), + node: Box::new(node), + type_name: std::any::type_name::(), + edges: Edges { + id, + input_edges: Vec::new(), + output_edges: Vec::new(), + }, + } + } + + /// Retrieves the [`Node`]. + pub fn node(&self) -> Result<&T, RenderGraphError> + where + T: Node, + { + self.node + .downcast_ref::() + .ok_or(RenderGraphError::WrongNodeType) + } + + /// Retrieves the [`Node`] mutably. + pub fn node_mut(&mut self) -> Result<&mut T, RenderGraphError> + where + T: Node, + { + self.node + .downcast_mut::() + .ok_or(RenderGraphError::WrongNodeType) + } + + /// Validates that each input slot corresponds to an input edge. + pub fn validate_input_slots(&self) -> Result<(), RenderGraphError> { + for i in 0..self.input_slots.len() { + self.edges.get_input_slot_edge(i)?; + } + + Ok(()) + } + + /// Validates that each output slot corresponds to an output edge. + pub fn validate_output_slots(&self) -> Result<(), RenderGraphError> { + for i in 0..self.output_slots.len() { + self.edges.get_output_slot_edge(i)?; + } + + Ok(()) + } +} + +/// A [`NodeLabel`] is used to reference a [`NodeState`] by either its name or [`NodeId`] +/// inside the [`RenderGraph`](super::RenderGraph). +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum NodeLabel { + Id(NodeId), + Name(Cow<'static, str>), +} + +impl From<&NodeLabel> for NodeLabel { + fn from(value: &NodeLabel) -> Self { + value.clone() + } +} + +impl From for NodeLabel { + fn from(value: String) -> Self { + NodeLabel::Name(value.into()) + } +} + +impl From<&'static str> for NodeLabel { + fn from(value: &'static str) -> Self { + NodeLabel::Name(value.into()) + } +} + +impl From for NodeLabel { + fn from(value: NodeId) -> Self { + NodeLabel::Id(value) + } +} + +/// A [`Node`] without any inputs, outputs and subgraphs, which does nothing when run. +/// Used (as a label) to bundle multiple dependencies into one inside +/// the [`RenderGraph`](super::RenderGraph). +pub struct EmptyNode; + +impl Node for EmptyNode { + fn run( + &self, + _graph: &mut RenderGraphContext, + _render_context: &mut RenderContext, + _world: &World, + ) -> Result<(), NodeRunError> { + Ok(()) + } +} diff --git a/pipelined/bevy_render2/src/render_graph/node_slot.rs b/pipelined/bevy_render2/src/render_graph/node_slot.rs new file mode 100644 index 0000000000..0789d62df0 --- /dev/null +++ b/pipelined/bevy_render2/src/render_graph/node_slot.rs @@ -0,0 +1,191 @@ +use bevy_ecs::entity::Entity; +use std::borrow::Cow; + +use crate::render_resource::{Buffer, Sampler, TextureView}; + +/// A value passed between render [`Nodes`](super::Node). +/// Corresponds to the [SlotType] specified in the [`RenderGraph`](super::RenderGraph). +/// +/// Slots can have four different types of values: +/// [`Buffer`], [`TextureView`], [`Sampler`] and [`Entity`]. +/// +/// These values do not contain the actual render data, but only the ids to retrieve them. +#[derive(Debug, Clone)] +pub enum SlotValue { + /// A GPU-accessible [`Buffer`]. + Buffer(Buffer), + /// A [`TextureView`] describes a texture used in a pipeline. + TextureView(TextureView), + /// A texture [`Sampler`] defines how a pipeline will sample from a [`TextureView`]. + Sampler(Sampler), + /// An entity from the ECS. + Entity(Entity), +} + +impl SlotValue { + /// Returns the [`SlotType`] of this value. + pub fn slot_type(&self) -> SlotType { + match self { + SlotValue::Buffer(_) => SlotType::Buffer, + SlotValue::TextureView(_) => SlotType::TextureView, + SlotValue::Sampler(_) => SlotType::Sampler, + SlotValue::Entity(_) => SlotType::Entity, + } + } +} + +impl From for SlotValue { + fn from(value: Buffer) -> Self { + SlotValue::Buffer(value) + } +} + +impl From for SlotValue { + fn from(value: TextureView) -> Self { + SlotValue::TextureView(value) + } +} + +impl From for SlotValue { + fn from(value: Sampler) -> Self { + SlotValue::Sampler(value) + } +} + +impl From for SlotValue { + fn from(value: Entity) -> Self { + SlotValue::Entity(value) + } +} + +/// Describes the render resources created (output) or used (input) by +/// the render [`Nodes`](super::Node). +/// +/// This should not be confused with [`SlotValue`], which actually contains the passed data. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum SlotType { + /// A GPU-accessible [`Buffer`]. + Buffer, + /// A [`TextureView`] describes a texture used in a pipeline. + TextureView, + /// A texture [`Sampler`] defines how a pipeline will sample from a [`TextureView`]. + Sampler, + /// An entity from the ECS. + Entity, +} + +/// A [`SlotLabel`] is used to reference a slot by either its name or index +/// inside the [`RenderGraph`](super::RenderGraph). +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum SlotLabel { + Index(usize), + Name(Cow<'static, str>), +} + +impl From<&SlotLabel> for SlotLabel { + fn from(value: &SlotLabel) -> Self { + value.clone() + } +} + +impl From for SlotLabel { + fn from(value: String) -> Self { + SlotLabel::Name(value.into()) + } +} + +impl From<&'static str> for SlotLabel { + fn from(value: &'static str) -> Self { + SlotLabel::Name(value.into()) + } +} + +impl From> for SlotLabel { + fn from(value: Cow<'static, str>) -> Self { + SlotLabel::Name(value.clone()) + } +} + +impl From for SlotLabel { + fn from(value: usize) -> Self { + SlotLabel::Index(value) + } +} + +/// The internal representation of a slot, which specifies its [`SlotType`] and name. +#[derive(Clone, Debug)] +pub struct SlotInfo { + pub name: Cow<'static, str>, + pub slot_type: SlotType, +} + +impl SlotInfo { + pub fn new(name: impl Into>, slot_type: SlotType) -> Self { + SlotInfo { + name: name.into(), + slot_type, + } + } +} + +/// A collection of input or output [`SlotInfos`](SlotInfo) for +/// a [`NodeState`](super::NodeState). +#[derive(Default, Debug)] +pub struct SlotInfos { + slots: Vec, +} + +impl> From for SlotInfos { + fn from(slots: T) -> Self { + SlotInfos { + slots: slots.into_iter().collect(), + } + } +} + +impl SlotInfos { + /// Returns the count of slots. + #[inline] + pub fn len(&self) -> usize { + self.slots.len() + } + + /// Returns true if there are no slots. + #[inline] + pub fn is_empty(&self) -> bool { + self.slots.is_empty() + } + + /// Retrieves the [`SlotInfo`] for the provided label. + pub fn get_slot(&self, label: impl Into) -> Option<&SlotInfo> { + let label = label.into(); + let index = self.get_slot_index(&label)?; + self.slots.get(index) + } + + /// Retrieves the [`SlotInfo`] for the provided label mutably. + pub fn get_slot_mut(&mut self, label: impl Into) -> Option<&mut SlotInfo> { + let label = label.into(); + let index = self.get_slot_index(&label)?; + self.slots.get_mut(index) + } + + /// Retrieves the index (inside input or output slots) of the slot for the provided label. + pub fn get_slot_index(&self, label: impl Into) -> Option { + let label = label.into(); + match label { + SlotLabel::Index(index) => Some(index), + SlotLabel::Name(ref name) => self + .slots + .iter() + .enumerate() + .find(|(_i, s)| s.name == *name) + .map(|(i, _s)| i), + } + } + + /// Returns an iterator over the slot infos. + pub fn iter(&self) -> impl Iterator { + self.slots.iter() + } +} diff --git a/pipelined/bevy_render2/src/render_phase/draw.rs b/pipelined/bevy_render2/src/render_phase/draw.rs new file mode 100644 index 0000000000..2f26c540cd --- /dev/null +++ b/pipelined/bevy_render2/src/render_phase/draw.rs @@ -0,0 +1,279 @@ +use crate::{ + render_phase::TrackedRenderPass, + render_resource::{CachedPipelineId, RenderPipelineCache}, +}; +use bevy_app::App; +use bevy_ecs::{ + all_tuples, + entity::Entity, + system::{ + lifetimeless::SRes, ReadOnlySystemParamFetch, SystemParam, SystemParamItem, SystemState, + }, + world::World, +}; +use bevy_utils::HashMap; +use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard}; +use std::{any::TypeId, fmt::Debug, hash::Hash}; + +/// A draw function which is used to draw a specific [`PhaseItem`]. +/// +/// They are the the general form of drawing items, whereas [`RenderCommands`](RenderCommand) +/// are more modular. +pub trait Draw: Send + Sync + 'static { + /// Draws the [`PhaseItem`] by issuing draw calls via the [`TrackedRenderPass`]. + fn draw<'w>( + &mut self, + world: &'w World, + pass: &mut TrackedRenderPass<'w>, + view: Entity, + item: &P, + ); +} + +/// An item which will be drawn to the screen. A phase item should be queued up for rendering +/// during the [`RenderStage::Queue`](crate::RenderStage::Queue) stage. +/// Afterwards it will be sorted and rendered automatically in the +/// [`RenderStage::PhaseSort`](crate::RenderStage::PhaseSort) stage and +/// [`RenderStage::Render`](crate::RenderStage::Render) stage, respectively. +pub trait PhaseItem: Send + Sync + 'static { + /// The type used for ordering the items. The smallest values are drawn first. + type SortKey: Ord; + /// Determines the order in which the items are drawn during the corresponding [`RenderPhase`]. + fn sort_key(&self) -> Self::SortKey; + /// Specifies the [`Draw`] function used to render the item. + fn draw_function(&self) -> DrawFunctionId; +} + +// TODO: make this generic? +/// /// A [`Draw`] function identifier. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct DrawFunctionId(usize); + +/// Stores all draw functions for the [`PhaseItem`] type. +/// For retrieval they are associated with their [`TypeId`]. +pub struct DrawFunctionsInternal { + pub draw_functions: Vec>>, + pub indices: HashMap, +} + +impl DrawFunctionsInternal

{ + /// Adds the [`Draw`] function and associates it to its own type. + pub fn add>(&mut self, draw_function: T) -> DrawFunctionId { + self.add_with::(draw_function) + } + + /// Adds the [`Draw`] function and associates it to the type `T` + pub fn add_with>(&mut self, draw_function: D) -> DrawFunctionId { + self.draw_functions.push(Box::new(draw_function)); + let id = DrawFunctionId(self.draw_functions.len() - 1); + self.indices.insert(TypeId::of::(), id); + id + } + + /// Retrieves the [`Draw`] function corresponding to the `id` mutably. + pub fn get_mut(&mut self, id: DrawFunctionId) -> Option<&mut dyn Draw

> { + self.draw_functions.get_mut(id.0).map(|f| &mut **f) + } + + /// Retrieves the id of the [`Draw`] function corresponding to their associated type `T`. + pub fn get_id(&self) -> Option { + self.indices.get(&TypeId::of::()).copied() + } +} + +/// Stores all draw functions for the [`PhaseItem`] type hidden behind a reader-writer lock. +/// To access them the [`DrawFunctions::read`] and [`DrawFunctions::write`] methods are used. +pub struct DrawFunctions { + internal: RwLock>, +} + +impl Default for DrawFunctions

{ + fn default() -> Self { + Self { + internal: RwLock::new(DrawFunctionsInternal { + draw_functions: Vec::new(), + indices: HashMap::default(), + }), + } + } +} + +impl DrawFunctions

{ + /// Accesses the draw functions in read mode. + pub fn read(&self) -> RwLockReadGuard<'_, DrawFunctionsInternal

> { + self.internal.read() + } + + /// Accesses the draw functions in write mode. + pub fn write(&self) -> RwLockWriteGuard<'_, DrawFunctionsInternal

> { + self.internal.write() + } +} + +/// RenderCommand is a trait that runs an ECS query and produces one or more +/// [`TrackedRenderPass`] calls. Types implementing this trait can be composed (as tuples). +/// +/// They can be registered as a [`Draw`] function via the +/// [`AddRenderCommand::add_render_command`] method. +/// +/// # Example +/// The `DrawPbr` draw function is created from the following render command +/// tuple. Const generics are used to set specific bind group locations: +/// +/// ```ignore +/// pub type DrawPbr = ( +/// SetItemPipeline, +/// SetMeshViewBindGroup<0>, +/// SetStandardMaterialBindGroup<1>, +/// SetTransformBindGroup<2>, +/// DrawMesh, +/// ); +/// ``` +pub trait RenderCommand { + /// Specifies all ECS data required by [`RenderCommand::render`]. + /// All parameters have to be read only. + type Param: SystemParam; + + /// Renders the [`PhaseItem`] by issuing draw calls via the [`TrackedRenderPass`]. + fn render<'w>( + view: Entity, + item: &P, + param: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult; +} + +pub enum RenderCommandResult { + Success, + Failure, +} + +pub trait EntityRenderCommand { + type Param: SystemParam; + fn render<'w>( + view: Entity, + item: Entity, + param: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult; +} + +pub trait EntityPhaseItem: PhaseItem { + fn entity(&self) -> Entity; +} + +pub trait CachedPipelinePhaseItem: PhaseItem { + fn cached_pipeline(&self) -> CachedPipelineId; +} + +impl RenderCommand

for E { + type Param = E::Param; + + #[inline] + fn render<'w>( + view: Entity, + item: &P, + param: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + ::render(view, item.entity(), param, pass) + } +} + +pub struct SetItemPipeline; +impl RenderCommand

for SetItemPipeline { + type Param = SRes; + #[inline] + fn render<'w>( + _view: Entity, + item: &P, + pipeline_cache: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + if let Some(pipeline) = pipeline_cache.into_inner().get(item.cached_pipeline()) { + pass.set_render_pipeline(pipeline); + RenderCommandResult::Success + } else { + RenderCommandResult::Failure + } + } +} + +macro_rules! render_command_tuple_impl { + ($($name: ident),*) => { + impl),*> RenderCommand

for ($($name,)*) { + type Param = ($($name::Param,)*); + + #[allow(non_snake_case)] + fn render<'w>( + _view: Entity, + _item: &P, + ($($name,)*): SystemParamItem<'w, '_, Self::Param>, + _pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult{ + $(if let RenderCommandResult::Failure = $name::render(_view, _item, $name, _pass) { + return RenderCommandResult::Failure; + })* + RenderCommandResult::Success + } + } + }; +} + +all_tuples!(render_command_tuple_impl, 0, 15, C); + +/// Wraps a [`RenderCommand`] into a state so that it can be used as a [`Draw`] function. +/// Therefore the [`RenderCommand::Param`] is queried from the ECS and passed to the command. +pub struct RenderCommandState> { + state: SystemState, +} + +impl> RenderCommandState { + pub fn new(world: &mut World) -> Self { + Self { + state: SystemState::new(world), + } + } +} + +impl + Send + Sync + 'static> Draw

for RenderCommandState +where + ::Fetch: ReadOnlySystemParamFetch, +{ + /// Prepares the ECS parameters for the wrapped [`RenderCommand`] and then renders it. + fn draw<'w>( + &mut self, + world: &'w World, + pass: &mut TrackedRenderPass<'w>, + view: Entity, + item: &P, + ) { + let param = self.state.get(world); + C::render(view, item, param, pass); + } +} + +/// Registers a [`RenderCommand`] as a [`Draw`] function. +/// They are stored inside the [`DrawFunctions`] resource of the app. +pub trait AddRenderCommand { + /// Adds the [`RenderCommand`] for the specified [`RenderPhase`](super::RenderPhase) to the app. + fn add_render_command + Send + Sync + 'static>( + &mut self, + ) -> &mut Self + where + ::Fetch: ReadOnlySystemParamFetch; +} + +impl AddRenderCommand for App { + fn add_render_command + Send + Sync + 'static>( + &mut self, + ) -> &mut Self + where + ::Fetch: ReadOnlySystemParamFetch, + { + let draw_function = RenderCommandState::::new(&mut self.world); + let draw_functions = self.world.get_resource::>().unwrap(); + draw_functions.write().add_with::(draw_function); + self + } +} diff --git a/pipelined/bevy_render2/src/render_phase/draw_state.rs b/pipelined/bevy_render2/src/render_phase/draw_state.rs new file mode 100644 index 0000000000..62c19706d1 --- /dev/null +++ b/pipelined/bevy_render2/src/render_phase/draw_state.rs @@ -0,0 +1,231 @@ +use crate::render_resource::{ + BindGroup, BindGroupId, BufferId, BufferSlice, RenderPipeline, RenderPipelineId, +}; +use bevy_utils::tracing::debug; +use std::ops::Range; +use wgpu::{IndexFormat, RenderPass}; + +/// Tracks the current [`TrackedRenderPipeline`] state to ensure draw calls are valid. +#[derive(Debug, Default)] +pub struct DrawState { + pipeline: Option, + bind_groups: Vec<(Option, Vec)>, + vertex_buffers: Vec>, + index_buffer: Option<(BufferId, u64, IndexFormat)>, +} + +impl DrawState { + pub fn set_bind_group( + &mut self, + index: usize, + bind_group: BindGroupId, + dynamic_indices: &[u32], + ) { + if index >= self.bind_groups.len() { + self.bind_groups.resize(index + 1, (None, Vec::new())); + } + self.bind_groups[index].0 = Some(bind_group); + self.bind_groups[index].1.clear(); + self.bind_groups[index].1.extend(dynamic_indices); + } + + pub fn is_bind_group_set( + &self, + index: usize, + bind_group: BindGroupId, + dynamic_indices: &[u32], + ) -> bool { + if let Some(current_bind_group) = self.bind_groups.get(index) { + current_bind_group.0 == Some(bind_group) && dynamic_indices == current_bind_group.1 + } else { + false + } + } + + pub fn set_vertex_buffer(&mut self, index: usize, buffer: BufferId, offset: u64) { + if index >= self.vertex_buffers.len() { + self.vertex_buffers.resize(index + 1, None); + } + self.vertex_buffers[index] = Some((buffer, offset)); + } + + pub fn is_vertex_buffer_set(&self, index: usize, buffer: BufferId, offset: u64) -> bool { + if let Some(current) = self.vertex_buffers.get(index) { + *current == Some((buffer, offset)) + } else { + false + } + } + + pub fn set_index_buffer(&mut self, buffer: BufferId, offset: u64, index_format: IndexFormat) { + self.index_buffer = Some((buffer, offset, index_format)); + } + + pub fn is_index_buffer_set( + &self, + buffer: BufferId, + offset: u64, + index_format: IndexFormat, + ) -> bool { + self.index_buffer == Some((buffer, offset, index_format)) + } + + pub fn is_pipeline_set(&self, pipeline: RenderPipelineId) -> bool { + self.pipeline == Some(pipeline) + } + + pub fn set_pipeline(&mut self, pipeline: RenderPipelineId) { + // TODO: do these need to be cleared? + // self.bind_groups.clear(); + // self.vertex_buffers.clear(); + // self.index_buffer = None; + self.pipeline = Some(pipeline); + } +} + +/// A [`RenderPass`], which tracks the current pipeline state to ensure all draw calls are valid. +/// It is used to set the current [`RenderPipeline`], [`BindGroups`](BindGroup) and buffers. +/// After all requirements are specified, draw calls can be issued. +pub struct TrackedRenderPass<'a> { + pass: RenderPass<'a>, + state: DrawState, +} + +impl<'a> TrackedRenderPass<'a> { + /// Tracks the supplied render pass. + pub fn new(pass: RenderPass<'a>) -> Self { + Self { + state: DrawState::default(), + pass, + } + } + + /// Sets the active [`RenderPipeline`]. + /// + /// Subsequent draw calls will exhibit the behavior defined by the `pipeline`. + pub fn set_render_pipeline(&mut self, pipeline: &'a RenderPipeline) { + debug!("set pipeline: {:?}", pipeline); + if self.state.is_pipeline_set(pipeline.id()) { + return; + } + self.pass.set_pipeline(pipeline); + self.state.set_pipeline(pipeline.id()); + } + + /// Sets the active [`BindGroup`] for a given bind group index. The bind group layout in the + /// active pipeline when any `draw()` function is called must match the layout + /// of this `bind group`. + pub fn set_bind_group( + &mut self, + index: usize, + bind_group: &'a BindGroup, + dynamic_uniform_indices: &[u32], + ) { + if self + .state + .is_bind_group_set(index as usize, bind_group.id(), dynamic_uniform_indices) + { + debug!( + "set bind_group {} (already set): {:?} ({:?})", + index, bind_group, dynamic_uniform_indices + ); + return; + } else { + debug!( + "set bind_group {}: {:?} ({:?})", + index, bind_group, dynamic_uniform_indices + ); + } + self.pass + .set_bind_group(index as u32, bind_group, dynamic_uniform_indices); + self.state + .set_bind_group(index as usize, bind_group.id(), dynamic_uniform_indices); + } + + /// Assign a vertex buffer to a slot. + /// + /// Subsequent calls to [`TrackedRenderPass::draw`] and [`TrackedRenderPass::draw_indexed`] + /// will use the `buffer` as one of the source vertex buffers. + /// + /// The `slot` refers to the index of the matching descriptor in + /// [`VertexState::buffers`](crate::render_resource::VertexState::buffers). + pub fn set_vertex_buffer(&mut self, index: usize, buffer_slice: BufferSlice<'a>) { + let offset = buffer_slice.offset(); + if self + .state + .is_vertex_buffer_set(index, buffer_slice.id(), offset) + { + debug!( + "set vertex buffer {} (already set): {:?} ({})", + index, + buffer_slice.id(), + offset + ); + return; + } else { + debug!( + "set vertex buffer {}: {:?} ({})", + index, + buffer_slice.id(), + offset + ); + } + self.pass.set_vertex_buffer(index as u32, *buffer_slice); + self.state + .set_vertex_buffer(index, buffer_slice.id(), offset); + } + + /// Sets the active index buffer. + /// + /// Subsequent calls to [`TrackedRenderPass::draw_indexed`] will use the `buffer` as + /// the source index buffer. + pub fn set_index_buffer( + &mut self, + buffer_slice: BufferSlice<'a>, + offset: u64, + index_format: IndexFormat, + ) { + if self + .state + .is_index_buffer_set(buffer_slice.id(), offset, index_format) + { + debug!( + "set index buffer (already set): {:?} ({})", + buffer_slice.id(), + offset + ); + return; + } else { + debug!("set index buffer: {:?} ({})", buffer_slice.id(), offset); + } + self.pass.set_index_buffer(*buffer_slice, index_format); + self.state + .set_index_buffer(buffer_slice.id(), offset, index_format); + } + + /// Draws primitives from the active vertex buffer(s). + /// + /// The active vertex buffers can be set with [`TrackedRenderPass::set_vertex_buffer`]. + pub fn draw(&mut self, vertices: Range, instances: Range) { + debug!("draw: {:?} {:?}", vertices, instances); + self.pass.draw(vertices, instances); + } + + /// Draws indexed primitives using the active index buffer and the active vertex buffer(s). + /// + /// The active index buffer can be set with [`TrackedRenderPass::set_index_buffer`], while the + /// active vertex buffers can be set with [`TrackedRenderPass::set_vertex_buffer`]. + pub fn draw_indexed(&mut self, indices: Range, base_vertex: i32, instances: Range) { + debug!( + "draw indexed: {:?} {} {:?}", + indices, base_vertex, instances + ); + self.pass.draw_indexed(indices, base_vertex, instances); + } + + pub fn set_stencil_reference(&mut self, reference: u32) { + debug!("set stencil reference: {}", reference); + + self.pass.set_stencil_reference(reference); + } +} diff --git a/pipelined/bevy_render2/src/render_phase/mod.rs b/pipelined/bevy_render2/src/render_phase/mod.rs new file mode 100644 index 0000000000..594b9bbfe0 --- /dev/null +++ b/pipelined/bevy_render2/src/render_phase/mod.rs @@ -0,0 +1,39 @@ +mod draw; +mod draw_state; + +pub use draw::*; +pub use draw_state::*; + +use bevy_ecs::prelude::{Component, Query}; + +/// A resource to collect and sort draw requests for specific [`PhaseItems`](PhaseItem). +#[derive(Component)] +pub struct RenderPhase { + pub items: Vec, +} + +impl Default for RenderPhase { + fn default() -> Self { + Self { items: Vec::new() } + } +} + +impl RenderPhase { + /// Adds a [`PhaseItem`] to this render phase. + #[inline] + pub fn add(&mut self, item: I) { + self.items.push(item); + } + + /// Sorts all of its [`PhaseItems`](PhaseItem). + pub fn sort(&mut self) { + self.items.sort_by_key(|d| d.sort_key()); + } +} + +/// This system sorts all [`RenderPhases`](RenderPhase) for the [`PhaseItem`] type. +pub fn sort_phase_system(mut render_phases: Query<&mut RenderPhase>) { + for mut phase in render_phases.iter_mut() { + phase.sort(); + } +} diff --git a/pipelined/bevy_render2/src/render_resource/bind_group.rs b/pipelined/bevy_render2/src/render_resource/bind_group.rs new file mode 100644 index 0000000000..6ee9633c2b --- /dev/null +++ b/pipelined/bevy_render2/src/render_resource/bind_group.rs @@ -0,0 +1,44 @@ +use bevy_reflect::Uuid; +use std::{ops::Deref, sync::Arc}; + +/// A [`BindGroup`] identifier. +#[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)] +pub struct BindGroupId(Uuid); + +/// Bind groups are responsible for binding render resources (e.g. buffers, textures, samplers) +/// to a [`TrackedRenderPass`](crate::render_phase::TrackedRenderPass). +/// This makes them accessible in the pipeline (shaders) as uniforms. +/// +/// May be converted from and dereferences to a wgpu [`BindGroup`](wgpu::BindGroup). +/// Can be created via [`RenderDevice::create_bind_group`](crate::renderer::RenderDevice::create_bind_group). +#[derive(Clone, Debug)] +pub struct BindGroup { + id: BindGroupId, + value: Arc, +} + +impl BindGroup { + /// Returns the [`BindGroupId`]. + #[inline] + pub fn id(&self) -> BindGroupId { + self.id + } +} + +impl From for BindGroup { + fn from(value: wgpu::BindGroup) -> Self { + BindGroup { + id: BindGroupId(Uuid::new_v4()), + value: Arc::new(value), + } + } +} + +impl Deref for BindGroup { + type Target = wgpu::BindGroup; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.value + } +} diff --git a/pipelined/bevy_render2/src/render_resource/bind_group_layout.rs b/pipelined/bevy_render2/src/render_resource/bind_group_layout.rs new file mode 100644 index 0000000000..d17d639c3a --- /dev/null +++ b/pipelined/bevy_render2/src/render_resource/bind_group_layout.rs @@ -0,0 +1,41 @@ +use bevy_reflect::Uuid; +use std::{ops::Deref, sync::Arc}; + +#[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)] +pub struct BindGroupLayoutId(Uuid); + +#[derive(Clone, Debug)] +pub struct BindGroupLayout { + id: BindGroupLayoutId, + value: Arc, +} + +impl BindGroupLayout { + #[inline] + pub fn id(&self) -> BindGroupLayoutId { + self.id + } + + #[inline] + pub fn value(&self) -> &wgpu::BindGroupLayout { + &self.value + } +} + +impl From for BindGroupLayout { + fn from(value: wgpu::BindGroupLayout) -> Self { + BindGroupLayout { + id: BindGroupLayoutId(Uuid::new_v4()), + value: Arc::new(value), + } + } +} + +impl Deref for BindGroupLayout { + type Target = wgpu::BindGroupLayout; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.value + } +} diff --git a/pipelined/bevy_render2/src/render_resource/buffer.rs b/pipelined/bevy_render2/src/render_resource/buffer.rs new file mode 100644 index 0000000000..a902af5834 --- /dev/null +++ b/pipelined/bevy_render2/src/render_resource/buffer.rs @@ -0,0 +1,85 @@ +use bevy_utils::Uuid; +use std::{ + ops::{Bound, Deref, RangeBounds}, + sync::Arc, +}; + +#[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)] +pub struct BufferId(Uuid); + +#[derive(Clone, Debug)] +pub struct Buffer { + id: BufferId, + value: Arc, +} + +impl Buffer { + #[inline] + pub fn id(&self) -> BufferId { + self.id + } + + pub fn slice(&self, bounds: impl RangeBounds) -> BufferSlice { + BufferSlice { + id: self.id, + // need to compute and store this manually because wgpu doesn't export offset on wgpu::BufferSlice + offset: match bounds.start_bound() { + Bound::Included(&bound) => bound, + Bound::Excluded(&bound) => bound + 1, + Bound::Unbounded => 0, + }, + value: self.value.slice(bounds), + } + } + + #[inline] + pub fn unmap(&self) { + self.value.unmap() + } +} + +impl From for Buffer { + fn from(value: wgpu::Buffer) -> Self { + Buffer { + id: BufferId(Uuid::new_v4()), + value: Arc::new(value), + } + } +} + +impl Deref for Buffer { + type Target = wgpu::Buffer; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.value + } +} + +#[derive(Clone, Debug)] +pub struct BufferSlice<'a> { + id: BufferId, + offset: wgpu::BufferAddress, + value: wgpu::BufferSlice<'a>, +} + +impl<'a> BufferSlice<'a> { + #[inline] + pub fn id(&self) -> BufferId { + self.id + } + + #[inline] + pub fn offset(&self) -> wgpu::BufferAddress { + self.offset + } +} + +impl<'a> Deref for BufferSlice<'a> { + type Target = wgpu::BufferSlice<'a>; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.value + } +} diff --git a/pipelined/bevy_render2/src/render_resource/buffer_vec.rs b/pipelined/bevy_render2/src/render_resource/buffer_vec.rs new file mode 100644 index 0000000000..ccdadb119c --- /dev/null +++ b/pipelined/bevy_render2/src/render_resource/buffer_vec.rs @@ -0,0 +1,90 @@ +use crate::{ + render_resource::Buffer, + renderer::{RenderDevice, RenderQueue}, +}; +use bevy_core::{cast_slice, Pod}; +use wgpu::BufferUsages; + +pub struct BufferVec { + values: Vec, + buffer: Option, + capacity: usize, + item_size: usize, + buffer_usage: BufferUsages, +} + +impl Default for BufferVec { + fn default() -> Self { + Self { + values: Vec::new(), + buffer: None, + capacity: 0, + buffer_usage: BufferUsages::all(), + item_size: std::mem::size_of::(), + } + } +} + +impl BufferVec { + pub fn new(buffer_usage: BufferUsages) -> Self { + Self { + buffer_usage, + ..Default::default() + } + } + + #[inline] + pub fn buffer(&self) -> Option<&Buffer> { + self.buffer.as_ref() + } + + #[inline] + pub fn capacity(&self) -> usize { + self.capacity + } + + #[inline] + pub fn len(&self) -> usize { + self.values.len() + } + + #[inline] + pub fn is_empty(&self) -> bool { + self.values.is_empty() + } + + pub fn push(&mut self, value: T) -> usize { + let index = self.values.len(); + self.values.push(value); + index + } + + pub fn reserve(&mut self, capacity: usize, device: &RenderDevice) { + if capacity > self.capacity { + self.capacity = capacity; + let size = self.item_size * capacity; + self.buffer = Some(device.create_buffer(&wgpu::BufferDescriptor { + label: None, + size: size as wgpu::BufferAddress, + usage: BufferUsages::COPY_DST | self.buffer_usage, + mapped_at_creation: false, + })); + } + } + + pub fn write_buffer(&mut self, device: &RenderDevice, queue: &RenderQueue) { + if self.values.is_empty() { + return; + } + self.reserve(self.values.len(), device); + if let Some(buffer) = &self.buffer { + let range = 0..self.item_size * self.values.len(); + let bytes: &[u8] = cast_slice(&self.values); + queue.write_buffer(buffer, 0, &bytes[range]); + } + } + + pub fn clear(&mut self) { + self.values.clear(); + } +} diff --git a/pipelined/bevy_render2/src/render_resource/mod.rs b/pipelined/bevy_render2/src/render_resource/mod.rs new file mode 100644 index 0000000000..144fe4917f --- /dev/null +++ b/pipelined/bevy_render2/src/render_resource/mod.rs @@ -0,0 +1,41 @@ +mod bind_group; +mod bind_group_layout; +mod buffer; +mod buffer_vec; +mod pipeline; +mod pipeline_cache; +mod pipeline_specializer; +mod shader; +mod texture; +mod uniform_vec; + +pub use bind_group::*; +pub use bind_group_layout::*; +pub use buffer::*; +pub use buffer_vec::*; +pub use pipeline::*; +pub use pipeline_cache::*; +pub use pipeline_specializer::*; +pub use shader::*; +pub use texture::*; +pub use uniform_vec::*; + +// TODO: decide where re-exports should go +pub use wgpu::{ + util::BufferInitDescriptor, AddressMode, BindGroupDescriptor, BindGroupEntry, + BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType, BlendComponent, + BlendFactor, BlendOperation, BlendState, BufferAddress, BufferBindingType, BufferSize, + BufferUsages, ColorTargetState, ColorWrites, CompareFunction, ComputePassDescriptor, + ComputePipelineDescriptor, DepthBiasState, DepthStencilState, Extent3d, Face, FilterMode, + FragmentState as RawFragmentState, FrontFace, ImageCopyBuffer, ImageCopyBufferBase, + ImageCopyTexture, ImageCopyTextureBase, ImageDataLayout, ImageSubresourceRange, IndexFormat, + LoadOp, MultisampleState, Operations, Origin3d, PipelineLayout, PipelineLayoutDescriptor, + PolygonMode, PrimitiveState, PrimitiveTopology, RenderPassColorAttachment, + RenderPassDepthStencilAttachment, RenderPassDescriptor, + RenderPipelineDescriptor as RawRenderPipelineDescriptor, SamplerDescriptor, ShaderModule, + ShaderModuleDescriptor, ShaderSource, ShaderStages, StencilFaceState, StencilOperation, + StencilState, StorageTextureAccess, TextureAspect, TextureDescriptor, TextureDimension, + TextureFormat, TextureSampleType, TextureUsages, TextureViewDescriptor, TextureViewDimension, + VertexAttribute, VertexBufferLayout as RawVertexBufferLayout, VertexFormat, + VertexState as RawVertexState, VertexStepMode, +}; diff --git a/pipelined/bevy_render2/src/render_resource/pipeline.rs b/pipelined/bevy_render2/src/render_resource/pipeline.rs new file mode 100644 index 0000000000..f462c98c53 --- /dev/null +++ b/pipelined/bevy_render2/src/render_resource/pipeline.rs @@ -0,0 +1,142 @@ +use crate::render_resource::{BindGroupLayout, Shader}; +use bevy_asset::Handle; +use bevy_reflect::Uuid; +use std::{borrow::Cow, ops::Deref, sync::Arc}; +use wgpu::{ + BufferAddress, ColorTargetState, DepthStencilState, MultisampleState, PrimitiveState, + VertexAttribute, VertexStepMode, +}; + +/// A [`RenderPipeline`] identifier. +#[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)] +pub struct RenderPipelineId(Uuid); + +/// A RenderPipeline represents a graphics pipeline and its stages (shaders), bindings and vertex buffers. +/// +/// May be converted from and dereferences to a wgpu [`RenderPipeline`](wgpu::RenderPipeline). +/// Can be created via [`RenderDevice::create_render_pipeline`](crate::renderer::RenderDevice::create_render_pipeline). +#[derive(Clone, Debug)] +pub struct RenderPipeline { + id: RenderPipelineId, + value: Arc, +} + +impl RenderPipeline { + #[inline] + pub fn id(&self) -> RenderPipelineId { + self.id + } +} + +impl From for RenderPipeline { + fn from(value: wgpu::RenderPipeline) -> Self { + RenderPipeline { + id: RenderPipelineId(Uuid::new_v4()), + value: Arc::new(value), + } + } +} + +impl Deref for RenderPipeline { + type Target = wgpu::RenderPipeline; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.value + } +} + +/// A [`ComputePipeline`] identifier. +#[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)] +pub struct ComputePipelineId(Uuid); + +/// A ComputePipeline represents a compute pipeline and its single shader stage. +/// +/// May be converted from and dereferences to a wgpu [`ComputePipeline`](wgpu::ComputePipeline). +/// Can be created via [`RenderDevice::create_compute_pipeline`](crate::renderer::RenderDevice::create_compute_pipeline). +#[derive(Clone, Debug)] +pub struct ComputePipeline { + id: ComputePipelineId, + value: Arc, +} + +impl ComputePipeline { + /// Returns the [`ComputePipelineId`]. + #[inline] + pub fn id(&self) -> ComputePipelineId { + self.id + } +} + +impl From for ComputePipeline { + fn from(value: wgpu::ComputePipeline) -> Self { + ComputePipeline { + id: ComputePipelineId(Uuid::new_v4()), + value: Arc::new(value), + } + } +} + +impl Deref for ComputePipeline { + type Target = wgpu::ComputePipeline; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.value + } +} + +/// Describes a render (graphics) pipeline. +#[derive(Clone, Debug)] +pub struct RenderPipelineDescriptor { + /// Debug label of the pipeline. This will show up in graphics debuggers for easy identification. + pub label: Option>, + /// The layout of bind groups for this pipeline. + pub layout: Option>, + /// The compiled vertex stage, its entry point, and the input buffers layout. + pub vertex: VertexState, + /// The properties of the pipeline at the primitive assembly and rasterization level. + pub primitive: PrimitiveState, + /// The effect of draw calls on the depth and stencil aspects of the output target, if any. + pub depth_stencil: Option, + /// The multi-sampling properties of the pipeline. + pub multisample: MultisampleState, + /// The compiled fragment stage, its entry point, and the color targets. + pub fragment: Option, +} + +#[derive(Clone, Debug)] +pub struct VertexState { + /// The compiled shader module for this stage. + pub shader: Handle, + pub shader_defs: Vec, + /// The name of the entry point in the compiled shader. There must be a function that returns + /// void with this name in the shader. + pub entry_point: Cow<'static, str>, + /// The format of any vertex buffers used with this pipeline. + pub buffers: Vec, +} + +/// Describes how the vertex buffer is interpreted. +#[derive(Clone, Debug, Hash, Eq, PartialEq)] +pub struct VertexBufferLayout { + /// The stride, in bytes, between elements of this buffer. + pub array_stride: BufferAddress, + /// How often this vertex buffer is "stepped" forward. + pub step_mode: VertexStepMode, + /// The list of attributes which comprise a single vertex. + pub attributes: Vec, +} + +/// Describes the fragment process in a render pipeline. +#[derive(Clone, Debug)] +pub struct FragmentState { + /// The compiled shader module for this stage. + pub shader: Handle, + pub shader_defs: Vec, + /// The name of the entry point in the compiled shader. There must be a function that returns + /// void with this name in the shader. + pub entry_point: Cow<'static, str>, + /// The color state of the render targets. + pub targets: Vec, +} diff --git a/pipelined/bevy_render2/src/render_resource/pipeline_cache.rs b/pipelined/bevy_render2/src/render_resource/pipeline_cache.rs new file mode 100644 index 0000000000..1dff4fe617 --- /dev/null +++ b/pipelined/bevy_render2/src/render_resource/pipeline_cache.rs @@ -0,0 +1,387 @@ +use crate::{ + render_resource::{ + AsModuleDescriptorError, BindGroupLayout, BindGroupLayoutId, ProcessShaderError, + RawFragmentState, RawRenderPipelineDescriptor, RawVertexState, RenderPipeline, + RenderPipelineDescriptor, Shader, ShaderImport, ShaderProcessor, + }, + renderer::RenderDevice, + RenderWorld, +}; +use bevy_app::EventReader; +use bevy_asset::{AssetEvent, Assets, Handle}; +use bevy_ecs::system::{Res, ResMut}; +use bevy_utils::{HashMap, HashSet}; +use std::{collections::hash_map::Entry, hash::Hash, ops::Deref, sync::Arc}; +use thiserror::Error; +use wgpu::{PipelineLayoutDescriptor, ShaderModule, VertexBufferLayout}; + +#[derive(Default)] +pub struct ShaderData { + pipelines: HashSet, + processed_shaders: HashMap, Arc>, + resolved_imports: HashMap>, + dependents: HashSet>, +} + +#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)] +pub struct CachedPipelineId(usize); + +impl CachedPipelineId { + pub const INVALID: Self = CachedPipelineId(usize::MAX); +} + +#[derive(Default)] +struct ShaderCache { + data: HashMap, ShaderData>, + shaders: HashMap, Shader>, + import_path_shaders: HashMap>, + waiting_on_import: HashMap>>, + processor: ShaderProcessor, +} + +impl ShaderCache { + fn get( + &mut self, + render_device: &RenderDevice, + pipeline: CachedPipelineId, + handle: &Handle, + shader_defs: &[String], + ) -> Result, RenderPipelineError> { + let shader = self + .shaders + .get(handle) + .ok_or_else(|| RenderPipelineError::ShaderNotLoaded(handle.clone_weak()))?; + let data = self.data.entry(handle.clone_weak()).or_default(); + if shader.imports().len() != data.resolved_imports.len() { + return Err(RenderPipelineError::ShaderImportNotYetAvailable); + } + + data.pipelines.insert(pipeline); + + // PERF: this shader_defs clone isn't great. use raw_entry_mut when it stabilizes + let module = match data.processed_shaders.entry(shader_defs.to_vec()) { + Entry::Occupied(entry) => entry.into_mut(), + Entry::Vacant(entry) => { + let processed = self.processor.process( + shader, + shader_defs, + &self.shaders, + &self.import_path_shaders, + )?; + let module_descriptor = processed.get_module_descriptor()?; + entry.insert(Arc::new( + render_device.create_shader_module(&module_descriptor), + )) + } + }; + + Ok(module.clone()) + } + + fn clear(&mut self, handle: &Handle) -> Vec { + let mut shaders_to_clear = vec![handle.clone_weak()]; + let mut pipelines_to_queue = Vec::new(); + while let Some(handle) = shaders_to_clear.pop() { + if let Some(data) = self.data.get_mut(&handle) { + data.processed_shaders.clear(); + pipelines_to_queue.extend(data.pipelines.iter().cloned()); + shaders_to_clear.extend(data.dependents.iter().map(|h| h.clone_weak())); + } + } + + pipelines_to_queue + } + + fn set_shader(&mut self, handle: &Handle, shader: Shader) -> Vec { + let pipelines_to_queue = self.clear(handle); + if let Some(path) = shader.import_path() { + self.import_path_shaders + .insert(path.clone(), handle.clone_weak()); + if let Some(waiting_shaders) = self.waiting_on_import.get_mut(path) { + for waiting_shader in waiting_shaders.drain(..) { + // resolve waiting shader import + let data = self.data.entry(waiting_shader.clone_weak()).or_default(); + data.resolved_imports + .insert(path.clone(), handle.clone_weak()); + // add waiting shader as dependent of this shader + let data = self.data.entry(handle.clone_weak()).or_default(); + data.dependents.insert(waiting_shader.clone_weak()); + } + } + } + + for import in shader.imports() { + if let Some(import_handle) = self.import_path_shaders.get(import) { + // resolve import because it is currently available + let data = self.data.entry(handle.clone_weak()).or_default(); + data.resolved_imports + .insert(import.clone(), import_handle.clone_weak()); + // add this shader as a dependent of the import + let data = self.data.entry(import_handle.clone_weak()).or_default(); + data.dependents.insert(handle.clone_weak()); + } else { + let waiting = self.waiting_on_import.entry(import.clone()).or_default(); + waiting.push(handle.clone_weak()); + } + } + + self.shaders.insert(handle.clone_weak(), shader); + pipelines_to_queue + } + + fn remove(&mut self, handle: &Handle) -> Vec { + let pipelines_to_queue = self.clear(handle); + if let Some(shader) = self.shaders.remove(handle) { + if let Some(import_path) = shader.import_path() { + self.import_path_shaders.remove(import_path); + } + } + + pipelines_to_queue + } +} + +#[derive(Default)] +struct LayoutCache { + layouts: HashMap, wgpu::PipelineLayout>, +} + +impl LayoutCache { + fn get( + &mut self, + render_device: &RenderDevice, + bind_group_layouts: &[BindGroupLayout], + ) -> &wgpu::PipelineLayout { + let key = bind_group_layouts.iter().map(|l| l.id()).collect(); + self.layouts.entry(key).or_insert_with(|| { + let bind_group_layouts = bind_group_layouts + .iter() + .map(|l| l.value()) + .collect::>(); + render_device.create_pipeline_layout(&PipelineLayoutDescriptor { + bind_group_layouts: &bind_group_layouts, + ..Default::default() + }) + }) + } +} + +pub struct RenderPipelineCache { + layout_cache: LayoutCache, + shader_cache: ShaderCache, + device: RenderDevice, + pipelines: Vec, + waiting_pipelines: HashSet, +} + +struct CachedPipeline { + descriptor: RenderPipelineDescriptor, + state: CachedPipelineState, +} + +#[derive(Debug)] +pub enum CachedPipelineState { + Queued, + Ok(RenderPipeline), + Err(RenderPipelineError), +} + +impl CachedPipelineState { + pub fn unwrap(&self) -> &RenderPipeline { + match self { + CachedPipelineState::Ok(pipeline) => pipeline, + CachedPipelineState::Queued => { + panic!("Pipeline has not been compiled yet. It is still in the 'Queued' state.") + } + CachedPipelineState::Err(err) => panic!("{}", err), + } + } +} + +#[derive(Error, Debug)] +pub enum RenderPipelineError { + #[error( + "Pipeline cound not be compiled because the following shader is not loaded yet: {0:?}" + )] + ShaderNotLoaded(Handle), + #[error(transparent)] + ProcessShaderError(#[from] ProcessShaderError), + #[error(transparent)] + AsModuleDescriptorError(#[from] AsModuleDescriptorError), + #[error("Shader import not yet available.")] + ShaderImportNotYetAvailable, +} + +impl RenderPipelineCache { + pub fn new(device: RenderDevice) -> Self { + Self { + device, + layout_cache: Default::default(), + shader_cache: Default::default(), + waiting_pipelines: Default::default(), + pipelines: Default::default(), + } + } + + #[inline] + pub fn get_state(&self, id: CachedPipelineId) -> &CachedPipelineState { + &self.pipelines[id.0].state + } + + #[inline] + pub fn get(&self, id: CachedPipelineId) -> Option<&RenderPipeline> { + if let CachedPipelineState::Ok(pipeline) = &self.pipelines[id.0].state { + Some(pipeline) + } else { + None + } + } + + pub fn queue(&mut self, descriptor: RenderPipelineDescriptor) -> CachedPipelineId { + let id = CachedPipelineId(self.pipelines.len()); + self.pipelines.push(CachedPipeline { + descriptor, + state: CachedPipelineState::Queued, + }); + self.waiting_pipelines.insert(id); + id + } + + fn set_shader(&mut self, handle: &Handle, shader: &Shader) { + let pipelines_to_queue = self.shader_cache.set_shader(handle, shader.clone()); + for cached_pipeline in pipelines_to_queue { + self.pipelines[cached_pipeline.0].state = CachedPipelineState::Queued; + self.waiting_pipelines.insert(cached_pipeline); + } + } + + fn remove_shader(&mut self, shader: &Handle) { + let pipelines_to_queue = self.shader_cache.remove(shader); + for cached_pipeline in pipelines_to_queue { + self.pipelines[cached_pipeline.0].state = CachedPipelineState::Queued; + self.waiting_pipelines.insert(cached_pipeline); + } + } + + pub fn process_queue(&mut self) { + let pipelines = std::mem::take(&mut self.waiting_pipelines); + for id in pipelines { + let state = &mut self.pipelines[id.0]; + match &state.state { + CachedPipelineState::Ok(_) => continue, + CachedPipelineState::Queued => {} + CachedPipelineState::Err(err) => { + match err { + RenderPipelineError::ShaderNotLoaded(_) + | RenderPipelineError::ShaderImportNotYetAvailable => { /* retry */ } + RenderPipelineError::ProcessShaderError(_) + | RenderPipelineError::AsModuleDescriptorError(_) => { + // shader could not be processed ... retrying won't help + continue; + } + } + } + } + + let descriptor = &state.descriptor; + let vertex_module = match self.shader_cache.get( + &self.device, + id, + &descriptor.vertex.shader, + &descriptor.vertex.shader_defs, + ) { + Ok(module) => module, + Err(err) => { + state.state = CachedPipelineState::Err(err); + self.waiting_pipelines.insert(id); + continue; + } + }; + + let fragment_data = if let Some(fragment) = &descriptor.fragment { + let fragment_module = match self.shader_cache.get( + &self.device, + id, + &fragment.shader, + &fragment.shader_defs, + ) { + Ok(module) => module, + Err(err) => { + state.state = CachedPipelineState::Err(err); + self.waiting_pipelines.insert(id); + continue; + } + }; + Some(( + fragment_module, + fragment.entry_point.deref(), + &fragment.targets, + )) + } else { + None + }; + + let vertex_buffer_layouts = descriptor + .vertex + .buffers + .iter() + .map(|layout| VertexBufferLayout { + array_stride: layout.array_stride, + attributes: &layout.attributes, + step_mode: layout.step_mode, + }) + .collect::>(); + + let layout = if let Some(layout) = &descriptor.layout { + Some(self.layout_cache.get(&self.device, layout)) + } else { + None + }; + + let descriptor = RawRenderPipelineDescriptor { + depth_stencil: descriptor.depth_stencil.clone(), + label: descriptor.label.as_deref(), + layout, + multisample: descriptor.multisample, + primitive: descriptor.primitive, + vertex: RawVertexState { + buffers: &vertex_buffer_layouts, + entry_point: descriptor.vertex.entry_point.deref(), + module: &vertex_module, + }, + fragment: fragment_data + .as_ref() + .map(|(module, entry_point, targets)| RawFragmentState { + entry_point, + module, + targets, + }), + }; + + let pipeline = self.device.create_render_pipeline(&descriptor); + state.state = CachedPipelineState::Ok(pipeline); + } + } + + pub(crate) fn process_pipeline_queue_system(mut cache: ResMut) { + cache.process_queue(); + } + + pub(crate) fn extract_shaders( + mut world: ResMut, + shaders: Res>, + mut events: EventReader>, + ) { + let mut cache = world.get_resource_mut::().unwrap(); + for event in events.iter() { + match event { + AssetEvent::Created { handle } | AssetEvent::Modified { handle } => { + if let Some(shader) = shaders.get(handle) { + cache.set_shader(handle, shader); + } + } + AssetEvent::Removed { handle } => cache.remove_shader(handle), + } + } + } +} diff --git a/pipelined/bevy_render2/src/render_resource/pipeline_specializer.rs b/pipelined/bevy_render2/src/render_resource/pipeline_specializer.rs new file mode 100644 index 0000000000..a5cb472aa4 --- /dev/null +++ b/pipelined/bevy_render2/src/render_resource/pipeline_specializer.rs @@ -0,0 +1,34 @@ +use crate::render_resource::{CachedPipelineId, RenderPipelineCache, RenderPipelineDescriptor}; +use bevy_utils::HashMap; +use std::hash::Hash; + +pub struct SpecializedPipelines { + cache: HashMap, +} + +impl Default for SpecializedPipelines { + fn default() -> Self { + Self { + cache: Default::default(), + } + } +} + +impl SpecializedPipelines { + pub fn specialize( + &mut self, + cache: &mut RenderPipelineCache, + specialize_pipeline: &S, + key: S::Key, + ) -> CachedPipelineId { + *self.cache.entry(key.clone()).or_insert_with(|| { + let descriptor = specialize_pipeline.specialize(key); + cache.queue(descriptor) + }) + } +} + +pub trait SpecializedPipeline { + type Key: Clone + Hash + PartialEq + Eq; + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor; +} diff --git a/pipelined/bevy_render2/src/render_resource/shader.rs b/pipelined/bevy_render2/src/render_resource/shader.rs new file mode 100644 index 0000000000..a278151379 --- /dev/null +++ b/pipelined/bevy_render2/src/render_resource/shader.rs @@ -0,0 +1,889 @@ +use bevy_asset::{AssetLoader, Handle, LoadContext, LoadedAsset}; +use bevy_reflect::{TypeUuid, Uuid}; +use bevy_utils::{tracing::error, BoxedFuture, HashMap}; +use naga::{valid::ModuleInfo, Module}; +use once_cell::sync::Lazy; +use regex::Regex; +use std::{ + borrow::Cow, collections::HashSet, marker::Copy, ops::Deref, path::PathBuf, str::FromStr, +}; +use thiserror::Error; +use wgpu::{ShaderModuleDescriptor, ShaderSource}; + +#[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)] +pub struct ShaderId(Uuid); + +impl ShaderId { + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + ShaderId(Uuid::new_v4()) + } +} + +#[derive(Error, Debug)] +pub enum ShaderReflectError { + #[error(transparent)] + WgslParse(#[from] naga::front::wgsl::ParseError), + #[error("GLSL Parse Error: {0:?}")] + GlslParse(Vec), + #[error(transparent)] + SpirVParse(#[from] naga::front::spv::Error), + #[error(transparent)] + Validation(#[from] naga::valid::ValidationError), +} + +/// A shader, as defined by its [ShaderSource] and [ShaderStage] +/// This is an "unprocessed" shader. It can contain preprocessor directives. +#[derive(Debug, Clone, TypeUuid)] +#[uuid = "d95bc916-6c55-4de3-9622-37e7b6969fda"] +pub struct Shader { + source: Source, + import_path: Option, + imports: Vec, +} + +impl Shader { + pub fn from_wgsl(source: impl Into>) -> Shader { + let source = source.into(); + Shader { + imports: SHADER_IMPORT_PROCESSOR.get_imports_from_str(&source), + source: Source::Wgsl(source), + import_path: None, + } + } + + pub fn from_glsl(source: impl Into>, stage: naga::ShaderStage) -> Shader { + let source = source.into(); + Shader { + imports: SHADER_IMPORT_PROCESSOR.get_imports_from_str(&source), + source: Source::Glsl(source, stage), + import_path: None, + } + } + + pub fn from_spirv(source: impl Into>) -> Shader { + Shader { + imports: Vec::new(), + source: Source::SpirV(source.into()), + import_path: None, + } + } + + pub fn set_import_path>(&mut self, import_path: P) { + self.import_path = Some(ShaderImport::Custom(import_path.into())); + } + + pub fn with_import_path>(mut self, import_path: P) -> Self { + self.set_import_path(import_path); + self + } + + #[inline] + pub fn import_path(&self) -> Option<&ShaderImport> { + self.import_path.as_ref() + } + + pub fn imports(&self) -> impl ExactSizeIterator { + self.imports.iter() + } +} + +#[derive(Debug, Clone)] +pub enum Source { + Wgsl(Cow<'static, str>), + Glsl(Cow<'static, str>, naga::ShaderStage), + SpirV(Cow<'static, [u8]>), + // TODO: consider the following + // PrecompiledSpirVMacros(HashMap, Vec>) + // NagaModule(Module) ... Module impls Serialize/Deserialize +} + +/// A processed [Shader]. This cannot contain preprocessor directions. It must be "ready to compile" +#[derive(PartialEq, Eq, Debug)] +pub enum ProcessedShader { + Wgsl(Cow<'static, str>), + Glsl(Cow<'static, str>, naga::ShaderStage), + SpirV(Cow<'static, [u8]>), +} + +impl ProcessedShader { + pub fn get_wgsl_source(&self) -> Option<&str> { + if let ProcessedShader::Wgsl(source) = self { + Some(source) + } else { + None + } + } + pub fn get_glsl_source(&self) -> Option<&str> { + if let ProcessedShader::Glsl(source, _stage) = self { + Some(source) + } else { + None + } + } + + pub fn reflect(&self) -> Result { + let module = match &self { + // TODO: process macros here + ProcessedShader::Wgsl(source) => naga::front::wgsl::parse_str(source)?, + ProcessedShader::Glsl(source, shader_stage) => { + let mut parser = naga::front::glsl::Parser::default(); + parser + .parse(&naga::front::glsl::Options::from(*shader_stage), source) + .map_err(ShaderReflectError::GlslParse)? + } + ProcessedShader::SpirV(source) => naga::front::spv::parse_u8_slice( + source, + &naga::front::spv::Options { + adjust_coordinate_space: false, + ..naga::front::spv::Options::default() + }, + )?, + }; + let module_info = naga::valid::Validator::new( + naga::valid::ValidationFlags::default(), + naga::valid::Capabilities::default(), + ) + .validate(&module)?; + + Ok(ShaderReflection { + module, + module_info, + }) + } + + pub fn get_module_descriptor(&self) -> Result { + Ok(ShaderModuleDescriptor { + label: None, + source: match self { + ProcessedShader::Wgsl(source) => ShaderSource::Wgsl(source.clone()), + ProcessedShader::Glsl(_source, _stage) => { + let reflection = self.reflect()?; + // TODO: it probably makes more sense to convert this to spirv, but as of writing + // this comment, naga's spirv conversion is broken + let wgsl = reflection.get_wgsl()?; + ShaderSource::Wgsl(wgsl.into()) + } + ProcessedShader::SpirV(_) => { + // TODO: we can probably just transmute the u8 array to u32? + let reflection = self.reflect()?; + let spirv = reflection.get_spirv()?; + ShaderSource::SpirV(Cow::Owned(spirv)) + } + }, + }) + } +} + +#[derive(Error, Debug)] +pub enum AsModuleDescriptorError { + #[error(transparent)] + ShaderReflectError(#[from] ShaderReflectError), + #[error(transparent)] + WgslConversion(#[from] naga::back::wgsl::Error), + #[error(transparent)] + SpirVConversion(#[from] naga::back::spv::Error), +} + +pub struct ShaderReflection { + pub module: Module, + pub module_info: ModuleInfo, +} + +impl ShaderReflection { + pub fn get_spirv(&self) -> Result, naga::back::spv::Error> { + naga::back::spv::write_vec( + &self.module, + &self.module_info, + &naga::back::spv::Options { + flags: naga::back::spv::WriterFlags::empty(), + ..naga::back::spv::Options::default() + }, + None, + ) + } + + pub fn get_wgsl(&self) -> Result { + naga::back::wgsl::write_string(&self.module, &self.module_info) + } +} + +#[derive(Default)] +pub struct ShaderLoader; + +impl AssetLoader for ShaderLoader { + fn load<'a>( + &'a self, + bytes: &'a [u8], + load_context: &'a mut LoadContext, + ) -> BoxedFuture<'a, Result<(), anyhow::Error>> { + Box::pin(async move { + let ext = load_context.path().extension().unwrap().to_str().unwrap(); + + let mut shader = match ext { + "spv" => Shader::from_spirv(Vec::from(bytes)), + "wgsl" => Shader::from_wgsl(String::from_utf8(Vec::from(bytes))?), + "vert" => Shader::from_glsl( + String::from_utf8(Vec::from(bytes))?, + naga::ShaderStage::Vertex, + ), + "frag" => Shader::from_glsl( + String::from_utf8(Vec::from(bytes))?, + naga::ShaderStage::Fragment, + ), + _ => panic!("unhandled extension: {}", ext), + }; + + shader.import_path = Some(ShaderImport::AssetPath( + load_context.path().to_string_lossy().to_string(), + )); + let imports = SHADER_IMPORT_PROCESSOR.get_imports(&shader); + let mut asset = LoadedAsset::new(shader); + for import in imports { + if let ShaderImport::AssetPath(asset_path) = import { + let path = PathBuf::from_str(&asset_path)?; + asset.add_dependency(path.into()); + } + } + + load_context.set_default_asset(asset); + Ok(()) + }) + } + + fn extensions(&self) -> &[&str] { + &["spv", "wgsl", "vert", "frag"] + } +} + +#[derive(Error, Debug, PartialEq, Eq)] +pub enum ProcessShaderError { + #[error("Too many '# endif' lines. Each endif should be preceded by an if statement.")] + TooManyEndIfs, + #[error( + "Not enough '# endif' lines. Each if statement should be followed by an endif statement." + )] + NotEnoughEndIfs, + #[error("This Shader's format does not support processing shader defs.")] + ShaderFormatDoesNotSupportShaderDefs, + #[error("This Shader's formatdoes not support imports.")] + ShaderFormatDoesNotSupportImports, + #[error("Unresolved import: {0:?}.")] + UnresolvedImport(ShaderImport), + #[error("The shader import {0:?} does not match the source file type. Support for this might be added in the future.")] + MismatchedImportFormat(ShaderImport), +} + +pub struct ShaderImportProcessor { + import_asset_path_regex: Regex, + import_custom_path_regex: Regex, +} + +#[derive(Debug, PartialEq, Eq, Clone, Hash)] +pub enum ShaderImport { + AssetPath(String), + Custom(String), +} + +impl Default for ShaderImportProcessor { + fn default() -> Self { + Self { + import_asset_path_regex: Regex::new(r#"^\s*#\s*import\s*"(.+)""#).unwrap(), + import_custom_path_regex: Regex::new(r"^\s*#\s*import\s*(.+)").unwrap(), + } + } +} + +impl ShaderImportProcessor { + pub fn get_imports(&self, shader: &Shader) -> Vec { + match &shader.source { + Source::Wgsl(source) => self.get_imports_from_str(source), + Source::Glsl(source, _stage) => self.get_imports_from_str(source), + Source::SpirV(_source) => Vec::new(), + } + } + + pub fn get_imports_from_str(&self, shader: &str) -> Vec { + let mut imports = Vec::new(); + for line in shader.split('\n') { + if let Some(cap) = self.import_asset_path_regex.captures(line) { + let import = cap.get(1).unwrap(); + imports.push(ShaderImport::AssetPath(import.as_str().to_string())); + } else if let Some(cap) = self.import_custom_path_regex.captures(line) { + let import = cap.get(1).unwrap(); + imports.push(ShaderImport::Custom(import.as_str().to_string())); + } + } + + imports + } +} + +pub static SHADER_IMPORT_PROCESSOR: Lazy = + Lazy::new(ShaderImportProcessor::default); + +pub struct ShaderProcessor { + ifdef_regex: Regex, + ifndef_regex: Regex, + endif_regex: Regex, +} + +impl Default for ShaderProcessor { + fn default() -> Self { + Self { + ifdef_regex: Regex::new(r"^\s*#\s*ifdef\s*([\w|\d|_]+)").unwrap(), + ifndef_regex: Regex::new(r"^\s*#\s*ifndef\s*([\w|\d|_]+)").unwrap(), + endif_regex: Regex::new(r"^\s*#\s*endif").unwrap(), + } + } +} + +impl ShaderProcessor { + pub fn process( + &self, + shader: &Shader, + shader_defs: &[String], + shaders: &HashMap, Shader>, + import_handles: &HashMap>, + ) -> Result { + let shader_str = match &shader.source { + Source::Wgsl(source) => source.deref(), + Source::Glsl(source, _stage) => source.deref(), + Source::SpirV(source) => { + if shader_defs.is_empty() { + return Ok(ProcessedShader::SpirV(source.clone())); + } else { + return Err(ProcessShaderError::ShaderFormatDoesNotSupportShaderDefs); + } + } + }; + + let shader_defs = HashSet::::from_iter(shader_defs.iter().cloned()); + let mut scopes = vec![true]; + let mut final_string = String::new(); + for line in shader_str.split('\n') { + if let Some(cap) = self.ifdef_regex.captures(line) { + let def = cap.get(1).unwrap(); + scopes.push(*scopes.last().unwrap() && shader_defs.contains(def.as_str())); + } else if let Some(cap) = self.ifndef_regex.captures(line) { + let def = cap.get(1).unwrap(); + scopes.push(*scopes.last().unwrap() && !shader_defs.contains(def.as_str())); + } else if self.endif_regex.is_match(line) { + scopes.pop(); + if scopes.is_empty() { + return Err(ProcessShaderError::TooManyEndIfs); + } + } else if let Some(cap) = SHADER_IMPORT_PROCESSOR + .import_asset_path_regex + .captures(line) + { + let import = ShaderImport::AssetPath(cap.get(1).unwrap().as_str().to_string()); + apply_import(import_handles, shaders, &import, shader, &mut final_string)?; + } else if let Some(cap) = SHADER_IMPORT_PROCESSOR + .import_custom_path_regex + .captures(line) + { + let import = ShaderImport::Custom(cap.get(1).unwrap().as_str().to_string()); + apply_import(import_handles, shaders, &import, shader, &mut final_string)?; + } else if *scopes.last().unwrap() { + final_string.push_str(line); + final_string.push('\n'); + } + } + + final_string.pop(); + + if scopes.len() != 1 { + return Err(ProcessShaderError::NotEnoughEndIfs); + } + + let processed_source = Cow::from(final_string); + + match &shader.source { + Source::Wgsl(_source) => Ok(ProcessedShader::Wgsl(processed_source)), + Source::Glsl(_source, stage) => Ok(ProcessedShader::Glsl(processed_source, *stage)), + Source::SpirV(_source) => { + unreachable!("SpirV has early return"); + } + } + } +} + +fn apply_import( + import_handles: &HashMap>, + shaders: &HashMap, Shader>, + import: &ShaderImport, + shader: &Shader, + final_string: &mut String, +) -> Result<(), ProcessShaderError> { + let imported_shader = import_handles + .get(import) + .and_then(|handle| shaders.get(handle)) + .ok_or_else(|| ProcessShaderError::UnresolvedImport(import.clone()))?; + match &shader.source { + Source::Wgsl(_) => { + if let Source::Wgsl(import_source) = &imported_shader.source { + final_string.push_str(import_source); + } else { + return Err(ProcessShaderError::MismatchedImportFormat(import.clone())); + } + } + Source::Glsl(_, _) => { + if let Source::Glsl(import_source, _) = &imported_shader.source { + final_string.push_str(import_source); + } else { + return Err(ProcessShaderError::MismatchedImportFormat(import.clone())); + } + } + Source::SpirV(_) => { + return Err(ProcessShaderError::ShaderFormatDoesNotSupportImports); + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use bevy_asset::Handle; + use bevy_utils::HashMap; + use naga::ShaderStage; + + use crate::render_resource::{ProcessShaderError, Shader, ShaderImport, ShaderProcessor}; + #[rustfmt::skip] +const WGSL: &str = r" +[[block]] +struct View { + view_proj: mat4x4; + world_position: vec3; +}; +[[group(0), binding(0)]] +var view: View; + +#ifdef TEXTURE +[[group(1), binding(0)]] +var sprite_texture: texture_2d; +#endif + +struct VertexOutput { + [[location(0)]] uv: vec2; + [[builtin(position)]] position: vec4; +}; + +[[stage(vertex)]] +fn vertex( + [[location(0)]] vertex_position: vec3, + [[location(1)]] vertex_uv: vec2 +) -> VertexOutput { + var out: VertexOutput; + out.uv = vertex_uv; + out.position = view.view_proj * vec4(vertex_position, 1.0); + return out; +} +"; + const WGSL_NESTED_IFDEF: &str = r" +[[block]] +struct View { + view_proj: mat4x4; + world_position: vec3; +}; +[[group(0), binding(0)]] +var view: View; + +# ifdef TEXTURE +# ifdef ATTRIBUTE +[[group(1), binding(0)]] +var sprite_texture: texture_2d; +# endif +# endif + +struct VertexOutput { + [[location(0)]] uv: vec2; + [[builtin(position)]] position: vec4; +}; + +[[stage(vertex)]] +fn vertex( + [[location(0)]] vertex_position: vec3, + [[location(1)]] vertex_uv: vec2 +) -> VertexOutput { + var out: VertexOutput; + out.uv = vertex_uv; + out.position = view.view_proj * vec4(vertex_position, 1.0); + return out; +} +"; + + #[test] + fn process_shader_def_defined() { + #[rustfmt::skip] + const EXPECTED: &str = r" +[[block]] +struct View { + view_proj: mat4x4; + world_position: vec3; +}; +[[group(0), binding(0)]] +var view: View; + +[[group(1), binding(0)]] +var sprite_texture: texture_2d; + +struct VertexOutput { + [[location(0)]] uv: vec2; + [[builtin(position)]] position: vec4; +}; + +[[stage(vertex)]] +fn vertex( + [[location(0)]] vertex_position: vec3, + [[location(1)]] vertex_uv: vec2 +) -> VertexOutput { + var out: VertexOutput; + out.uv = vertex_uv; + out.position = view.view_proj * vec4(vertex_position, 1.0); + return out; +} +"; + let processor = ShaderProcessor::default(); + let result = processor + .process( + &Shader::from_wgsl(WGSL), + &["TEXTURE".to_string()], + &HashMap::default(), + &HashMap::default(), + ) + .unwrap(); + assert_eq!(result.get_wgsl_source().unwrap(), EXPECTED); + } + + #[test] + fn process_shader_def_not_defined() { + #[rustfmt::skip] + const EXPECTED: &str = r" +[[block]] +struct View { + view_proj: mat4x4; + world_position: vec3; +}; +[[group(0), binding(0)]] +var view: View; + + +struct VertexOutput { + [[location(0)]] uv: vec2; + [[builtin(position)]] position: vec4; +}; + +[[stage(vertex)]] +fn vertex( + [[location(0)]] vertex_position: vec3, + [[location(1)]] vertex_uv: vec2 +) -> VertexOutput { + var out: VertexOutput; + out.uv = vertex_uv; + out.position = view.view_proj * vec4(vertex_position, 1.0); + return out; +} +"; + let processor = ShaderProcessor::default(); + let result = processor + .process( + &Shader::from_wgsl(WGSL), + &[], + &HashMap::default(), + &HashMap::default(), + ) + .unwrap(); + assert_eq!(result.get_wgsl_source().unwrap(), EXPECTED); + } + + #[test] + fn process_shader_def_unclosed() { + #[rustfmt::skip] + const INPUT: &str = r" +#ifdef FOO +"; + let processor = ShaderProcessor::default(); + let result = processor.process( + &Shader::from_wgsl(INPUT), + &[], + &HashMap::default(), + &HashMap::default(), + ); + assert_eq!(result, Err(ProcessShaderError::NotEnoughEndIfs)); + } + + #[test] + fn process_shader_def_too_closed() { + #[rustfmt::skip] + const INPUT: &str = r" +#endif +"; + let processor = ShaderProcessor::default(); + let result = processor.process( + &Shader::from_wgsl(INPUT), + &[], + &HashMap::default(), + &HashMap::default(), + ); + assert_eq!(result, Err(ProcessShaderError::TooManyEndIfs)); + } + + #[test] + fn process_shader_def_commented() { + #[rustfmt::skip] + const INPUT: &str = r" +// #ifdef FOO +fn foo() { } +"; + let processor = ShaderProcessor::default(); + let result = processor + .process( + &Shader::from_wgsl(INPUT), + &[], + &HashMap::default(), + &HashMap::default(), + ) + .unwrap(); + assert_eq!(result.get_wgsl_source().unwrap(), INPUT); + } + + #[test] + fn process_import_wgsl() { + #[rustfmt::skip] + const FOO: &str = r" +fn foo() { } +"; + #[rustfmt::skip] + const INPUT: &str = r" +#import FOO +fn bar() { } +"; + #[rustfmt::skip] + const EXPECTED: &str = r" + +fn foo() { } +fn bar() { } +"; + let processor = ShaderProcessor::default(); + let mut shaders = HashMap::default(); + let mut import_handles = HashMap::default(); + let foo_handle = Handle::::default(); + shaders.insert(foo_handle.clone_weak(), Shader::from_wgsl(FOO)); + import_handles.insert( + ShaderImport::Custom("FOO".to_string()), + foo_handle.clone_weak(), + ); + let result = processor + .process(&Shader::from_wgsl(INPUT), &[], &shaders, &import_handles) + .unwrap(); + assert_eq!(result.get_wgsl_source().unwrap(), EXPECTED); + } + + #[test] + fn process_import_glsl() { + #[rustfmt::skip] + const FOO: &str = r" +void foo() { } +"; + #[rustfmt::skip] + const INPUT: &str = r" +#import FOO +void bar() { } +"; + #[rustfmt::skip] + const EXPECTED: &str = r" + +void foo() { } +void bar() { } +"; + let processor = ShaderProcessor::default(); + let mut shaders = HashMap::default(); + let mut import_handles = HashMap::default(); + let foo_handle = Handle::::default(); + shaders.insert( + foo_handle.clone_weak(), + Shader::from_glsl(FOO, ShaderStage::Vertex), + ); + import_handles.insert( + ShaderImport::Custom("FOO".to_string()), + foo_handle.clone_weak(), + ); + let result = processor + .process( + &Shader::from_glsl(INPUT, ShaderStage::Vertex), + &[], + &shaders, + &import_handles, + ) + .unwrap(); + assert_eq!(result.get_glsl_source().unwrap(), EXPECTED); + } + + #[test] + fn process_nested_shader_def_outer_defined_inner_not() { + #[rustfmt::skip] + const EXPECTED: &str = r" +[[block]] +struct View { + view_proj: mat4x4; + world_position: vec3; +}; +[[group(0), binding(0)]] +var view: View; + + +struct VertexOutput { + [[location(0)]] uv: vec2; + [[builtin(position)]] position: vec4; +}; + +[[stage(vertex)]] +fn vertex( + [[location(0)]] vertex_position: vec3, + [[location(1)]] vertex_uv: vec2 +) -> VertexOutput { + var out: VertexOutput; + out.uv = vertex_uv; + out.position = view.view_proj * vec4(vertex_position, 1.0); + return out; +} +"; + let processor = ShaderProcessor::default(); + let result = processor + .process( + &Shader::from_wgsl(WGSL_NESTED_IFDEF), + &["TEXTURE".to_string()], + &HashMap::default(), + &HashMap::default(), + ) + .unwrap(); + assert_eq!(result.get_wgsl_source().unwrap(), EXPECTED); + } + + #[test] + fn process_nested_shader_def_neither_defined() { + #[rustfmt::skip] + const EXPECTED: &str = r" +[[block]] +struct View { + view_proj: mat4x4; + world_position: vec3; +}; +[[group(0), binding(0)]] +var view: View; + + +struct VertexOutput { + [[location(0)]] uv: vec2; + [[builtin(position)]] position: vec4; +}; + +[[stage(vertex)]] +fn vertex( + [[location(0)]] vertex_position: vec3, + [[location(1)]] vertex_uv: vec2 +) -> VertexOutput { + var out: VertexOutput; + out.uv = vertex_uv; + out.position = view.view_proj * vec4(vertex_position, 1.0); + return out; +} +"; + let processor = ShaderProcessor::default(); + let result = processor + .process( + &Shader::from_wgsl(WGSL_NESTED_IFDEF), + &[], + &HashMap::default(), + &HashMap::default(), + ) + .unwrap(); + assert_eq!(result.get_wgsl_source().unwrap(), EXPECTED); + } + + #[test] + fn process_nested_shader_def_inner_defined_outer_not() { + #[rustfmt::skip] + const EXPECTED: &str = r" +[[block]] +struct View { + view_proj: mat4x4; + world_position: vec3; +}; +[[group(0), binding(0)]] +var view: View; + + +struct VertexOutput { + [[location(0)]] uv: vec2; + [[builtin(position)]] position: vec4; +}; + +[[stage(vertex)]] +fn vertex( + [[location(0)]] vertex_position: vec3, + [[location(1)]] vertex_uv: vec2 +) -> VertexOutput { + var out: VertexOutput; + out.uv = vertex_uv; + out.position = view.view_proj * vec4(vertex_position, 1.0); + return out; +} +"; + let processor = ShaderProcessor::default(); + let result = processor + .process( + &Shader::from_wgsl(WGSL_NESTED_IFDEF), + &["ATTRIBUTE".to_string()], + &HashMap::default(), + &HashMap::default(), + ) + .unwrap(); + assert_eq!(result.get_wgsl_source().unwrap(), EXPECTED); + } + + #[test] + fn process_nested_shader_def_both_defined() { + #[rustfmt::skip] + const EXPECTED: &str = r" +[[block]] +struct View { + view_proj: mat4x4; + world_position: vec3; +}; +[[group(0), binding(0)]] +var view: View; + +[[group(1), binding(0)]] +var sprite_texture: texture_2d; + +struct VertexOutput { + [[location(0)]] uv: vec2; + [[builtin(position)]] position: vec4; +}; + +[[stage(vertex)]] +fn vertex( + [[location(0)]] vertex_position: vec3, + [[location(1)]] vertex_uv: vec2 +) -> VertexOutput { + var out: VertexOutput; + out.uv = vertex_uv; + out.position = view.view_proj * vec4(vertex_position, 1.0); + return out; +} +"; + let processor = ShaderProcessor::default(); + let result = processor + .process( + &Shader::from_wgsl(WGSL_NESTED_IFDEF), + &["TEXTURE".to_string(), "ATTRIBUTE".to_string()], + &HashMap::default(), + &HashMap::default(), + ) + .unwrap(); + assert_eq!(result.get_wgsl_source().unwrap(), EXPECTED); + } +} diff --git a/pipelined/bevy_render2/src/render_resource/texture.rs b/pipelined/bevy_render2/src/render_resource/texture.rs new file mode 100644 index 0000000000..047c042123 --- /dev/null +++ b/pipelined/bevy_render2/src/render_resource/texture.rs @@ -0,0 +1,169 @@ +use bevy_utils::Uuid; +use std::{ops::Deref, sync::Arc}; + +/// A [`Texture`] identifier. +#[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)] +pub struct TextureId(Uuid); + +/// A GPU-accessible texture. +/// +/// May be converted from and dereferences to a wgpu [`Texture`](wgpu::Texture). +/// Can be created via [`RenderDevice::create_texture`](crate::renderer::RenderDevice::create_texture). +#[derive(Clone, Debug)] +pub struct Texture { + id: TextureId, + value: Arc, +} + +impl Texture { + /// Returns the [`TextureId`]. + #[inline] + pub fn id(&self) -> TextureId { + self.id + } + + /// Creates a view of this texture. + pub fn create_view(&self, desc: &wgpu::TextureViewDescriptor) -> TextureView { + TextureView::from(self.value.create_view(desc)) + } +} + +impl From for Texture { + fn from(value: wgpu::Texture) -> Self { + Texture { + id: TextureId(Uuid::new_v4()), + value: Arc::new(value), + } + } +} + +impl Deref for Texture { + type Target = wgpu::Texture; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.value + } +} + +/// A [`TextureView`] identifier. +#[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)] +pub struct TextureViewId(Uuid); + +/// This type combines wgpu's [`TextureView`](wgpu::TextureView) and +/// [SurfaceTexture`](wgpu::SurfaceTexture) into the same interface. +#[derive(Clone, Debug)] +pub enum TextureViewValue { + /// The value is an actual wgpu [`TextureView`](wgpu::TextureView). + TextureView(Arc), + + /// The value is a wgpu [`SurfaceTexture`](wgpu::SurfaceTexture), but dereferences to + /// a [`TextureView`](wgpu::TextureView). + SurfaceTexture { + // NOTE: The order of these fields is important because the view must be dropped before the + // frame is dropped + view: Arc, + texture: Arc, + }, +} + +/// Describes a [`Texture`] with its associated metadata required by a pipeline or [`BindGroup`](super::BindGroup). +/// +/// May be converted from a [`TextureView`](wgpu::TextureView) or [`SurfaceTexture`](wgpu::SurfaceTexture) +/// or dereferences to a wgpu [`TextureView`](wgpu::TextureView). +#[derive(Clone, Debug)] +pub struct TextureView { + id: TextureViewId, + value: TextureViewValue, +} + +impl TextureView { + /// Returns the [`TextureViewId`]. + #[inline] + pub fn id(&self) -> TextureViewId { + self.id + } + + /// Returns the [`SurfaceTexture`](wgpu::SurfaceTexture) of the texture view if it is of that type. + #[inline] + pub fn take_surface_texture(self) -> Option { + match self.value { + TextureViewValue::TextureView(_) => None, + TextureViewValue::SurfaceTexture { texture, .. } => Arc::try_unwrap(texture).ok(), + } + } +} + +impl From for TextureView { + fn from(value: wgpu::TextureView) -> Self { + TextureView { + id: TextureViewId(Uuid::new_v4()), + value: TextureViewValue::TextureView(Arc::new(value)), + } + } +} + +impl From for TextureView { + fn from(value: wgpu::SurfaceTexture) -> Self { + let texture = Arc::new(value); + let view = Arc::new(texture.texture.create_view(&Default::default())); + + TextureView { + id: TextureViewId(Uuid::new_v4()), + value: TextureViewValue::SurfaceTexture { texture, view }, + } + } +} + +impl Deref for TextureView { + type Target = wgpu::TextureView; + + #[inline] + fn deref(&self) -> &Self::Target { + match &self.value { + TextureViewValue::TextureView(value) => value, + TextureViewValue::SurfaceTexture { view, .. } => view, + } + } +} + +/// A [`Sampler`] identifier. +#[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)] +pub struct SamplerId(Uuid); + +/// A Sampler defines how a pipeline will sample from a [`TextureView`]. +/// They define image filters (including anisotropy) and address (wrapping) modes, among other things. +/// +/// May be converted from and dereferences to a wgpu [`Sampler`](wgpu::Sampler). +/// Can be created via [`RenderDevice::create_sampler`](crate::renderer::RenderDevice::create_sampler). +#[derive(Clone, Debug)] +pub struct Sampler { + id: SamplerId, + value: Arc, +} + +impl Sampler { + /// Returns the [`SamplerId`]. + #[inline] + pub fn id(&self) -> SamplerId { + self.id + } +} + +impl From for Sampler { + fn from(value: wgpu::Sampler) -> Self { + Sampler { + id: SamplerId(Uuid::new_v4()), + value: Arc::new(value), + } + } +} + +impl Deref for Sampler { + type Target = wgpu::Sampler; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.value + } +} diff --git a/pipelined/bevy_render2/src/render_resource/uniform_vec.rs b/pipelined/bevy_render2/src/render_resource/uniform_vec.rs new file mode 100644 index 0000000000..73d9c2a535 --- /dev/null +++ b/pipelined/bevy_render2/src/render_resource/uniform_vec.rs @@ -0,0 +1,158 @@ +use crate::{ + render_resource::Buffer, + renderer::{RenderDevice, RenderQueue}, +}; +use crevice::std140::{self, AsStd140, DynamicUniform, Std140}; +use std::num::NonZeroU64; +use wgpu::{BindingResource, BufferBinding, BufferDescriptor, BufferUsages}; + +pub struct UniformVec { + values: Vec, + scratch: Vec, + uniform_buffer: Option, + capacity: usize, + item_size: usize, +} + +impl Default for UniformVec { + fn default() -> Self { + Self { + values: Vec::new(), + scratch: Vec::new(), + uniform_buffer: None, + capacity: 0, + item_size: (T::std140_size_static() + ::Output::ALIGNMENT - 1) + & !(::Output::ALIGNMENT - 1), + } + } +} + +impl UniformVec { + #[inline] + pub fn uniform_buffer(&self) -> Option<&Buffer> { + self.uniform_buffer.as_ref() + } + + #[inline] + pub fn binding(&self) -> Option { + Some(BindingResource::Buffer(BufferBinding { + buffer: self.uniform_buffer()?, + offset: 0, + size: Some(NonZeroU64::new(self.item_size as u64).unwrap()), + })) + } + + #[inline] + pub fn len(&self) -> usize { + self.values.len() + } + + #[inline] + pub fn is_empty(&self) -> bool { + self.values.is_empty() + } + + #[inline] + pub fn capacity(&self) -> usize { + self.capacity + } + + pub fn push(&mut self, value: T) -> usize { + let index = self.values.len(); + self.values.push(value); + index + } + + pub fn reserve(&mut self, capacity: usize, device: &RenderDevice) -> bool { + if capacity > self.capacity { + self.capacity = capacity; + let size = self.item_size * capacity; + self.scratch.resize(size, 0); + self.uniform_buffer = Some(device.create_buffer(&BufferDescriptor { + label: None, + size: size as wgpu::BufferAddress, + usage: BufferUsages::COPY_DST | BufferUsages::UNIFORM, + mapped_at_creation: false, + })); + true + } else { + false + } + } + + pub fn write_buffer(&mut self, device: &RenderDevice, queue: &RenderQueue) { + if self.values.is_empty() { + return; + } + self.reserve(self.values.len(), device); + if let Some(uniform_buffer) = &self.uniform_buffer { + let range = 0..self.item_size * self.values.len(); + let mut writer = std140::Writer::new(&mut self.scratch[range.clone()]); + writer.write(self.values.as_slice()).unwrap(); + queue.write_buffer(uniform_buffer, 0, &self.scratch[range]); + } + } + + pub fn clear(&mut self) { + self.values.clear(); + } +} + +pub struct DynamicUniformVec { + uniform_vec: UniformVec>, +} + +impl Default for DynamicUniformVec { + fn default() -> Self { + Self { + uniform_vec: Default::default(), + } + } +} + +impl DynamicUniformVec { + #[inline] + pub fn uniform_buffer(&self) -> Option<&Buffer> { + self.uniform_vec.uniform_buffer() + } + + #[inline] + pub fn binding(&self) -> Option { + self.uniform_vec.binding() + } + + #[inline] + pub fn len(&self) -> usize { + self.uniform_vec.len() + } + + #[inline] + pub fn is_empty(&self) -> bool { + self.uniform_vec.is_empty() + } + + #[inline] + pub fn capacity(&self) -> usize { + self.uniform_vec.capacity() + } + + #[inline] + pub fn push(&mut self, value: T) -> u32 { + (self.uniform_vec.push(DynamicUniform(value)) * self.uniform_vec.item_size) as u32 + } + + #[inline] + pub fn reserve(&mut self, capacity: usize, device: &RenderDevice) { + self.uniform_vec.reserve(capacity, device); + } + + #[inline] + pub fn write_buffer(&mut self, device: &RenderDevice, queue: &RenderQueue) { + self.uniform_vec.write_buffer(device, queue); + } + + #[inline] + pub fn clear(&mut self) { + self.uniform_vec.clear(); + } +} diff --git a/pipelined/bevy_render2/src/renderer/graph_runner.rs b/pipelined/bevy_render2/src/renderer/graph_runner.rs new file mode 100644 index 0000000000..5df63dd3ad --- /dev/null +++ b/pipelined/bevy_render2/src/renderer/graph_runner.rs @@ -0,0 +1,219 @@ +use bevy_ecs::world::World; +#[cfg(feature = "trace")] +use bevy_utils::tracing::info_span; +use bevy_utils::HashMap; +use smallvec::{smallvec, SmallVec}; +#[cfg(feature = "trace")] +use std::ops::Deref; +use std::{borrow::Cow, collections::VecDeque}; +use thiserror::Error; + +use crate::{ + render_graph::{ + Edge, NodeId, NodeRunError, NodeState, RenderGraph, RenderGraphContext, SlotLabel, + SlotType, SlotValue, + }, + renderer::{RenderContext, RenderDevice}, +}; + +pub(crate) struct RenderGraphRunner; + +#[derive(Error, Debug)] +pub enum RenderGraphRunnerError { + #[error(transparent)] + NodeRunError(#[from] NodeRunError), + #[error("node output slot not set (index {slot_index}, name {slot_name})")] + EmptyNodeOutputSlot { + type_name: &'static str, + slot_index: usize, + slot_name: Cow<'static, str>, + }, + #[error("graph (name: '{graph_name:?}') could not be run because slot '{slot_name}' at index {slot_index} has no value")] + MissingInput { + slot_index: usize, + slot_name: Cow<'static, str>, + graph_name: Option>, + }, + #[error("attempted to use the wrong type for input slot")] + MismatchedInputSlotType { + slot_index: usize, + label: SlotLabel, + expected: SlotType, + actual: SlotType, + }, +} + +impl RenderGraphRunner { + pub fn run( + graph: &RenderGraph, + render_device: RenderDevice, + queue: &wgpu::Queue, + world: &World, + ) -> Result<(), RenderGraphRunnerError> { + let command_encoder = + render_device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); + let mut render_context = RenderContext { + render_device, + command_encoder, + }; + + Self::run_graph(graph, None, &mut render_context, world, &[])?; + { + #[cfg(feature = "trace")] + let span = info_span!("submit_graph_commands"); + #[cfg(feature = "trace")] + let _guard = span.enter(); + queue.submit(vec![render_context.command_encoder.finish()]); + } + Ok(()) + } + + fn run_graph( + graph: &RenderGraph, + graph_name: Option>, + render_context: &mut RenderContext, + world: &World, + inputs: &[SlotValue], + ) -> Result<(), RenderGraphRunnerError> { + let mut node_outputs: HashMap> = HashMap::default(); + #[cfg(feature = "trace")] + let span = if let Some(name) = &graph_name { + info_span!("run_graph", name = name.deref()) + } else { + info_span!("run_graph", name = "main_graph") + }; + #[cfg(feature = "trace")] + let _guard = span.enter(); + + // Queue up nodes without inputs, which can be run immediately + let mut node_queue: VecDeque<&NodeState> = graph + .iter_nodes() + .filter(|node| node.input_slots.is_empty()) + .collect(); + + // pass inputs into the graph + if let Some(input_node) = graph.input_node() { + let mut input_values: SmallVec<[SlotValue; 4]> = SmallVec::new(); + for (i, input_slot) in input_node.input_slots.iter().enumerate() { + if let Some(input_value) = inputs.get(i) { + if input_slot.slot_type != input_value.slot_type() { + return Err(RenderGraphRunnerError::MismatchedInputSlotType { + slot_index: i, + actual: input_value.slot_type(), + expected: input_slot.slot_type, + label: input_slot.name.clone().into(), + }); + } else { + input_values.push(input_value.clone()); + } + } else { + return Err(RenderGraphRunnerError::MissingInput { + slot_index: i, + slot_name: input_slot.name.clone(), + graph_name: graph_name.clone(), + }); + } + } + + node_outputs.insert(input_node.id, input_values); + + for (_, node_state) in graph.iter_node_outputs(input_node.id).expect("node exists") { + node_queue.push_front(node_state); + } + } + + 'handle_node: while let Some(node_state) = node_queue.pop_back() { + // skip nodes that are already processed + if node_outputs.contains_key(&node_state.id) { + continue; + } + + let mut slot_indices_and_inputs: SmallVec<[(usize, SlotValue); 4]> = SmallVec::new(); + // check if all dependencies have finished running + for (edge, input_node) in graph + .iter_node_inputs(node_state.id) + .expect("node is in graph") + { + match edge { + Edge::SlotEdge { + output_index, + input_index, + .. + } => { + if let Some(outputs) = node_outputs.get(&input_node.id) { + slot_indices_and_inputs + .push((*input_index, outputs[*output_index].clone())); + } else { + node_queue.push_front(node_state); + continue 'handle_node; + } + } + Edge::NodeEdge { .. } => { + if !node_outputs.contains_key(&input_node.id) { + node_queue.push_front(node_state); + continue 'handle_node; + } + } + } + } + + // construct final sorted input list + slot_indices_and_inputs.sort_by_key(|(index, _)| *index); + let inputs: SmallVec<[SlotValue; 4]> = slot_indices_and_inputs + .into_iter() + .map(|(_, value)| value) + .collect(); + + assert_eq!(inputs.len(), node_state.input_slots.len()); + + let mut outputs: SmallVec<[Option; 4]> = + smallvec![None; node_state.output_slots.len()]; + { + let mut context = RenderGraphContext::new(graph, node_state, &inputs, &mut outputs); + #[cfg(feature = "trace")] + let span = info_span!("node", name = node_state.type_name); + #[cfg(feature = "trace")] + let guard = span.enter(); + + node_state.node.run(&mut context, render_context, world)?; + + #[cfg(feature = "trace")] + drop(guard); + + for run_sub_graph in context.finish() { + let sub_graph = graph + .get_sub_graph(&run_sub_graph.name) + .expect("sub graph exists because it was validated when queued."); + Self::run_graph( + sub_graph, + Some(run_sub_graph.name), + render_context, + world, + &run_sub_graph.inputs, + )?; + } + } + + let mut values: SmallVec<[SlotValue; 4]> = SmallVec::new(); + for (i, output) in outputs.into_iter().enumerate() { + if let Some(value) = output { + values.push(value); + } else { + let empty_slot = node_state.output_slots.get_slot(i).unwrap(); + return Err(RenderGraphRunnerError::EmptyNodeOutputSlot { + type_name: node_state.type_name, + slot_index: i, + slot_name: empty_slot.name.clone(), + }); + } + } + node_outputs.insert(node_state.id, values); + + for (_, node_state) in graph.iter_node_outputs(node_state.id).expect("node exists") { + node_queue.push_front(node_state); + } + } + + Ok(()) + } +} diff --git a/pipelined/bevy_render2/src/renderer/mod.rs b/pipelined/bevy_render2/src/renderer/mod.rs new file mode 100644 index 0000000000..dab1295dba --- /dev/null +++ b/pipelined/bevy_render2/src/renderer/mod.rs @@ -0,0 +1,104 @@ +mod graph_runner; +mod render_device; + +use bevy_utils::tracing::{info, info_span}; +pub use graph_runner::*; +pub use render_device::*; + +use crate::{ + render_graph::RenderGraph, + view::{ExtractedWindows, ViewTarget}, +}; +use bevy_ecs::prelude::*; +use std::sync::Arc; +use wgpu::{CommandEncoder, DeviceDescriptor, Instance, Queue, RequestAdapterOptions}; + +/// Updates the [`RenderGraph`] with all of its nodes and then runs it to render the entire frame. +pub fn render_system(world: &mut World) { + world.resource_scope(|world, mut graph: Mut| { + graph.update(world); + }); + let graph = world.get_resource::().unwrap(); + let render_device = world.get_resource::().unwrap(); + let render_queue = world.get_resource::().unwrap(); + RenderGraphRunner::run( + graph, + render_device.clone(), // TODO: is this clone really necessary? + render_queue, + world, + ) + .unwrap(); + { + let span = info_span!("present_frames"); + let _guard = span.enter(); + + // Remove ViewTarget components to ensure swap chain TextureViews are dropped. + // If all TextureViews aren't dropped before present, acquiring the next swap chain texture will fail. + let view_entities = world + .query_filtered::>() + .iter(world) + .collect::>(); + for view_entity in view_entities { + world.entity_mut(view_entity).remove::(); + } + + let mut windows = world.get_resource_mut::().unwrap(); + for window in windows.values_mut() { + if let Some(texture_view) = window.swap_chain_texture.take() { + if let Some(surface_texture) = texture_view.take_surface_texture() { + surface_texture.present(); + } + } + } + } +} + +/// This queue is used to enqueue tasks for the GPU to execute asynchronously. +pub type RenderQueue = Arc; + +/// The GPU instance is used to initialize the [`RenderQueue`] and [`RenderDevice`], +/// aswell as to create [`WindowSurfaces`](crate::view::window::WindowSurfaces). +pub type RenderInstance = Instance; + +/// Initializes the renderer by retrieving and preparing the GPU instance, device and queue +/// for the specified backend. +pub async fn initialize_renderer( + instance: &Instance, + request_adapter_options: &RequestAdapterOptions<'_>, + device_descriptor: &DeviceDescriptor<'_>, +) -> (RenderDevice, RenderQueue) { + let adapter = instance + .request_adapter(request_adapter_options) + .await + .expect("Unable to find a GPU! Make sure you have installed required drivers!"); + + #[cfg(not(target_arch = "wasm32"))] + info!("{:?}", adapter.get_info()); + + #[cfg(feature = "trace")] + let trace_path = { + let path = std::path::Path::new("wgpu_trace"); + // ignore potential error, wgpu will log it + let _ = std::fs::create_dir(path); + Some(path) + }; + #[cfg(not(feature = "trace"))] + let trace_path = None; + + let (device, queue) = adapter + .request_device(device_descriptor, trace_path) + .await + .unwrap(); + let device = Arc::new(device); + let queue = Arc::new(queue); + (RenderDevice::from(device), queue) +} + +/// The context with all information required to interact with the GPU. +/// +/// The [`RenderDevice`] is used to create render resources and the +/// the [`CommandEncoder`] is used to record a series of GPU operations. +pub struct RenderContext { + pub render_device: RenderDevice, + pub command_encoder: CommandEncoder, +} diff --git a/pipelined/bevy_render2/src/renderer/render_device.rs b/pipelined/bevy_render2/src/renderer/render_device.rs new file mode 100644 index 0000000000..dda70368aa --- /dev/null +++ b/pipelined/bevy_render2/src/renderer/render_device.rs @@ -0,0 +1,168 @@ +use crate::render_resource::{ + BindGroup, BindGroupLayout, Buffer, ComputePipeline, RawRenderPipelineDescriptor, + RenderPipeline, Sampler, Texture, +}; +use futures_lite::future; +use std::sync::Arc; +use wgpu::util::DeviceExt; + +/// This GPU device is responsible for the creation of most rendering and compute resources. +#[derive(Clone)] +pub struct RenderDevice { + device: Arc, +} + +impl From> for RenderDevice { + fn from(device: Arc) -> Self { + Self { device } + } +} + +impl RenderDevice { + /// List all [`Features`](wgpu::Features) that may be used with this device. + /// + /// Functions may panic if you use unsupported features. + #[inline] + pub fn features(&self) -> wgpu::Features { + self.device.features() + } + + /// List all [`Limits`](wgpu::Limits) that were requested of this device. + /// + /// If any of these limits are exceeded, functions may panic. + #[inline] + pub fn limits(&self) -> wgpu::Limits { + self.device.limits() + } + + /// Creates a [ShaderModule](wgpu::ShaderModule) from either SPIR-V or WGSL source code. + #[inline] + pub fn create_shader_module(&self, desc: &wgpu::ShaderModuleDescriptor) -> wgpu::ShaderModule { + self.device.create_shader_module(desc) + } + + /// Check for resource cleanups and mapping callbacks. + /// + /// no-op on the web, device is automatically polled. + #[inline] + pub fn poll(&self, maintain: wgpu::Maintain) { + self.device.poll(maintain) + } + + /// Creates an empty [`CommandEncoder`](wgpu::CommandEncoder). + #[inline] + pub fn create_command_encoder( + &self, + desc: &wgpu::CommandEncoderDescriptor, + ) -> wgpu::CommandEncoder { + self.device.create_command_encoder(desc) + } + + /// Creates an empty [`RenderBundleEncoder`](wgpu::RenderBundleEncoder). + #[inline] + pub fn create_render_bundle_encoder( + &self, + desc: &wgpu::RenderBundleEncoderDescriptor, + ) -> wgpu::RenderBundleEncoder { + self.device.create_render_bundle_encoder(desc) + } + + /// Creates a new [`BindGroup`](wgpu::BindGroup). + #[inline] + pub fn create_bind_group(&self, desc: &wgpu::BindGroupDescriptor) -> BindGroup { + let wgpu_bind_group = self.device.create_bind_group(desc); + BindGroup::from(wgpu_bind_group) + } + + /// Creates a [`BindGroupLayout`](wgpu::BindGroupLayout). + #[inline] + pub fn create_bind_group_layout( + &self, + desc: &wgpu::BindGroupLayoutDescriptor, + ) -> BindGroupLayout { + BindGroupLayout::from(self.device.create_bind_group_layout(desc)) + } + + /// Creates a [`PipelineLayout`](wgpu::PipelineLayout). + #[inline] + pub fn create_pipeline_layout( + &self, + desc: &wgpu::PipelineLayoutDescriptor, + ) -> wgpu::PipelineLayout { + self.device.create_pipeline_layout(desc) + } + + /// Creates a [`RenderPipeline`]. + #[inline] + pub fn create_render_pipeline(&self, desc: &RawRenderPipelineDescriptor) -> RenderPipeline { + let wgpu_render_pipeline = self.device.create_render_pipeline(desc); + RenderPipeline::from(wgpu_render_pipeline) + } + + /// Creates a [`ComputePipeline`]. + #[inline] + pub fn create_compute_pipeline( + &self, + desc: &wgpu::ComputePipelineDescriptor, + ) -> ComputePipeline { + let wgpu_compute_pipeline = self.device.create_compute_pipeline(desc); + ComputePipeline::from(wgpu_compute_pipeline) + } + + /// Creates a [`Buffer`]. + pub fn create_buffer(&self, desc: &wgpu::BufferDescriptor) -> Buffer { + let wgpu_buffer = self.device.create_buffer(desc); + Buffer::from(wgpu_buffer) + } + + /// Creates a [`Buffer`] and initializes it with the specified data. + pub fn create_buffer_with_data(&self, desc: &wgpu::util::BufferInitDescriptor) -> Buffer { + let wgpu_buffer = self.device.create_buffer_init(desc); + Buffer::from(wgpu_buffer) + } + + /// Creates a new [`Texture`]. + /// + /// `desc` specifies the general format of the texture. + pub fn create_texture(&self, desc: &wgpu::TextureDescriptor) -> Texture { + let wgpu_texture = self.device.create_texture(desc); + Texture::from(wgpu_texture) + } + + /// Creates a new [`Sampler`]. + /// + /// `desc` specifies the behavior of the sampler. + pub fn create_sampler(&self, desc: &wgpu::SamplerDescriptor) -> Sampler { + let wgpu_sampler = self.device.create_sampler(desc); + Sampler::from(wgpu_sampler) + } + + /// Create a new [`SwapChain`](wgpu::SwapChain) which targets `surface`. + /// + /// # Panics + /// + /// - A old [`SwapChainFrame`](wgpu::SwapChain) is still alive referencing an old swap chain. + /// - Texture format requested is unsupported on the swap chain. + pub fn configure_surface(&self, surface: &wgpu::Surface, config: &wgpu::SurfaceConfiguration) { + surface.configure(&self.device, config) + } + + /// Returns the wgpu [`Device`](wgpu::Device). + pub fn wgpu_device(&self) -> &wgpu::Device { + &self.device + } + + pub fn map_buffer(&self, buffer: &wgpu::BufferSlice, map_mode: wgpu::MapMode) { + let data = buffer.map_async(map_mode); + self.poll(wgpu::Maintain::Wait); + if future::block_on(data).is_err() { + panic!("Failed to map buffer to host."); + } + } + + pub fn align_copy_bytes_per_row(row_bytes: usize) -> usize { + let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize; + let padded_bytes_per_row_padding = (align - row_bytes % align) % align; + row_bytes + padded_bytes_per_row_padding + } +} diff --git a/pipelined/bevy_render2/src/texture/hdr_texture_loader.rs b/pipelined/bevy_render2/src/texture/hdr_texture_loader.rs new file mode 100644 index 0000000000..09890c0033 --- /dev/null +++ b/pipelined/bevy_render2/src/texture/hdr_texture_loader.rs @@ -0,0 +1,58 @@ +use crate::texture::{Image, TextureFormatPixelInfo}; +use anyhow::Result; +use bevy_asset::{AssetLoader, LoadContext, LoadedAsset}; +use bevy_utils::BoxedFuture; +use wgpu::{Extent3d, TextureDimension, TextureFormat}; + +/// Loads HDR textures as Texture assets +#[derive(Clone, Default)] +pub struct HdrTextureLoader; + +impl AssetLoader for HdrTextureLoader { + fn load<'a>( + &'a self, + bytes: &'a [u8], + load_context: &'a mut LoadContext, + ) -> BoxedFuture<'a, Result<()>> { + Box::pin(async move { + let format = TextureFormat::Rgba32Float; + debug_assert_eq!( + format.pixel_size(), + 4 * 4, + "Format should have 32bit x 4 size" + ); + + let decoder = image::hdr::HdrDecoder::new(bytes)?; + let info = decoder.metadata(); + let rgb_data = decoder.read_image_hdr()?; + let mut rgba_data = Vec::with_capacity(rgb_data.len() * format.pixel_size()); + + for rgb in rgb_data { + let alpha = 1.0f32; + + rgba_data.extend_from_slice(&rgb.0[0].to_ne_bytes()); + rgba_data.extend_from_slice(&rgb.0[1].to_ne_bytes()); + rgba_data.extend_from_slice(&rgb.0[2].to_ne_bytes()); + rgba_data.extend_from_slice(&alpha.to_ne_bytes()); + } + + let texture = Image::new( + Extent3d { + width: info.width, + height: info.height, + depth_or_array_layers: 1, + }, + TextureDimension::D2, + rgba_data, + format, + ); + + load_context.set_default_asset(LoadedAsset::new(texture)); + Ok(()) + }) + } + + fn extensions(&self) -> &[&str] { + &["hdr"] + } +} diff --git a/pipelined/bevy_render2/src/texture/image.rs b/pipelined/bevy_render2/src/texture/image.rs new file mode 100644 index 0000000000..30b7b84394 --- /dev/null +++ b/pipelined/bevy_render2/src/texture/image.rs @@ -0,0 +1,432 @@ +use super::image_texture_conversion::image_to_texture; +use crate::{ + render_asset::{PrepareAssetError, RenderAsset}, + render_resource::{Sampler, Texture, TextureView}, + renderer::{RenderDevice, RenderQueue}, + texture::BevyDefault, +}; +use bevy_ecs::system::{lifetimeless::SRes, SystemParamItem}; +use bevy_reflect::TypeUuid; +use thiserror::Error; +use wgpu::{ + Extent3d, ImageCopyTexture, ImageDataLayout, Origin3d, TextureDimension, TextureFormat, + TextureViewDescriptor, +}; + +pub const TEXTURE_ASSET_INDEX: u64 = 0; +pub const SAMPLER_ASSET_INDEX: u64 = 1; + +#[derive(Debug, Clone, TypeUuid)] +#[uuid = "6ea26da6-6cf8-4ea2-9986-1d7bf6c17d6f"] +pub struct Image { + pub data: Vec, + // TODO: this nesting makes accessing Image metadata verbose. Either flatten out descriptor or add accessors + pub texture_descriptor: wgpu::TextureDescriptor<'static>, + pub sampler_descriptor: wgpu::SamplerDescriptor<'static>, +} + +impl Default for Image { + fn default() -> Self { + let format = wgpu::TextureFormat::bevy_default(); + let data = vec![1; format.pixel_size() as usize]; + Image { + data, + texture_descriptor: wgpu::TextureDescriptor { + size: wgpu::Extent3d { + width: 1, + height: 1, + depth_or_array_layers: 1, + }, + format, + dimension: wgpu::TextureDimension::D2, + label: None, + mip_level_count: 1, + sample_count: 1, + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + }, + sampler_descriptor: wgpu::SamplerDescriptor::default(), + } + } +} + +impl Image { + /// Creates a new image from raw binary data and the corresponding metadata. + /// + /// # Panics + /// Panics if the length of the `data`, volume of the `size` and the size of the `format` + /// do not match. + pub fn new( + size: Extent3d, + dimension: TextureDimension, + data: Vec, + format: TextureFormat, + ) -> Self { + debug_assert_eq!( + size.volume() * format.pixel_size(), + data.len(), + "Pixel data, size and format have to match", + ); + let mut image = Self { + data, + ..Default::default() + }; + image.texture_descriptor.dimension = dimension; + image.texture_descriptor.size = size; + image.texture_descriptor.format = format; + image + } + + /// Creates a new image from raw binary data and the corresponding metadata, by filling + /// the image data with the `pixel` data repeated multiple times. + /// + /// # Panics + /// Panics if the size of the `format` is not a multiple of the length of the `pixel` data. + /// do not match. + pub fn new_fill( + size: Extent3d, + dimension: TextureDimension, + pixel: &[u8], + format: TextureFormat, + ) -> Self { + let mut value = Image::default(); + value.texture_descriptor.format = format; + value.texture_descriptor.dimension = dimension; + value.resize(size); + + debug_assert_eq!( + pixel.len() % format.pixel_size(), + 0, + "Must not have incomplete pixel data." + ); + debug_assert!( + pixel.len() <= value.data.len(), + "Fill data must fit within pixel buffer." + ); + + for current_pixel in value.data.chunks_exact_mut(pixel.len()) { + current_pixel.copy_from_slice(pixel); + } + value + } + + /// Returns the aspect ratio (height/width) of a 2D image. + pub fn aspect_2d(&self) -> f32 { + self.texture_descriptor.size.height as f32 / self.texture_descriptor.size.width as f32 + } + + /// Resizes the image to the new size, by removing information or appending 0 to the `data`. + /// Does not properly resize the contents of the image, but only its internal `data` buffer. + pub fn resize(&mut self, size: Extent3d) { + self.texture_descriptor.size = size; + self.data.resize( + size.volume() * self.texture_descriptor.format.pixel_size(), + 0, + ); + } + + /// Changes the `size`, asserting that the total number of data elements (pixels) remains the + /// same. + /// + /// # Panics + /// Panics if the `new_size` does not have the same volume as to old one. + pub fn reinterpret_size(&mut self, new_size: Extent3d) { + assert!( + new_size.volume() == self.texture_descriptor.size.volume(), + "Incompatible sizes: old = {:?} new = {:?}", + self.texture_descriptor.size, + new_size + ); + + self.texture_descriptor.size = new_size; + } + + /// Takes a 2D image containing vertically stacked images of the same size, and reinterprets + /// it as a 2D array texture, where each of the stacked images becomes one layer of the + /// array. This is primarily for use with the `texture2DArray` shader uniform type. + /// + /// # Panics + /// Panics if the texture is not 2D, has more than one layers or is not evenly dividable into + /// the `layers`. + pub fn reinterpret_stacked_2d_as_array(&mut self, layers: u32) { + // Must be a stacked image, and the height must be divisible by layers. + assert!(self.texture_descriptor.dimension == TextureDimension::D2); + assert!(self.texture_descriptor.size.depth_or_array_layers == 1); + assert_eq!(self.texture_descriptor.size.height % layers, 0); + + self.reinterpret_size(Extent3d { + width: self.texture_descriptor.size.width, + height: self.texture_descriptor.size.height / layers, + depth_or_array_layers: layers, + }); + } + + /// Convert a texture from a format to another + /// Only a few formats are supported as input and output: + /// - `TextureFormat::R8Unorm` + /// - `TextureFormat::Rg8Unorm` + /// - `TextureFormat::Rgba8UnormSrgb` + /// - `TextureFormat::Bgra8UnormSrgb` + pub fn convert(&self, new_format: TextureFormat) -> Option { + super::image_texture_conversion::texture_to_image(self) + .and_then(|img| match new_format { + TextureFormat::R8Unorm => Some(image::DynamicImage::ImageLuma8(img.into_luma8())), + TextureFormat::Rg8Unorm => { + Some(image::DynamicImage::ImageLumaA8(img.into_luma_alpha8())) + } + TextureFormat::Rgba8UnormSrgb => { + Some(image::DynamicImage::ImageRgba8(img.into_rgba8())) + } + TextureFormat::Bgra8UnormSrgb => { + Some(image::DynamicImage::ImageBgra8(img.into_bgra8())) + } + _ => None, + }) + .map(super::image_texture_conversion::image_to_texture) + } + + /// Load a bytes buffer in a [`Texture`], according to type `image_type`, using the `image` + /// crate` + pub fn from_buffer(buffer: &[u8], image_type: ImageType) -> Result { + let format = match image_type { + ImageType::MimeType(mime_type) => match mime_type { + "image/png" => Ok(image::ImageFormat::Png), + "image/vnd-ms.dds" => Ok(image::ImageFormat::Dds), + "image/x-targa" => Ok(image::ImageFormat::Tga), + "image/x-tga" => Ok(image::ImageFormat::Tga), + "image/jpeg" => Ok(image::ImageFormat::Jpeg), + "image/bmp" => Ok(image::ImageFormat::Bmp), + "image/x-bmp" => Ok(image::ImageFormat::Bmp), + _ => Err(TextureError::InvalidImageMimeType(mime_type.to_string())), + }, + ImageType::Extension(extension) => image::ImageFormat::from_extension(extension) + .ok_or_else(|| TextureError::InvalidImageMimeType(extension.to_string())), + }?; + + // Load the image in the expected format. + // Some formats like PNG allow for R or RG textures too, so the texture + // format needs to be determined. For RGB textures an alpha channel + // needs to be added, so the image data needs to be converted in those + // cases. + + let dyn_img = image::load_from_memory_with_format(buffer, format)?; + Ok(image_to_texture(dyn_img)) + } +} + +/// An error that occurs when loading a texture +#[derive(Error, Debug)] +pub enum TextureError { + #[error("invalid image mime type")] + InvalidImageMimeType(String), + #[error("invalid image extension")] + InvalidImageExtension(String), + #[error("failed to load an image: {0}")] + ImageError(#[from] image::ImageError), +} + +/// The type of a raw image buffer. +pub enum ImageType<'a> { + /// The mime type of an image, for example `"image/png"`. + MimeType(&'a str), + /// The extension of an image file, for example `"png"`. + Extension(&'a str), +} + +/// Used to calculate the volume of an item. +pub trait Volume { + fn volume(&self) -> usize; +} + +impl Volume for Extent3d { + /// Calculates the volume of the [`Extent3D`]. + fn volume(&self) -> usize { + (self.width * self.height * self.depth_or_array_layers) as usize + } +} + +/// Information about the pixel size in bytes and the number of different components. +pub struct PixelInfo { + /// The size of a component of a pixel in bytes. + pub type_size: usize, + /// The amount of different components (color channels). + pub num_components: usize, +} + +/// Extends the wgpu [`TextureFormat`] with information about the pixel. +pub trait TextureFormatPixelInfo { + /// Returns the pixel information of the format. + fn pixel_info(&self) -> PixelInfo; + /// Returns the size of a pixel of the format. + fn pixel_size(&self) -> usize { + let info = self.pixel_info(); + info.type_size * info.num_components + } +} + +impl TextureFormatPixelInfo for TextureFormat { + fn pixel_info(&self) -> PixelInfo { + let type_size = match self { + // 8bit + TextureFormat::R8Unorm + | TextureFormat::R8Snorm + | TextureFormat::R8Uint + | TextureFormat::R8Sint + | TextureFormat::Rg8Unorm + | TextureFormat::Rg8Snorm + | TextureFormat::Rg8Uint + | TextureFormat::Rg8Sint + | TextureFormat::Rgba8Unorm + | TextureFormat::Rgba8UnormSrgb + | TextureFormat::Rgba8Snorm + | TextureFormat::Rgba8Uint + | TextureFormat::Rgba8Sint + | TextureFormat::Bgra8Unorm + | TextureFormat::Bgra8UnormSrgb => 1, + + // 16bit + TextureFormat::R16Uint + | TextureFormat::R16Sint + | TextureFormat::R16Float + | TextureFormat::Rg16Uint + | TextureFormat::Rg16Sint + | TextureFormat::Rg16Float + | TextureFormat::Rgba16Uint + | TextureFormat::Rgba16Sint + | TextureFormat::Rgba16Float => 2, + + // 32bit + TextureFormat::R32Uint + | TextureFormat::R32Sint + | TextureFormat::R32Float + | TextureFormat::Rg32Uint + | TextureFormat::Rg32Sint + | TextureFormat::Rg32Float + | TextureFormat::Rgba32Uint + | TextureFormat::Rgba32Sint + | TextureFormat::Rgba32Float + | TextureFormat::Depth32Float => 4, + + // special cases + TextureFormat::Rgb10a2Unorm => 4, + TextureFormat::Rg11b10Float => 4, + TextureFormat::Depth24Plus => 3, // FIXME is this correct? + TextureFormat::Depth24PlusStencil8 => 4, + // TODO: this is not good! this is a temporary step while porting bevy_render to direct wgpu usage + _ => panic!("cannot get pixel info for type"), + }; + + let components = match self { + TextureFormat::R8Unorm + | TextureFormat::R8Snorm + | TextureFormat::R8Uint + | TextureFormat::R8Sint + | TextureFormat::R16Uint + | TextureFormat::R16Sint + | TextureFormat::R16Float + | TextureFormat::R32Uint + | TextureFormat::R32Sint + | TextureFormat::R32Float => 1, + + TextureFormat::Rg8Unorm + | TextureFormat::Rg8Snorm + | TextureFormat::Rg8Uint + | TextureFormat::Rg8Sint + | TextureFormat::Rg16Uint + | TextureFormat::Rg16Sint + | TextureFormat::Rg16Float + | TextureFormat::Rg32Uint + | TextureFormat::Rg32Sint + | TextureFormat::Rg32Float => 2, + + TextureFormat::Rgba8Unorm + | TextureFormat::Rgba8UnormSrgb + | TextureFormat::Rgba8Snorm + | TextureFormat::Rgba8Uint + | TextureFormat::Rgba8Sint + | TextureFormat::Bgra8Unorm + | TextureFormat::Bgra8UnormSrgb + | TextureFormat::Rgba16Uint + | TextureFormat::Rgba16Sint + | TextureFormat::Rgba16Float + | TextureFormat::Rgba32Uint + | TextureFormat::Rgba32Sint + | TextureFormat::Rgba32Float => 4, + + // special cases + TextureFormat::Rgb10a2Unorm + | TextureFormat::Rg11b10Float + | TextureFormat::Depth32Float + | TextureFormat::Depth24Plus + | TextureFormat::Depth24PlusStencil8 => 1, + // TODO: this is not good! this is a temporary step while porting bevy_render to direct wgpu usage + _ => panic!("cannot get pixel info for type"), + }; + + PixelInfo { + type_size, + num_components: components, + } + } +} + +/// The GPU-representation of an [`Image`]. +/// Consists of the [`Texture`], its [`TextureView`] and the corresponding [`Sampler`]. +#[derive(Debug, Clone)] +pub struct GpuImage { + pub texture: Texture, + pub texture_view: TextureView, + pub sampler: Sampler, +} + +impl RenderAsset for Image { + type ExtractedAsset = Image; + type PreparedAsset = GpuImage; + type Param = (SRes, SRes); + + /// Clones the Image. + fn extract_asset(&self) -> Self::ExtractedAsset { + self.clone() + } + + /// Converts the extracted image into a [`GpuImage`]. + fn prepare_asset( + image: Self::ExtractedAsset, + (render_device, render_queue): &mut SystemParamItem, + ) -> Result> { + let texture = render_device.create_texture(&image.texture_descriptor); + let sampler = render_device.create_sampler(&image.sampler_descriptor); + + let format_size = image.texture_descriptor.format.pixel_size(); + render_queue.write_texture( + ImageCopyTexture { + texture: &texture, + mip_level: 0, + origin: Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + &image.data, + ImageDataLayout { + offset: 0, + bytes_per_row: Some( + std::num::NonZeroU32::new( + image.texture_descriptor.size.width * format_size as u32, + ) + .unwrap(), + ), + rows_per_image: if image.texture_descriptor.size.depth_or_array_layers > 1 { + std::num::NonZeroU32::new(image.texture_descriptor.size.height) + } else { + None + }, + }, + image.texture_descriptor.size, + ); + + let texture_view = texture.create_view(&TextureViewDescriptor::default()); + Ok(GpuImage { + texture, + texture_view, + sampler, + }) + } +} diff --git a/pipelined/bevy_render2/src/texture/image_texture_conversion.rs b/pipelined/bevy_render2/src/texture/image_texture_conversion.rs new file mode 100644 index 0000000000..d3c243bad4 --- /dev/null +++ b/pipelined/bevy_render2/src/texture/image_texture_conversion.rs @@ -0,0 +1,158 @@ +use crate::texture::{Image, TextureFormatPixelInfo}; +use wgpu::{Extent3d, TextureDimension, TextureFormat}; + +// TODO: fix name? +/// Converts a [`DynamicImage`] to an [`Image`]. +pub(crate) fn image_to_texture(dyn_img: image::DynamicImage) -> Image { + use bevy_core::cast_slice; + let width; + let height; + + let data: Vec; + let format: TextureFormat; + + match dyn_img { + image::DynamicImage::ImageLuma8(i) => { + let i = image::DynamicImage::ImageLuma8(i).into_rgba8(); + width = i.width(); + height = i.height(); + format = TextureFormat::Rgba8UnormSrgb; + + data = i.into_raw(); + } + image::DynamicImage::ImageLumaA8(i) => { + let i = image::DynamicImage::ImageLumaA8(i).into_rgba8(); + width = i.width(); + height = i.height(); + format = TextureFormat::Rgba8UnormSrgb; + + data = i.into_raw(); + } + image::DynamicImage::ImageRgb8(i) => { + let i = image::DynamicImage::ImageRgb8(i).into_rgba8(); + width = i.width(); + height = i.height(); + format = TextureFormat::Rgba8UnormSrgb; + + data = i.into_raw(); + } + image::DynamicImage::ImageRgba8(i) => { + width = i.width(); + height = i.height(); + format = TextureFormat::Rgba8UnormSrgb; + + data = i.into_raw(); + } + image::DynamicImage::ImageBgr8(i) => { + let i = image::DynamicImage::ImageBgr8(i).into_bgra8(); + + width = i.width(); + height = i.height(); + format = TextureFormat::Bgra8UnormSrgb; + + data = i.into_raw(); + } + image::DynamicImage::ImageBgra8(i) => { + width = i.width(); + height = i.height(); + format = TextureFormat::Bgra8UnormSrgb; + + data = i.into_raw(); + } + image::DynamicImage::ImageLuma16(i) => { + width = i.width(); + height = i.height(); + format = TextureFormat::R16Uint; + + let raw_data = i.into_raw(); + + data = cast_slice(&raw_data).to_owned(); + } + image::DynamicImage::ImageLumaA16(i) => { + width = i.width(); + height = i.height(); + format = TextureFormat::Rg16Uint; + + let raw_data = i.into_raw(); + + data = cast_slice(&raw_data).to_owned(); + } + + image::DynamicImage::ImageRgb16(image) => { + width = image.width(); + height = image.height(); + format = TextureFormat::Rgba16Uint; + + let mut local_data = + Vec::with_capacity(width as usize * height as usize * format.pixel_size()); + + for pixel in image.into_raw().chunks_exact(3) { + // TODO use the array_chunks method once stabilised + // https://github.com/rust-lang/rust/issues/74985 + let r = pixel[0]; + let g = pixel[1]; + let b = pixel[2]; + let a = u16::max_value(); + + local_data.extend_from_slice(&r.to_ne_bytes()); + local_data.extend_from_slice(&g.to_ne_bytes()); + local_data.extend_from_slice(&b.to_ne_bytes()); + local_data.extend_from_slice(&a.to_ne_bytes()); + } + + data = local_data; + } + image::DynamicImage::ImageRgba16(i) => { + width = i.width(); + height = i.height(); + format = TextureFormat::Rgba16Uint; + + let raw_data = i.into_raw(); + + data = cast_slice(&raw_data).to_owned(); + } + } + + Image::new( + Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + TextureDimension::D2, + data, + format, + ) +} + +/// Converts an [`Image`] to a [`DynamicImage`]. Not all [`TextureFormat`] are +/// covered, therefore it will return `None` if the format is unsupported. +pub(crate) fn texture_to_image(texture: &Image) -> Option { + match texture.texture_descriptor.format { + TextureFormat::R8Unorm => image::ImageBuffer::from_raw( + texture.texture_descriptor.size.width, + texture.texture_descriptor.size.height, + texture.data.clone(), + ) + .map(image::DynamicImage::ImageLuma8), + TextureFormat::Rg8Unorm => image::ImageBuffer::from_raw( + texture.texture_descriptor.size.width, + texture.texture_descriptor.size.height, + texture.data.clone(), + ) + .map(image::DynamicImage::ImageLumaA8), + TextureFormat::Rgba8UnormSrgb => image::ImageBuffer::from_raw( + texture.texture_descriptor.size.width, + texture.texture_descriptor.size.height, + texture.data.clone(), + ) + .map(image::DynamicImage::ImageRgba8), + TextureFormat::Bgra8UnormSrgb => image::ImageBuffer::from_raw( + texture.texture_descriptor.size.width, + texture.texture_descriptor.size.height, + texture.data.clone(), + ) + .map(image::DynamicImage::ImageBgra8), + _ => None, + } +} diff --git a/pipelined/bevy_render2/src/texture/image_texture_loader.rs b/pipelined/bevy_render2/src/texture/image_texture_loader.rs new file mode 100644 index 0000000000..20e6c774cc --- /dev/null +++ b/pipelined/bevy_render2/src/texture/image_texture_loader.rs @@ -0,0 +1,67 @@ +use anyhow::Result; +use bevy_asset::{AssetLoader, LoadContext, LoadedAsset}; +use bevy_utils::BoxedFuture; +use thiserror::Error; + +use crate::texture::{Image, ImageType, TextureError}; + +/// Loader for images that can be read by the `image` crate. +#[derive(Clone, Default)] +pub struct ImageTextureLoader; + +const FILE_EXTENSIONS: &[&str] = &["png", "dds", "tga", "jpg", "jpeg", "bmp"]; + +impl AssetLoader for ImageTextureLoader { + fn load<'a>( + &'a self, + bytes: &'a [u8], + load_context: &'a mut LoadContext, + ) -> BoxedFuture<'a, Result<()>> { + Box::pin(async move { + // use the file extension for the image type + let ext = load_context.path().extension().unwrap().to_str().unwrap(); + + let dyn_img = Image::from_buffer(bytes, ImageType::Extension(ext)).map_err(|err| { + FileTextureError { + error: err, + path: format!("{}", load_context.path().display()), + } + })?; + + load_context.set_default_asset(LoadedAsset::new(dyn_img)); + Ok(()) + }) + } + + fn extensions(&self) -> &[&str] { + FILE_EXTENSIONS + } +} + +/// An error that occurs when loading a texture from a file. +#[derive(Error, Debug)] +pub struct FileTextureError { + error: TextureError, + path: String, +} +impl std::fmt::Display for FileTextureError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + write!( + f, + "Error reading image file {}: {}, this is an error in `bevy_render`.", + self.path, self.error + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_supported_file_extensions() { + for ext in FILE_EXTENSIONS { + assert!(image::ImageFormat::from_extension(ext).is_some()) + } + } +} diff --git a/pipelined/bevy_render2/src/texture/mod.rs b/pipelined/bevy_render2/src/texture/mod.rs new file mode 100644 index 0000000000..1b99642a5f --- /dev/null +++ b/pipelined/bevy_render2/src/texture/mod.rs @@ -0,0 +1,53 @@ +#[cfg(feature = "hdr")] +mod hdr_texture_loader; +#[allow(clippy::module_inception)] +mod image; +mod image_texture_loader; +mod texture_cache; + +pub(crate) mod image_texture_conversion; + +pub use self::image::*; +#[cfg(feature = "hdr")] +pub use hdr_texture_loader::*; +pub use image_texture_loader::*; +pub use texture_cache::*; + +use crate::{render_asset::RenderAssetPlugin, RenderApp, RenderStage}; +use bevy_app::{App, Plugin}; +use bevy_asset::AddAsset; + +// TODO: replace Texture names with Image names? +/// Adds the [`Image`] as an asset and makes sure that they are extracted and prepared for the GPU. +pub struct ImagePlugin; + +impl Plugin for ImagePlugin { + fn build(&self, app: &mut App) { + #[cfg(feature = "png")] + { + app.init_asset_loader::(); + } + + app.add_plugin(RenderAssetPlugin::::default()) + .add_asset::(); + + app.sub_app(RenderApp) + .init_resource::() + .add_system_to_stage(RenderStage::Cleanup, update_texture_cache_system); + } +} + +pub trait BevyDefault { + fn bevy_default() -> Self; +} + +impl BevyDefault for wgpu::TextureFormat { + fn bevy_default() -> Self { + if cfg!(target_os = "android") || cfg!(target_arch = "wasm32") { + // Bgra8UnormSrgb texture missing on some Android devices + wgpu::TextureFormat::Rgba8UnormSrgb + } else { + wgpu::TextureFormat::Bgra8UnormSrgb + } + } +} diff --git a/pipelined/bevy_render2/src/texture/texture_cache.rs b/pipelined/bevy_render2/src/texture/texture_cache.rs new file mode 100644 index 0000000000..cca353a1d5 --- /dev/null +++ b/pipelined/bevy_render2/src/texture/texture_cache.rs @@ -0,0 +1,100 @@ +use crate::{ + render_resource::{Texture, TextureView}, + renderer::RenderDevice, +}; +use bevy_ecs::prelude::ResMut; +use bevy_utils::HashMap; +use wgpu::{TextureDescriptor, TextureViewDescriptor}; + +/// The internal representation of a [`CachedTexture`] used to track whether it was recently used +/// and is currently taken. +struct CachedTextureMeta { + texture: Texture, + default_view: TextureView, + taken: bool, + frames_since_last_use: usize, +} + +/// A cached GPU [`Texture`] with corresponding [`TextureView`]. +/// This is useful for textures that are created repeatedly (each frame) in the rendering process +/// to reduce the amount of GPU memory allocations. +pub struct CachedTexture { + pub texture: Texture, + pub default_view: TextureView, +} + +/// This resource caches textures that are created repeatedly in the rendering process and +/// are only required for one frame. +#[derive(Default)] +pub struct TextureCache { + textures: HashMap, Vec>, +} + +impl TextureCache { + /// Retrieves a texture that matches the `descriptor`. If no matching one is found a new + /// [`CachedTexture`] is created. + pub fn get( + &mut self, + render_device: &RenderDevice, + descriptor: TextureDescriptor<'static>, + ) -> CachedTexture { + match self.textures.entry(descriptor) { + std::collections::hash_map::Entry::Occupied(mut entry) => { + for texture in entry.get_mut().iter_mut() { + if !texture.taken { + texture.frames_since_last_use = 0; + texture.taken = true; + return CachedTexture { + texture: texture.texture.clone(), + default_view: texture.default_view.clone(), + }; + } + } + + let texture = render_device.create_texture(&entry.key().clone()); + let default_view = texture.create_view(&TextureViewDescriptor::default()); + entry.get_mut().push(CachedTextureMeta { + texture: texture.clone(), + default_view: default_view.clone(), + frames_since_last_use: 0, + taken: true, + }); + CachedTexture { + texture, + default_view, + } + } + std::collections::hash_map::Entry::Vacant(entry) => { + let texture = render_device.create_texture(entry.key()); + let default_view = texture.create_view(&TextureViewDescriptor::default()); + entry.insert(vec![CachedTextureMeta { + texture: texture.clone(), + default_view: default_view.clone(), + taken: true, + frames_since_last_use: 0, + }]); + CachedTexture { + texture, + default_view, + } + } + } + } + + /// Updates the cache and only retains recently used textures. + pub fn update(&mut self) { + for textures in self.textures.values_mut() { + for texture in textures.iter_mut() { + texture.frames_since_last_use += 1; + texture.taken = false; + } + + textures.retain(|texture| texture.frames_since_last_use < 3); + } + } +} + +/// Updates the [`TextureCache`] to only retains recently used textures. +pub fn update_texture_cache_system(mut texture_cache: ResMut) { + texture_cache.update(); +} diff --git a/pipelined/bevy_render2/src/view/mod.rs b/pipelined/bevy_render2/src/view/mod.rs new file mode 100644 index 0000000000..2db9a26176 --- /dev/null +++ b/pipelined/bevy_render2/src/view/mod.rs @@ -0,0 +1,194 @@ +pub mod visibility; +pub mod window; + +pub use visibility::*; +use wgpu::{ + Color, Extent3d, Operations, RenderPassColorAttachment, TextureDescriptor, TextureDimension, + TextureFormat, TextureUsages, +}; +pub use window::*; + +use crate::{ + camera::{ExtractedCamera, ExtractedCameraNames}, + render_resource::{DynamicUniformVec, Texture, TextureView}, + renderer::{RenderDevice, RenderQueue}, + texture::{BevyDefault, TextureCache}, + RenderApp, RenderStage, +}; +use bevy_app::{App, Plugin}; +use bevy_ecs::prelude::*; +use bevy_math::{Mat4, Vec3}; +use bevy_transform::components::GlobalTransform; +use crevice::std140::AsStd140; + +pub struct ViewPlugin; + +impl Plugin for ViewPlugin { + fn build(&self, app: &mut App) { + app.init_resource::().add_plugin(VisibilityPlugin); + + app.sub_app(RenderApp) + .init_resource::() + .add_system_to_stage(RenderStage::Extract, extract_msaa) + .add_system_to_stage(RenderStage::Prepare, prepare_view_uniforms) + .add_system_to_stage( + RenderStage::Prepare, + prepare_view_targets.after(WindowSystem::Prepare), + ); + } +} + +#[derive(Clone)] +pub struct Msaa { + /// The number of samples to run for Multi-Sample Anti-Aliasing. Higher numbers result in + /// smoother edges. Note that WGPU currently only supports 1 or 4 samples. + /// Ultimately we plan on supporting whatever is natively supported on a given device. + /// Check out this issue for more info: https://github.com/gfx-rs/wgpu/issues/1832 + pub samples: u32, +} + +impl Default for Msaa { + fn default() -> Self { + Self { samples: 4 } + } +} + +pub fn extract_msaa(mut commands: Commands, msaa: Res) { + // NOTE: windows.is_changed() handles cases where a window was resized + commands.insert_resource(msaa.clone()); +} + +#[derive(Component)] +pub struct ExtractedView { + pub projection: Mat4, + pub transform: GlobalTransform, + pub width: u32, + pub height: u32, +} + +#[derive(Clone, AsStd140)] +pub struct ViewUniform { + view_proj: Mat4, + projection: Mat4, + world_position: Vec3, +} + +#[derive(Default)] +pub struct ViewUniforms { + pub uniforms: DynamicUniformVec, +} + +#[derive(Component)] +pub struct ViewUniformOffset { + pub offset: u32, +} + +#[derive(Component)] +pub struct ViewTarget { + pub view: TextureView, + pub sampled_target: Option, +} + +impl ViewTarget { + pub fn get_color_attachment(&self, ops: Operations) -> RenderPassColorAttachment { + RenderPassColorAttachment { + view: if let Some(sampled_target) = &self.sampled_target { + sampled_target + } else { + &self.view + }, + resolve_target: if self.sampled_target.is_some() { + Some(&self.view) + } else { + None + }, + ops, + } + } +} + +#[derive(Component)] +pub struct ViewDepthTexture { + pub texture: Texture, + pub view: TextureView, +} + +fn prepare_view_uniforms( + mut commands: Commands, + render_device: Res, + render_queue: Res, + mut view_uniforms: ResMut, + views: Query<(Entity, &ExtractedView)>, +) { + view_uniforms.uniforms.clear(); + for (entity, camera) in views.iter() { + let projection = camera.projection; + let view_uniforms = ViewUniformOffset { + offset: view_uniforms.uniforms.push(ViewUniform { + view_proj: projection * camera.transform.compute_matrix().inverse(), + projection, + world_position: camera.transform.translation, + }), + }; + + commands.entity(entity).insert(view_uniforms); + } + + view_uniforms + .uniforms + .write_buffer(&render_device, &render_queue); +} + +fn prepare_view_targets( + mut commands: Commands, + camera_names: Res, + windows: Res, + msaa: Res, + render_device: Res, + mut texture_cache: ResMut, + cameras: Query<&ExtractedCamera>, +) { + for entity in camera_names.entities.values().copied() { + let camera = if let Ok(camera) = cameras.get(entity) { + camera + } else { + continue; + }; + let window = if let Some(window) = windows.get(&camera.window_id) { + window + } else { + continue; + }; + let swap_chain_texture = if let Some(texture) = &window.swap_chain_texture { + texture + } else { + continue; + }; + let sampled_target = if msaa.samples > 1 { + let sampled_texture = texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("sampled_color_attachment_texture"), + size: Extent3d { + width: window.physical_width, + height: window.physical_height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: msaa.samples, + dimension: TextureDimension::D2, + format: TextureFormat::bevy_default(), + usage: TextureUsages::RENDER_ATTACHMENT, + }, + ); + Some(sampled_texture.default_view.clone()) + } else { + None + }; + + commands.entity(entity).insert(ViewTarget { + view: swap_chain_texture.clone(), + sampled_target, + }); + } +} diff --git a/pipelined/bevy_render2/src/view/visibility/mod.rs b/pipelined/bevy_render2/src/view/visibility/mod.rs new file mode 100644 index 0000000000..5fee0293ec --- /dev/null +++ b/pipelined/bevy_render2/src/view/visibility/mod.rs @@ -0,0 +1,187 @@ +mod render_layers; + +pub use render_layers::*; + +use bevy_app::{CoreStage, Plugin}; +use bevy_asset::{Assets, Handle}; +use bevy_ecs::prelude::*; +use bevy_reflect::Reflect; +use bevy_transform::{components::GlobalTransform, TransformSystem}; + +use crate::{ + camera::{Camera, CameraProjection, OrthographicProjection, PerspectiveProjection}, + mesh::Mesh, + primitives::{Aabb, Frustum}, +}; + +/// User indication of whether an entity is visible +#[derive(Component, Clone, Reflect)] +#[reflect(Component)] +pub struct Visibility { + pub is_visible: bool, +} + +impl Default for Visibility { + fn default() -> Self { + Self { is_visible: true } + } +} + +/// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering +#[derive(Component, Clone, Reflect)] +#[reflect(Component)] +pub struct ComputedVisibility { + pub is_visible: bool, +} + +impl Default for ComputedVisibility { + fn default() -> Self { + Self { is_visible: true } + } +} + +#[derive(Clone, Debug)] +pub struct VisibleEntity { + pub entity: Entity, +} + +#[derive(Component, Clone, Default, Debug, Reflect)] +#[reflect(Component)] +pub struct VisibleEntities { + #[reflect(ignore)] + pub entities: Vec, +} + +impl VisibleEntities { + pub fn iter(&self) -> impl DoubleEndedIterator { + self.entities.iter() + } +} + +#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)] +pub enum VisibilitySystems { + CalculateBounds, + UpdateOrthographicFrusta, + UpdatePerspectiveFrusta, + CheckVisibility, +} + +pub struct VisibilityPlugin; + +impl Plugin for VisibilityPlugin { + fn build(&self, app: &mut bevy_app::App) { + use VisibilitySystems::*; + + app.add_system_to_stage( + CoreStage::PostUpdate, + calculate_bounds.label(CalculateBounds), + ) + .add_system_to_stage( + CoreStage::PostUpdate, + update_frusta:: + .label(UpdateOrthographicFrusta) + .after(TransformSystem::TransformPropagate), + ) + .add_system_to_stage( + CoreStage::PostUpdate, + update_frusta:: + .label(UpdatePerspectiveFrusta) + .after(TransformSystem::TransformPropagate), + ) + .add_system_to_stage( + CoreStage::PostUpdate, + check_visibility + .label(CheckVisibility) + .after(CalculateBounds) + .after(UpdateOrthographicFrusta) + .after(UpdatePerspectiveFrusta) + .after(TransformSystem::TransformPropagate), + ); + } +} + +pub fn calculate_bounds( + mut commands: Commands, + meshes: Res>, + without_aabb: Query<(Entity, &Handle), Without>, +) { + for (entity, mesh_handle) in without_aabb.iter() { + if let Some(mesh) = meshes.get(mesh_handle) { + if let Some(aabb) = mesh.compute_aabb() { + commands.entity(entity).insert(aabb); + } + } + } +} + +pub fn update_frusta( + mut views: Query<(&GlobalTransform, &T, &mut Frustum)>, +) { + for (transform, projection, mut frustum) in views.iter_mut() { + let view_projection = + projection.get_projection_matrix() * transform.compute_matrix().inverse(); + *frustum = Frustum::from_view_projection( + &view_projection, + &transform.translation, + &transform.back(), + projection.far(), + ); + } +} + +pub fn check_visibility( + mut view_query: Query<(&mut VisibleEntities, &Frustum, Option<&RenderLayers>), With>, + mut visible_entity_query: QuerySet<( + QueryState<&mut ComputedVisibility>, + QueryState<( + Entity, + &Visibility, + &mut ComputedVisibility, + Option<&RenderLayers>, + Option<&Aabb>, + Option<&GlobalTransform>, + )>, + )>, +) { + // Reset the computed visibility to false + for mut computed_visibility in visible_entity_query.q0().iter_mut() { + computed_visibility.is_visible = false; + } + + for (mut visible_entities, frustum, maybe_view_mask) in view_query.iter_mut() { + visible_entities.entities.clear(); + let view_mask = maybe_view_mask.copied().unwrap_or_default(); + + for ( + entity, + visibility, + mut computed_visibility, + maybe_entity_mask, + maybe_aabb, + maybe_transform, + ) in visible_entity_query.q1().iter_mut() + { + if !visibility.is_visible { + continue; + } + + let entity_mask = maybe_entity_mask.copied().unwrap_or_default(); + if !view_mask.intersects(&entity_mask) { + continue; + } + + // If we have an aabb and transform, do frustum culling + if let (Some(aabb), Some(transform)) = (maybe_aabb, maybe_transform) { + if !frustum.intersects_obb(aabb, &transform.compute_matrix()) { + continue; + } + } + + computed_visibility.is_visible = true; + visible_entities.entities.push(VisibleEntity { entity }); + } + + // TODO: check for big changes in visible entities len() vs capacity() (ex: 2x) and resize + // to prevent holding unneeded memory + } +} diff --git a/pipelined/bevy_render2/src/view/visibility/render_layers.rs b/pipelined/bevy_render2/src/view/visibility/render_layers.rs new file mode 100644 index 0000000000..59cc40e41c --- /dev/null +++ b/pipelined/bevy_render2/src/view/visibility/render_layers.rs @@ -0,0 +1,178 @@ +use bevy_ecs::prelude::{Component, ReflectComponent}; +use bevy_reflect::Reflect; + +type LayerMask = u32; + +/// An identifier for a rendering layer. +pub type Layer = u8; + +/// Describes which rendering layers an entity belongs to. +/// +/// Cameras with this component will only render entities with intersecting +/// layers. +/// +/// There are 32 layers numbered `0` - [`TOTAL_LAYERS`](RenderLayers::TOTAL_LAYERS). Entities may +/// belong to one or more layers, or no layer at all. +/// +/// The [`Default`] instance of `RenderLayers` contains layer `0`, the first layer. +/// +/// An entity with this component without any layers is invisible. +/// +/// Entities without this component belong to layer `0`. +#[derive(Component, Copy, Clone, Reflect, PartialEq, Eq, PartialOrd, Ord)] +#[reflect(Component, PartialEq)] +pub struct RenderLayers(LayerMask); + +impl std::fmt::Debug for RenderLayers { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("RenderLayers") + .field(&self.iter().collect::>()) + .finish() + } +} + +impl std::iter::FromIterator for RenderLayers { + fn from_iter>(i: T) -> Self { + i.into_iter().fold(Self::none(), |mask, g| mask.with(g)) + } +} + +/// Defaults to containing to layer `0`, the first layer. +impl Default for RenderLayers { + fn default() -> Self { + RenderLayers::layer(0) + } +} + +impl RenderLayers { + /// The total number of layers supported. + pub const TOTAL_LAYERS: usize = std::mem::size_of::() * 8; + + /// Create a new `RenderLayers` belonging to the given layer. + pub fn layer(n: Layer) -> Self { + RenderLayers(0).with(n) + } + + /// Create a new `RenderLayers` that belongs to all layers. + pub fn all() -> Self { + RenderLayers(u32::MAX) + } + + /// Create a new `RenderLayers` that belongs to no layers. + pub fn none() -> Self { + RenderLayers(0) + } + + /// Create a `RenderLayers` from a list of layers. + pub fn from_layers(layers: &[Layer]) -> Self { + layers.iter().copied().collect() + } + + /// Add the given layer. + /// + /// This may be called multiple times to allow an entity to belong + /// to multiple rendering layers. The maximum layer is `TOTAL_LAYERS - 1`. + /// + /// # Panics + /// Panics when called with a layer greater than `TOTAL_LAYERS - 1`. + pub fn with(mut self, layer: Layer) -> Self { + assert!(usize::from(layer) < Self::TOTAL_LAYERS); + self.0 |= 1 << layer; + self + } + + /// Removes the given rendering layer. + /// + /// # Panics + /// Panics when called with a layer greater than `TOTAL_LAYERS - 1`. + pub fn without(mut self, layer: Layer) -> Self { + assert!(usize::from(layer) < Self::TOTAL_LAYERS); + self.0 &= !(1 << layer); + self + } + + /// Get an iterator of the layers. + pub fn iter(&self) -> impl Iterator { + let total: Layer = std::convert::TryInto::try_into(Self::TOTAL_LAYERS).unwrap(); + let mask = *self; + (0..total).filter(move |g| RenderLayers::layer(*g).intersects(&mask)) + } + + /// Determine if a `RenderLayers` intersects another. + /// + /// `RenderLayers`s intersect if they share any common layers. + /// + /// A `RenderLayers` with no layers will not match any other + /// `RenderLayers`, even another with no layers. + pub fn intersects(&self, other: &RenderLayers) -> bool { + (self.0 & other.0) > 0 + } +} + +#[cfg(test)] +mod rendering_mask_tests { + use super::{Layer, RenderLayers}; + + #[test] + fn rendering_mask_sanity() { + assert_eq!( + RenderLayers::TOTAL_LAYERS, + 32, + "total layers is what we think it is" + ); + assert_eq!(RenderLayers::layer(0).0, 1, "layer 0 is mask 1"); + assert_eq!(RenderLayers::layer(1).0, 2, "layer 1 is mask 2"); + assert_eq!(RenderLayers::layer(0).with(1).0, 3, "layer 0 + 1 is mask 3"); + assert_eq!( + RenderLayers::layer(0).with(1).without(0).0, + 2, + "layer 0 + 1 - 0 is mask 2" + ); + assert!( + RenderLayers::layer(1).intersects(&RenderLayers::layer(1)), + "layers match like layers" + ); + assert!( + RenderLayers::layer(0).intersects(&RenderLayers(1)), + "a layer of 0 means the mask is just 1 bit" + ); + + assert!( + RenderLayers::layer(0) + .with(3) + .intersects(&RenderLayers::layer(3)), + "a mask will match another mask containing any similar layers" + ); + + assert!( + RenderLayers::default().intersects(&RenderLayers::default()), + "default masks match each other" + ); + + assert!( + !RenderLayers::layer(0).intersects(&RenderLayers::layer(1)), + "masks with differing layers do not match" + ); + assert!( + !RenderLayers(0).intersects(&RenderLayers(0)), + "empty masks don't match" + ); + assert_eq!( + RenderLayers::from_layers(&[0, 2, 16, 30]) + .iter() + .collect::>(), + vec![0, 2, 16, 30], + "from_layers and get_layers should roundtrip" + ); + assert_eq!( + format!("{:?}", RenderLayers::from_layers(&[0, 1, 2, 3])).as_str(), + "RenderLayers([0, 1, 2, 3])", + "Debug instance shows layers" + ); + assert_eq!( + RenderLayers::from_layers(&[0, 1, 2]), + >::from_iter(vec![0, 1, 2]), + "from_layers and from_iter are equivalent" + ) + } +} diff --git a/pipelined/bevy_render2/src/view/window.rs b/pipelined/bevy_render2/src/view/window.rs new file mode 100644 index 0000000000..6bc9dafc2a --- /dev/null +++ b/pipelined/bevy_render2/src/view/window.rs @@ -0,0 +1,164 @@ +use crate::{ + render_resource::TextureView, + renderer::{RenderDevice, RenderInstance}, + texture::BevyDefault, + RenderApp, RenderStage, RenderWorld, +}; +use bevy_app::{App, Plugin}; +use bevy_ecs::prelude::*; +use bevy_utils::{tracing::debug, HashMap, HashSet}; +use bevy_window::{RawWindowHandleWrapper, WindowId, Windows}; +use std::ops::{Deref, DerefMut}; +use wgpu::TextureFormat; + +/// Token to ensure a system runs on the main thread. +#[derive(Default)] +pub struct NonSendMarker; + +pub struct WindowRenderPlugin; + +#[derive(SystemLabel, Debug, Clone, PartialEq, Eq, Hash)] +pub enum WindowSystem { + Prepare, +} + +impl Plugin for WindowRenderPlugin { + fn build(&self, app: &mut App) { + app.sub_app(RenderApp) + .init_resource::() + .init_resource::() + .init_resource::() + .add_system_to_stage(RenderStage::Extract, extract_windows) + .add_system_to_stage( + RenderStage::Prepare, + prepare_windows.label(WindowSystem::Prepare), + ); + } +} + +pub struct ExtractedWindow { + pub id: WindowId, + pub handle: RawWindowHandleWrapper, + pub physical_width: u32, + pub physical_height: u32, + pub vsync: bool, + pub swap_chain_texture: Option, + pub size_changed: bool, +} + +#[derive(Default)] +pub struct ExtractedWindows { + pub windows: HashMap, +} + +impl Deref for ExtractedWindows { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.windows + } +} + +impl DerefMut for ExtractedWindows { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.windows + } +} + +fn extract_windows(mut render_world: ResMut, windows: Res) { + let mut extracted_windows = render_world.get_resource_mut::().unwrap(); + for window in windows.iter() { + let (new_width, new_height) = ( + window.physical_width().max(1), + window.physical_height().max(1), + ); + + let mut extracted_window = + extracted_windows + .entry(window.id()) + .or_insert(ExtractedWindow { + id: window.id(), + handle: window.raw_window_handle(), + physical_width: new_width, + physical_height: new_height, + vsync: window.vsync(), + swap_chain_texture: None, + size_changed: false, + }); + + // NOTE: Drop the swap chain frame here + extracted_window.swap_chain_texture = None; + extracted_window.size_changed = new_width != extracted_window.physical_width + || new_height != extracted_window.physical_height; + + if extracted_window.size_changed { + debug!( + "Window size changed from {}x{} to {}x{}", + extracted_window.physical_width, + extracted_window.physical_height, + new_width, + new_height + ); + extracted_window.physical_width = new_width; + extracted_window.physical_height = new_height; + } + } +} + +#[derive(Default)] +pub struct WindowSurfaces { + surfaces: HashMap, + /// List of windows that we have already called the initial `configure_surface` for + configured_windows: HashSet, +} + +pub fn prepare_windows( + // By accessing a NonSend resource, we tell the scheduler to put this system on the main thread, + // which is necessary for some OS s + _marker: NonSend, + mut windows: ResMut, + mut window_surfaces: ResMut, + render_device: Res, + render_instance: Res, +) { + let window_surfaces = window_surfaces.deref_mut(); + for window in windows.windows.values_mut() { + let surface = window_surfaces + .surfaces + .entry(window.id) + .or_insert_with(|| unsafe { + // NOTE: On some OSes this MUST be called from the main thread. + render_instance.create_surface(&window.handle.get_handle()) + }); + + let swap_chain_descriptor = wgpu::SurfaceConfiguration { + format: TextureFormat::bevy_default(), + width: window.physical_width, + height: window.physical_height, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + present_mode: if window.vsync { + wgpu::PresentMode::Fifo + } else { + wgpu::PresentMode::Immediate + }, + }; + + // Do the initial surface configuration if it hasn't been configured yet + if window_surfaces.configured_windows.insert(window.id) || window.size_changed { + render_device.configure_surface(surface, &swap_chain_descriptor); + } + + let frame = match surface.get_current_texture() { + Ok(swap_chain_frame) => swap_chain_frame, + Err(wgpu::SurfaceError::Outdated) => { + render_device.configure_surface(surface, &swap_chain_descriptor); + surface + .get_current_texture() + .expect("Error reconfiguring surface") + } + err => err.expect("Failed to acquire next swap chain texture!"), + }; + + window.swap_chain_texture = Some(TextureView::from(frame)); + } +} diff --git a/pipelined/bevy_sprite2/Cargo.toml b/pipelined/bevy_sprite2/Cargo.toml new file mode 100644 index 0000000000..9a52e71199 --- /dev/null +++ b/pipelined/bevy_sprite2/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "bevy_sprite2" +version = "0.5.0" +edition = "2021" +authors = [ + "Bevy Contributors ", + "Carter Anderson ", +] +description = "Provides sprite functionality for Bevy Engine" +homepage = "https://bevyengine.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT" +keywords = ["bevy"] + +[dependencies] +# bevy +bevy_app = { path = "../../crates/bevy_app", version = "0.5.0" } +bevy_asset = { path = "../../crates/bevy_asset", version = "0.5.0" } +bevy_core = { path = "../../crates/bevy_core", version = "0.5.0" } +bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.5.0" } +bevy_ecs = { path = "../../crates/bevy_ecs", version = "0.5.0" } +bevy_log = { path = "../../crates/bevy_log", version = "0.5.0" } +bevy_math = { path = "../../crates/bevy_math", version = "0.5.0" } +bevy_reflect = { path = "../../crates/bevy_reflect", version = "0.5.0", features = [ + "bevy", +] } +bevy_render2 = { path = "../bevy_render2", version = "0.5.0" } +bevy_transform = { path = "../../crates/bevy_transform", version = "0.5.0" } +bevy_utils = { path = "../../crates/bevy_utils", version = "0.5.0" } + +# other +bytemuck = { version = "1.5", features = ["derive"] } +crevice = { path = "../../crates/crevice", version = "0.8.0", features = ["glam"] } +guillotiere = "0.6.0" +thiserror = "1.0" +rectangle-pack = "0.4" +serde = { version = "1", features = ["derive"] } diff --git a/pipelined/bevy_sprite2/src/bundle.rs b/pipelined/bevy_sprite2/src/bundle.rs new file mode 100644 index 0000000000..77c2a5ac15 --- /dev/null +++ b/pipelined/bevy_sprite2/src/bundle.rs @@ -0,0 +1,66 @@ +use crate::{ + texture_atlas::{TextureAtlas, TextureAtlasSprite}, + Sprite, +}; +use bevy_asset::Handle; +use bevy_ecs::bundle::Bundle; +use bevy_render2::{ + texture::Image, + view::{ComputedVisibility, Visibility}, +}; +use bevy_transform::components::{GlobalTransform, Transform}; + +#[derive(Bundle, Clone)] +pub struct PipelinedSpriteBundle { + pub sprite: Sprite, + pub transform: Transform, + pub global_transform: GlobalTransform, + pub texture: Handle, + /// User indication of whether an entity is visible + pub visibility: Visibility, + /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering + pub computed_visibility: ComputedVisibility, +} + +impl Default for PipelinedSpriteBundle { + fn default() -> Self { + Self { + sprite: Default::default(), + transform: Default::default(), + global_transform: Default::default(), + texture: Default::default(), + visibility: Default::default(), + computed_visibility: Default::default(), + } + } +} + +/// A Bundle of components for drawing a single sprite from a sprite sheet (also referred +/// to as a `TextureAtlas`) +#[derive(Bundle, Clone)] +pub struct PipelinedSpriteSheetBundle { + /// The specific sprite from the texture atlas to be drawn + pub sprite: TextureAtlasSprite, + /// A handle to the texture atlas that holds the sprite images + pub texture_atlas: Handle, + /// Data pertaining to how the sprite is drawn on the screen + pub transform: Transform, + pub global_transform: GlobalTransform, + /// User indication of whether an entity is visible + pub visibility: Visibility, + /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering + pub computed_visibility: ComputedVisibility, +} + +impl Default for PipelinedSpriteSheetBundle { + fn default() -> Self { + Self { + sprite: Default::default(), + texture_atlas: Default::default(), + transform: Default::default(), + global_transform: Default::default(), + visibility: Default::default(), + computed_visibility: Default::default(), + } + } +} diff --git a/pipelined/bevy_sprite2/src/dynamic_texture_atlas_builder.rs b/pipelined/bevy_sprite2/src/dynamic_texture_atlas_builder.rs new file mode 100644 index 0000000000..21928ed297 --- /dev/null +++ b/pipelined/bevy_sprite2/src/dynamic_texture_atlas_builder.rs @@ -0,0 +1,100 @@ +use crate::{Rect, TextureAtlas}; +use bevy_asset::Assets; +use bevy_math::Vec2; +use bevy_render2::texture::{Image, TextureFormatPixelInfo}; +use guillotiere::{size2, Allocation, AtlasAllocator}; + +pub struct DynamicTextureAtlasBuilder { + pub atlas_allocator: AtlasAllocator, + pub padding: i32, +} + +impl DynamicTextureAtlasBuilder { + pub fn new(size: Vec2, padding: i32) -> Self { + Self { + atlas_allocator: AtlasAllocator::new(to_size2(size)), + padding, + } + } + + pub fn add_texture( + &mut self, + texture_atlas: &mut TextureAtlas, + textures: &mut Assets, + texture: &Image, + ) -> Option { + let allocation = self.atlas_allocator.allocate(size2( + texture.texture_descriptor.size.width as i32 + self.padding, + texture.texture_descriptor.size.height as i32 + self.padding, + )); + if let Some(allocation) = allocation { + let atlas_texture = textures.get_mut(&texture_atlas.texture).unwrap(); + self.place_texture(atlas_texture, allocation, texture); + let mut rect: Rect = allocation.rectangle.into(); + rect.max.x -= self.padding as f32; + rect.max.y -= self.padding as f32; + Some(texture_atlas.add_texture(rect)) + } else { + None + } + } + + // fn resize( + // &mut self, + // texture_atlas: &mut TextureAtlas, + // textures: &mut Assets, + // size: Vec2, + // ) { + // let new_size2 = to_size2(new_size); + // self.atlas_texture = Texture::new_fill(new_size, &[0,0,0,0]); + // let change_list = self.atlas_allocator.resize_and_rearrange(new_size2); + + // for change in change_list.changes { + // if let Some(changed_texture_handle) = self.allocation_textures.remove(&change.old.id) + // { let changed_texture = textures.get(&changed_texture_handle).unwrap(); + // self.place_texture(change.new, changed_texture_handle, changed_texture); + // } + // } + + // for failure in change_list.failures { + // let failed_texture = self.allocation_textures.remove(&failure.id).unwrap(); + // queued_textures.push(failed_texture); + // } + // } + + fn place_texture( + &mut self, + atlas_texture: &mut Image, + allocation: Allocation, + texture: &Image, + ) { + let mut rect = allocation.rectangle; + rect.max.x -= self.padding; + rect.max.y -= self.padding; + let atlas_width = atlas_texture.texture_descriptor.size.width as usize; + let rect_width = rect.width() as usize; + let format_size = atlas_texture.texture_descriptor.format.pixel_size(); + + for (texture_y, bound_y) in (rect.min.y..rect.max.y).map(|i| i as usize).enumerate() { + let begin = (bound_y * atlas_width + rect.min.x as usize) * format_size; + let end = begin + rect_width * format_size; + let texture_begin = texture_y * rect_width * format_size; + let texture_end = texture_begin + rect_width * format_size; + atlas_texture.data[begin..end] + .copy_from_slice(&texture.data[texture_begin..texture_end]); + } + } +} + +impl From for Rect { + fn from(rectangle: guillotiere::Rectangle) -> Self { + Rect { + min: Vec2::new(rectangle.min.x as f32, rectangle.min.y as f32), + max: Vec2::new(rectangle.max.x as f32, rectangle.max.y as f32), + } + } +} + +fn to_size2(vec2: Vec2) -> guillotiere::Size { + guillotiere::Size::new(vec2.x as i32, vec2.y as i32) +} diff --git a/pipelined/bevy_sprite2/src/lib.rs b/pipelined/bevy_sprite2/src/lib.rs new file mode 100644 index 0000000000..27d36548c8 --- /dev/null +++ b/pipelined/bevy_sprite2/src/lib.rs @@ -0,0 +1,58 @@ +mod bundle; +mod dynamic_texture_atlas_builder; +mod rect; +mod render; +mod sprite; +mod texture_atlas; +mod texture_atlas_builder; + +pub use bundle::*; +pub use dynamic_texture_atlas_builder::*; +pub use rect::*; +pub use render::*; +pub use sprite::*; +pub use texture_atlas::*; +pub use texture_atlas_builder::*; + +use bevy_app::prelude::*; +use bevy_asset::{AddAsset, Assets, HandleUntyped}; +use bevy_core_pipeline::Transparent2d; +use bevy_reflect::TypeUuid; +use bevy_render2::{ + render_phase::DrawFunctions, + render_resource::{Shader, SpecializedPipelines}, + RenderApp, RenderStage, +}; + +#[derive(Default)] +pub struct SpritePlugin; + +pub const SPRITE_SHADER_HANDLE: HandleUntyped = + HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 2763343953151597127); + +impl Plugin for SpritePlugin { + fn build(&self, app: &mut App) { + let mut shaders = app.world.get_resource_mut::>().unwrap(); + let sprite_shader = Shader::from_wgsl(include_str!("render/sprite.wgsl")); + shaders.set_untracked(SPRITE_SHADER_HANDLE, sprite_shader); + app.add_asset::().register_type::(); + let render_app = app.sub_app(RenderApp); + render_app + .init_resource::() + .init_resource::() + .init_resource::>() + .init_resource::() + .init_resource::() + .add_system_to_stage(RenderStage::Extract, render::extract_sprites) + .add_system_to_stage(RenderStage::Prepare, render::prepare_sprites) + .add_system_to_stage(RenderStage::Queue, queue_sprites); + + let draw_sprite = DrawSprite::new(&mut render_app.world); + render_app + .world + .get_resource::>() + .unwrap() + .write() + .add(draw_sprite); + } +} diff --git a/pipelined/bevy_sprite2/src/rect.rs b/pipelined/bevy_sprite2/src/rect.rs new file mode 100644 index 0000000000..a90a59d71d --- /dev/null +++ b/pipelined/bevy_sprite2/src/rect.rs @@ -0,0 +1,26 @@ +use bevy_math::Vec2; + +/// A rectangle defined by two points. There is no defined origin, so 0,0 could be anywhere +/// (top-left, bottom-left, etc) +#[repr(C)] +#[derive(Default, Clone, Copy, Debug)] +pub struct Rect { + /// The beginning point of the rect + pub min: Vec2, + /// The ending point of the rect + pub max: Vec2, +} + +impl Rect { + pub fn width(&self) -> f32 { + self.max.x - self.min.x + } + + pub fn height(&self) -> f32 { + self.max.y - self.min.y + } + + pub fn size(&self) -> Vec2 { + Vec2::new(self.width(), self.height()) + } +} diff --git a/pipelined/bevy_sprite2/src/render/mod.rs b/pipelined/bevy_sprite2/src/render/mod.rs new file mode 100644 index 0000000000..eb33090003 --- /dev/null +++ b/pipelined/bevy_sprite2/src/render/mod.rs @@ -0,0 +1,566 @@ +use std::{cmp::Ordering, ops::Range}; + +use crate::{ + texture_atlas::{TextureAtlas, TextureAtlasSprite}, + Rect, Sprite, SPRITE_SHADER_HANDLE, +}; +use bevy_asset::{Assets, Handle}; +use bevy_core::FloatOrd; +use bevy_core_pipeline::Transparent2d; +use bevy_ecs::{ + prelude::*, + system::{lifetimeless::*, SystemState}, +}; +use bevy_math::{const_vec3, Mat4, Vec2, Vec3, Vec4Swizzles}; +use bevy_render2::{ + color::Color, + render_asset::RenderAssets, + render_phase::{Draw, DrawFunctions, RenderPhase, TrackedRenderPass}, + render_resource::*, + renderer::{RenderDevice, RenderQueue}, + texture::{BevyDefault, Image}, + view::{ComputedVisibility, ViewUniform, ViewUniformOffset, ViewUniforms}, + RenderWorld, +}; +use bevy_transform::components::GlobalTransform; +use bevy_utils::HashMap; +use bytemuck::{Pod, Zeroable}; +use crevice::std140::AsStd140; + +pub struct SpritePipeline { + view_layout: BindGroupLayout, + material_layout: BindGroupLayout, +} + +impl FromWorld for SpritePipeline { + fn from_world(world: &mut World) -> Self { + let world = world.cell(); + let render_device = world.get_resource::().unwrap(); + + let view_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { + entries: &[BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: true, + min_binding_size: BufferSize::new(ViewUniform::std140_size_static() as u64), + }, + count: None, + }], + label: Some("sprite_view_layout"), + }); + + let material_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { + entries: &[ + BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Texture { + multisampled: false, + sample_type: TextureSampleType::Float { filterable: false }, + view_dimension: TextureViewDimension::D2, + }, + count: None, + }, + BindGroupLayoutEntry { + binding: 1, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Sampler { + comparison: false, + filtering: true, + }, + count: None, + }, + ], + label: Some("sprite_material_layout"), + }); + + SpritePipeline { + view_layout, + material_layout, + } + } +} + +#[derive(Clone, Copy, Hash, PartialEq, Eq)] +pub struct SpritePipelineKey { + colored: bool, +} + +impl SpecializedPipeline for SpritePipeline { + type Key = SpritePipelineKey; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + let mut vertex_buffer_layout = VertexBufferLayout { + array_stride: 20, + step_mode: VertexStepMode::Vertex, + attributes: vec![ + VertexAttribute { + format: VertexFormat::Float32x3, + offset: 0, + shader_location: 0, + }, + VertexAttribute { + format: VertexFormat::Float32x2, + offset: 12, + shader_location: 1, + }, + ], + }; + let mut shader_defs = Vec::new(); + if key.colored { + shader_defs.push("COLORED".to_string()); + vertex_buffer_layout.attributes.push(VertexAttribute { + format: VertexFormat::Uint32, + offset: 20, + shader_location: 2, + }); + vertex_buffer_layout.array_stride += 4; + } + + RenderPipelineDescriptor { + vertex: VertexState { + shader: SPRITE_SHADER_HANDLE.typed::(), + entry_point: "vertex".into(), + shader_defs: shader_defs.clone(), + buffers: vec![vertex_buffer_layout], + }, + fragment: Some(FragmentState { + shader: SPRITE_SHADER_HANDLE.typed::(), + shader_defs, + entry_point: "fragment".into(), + targets: vec![ColorTargetState { + format: TextureFormat::bevy_default(), + blend: Some(BlendState::ALPHA_BLENDING), + write_mask: ColorWrites::ALL, + }], + }), + layout: Some(vec![self.view_layout.clone(), self.material_layout.clone()]), + primitive: PrimitiveState { + front_face: FrontFace::Ccw, + cull_mode: None, + polygon_mode: PolygonMode::Fill, + clamp_depth: false, + conservative: false, + topology: PrimitiveTopology::TriangleList, + strip_index_format: None, + }, + depth_stencil: None, + multisample: MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + label: Some("sprite_pipeline".into()), + } + } +} + +pub struct ExtractedSprite { + transform: Mat4, + color: Color, + rect: Rect, + handle: Handle, + atlas_size: Option, + flip_x: bool, + flip_y: bool, +} + +#[derive(Default)] +pub struct ExtractedSprites { + sprites: Vec, +} + +pub fn extract_sprites( + mut render_world: ResMut, + images: Res>, + texture_atlases: Res>, + sprite_query: Query<( + &ComputedVisibility, + &Sprite, + &GlobalTransform, + &Handle, + )>, + atlas_query: Query<( + &ComputedVisibility, + &TextureAtlasSprite, + &GlobalTransform, + &Handle, + )>, +) { + let mut extracted_sprites = render_world.get_resource_mut::().unwrap(); + extracted_sprites.sprites.clear(); + for (computed_visibility, sprite, transform, handle) in sprite_query.iter() { + if !computed_visibility.is_visible { + continue; + } + if let Some(image) = images.get(handle) { + let size = image.texture_descriptor.size; + + extracted_sprites.sprites.push(ExtractedSprite { + atlas_size: None, + color: sprite.color, + transform: transform.compute_matrix(), + rect: Rect { + min: Vec2::ZERO, + max: sprite + .custom_size + .unwrap_or_else(|| Vec2::new(size.width as f32, size.height as f32)), + }, + flip_x: sprite.flip_x, + flip_y: sprite.flip_y, + handle: handle.clone_weak(), + }); + }; + } + for (computed_visibility, atlas_sprite, transform, texture_atlas_handle) in atlas_query.iter() { + if !computed_visibility.is_visible { + continue; + } + if let Some(texture_atlas) = texture_atlases.get(texture_atlas_handle) { + let rect = texture_atlas.textures[atlas_sprite.index as usize]; + extracted_sprites.sprites.push(ExtractedSprite { + atlas_size: Some(texture_atlas.size), + color: atlas_sprite.color, + transform: transform.compute_matrix(), + rect, + flip_x: atlas_sprite.flip_x, + flip_y: atlas_sprite.flip_y, + handle: texture_atlas.texture.clone_weak(), + }); + } + } +} + +#[repr(C)] +#[derive(Copy, Clone, Pod, Zeroable)] +struct SpriteVertex { + pub position: [f32; 3], + pub uv: [f32; 2], +} + +#[repr(C)] +#[derive(Copy, Clone, Pod, Zeroable)] +struct ColoredSpriteVertex { + pub position: [f32; 3], + pub uv: [f32; 2], + pub color: u32, +} + +pub struct SpriteMeta { + vertices: BufferVec, + colored_vertices: BufferVec, + view_bind_group: Option, +} + +impl Default for SpriteMeta { + fn default() -> Self { + Self { + vertices: BufferVec::new(BufferUsages::VERTEX), + colored_vertices: BufferVec::new(BufferUsages::VERTEX), + view_bind_group: None, + } + } +} + +const QUAD_VERTEX_POSITIONS: &[Vec3] = &[ + const_vec3!([-0.5, -0.5, 0.0]), + const_vec3!([0.5, 0.5, 0.0]), + const_vec3!([-0.5, 0.5, 0.0]), + const_vec3!([-0.5, -0.5, 0.0]), + const_vec3!([0.5, -0.5, 0.0]), + const_vec3!([0.5, 0.5, 0.0]), +]; + +#[derive(Component)] +pub struct SpriteBatch { + range: Range, + handle: Handle, + z: f32, + colored: bool, +} + +pub fn prepare_sprites( + mut commands: Commands, + render_device: Res, + render_queue: Res, + mut sprite_meta: ResMut, + mut extracted_sprites: ResMut, +) { + sprite_meta.vertices.clear(); + sprite_meta.colored_vertices.clear(); + + // sort first by z and then by handle. this ensures that, when possible, batches span multiple z layers + // batches won't span z-layers if there is another batch between them + extracted_sprites.sprites.sort_by(|a, b| { + match FloatOrd(a.transform.w_axis[2]).cmp(&FloatOrd(b.transform.w_axis[2])) { + Ordering::Equal => a.handle.cmp(&b.handle), + other => other, + } + }); + + let mut start = 0; + let mut end = 0; + let mut colored_start = 0; + let mut colored_end = 0; + let mut current_batch_handle: Option> = None; + let mut current_batch_colored = false; + let mut last_z = 0.0; + for extracted_sprite in extracted_sprites.sprites.iter() { + let colored = extracted_sprite.color != Color::WHITE; + if let Some(current_batch_handle) = ¤t_batch_handle { + if *current_batch_handle != extracted_sprite.handle || current_batch_colored != colored + { + if current_batch_colored { + commands.spawn_bundle((SpriteBatch { + range: colored_start..colored_end, + handle: current_batch_handle.clone_weak(), + z: last_z, + colored: true, + },)); + colored_start = colored_end; + } else { + commands.spawn_bundle((SpriteBatch { + range: start..end, + handle: current_batch_handle.clone_weak(), + z: last_z, + colored: false, + },)); + start = end; + } + } + } + current_batch_handle = Some(extracted_sprite.handle.clone_weak()); + current_batch_colored = colored; + let sprite_rect = extracted_sprite.rect; + + // Specify the corners of the sprite + let mut bottom_left = Vec2::new(sprite_rect.min.x, sprite_rect.max.y); + let mut top_left = sprite_rect.min; + let mut top_right = Vec2::new(sprite_rect.max.x, sprite_rect.min.y); + let mut bottom_right = sprite_rect.max; + + if extracted_sprite.flip_x { + bottom_left.x = sprite_rect.max.x; + top_left.x = sprite_rect.max.x; + bottom_right.x = sprite_rect.min.x; + top_right.x = sprite_rect.min.x; + } + + if extracted_sprite.flip_y { + bottom_left.y = sprite_rect.min.y; + bottom_right.y = sprite_rect.min.y; + top_left.y = sprite_rect.max.y; + top_right.y = sprite_rect.max.y; + } + + let atlas_extent = extracted_sprite.atlas_size.unwrap_or(sprite_rect.max); + bottom_left /= atlas_extent; + bottom_right /= atlas_extent; + top_left /= atlas_extent; + top_right /= atlas_extent; + + let uvs: [[f32; 2]; 6] = [ + bottom_left.into(), + top_right.into(), + top_left.into(), + bottom_left.into(), + bottom_right.into(), + top_right.into(), + ]; + + let rect_size = extracted_sprite.rect.size().extend(1.0); + if current_batch_colored { + let color = extracted_sprite.color.as_linear_rgba_f32(); + // encode color as a single u32 to save space + let color = (color[0] * 255.0) as u32 + | ((color[1] * 255.0) as u32) << 8 + | ((color[2] * 255.0) as u32) << 16 + | ((color[3] * 255.0) as u32) << 24; + for (index, vertex_position) in QUAD_VERTEX_POSITIONS.iter().enumerate() { + let mut final_position = *vertex_position * rect_size; + final_position = (extracted_sprite.transform * final_position.extend(1.0)).xyz(); + sprite_meta.colored_vertices.push(ColoredSpriteVertex { + position: final_position.into(), + uv: uvs[index], + color, + }); + } + } else { + for (index, vertex_position) in QUAD_VERTEX_POSITIONS.iter().enumerate() { + let mut final_position = *vertex_position * rect_size; + final_position = (extracted_sprite.transform * final_position.extend(1.0)).xyz(); + sprite_meta.vertices.push(SpriteVertex { + position: final_position.into(), + uv: uvs[index], + }); + } + } + + last_z = extracted_sprite.transform.w_axis[2]; + if current_batch_colored { + colored_end += QUAD_VERTEX_POSITIONS.len() as u32; + } else { + end += QUAD_VERTEX_POSITIONS.len() as u32; + } + } + + // if start != end, there is one last batch to process + if start != end { + if let Some(current_batch_handle) = current_batch_handle { + commands.spawn_bundle((SpriteBatch { + range: start..end, + handle: current_batch_handle, + colored: false, + z: last_z, + },)); + } + } else if colored_start != colored_end { + if let Some(current_batch_handle) = current_batch_handle { + commands.spawn_bundle((SpriteBatch { + range: colored_start..colored_end, + handle: current_batch_handle, + colored: true, + z: last_z, + },)); + } + } + + sprite_meta + .vertices + .write_buffer(&render_device, &render_queue); + sprite_meta + .colored_vertices + .write_buffer(&render_device, &render_queue); +} + +#[derive(Default)] +pub struct ImageBindGroups { + values: HashMap, BindGroup>, +} + +#[allow(clippy::too_many_arguments)] +pub fn queue_sprites( + draw_functions: Res>, + render_device: Res, + mut sprite_meta: ResMut, + view_uniforms: Res, + sprite_pipeline: Res, + mut pipelines: ResMut>, + mut pipeline_cache: ResMut, + mut image_bind_groups: ResMut, + gpu_images: Res>, + mut sprite_batches: Query<(Entity, &SpriteBatch)>, + mut views: Query<&mut RenderPhase>, +) { + if let Some(view_binding) = view_uniforms.uniforms.binding() { + sprite_meta.view_bind_group = Some(render_device.create_bind_group(&BindGroupDescriptor { + entries: &[BindGroupEntry { + binding: 0, + resource: view_binding, + }], + label: Some("sprite_view_bind_group"), + layout: &sprite_pipeline.view_layout, + })); + let draw_sprite_function = draw_functions.read().get_id::().unwrap(); + let pipeline = pipelines.specialize( + &mut pipeline_cache, + &sprite_pipeline, + SpritePipelineKey { colored: false }, + ); + let colored_pipeline = pipelines.specialize( + &mut pipeline_cache, + &sprite_pipeline, + SpritePipelineKey { colored: true }, + ); + for mut transparent_phase in views.iter_mut() { + for (entity, batch) in sprite_batches.iter_mut() { + image_bind_groups + .values + .entry(batch.handle.clone_weak()) + .or_insert_with(|| { + let gpu_image = gpu_images.get(&batch.handle).unwrap(); + render_device.create_bind_group(&BindGroupDescriptor { + entries: &[ + BindGroupEntry { + binding: 0, + resource: BindingResource::TextureView(&gpu_image.texture_view), + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::Sampler(&gpu_image.sampler), + }, + ], + label: Some("sprite_material_bind_group"), + layout: &sprite_pipeline.material_layout, + }) + }); + transparent_phase.add(Transparent2d { + draw_function: draw_sprite_function, + pipeline: if batch.colored { + colored_pipeline + } else { + pipeline + }, + entity, + sort_key: FloatOrd(batch.z), + }); + } + } + } +} + +pub struct DrawSprite { + params: SystemState<( + SRes, + SRes, + SRes, + SQuery>, + SQuery>, + )>, +} + +impl DrawSprite { + pub fn new(world: &mut World) -> Self { + Self { + params: SystemState::new(world), + } + } +} + +impl Draw for DrawSprite { + fn draw<'w>( + &mut self, + world: &'w World, + pass: &mut TrackedRenderPass<'w>, + view: Entity, + item: &Transparent2d, + ) { + let (sprite_meta, image_bind_groups, pipelines, views, sprites) = self.params.get(world); + let view_uniform = views.get(view).unwrap(); + let sprite_meta = sprite_meta.into_inner(); + let image_bind_groups = image_bind_groups.into_inner(); + let sprite_batch = sprites.get(item.entity).unwrap(); + if let Some(pipeline) = pipelines.into_inner().get(item.pipeline) { + pass.set_render_pipeline(pipeline); + if sprite_batch.colored { + pass.set_vertex_buffer(0, sprite_meta.colored_vertices.buffer().unwrap().slice(..)); + } else { + pass.set_vertex_buffer(0, sprite_meta.vertices.buffer().unwrap().slice(..)); + } + pass.set_bind_group( + 0, + sprite_meta.view_bind_group.as_ref().unwrap(), + &[view_uniform.offset], + ); + pass.set_bind_group( + 1, + image_bind_groups.values.get(&sprite_batch.handle).unwrap(), + &[], + ); + + pass.draw(sprite_batch.range.clone(), 0..1); + } + } +} diff --git a/pipelined/bevy_sprite2/src/render/sprite.wgsl b/pipelined/bevy_sprite2/src/render/sprite.wgsl new file mode 100644 index 0000000000..e331817950 --- /dev/null +++ b/pipelined/bevy_sprite2/src/render/sprite.wgsl @@ -0,0 +1,46 @@ +[[block]] +struct View { + view_proj: mat4x4; + world_position: vec3; +}; +[[group(0), binding(0)]] +var view: View; + +struct VertexOutput { + [[location(0)]] uv: vec2; +#ifdef COLORED + [[location(1)]] color: vec4; +#endif + [[builtin(position)]] position: vec4; +}; + +[[stage(vertex)]] +fn vertex( + [[location(0)]] vertex_position: vec3, + [[location(1)]] vertex_uv: vec2, +#ifdef COLORED + [[location(2)]] vertex_color: u32, +#endif +) -> VertexOutput { + var out: VertexOutput; + out.uv = vertex_uv; + out.position = view.view_proj * vec4(vertex_position, 1.0); +#ifdef COLORED + out.color = vec4((vec4(vertex_color) >> vec4(0u, 8u, 16u, 24u)) & vec4(255u)) / 255.0; +#endif + return out; +} + +[[group(1), binding(0)]] +var sprite_texture: texture_2d; +[[group(1), binding(1)]] +var sprite_sampler: sampler; + +[[stage(fragment)]] +fn fragment(in: VertexOutput) -> [[location(0)]] vec4 { + var color = textureSample(sprite_texture, sprite_sampler, in.uv); +#ifdef COLORED + color = in.color * color; +#endif + return color; +} \ No newline at end of file diff --git a/pipelined/bevy_sprite2/src/sprite.rs b/pipelined/bevy_sprite2/src/sprite.rs new file mode 100644 index 0000000000..24b6e58d29 --- /dev/null +++ b/pipelined/bevy_sprite2/src/sprite.rs @@ -0,0 +1,19 @@ +use bevy_ecs::component::Component; +use bevy_math::Vec2; +use bevy_reflect::{Reflect, TypeUuid}; +use bevy_render2::color::Color; + +#[derive(Component, Debug, Default, Clone, TypeUuid, Reflect)] +#[uuid = "7233c597-ccfa-411f-bd59-9af349432ada"] +#[repr(C)] +pub struct Sprite { + /// The sprite's color tint + pub color: Color, + /// Flip the sprite along the X axis + pub flip_x: bool, + /// Flip the sprite along the Y axis + pub flip_y: bool, + /// An optional custom size for the sprite that will be used when rendering, instead of the size + /// of the sprite's image + pub custom_size: Option, +} diff --git a/pipelined/bevy_sprite2/src/texture_atlas.rs b/pipelined/bevy_sprite2/src/texture_atlas.rs new file mode 100644 index 0000000000..78886a279c --- /dev/null +++ b/pipelined/bevy_sprite2/src/texture_atlas.rs @@ -0,0 +1,148 @@ +use crate::Rect; +use bevy_asset::Handle; +use bevy_ecs::component::Component; +use bevy_math::Vec2; +use bevy_reflect::{Reflect, TypeUuid}; +use bevy_render2::{color::Color, texture::Image}; +use bevy_utils::HashMap; + +/// An atlas containing multiple textures (like a spritesheet or a tilemap). +/// [Example usage animating sprite.](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_sheet.rs) +/// [Example usage loading sprite sheet.](https://github.com/bevyengine/bevy/blob/latest/examples/2d/texture_atlas.rs) +#[derive(Debug, Clone, TypeUuid)] +#[uuid = "7233c597-ccfa-411f-bd59-9af349432ada"] +pub struct TextureAtlas { + /// The handle to the texture in which the sprites are stored + pub texture: Handle, + // TODO: add support to Uniforms derive to write dimensions and sprites to the same buffer + pub size: Vec2, + /// The specific areas of the atlas where each texture can be found + pub textures: Vec, + pub texture_handles: Option, usize>>, +} + +#[derive(Component, Debug, Clone, TypeUuid, Reflect)] +#[uuid = "7233c597-ccfa-411f-bd59-9af349432ada"] +pub struct TextureAtlasSprite { + pub color: Color, + pub index: usize, + pub flip_x: bool, + pub flip_y: bool, +} + +impl Default for TextureAtlasSprite { + fn default() -> Self { + Self { + index: 0, + color: Color::WHITE, + flip_x: false, + flip_y: false, + } + } +} + +impl TextureAtlasSprite { + pub fn new(index: usize) -> TextureAtlasSprite { + Self { + index, + ..Default::default() + } + } +} + +impl TextureAtlas { + /// Create a new `TextureAtlas` that has a texture, but does not have + /// any individual sprites specified + pub fn new_empty(texture: Handle, dimensions: Vec2) -> Self { + Self { + texture, + size: dimensions, + texture_handles: None, + textures: Vec::new(), + } + } + + /// Generate a `TextureAtlas` by splitting a texture into a grid where each + /// cell of the grid of `tile_size` is one of the textures in the atlas + pub fn from_grid( + texture: Handle, + tile_size: Vec2, + columns: usize, + rows: usize, + ) -> TextureAtlas { + Self::from_grid_with_padding(texture, tile_size, columns, rows, Vec2::new(0f32, 0f32)) + } + + /// Generate a `TextureAtlas` by splitting a texture into a grid where each + /// cell of the grid of `tile_size` is one of the textures in the atlas and is separated by + /// some `padding` in the texture + pub fn from_grid_with_padding( + texture: Handle, + tile_size: Vec2, + columns: usize, + rows: usize, + padding: Vec2, + ) -> TextureAtlas { + let mut sprites = Vec::new(); + let mut x_padding = 0.0; + let mut y_padding = 0.0; + + for y in 0..rows { + if y > 0 { + y_padding = padding.y; + } + for x in 0..columns { + if x > 0 { + x_padding = padding.x; + } + + let rect_min = Vec2::new( + (tile_size.x + x_padding) * x as f32, + (tile_size.y + y_padding) * y as f32, + ); + + sprites.push(Rect { + min: rect_min, + max: Vec2::new(rect_min.x + tile_size.x, rect_min.y + tile_size.y), + }) + } + } + + TextureAtlas { + size: Vec2::new( + ((tile_size.x + x_padding) * columns as f32) - x_padding, + ((tile_size.y + y_padding) * rows as f32) - y_padding, + ), + textures: sprites, + texture, + texture_handles: None, + } + } + + /// Add a sprite to the list of textures in the `TextureAtlas` + /// returns an index to the texture which can be used with `TextureAtlasSprite` + /// + /// # Arguments + /// + /// * `rect` - The section of the atlas that contains the texture to be added, + /// from the top-left corner of the texture to the bottom-right corner + pub fn add_texture(&mut self, rect: Rect) -> usize { + self.textures.push(rect); + self.textures.len() - 1 + } + + /// How many textures are in the `TextureAtlas` + pub fn len(&self) -> usize { + self.textures.len() + } + + pub fn is_empty(&self) -> bool { + self.textures.is_empty() + } + + pub fn get_texture_index(&self, texture: &Handle) -> Option { + self.texture_handles + .as_ref() + .and_then(|texture_handles| texture_handles.get(texture).cloned()) + } +} diff --git a/pipelined/bevy_sprite2/src/texture_atlas_builder.rs b/pipelined/bevy_sprite2/src/texture_atlas_builder.rs new file mode 100644 index 0000000000..d89389e910 --- /dev/null +++ b/pipelined/bevy_sprite2/src/texture_atlas_builder.rs @@ -0,0 +1,239 @@ +use bevy_asset::{Assets, Handle}; +use bevy_log::{debug, error, warn}; +use bevy_math::Vec2; +use bevy_render2::{ + render_resource::{Extent3d, TextureDimension, TextureFormat}, + texture::{Image, TextureFormatPixelInfo}, +}; +use bevy_utils::HashMap; +use rectangle_pack::{ + contains_smallest_box, pack_rects, volume_heuristic, GroupedRectsToPlace, PackedLocation, + RectToInsert, TargetBin, +}; +use thiserror::Error; + +use crate::{texture_atlas::TextureAtlas, Rect}; + +#[derive(Debug, Error)] +pub enum TextureAtlasBuilderError { + #[error("could not pack textures into an atlas within the given bounds")] + NotEnoughSpace, + #[error("added a texture with the wrong format in an atlas")] + WrongFormat, +} + +#[derive(Debug)] +/// A builder which is used to create a texture atlas from many individual +/// sprites. +pub struct TextureAtlasBuilder { + /// The grouped rects which must be placed with a key value pair of a + /// texture handle to an index. + rects_to_place: GroupedRectsToPlace>, + /// The initial atlas size in pixels. + initial_size: Vec2, + /// The absolute maximum size of the texture atlas in pixels. + max_size: Vec2, + /// The texture format for the textures that will be loaded in the atlas. + format: TextureFormat, + /// Enable automatic format conversion for textures if they are not in the atlas format. + auto_format_conversion: bool, +} + +impl Default for TextureAtlasBuilder { + fn default() -> Self { + Self { + rects_to_place: GroupedRectsToPlace::new(), + initial_size: Vec2::new(256., 256.), + max_size: Vec2::new(2048., 2048.), + format: TextureFormat::Rgba8UnormSrgb, + auto_format_conversion: true, + } + } +} + +pub type TextureAtlasBuilderResult = Result; + +impl TextureAtlasBuilder { + /// Sets the initial size of the atlas in pixels. + pub fn initial_size(mut self, size: Vec2) -> Self { + self.initial_size = size; + self + } + + /// Sets the max size of the atlas in pixels. + pub fn max_size(mut self, size: Vec2) -> Self { + self.max_size = size; + self + } + + /// Sets the texture format for textures in the atlas. + pub fn format(mut self, format: TextureFormat) -> Self { + self.format = format; + self + } + + /// Control whether the added texture should be converted to the atlas format, if different. + pub fn auto_format_conversion(mut self, auto_format_conversion: bool) -> Self { + self.auto_format_conversion = auto_format_conversion; + self + } + + /// Adds a texture to be copied to the texture atlas. + pub fn add_texture(&mut self, texture_handle: Handle, texture: &Image) { + self.rects_to_place.push_rect( + texture_handle, + None, + RectToInsert::new( + texture.texture_descriptor.size.width, + texture.texture_descriptor.size.height, + 1, + ), + ) + } + + fn copy_texture_to_atlas( + atlas_texture: &mut Image, + texture: &Image, + packed_location: &PackedLocation, + ) { + let rect_width = packed_location.width() as usize; + let rect_height = packed_location.height() as usize; + let rect_x = packed_location.x() as usize; + let rect_y = packed_location.y() as usize; + let atlas_width = atlas_texture.texture_descriptor.size.width as usize; + let format_size = atlas_texture.texture_descriptor.format.pixel_size(); + + for (texture_y, bound_y) in (rect_y..rect_y + rect_height).enumerate() { + let begin = (bound_y * atlas_width + rect_x) * format_size; + let end = begin + rect_width * format_size; + let texture_begin = texture_y * rect_width * format_size; + let texture_end = texture_begin + rect_width * format_size; + atlas_texture.data[begin..end] + .copy_from_slice(&texture.data[texture_begin..texture_end]); + } + } + + fn copy_converted_texture( + &self, + atlas_texture: &mut Image, + texture: &Image, + packed_location: &PackedLocation, + ) { + if self.format == texture.texture_descriptor.format { + Self::copy_texture_to_atlas(atlas_texture, texture, packed_location); + } else if let Some(converted_texture) = texture.convert(self.format) { + debug!( + "Converting texture from '{:?}' to '{:?}'", + texture.texture_descriptor.format, self.format + ); + Self::copy_texture_to_atlas(atlas_texture, &converted_texture, packed_location); + } else { + error!( + "Error converting texture from '{:?}' to '{:?}', ignoring", + texture.texture_descriptor.format, self.format + ); + } + } + + /// Consumes the builder and returns a result with a new texture atlas. + /// + /// Internally it copies all rectangles from the textures and copies them + /// into a new texture which the texture atlas will use. It is not useful to + /// hold a strong handle to the texture afterwards else it will exist twice + /// in memory. + /// + /// # Errors + /// + /// If there is not enough space in the atlas texture, an error will + /// be returned. It is then recommended to make a larger sprite sheet. + pub fn finish( + self, + textures: &mut Assets, + ) -> Result { + let initial_width = self.initial_size.x as u32; + let initial_height = self.initial_size.y as u32; + let max_width = self.max_size.x as u32; + let max_height = self.max_size.y as u32; + + let mut current_width = initial_width; + let mut current_height = initial_height; + let mut rect_placements = None; + let mut atlas_texture = Image::default(); + + while rect_placements.is_none() { + if current_width > max_width || current_height > max_height { + break; + } + + let last_attempt = current_height == max_height && current_width == max_width; + + let mut target_bins = std::collections::BTreeMap::new(); + target_bins.insert(0, TargetBin::new(current_width, current_height, 1)); + rect_placements = match pack_rects( + &self.rects_to_place, + &mut target_bins, + &volume_heuristic, + &contains_smallest_box, + ) { + Ok(rect_placements) => { + atlas_texture = Image::new( + Extent3d { + width: current_width, + height: current_height, + depth_or_array_layers: 1, + }, + TextureDimension::D2, + vec![ + 0; + self.format.pixel_size() * (current_width * current_height) as usize + ], + self.format, + ); + Some(rect_placements) + } + Err(rectangle_pack::RectanglePackError::NotEnoughBinSpace) => { + current_height = (current_height * 2).clamp(0, max_height); + current_width = (current_width * 2).clamp(0, max_width); + None + } + }; + + if last_attempt { + break; + } + } + + let rect_placements = rect_placements.ok_or(TextureAtlasBuilderError::NotEnoughSpace)?; + + let mut texture_rects = Vec::with_capacity(rect_placements.packed_locations().len()); + let mut texture_handles = HashMap::default(); + for (texture_handle, (_, packed_location)) in rect_placements.packed_locations().iter() { + let texture = textures.get(texture_handle).unwrap(); + let min = Vec2::new(packed_location.x() as f32, packed_location.y() as f32); + let max = min + + Vec2::new( + packed_location.width() as f32, + packed_location.height() as f32, + ); + texture_handles.insert(texture_handle.clone_weak(), texture_rects.len()); + texture_rects.push(Rect { min, max }); + if texture.texture_descriptor.format != self.format && !self.auto_format_conversion { + warn!( + "Loading a texture of format '{:?}' in an atlas with format '{:?}'", + texture.texture_descriptor.format, self.format + ); + return Err(TextureAtlasBuilderError::WrongFormat); + } + self.copy_converted_texture(&mut atlas_texture, texture, packed_location); + } + Ok(TextureAtlas { + size: Vec2::new( + atlas_texture.texture_descriptor.size.width as f32, + atlas_texture.texture_descriptor.size.height as f32, + ), + texture: textures.add(atlas_texture), + textures: texture_rects, + texture_handles: Some(texture_handles), + }) + } +}