diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 28a3c16a5b..08d34fec7a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ env: # If nightly is breaking CI, modify this variable to target a specific nightly version. NIGHTLY_TOOLCHAIN: nightly RUSTFLAGS: "-D warnings" - BINSTALL_VERSION: "v1.12.5" + BINSTALL_VERSION: "v1.14.1" concurrency: group: ${{github.workflow}}-${{github.ref}} @@ -260,7 +260,7 @@ jobs: # Full git history is needed to get a proper list of changed files within `super-linter` fetch-depth: 0 - name: Run Markdown Lint - uses: super-linter/super-linter/slim@v7.3.0 + uses: super-linter/super-linter/slim@v7.4.0 env: MULTI_STATUS: false VALIDATE_ALL_CODEBASE: false @@ -272,7 +272,8 @@ jobs: timeout-minutes: 30 steps: - uses: actions/checkout@v4 - - uses: cargo-bins/cargo-binstall@v1.12.5 + # Update in sync with BINSTALL_VERSION + - uses: cargo-bins/cargo-binstall@v1.14.1 - name: Install taplo run: cargo binstall taplo-cli@0.9.3 --locked - name: Run Taplo diff --git a/Cargo.toml b/Cargo.toml index 688147ff9b..46b8238596 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -306,6 +306,9 @@ bevy_input_focus = ["bevy_internal/bevy_input_focus"] # Headless widget collection for Bevy UI. bevy_core_widgets = ["bevy_internal/bevy_core_widgets"] +# Feathers widget collection. +experimental_bevy_feathers = ["bevy_internal/bevy_feathers"] + # Enable passthrough loading for SPIR-V shaders (Only supported on Vulkan, shader capabilities and extensions must agree with the platform implementation) spirv_shader_passthrough = ["bevy_internal/spirv_shader_passthrough"] @@ -484,6 +487,12 @@ shader_format_wesl = ["bevy_internal/shader_format_wesl"] # Enable support for transmission-related textures in the `StandardMaterial`, at the risk of blowing past the global, per-shader texture limit on older/lower-end GPUs pbr_transmission_textures = ["bevy_internal/pbr_transmission_textures"] +# Enable support for Clustered Decals +pbr_clustered_decals = ["bevy_internal/pbr_clustered_decals"] + +# Enable support for Light Textures +pbr_light_textures = ["bevy_internal/pbr_light_textures"] + # Enable support for multi-layer material textures in the `StandardMaterial`, at the risk of blowing past the global, per-shader texture limit on older/lower-end GPUs pbr_multi_layer_material_textures = [ "bevy_internal/pbr_multi_layer_material_textures", @@ -561,6 +570,11 @@ web = ["bevy_internal/web"] # Enable hotpatching of Bevy systems hotpatching = ["bevy_internal/hotpatching"] +# Enable converting glTF coordinates to Bevy's coordinate system by default. This will be Bevy's default behavior starting in 0.18. +gltf_convert_coordinates_default = [ + "bevy_internal/gltf_convert_coordinates_default", +] + # Enable collecting debug information about systems and components to help with diagnostics debug = ["bevy_internal/debug"] @@ -579,7 +593,7 @@ ron = "0.10" flate2 = "1.0" serde = { version = "1", features = ["derive"] } serde_json = "1.0.140" -bytemuck = "1.7" +bytemuck = "1" bevy_render = { path = "crates/bevy_render", version = "0.17.0-dev", default-features = false } # The following explicit dependencies are needed for proc macros to work inside of examples as they are part of the bevy crate itself. bevy_ecs = { path = "crates/bevy_ecs", version = "0.17.0-dev", default-features = false } @@ -1051,6 +1065,17 @@ description = "Showcases different blend modes" category = "3D Rendering" wasm = true +[[example]] +name = "manual_material" +path = "examples/3d/manual_material.rs" +doc-scrape-examples = true + +[package.metadata.example.manual_material] +name = "Manual Material Implementation" +description = "Demonstrates how to implement a material manually using the mid-level render APIs" +category = "3D Rendering" +wasm = true + [[example]] name = "edit_material_on_gltf" path = "examples/3d/edit_material_on_gltf.rs" @@ -4401,6 +4426,7 @@ wasm = true name = "clustered_decals" path = "examples/3d/clustered_decals.rs" doc-scrape-examples = true +required-features = ["pbr_clustered_decals"] [package.metadata.example.clustered_decals] name = "Clustered Decals" @@ -4408,6 +4434,18 @@ description = "Demonstrates clustered decals" category = "3D Rendering" wasm = false +[[example]] +name = "light_textures" +path = "examples/3d/light_textures.rs" +doc-scrape-examples = true +required-features = ["pbr_light_textures"] + +[package.metadata.example.light_textures] +name = "Light Textures" +description = "Demonstrates light textures" +category = "3D Rendering" +wasm = false + [[example]] name = "occlusion_culling" path = "examples/3d/occlusion_culling.rs" @@ -4510,3 +4548,27 @@ name = "Core Widgets (w/Observers)" description = "Demonstrates use of core (headless) widgets in Bevy UI, with Observers" category = "UI (User Interface)" wasm = true + +[[example]] +name = "scrollbars" +path = "examples/ui/scrollbars.rs" +doc-scrape-examples = true + +[package.metadata.example.scrollbars] +name = "Scrollbars" +description = "Demonstrates use of core scrollbar in Bevy UI" +category = "UI (User Interface)" +wasm = true + +[[example]] +name = "feathers" +path = "examples/ui/feathers.rs" +doc-scrape-examples = true +required-features = ["experimental_bevy_feathers"] + +[package.metadata.example.feathers] +name = "Feathers Widgets" +description = "Gallery of Feathers Widgets" +category = "UI (User Interface)" +wasm = true +hidden = true diff --git a/assets/animation_graphs/Fox.animgraph.ron b/assets/animation_graphs/Fox.animgraph.ron index e9d6f4f9cf..b91869b118 100644 --- a/assets/animation_graphs/Fox.animgraph.ron +++ b/assets/animation_graphs/Fox.animgraph.ron @@ -9,20 +9,20 @@ ( node_type: Blend, mask: 0, - weight: 1.0, + weight: 0.5, ), ( - node_type: Clip(AssetPath("models/animated/Fox.glb#Animation0")), + node_type: Clip("models/animated/Fox.glb#Animation0"), mask: 0, weight: 1.0, ), ( - node_type: Clip(AssetPath("models/animated/Fox.glb#Animation1")), + node_type: Clip("models/animated/Fox.glb#Animation1"), mask: 0, weight: 1.0, ), ( - node_type: Clip(AssetPath("models/animated/Fox.glb#Animation2")), + node_type: Clip("models/animated/Fox.glb#Animation2"), mask: 0, weight: 1.0, ), diff --git a/assets/lightmaps/caustic_directional_texture.png b/assets/lightmaps/caustic_directional_texture.png new file mode 100644 index 0000000000..0c082a46cf Binary files /dev/null and b/assets/lightmaps/caustic_directional_texture.png differ diff --git a/assets/lightmaps/faces_pointlight_texture_blurred.png b/assets/lightmaps/faces_pointlight_texture_blurred.png new file mode 100644 index 0000000000..1adf2e954d Binary files /dev/null and b/assets/lightmaps/faces_pointlight_texture_blurred.png differ diff --git a/assets/lightmaps/torch_spotlight_texture.png b/assets/lightmaps/torch_spotlight_texture.png new file mode 100644 index 0000000000..23a7b39719 Binary files /dev/null and b/assets/lightmaps/torch_spotlight_texture.png differ diff --git a/assets/models/Faces/faces.glb b/assets/models/Faces/faces.glb new file mode 100644 index 0000000000..23cbb2f7c0 Binary files /dev/null and b/assets/models/Faces/faces.glb differ diff --git a/assets/shaders/manual_material.wgsl b/assets/shaders/manual_material.wgsl new file mode 100644 index 0000000000..557a1412c7 --- /dev/null +++ b/assets/shaders/manual_material.wgsl @@ -0,0 +1,11 @@ +#import bevy_pbr::forward_io::VertexOutput + +@group(3) @binding(0) var material_color_texture: texture_2d; +@group(3) @binding(1) var material_color_sampler: sampler; + +@fragment +fn fragment( + mesh: VertexOutput, +) -> @location(0) vec4 { + return textureSample(material_color_texture, material_color_sampler, mesh.uv); +} diff --git a/benches/Cargo.toml b/benches/Cargo.toml index a299a6526c..789207d823 100644 --- a/benches/Cargo.toml +++ b/benches/Cargo.toml @@ -10,7 +10,7 @@ autobenches = false [dependencies] # The primary crate that runs and analyzes our benchmarks. This is a regular dependency because the # `bench!` macro refers to it in its documentation. -criterion = { version = "0.5.1", features = ["html_reports"] } +criterion = { version = "0.6.0", features = ["html_reports"] } [dev-dependencies] # Bevy crates diff --git a/benches/benches/bevy_ecs/bundles/insert_many.rs b/benches/benches/bevy_ecs/bundles/insert_many.rs new file mode 100644 index 0000000000..2e9bfbd8b0 --- /dev/null +++ b/benches/benches/bevy_ecs/bundles/insert_many.rs @@ -0,0 +1,67 @@ +use benches::bench; +use bevy_ecs::{component::Component, world::World}; +use criterion::Criterion; + +const ENTITY_COUNT: usize = 2_000; + +#[derive(Component)] +struct C(usize); + +pub fn insert_many(criterion: &mut Criterion) { + let mut group = criterion.benchmark_group(bench!("insert_many")); + + group.bench_function("all", |bencher| { + let mut world = World::new(); + bencher.iter(|| { + for _ in 0..ENTITY_COUNT { + world + .spawn_empty() + .insert(C::<0>(1)) + .insert(C::<1>(1)) + .insert(C::<2>(1)) + .insert(C::<3>(1)) + .insert(C::<4>(1)) + .insert(C::<5>(1)) + .insert(C::<6>(1)) + .insert(C::<7>(1)) + .insert(C::<8>(1)) + .insert(C::<9>(1)) + .insert(C::<10>(1)) + .insert(C::<11>(1)) + .insert(C::<12>(1)) + .insert(C::<13>(1)) + .insert(C::<14>(1)); + } + world.clear_entities(); + }); + }); + + group.bench_function("only_last", |bencher| { + let mut world = World::new(); + bencher.iter(|| { + for _ in 0..ENTITY_COUNT { + world + .spawn(( + C::<0>(1), + C::<1>(1), + C::<2>(1), + C::<3>(1), + C::<4>(1), + C::<5>(1), + C::<6>(1), + C::<7>(1), + C::<8>(1), + C::<9>(1), + C::<10>(1), + C::<11>(1), + C::<12>(1), + C::<13>(1), + )) + .insert(C::<14>(1)); + } + world.clear_entities(); + }); + }); + + group.finish(); +} diff --git a/benches/benches/bevy_ecs/bundles/mod.rs b/benches/benches/bevy_ecs/bundles/mod.rs new file mode 100644 index 0000000000..21ef05eb3b --- /dev/null +++ b/benches/benches/bevy_ecs/bundles/mod.rs @@ -0,0 +1,14 @@ +use criterion::criterion_group; + +mod insert_many; +mod spawn_many; +mod spawn_many_zst; +mod spawn_one_zst; + +criterion_group!( + benches, + spawn_one_zst::spawn_one_zst, + spawn_many_zst::spawn_many_zst, + spawn_many::spawn_many, + insert_many::insert_many, +); diff --git a/benches/benches/bevy_ecs/bundles/spawn_many.rs b/benches/benches/bevy_ecs/bundles/spawn_many.rs new file mode 100644 index 0000000000..bd99cf3c56 --- /dev/null +++ b/benches/benches/bevy_ecs/bundles/spawn_many.rs @@ -0,0 +1,40 @@ +use benches::bench; +use bevy_ecs::{component::Component, world::World}; +use criterion::Criterion; + +const ENTITY_COUNT: usize = 2_000; + +#[derive(Component)] +struct C(usize); + +pub fn spawn_many(criterion: &mut Criterion) { + let mut group = criterion.benchmark_group(bench!("spawn_many")); + + group.bench_function("static", |bencher| { + let mut world = World::new(); + bencher.iter(|| { + for _ in 0..ENTITY_COUNT { + world.spawn(( + C::<0>(1), + C::<1>(1), + C::<2>(1), + C::<3>(1), + C::<4>(1), + C::<5>(1), + C::<6>(1), + C::<7>(1), + C::<8>(1), + C::<9>(1), + C::<10>(1), + C::<11>(1), + C::<12>(1), + C::<13>(1), + C::<14>(1), + )); + } + world.clear_entities(); + }); + }); + + group.finish(); +} diff --git a/benches/benches/bevy_ecs/bundles/spawn_many_zst.rs b/benches/benches/bevy_ecs/bundles/spawn_many_zst.rs new file mode 100644 index 0000000000..b4305f9e43 --- /dev/null +++ b/benches/benches/bevy_ecs/bundles/spawn_many_zst.rs @@ -0,0 +1,27 @@ +use benches::bench; +use bevy_ecs::{component::Component, world::World}; +use criterion::Criterion; + +const ENTITY_COUNT: usize = 2_000; + +#[derive(Component)] +struct C; + +pub fn spawn_many_zst(criterion: &mut Criterion) { + let mut group = criterion.benchmark_group(bench!("spawn_many_zst")); + + group.bench_function("static", |bencher| { + let mut world = World::new(); + bencher.iter(|| { + for _ in 0..ENTITY_COUNT { + world.spawn(( + C::<0>, C::<1>, C::<2>, C::<3>, C::<4>, C::<5>, C::<6>, C::<7>, C::<8>, C::<9>, + C::<10>, C::<11>, C::<12>, C::<13>, C::<14>, + )); + } + world.clear_entities(); + }); + }); + + group.finish(); +} diff --git a/benches/benches/bevy_ecs/bundles/spawn_one_zst.rs b/benches/benches/bevy_ecs/bundles/spawn_one_zst.rs new file mode 100644 index 0000000000..acb006c1c7 --- /dev/null +++ b/benches/benches/bevy_ecs/bundles/spawn_one_zst.rs @@ -0,0 +1,24 @@ +use benches::bench; +use bevy_ecs::{component::Component, world::World}; +use criterion::Criterion; + +const ENTITY_COUNT: usize = 10_000; + +#[derive(Component)] +struct A; + +pub fn spawn_one_zst(criterion: &mut Criterion) { + let mut group = criterion.benchmark_group(bench!("spawn_one_zst")); + + group.bench_function("static", |bencher| { + let mut world = World::new(); + bencher.iter(|| { + for _ in 0..ENTITY_COUNT { + world.spawn(A); + } + world.clear_entities(); + }); + }); + + group.finish(); +} diff --git a/benches/benches/bevy_ecs/main.rs b/benches/benches/bevy_ecs/main.rs index 4a025ab829..59b4c1fd73 100644 --- a/benches/benches/bevy_ecs/main.rs +++ b/benches/benches/bevy_ecs/main.rs @@ -5,6 +5,7 @@ use criterion::criterion_main; +mod bundles; mod change_detection; mod components; mod empty_archetypes; @@ -18,6 +19,7 @@ mod scheduling; mod world; criterion_main!( + bundles::benches, change_detection::benches, components::benches, empty_archetypes::benches, diff --git a/crates/bevy_animation/Cargo.toml b/crates/bevy_animation/Cargo.toml index 637231fd4c..380c276539 100644 --- a/crates/bevy_animation/Cargo.toml +++ b/crates/bevy_animation/Cargo.toml @@ -31,7 +31,7 @@ bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-fea ] } # other -petgraph = { version = "0.7", features = ["serde-1"] } +petgraph = { version = "0.8", features = ["serde-1"] } ron = "0.10" serde = "1" blake3 = { version = "1.0" } diff --git a/crates/bevy_animation/src/graph.rs b/crates/bevy_animation/src/graph.rs index a5f4041ac7..adb4a7c7ac 100644 --- a/crates/bevy_animation/src/graph.rs +++ b/crates/bevy_animation/src/graph.rs @@ -19,7 +19,7 @@ use bevy_ecs::{ system::{Res, ResMut}, }; use bevy_platform::collections::HashMap; -use bevy_reflect::{prelude::ReflectDefault, Reflect, ReflectSerialize}; +use bevy_reflect::{prelude::ReflectDefault, Reflect}; use derive_more::derive::From; use petgraph::{ graph::{DiGraph, NodeIndex}, @@ -29,6 +29,7 @@ use ron::de::SpannedError; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; use thiserror::Error; +use tracing::warn; use crate::{AnimationClip, AnimationTargetId}; @@ -108,9 +109,8 @@ use crate::{AnimationClip, AnimationTargetId}; /// [RON]: https://github.com/ron-rs/ron /// /// [RFC 51]: https://github.com/bevyengine/rfcs/blob/main/rfcs/51-animation-composition.md -#[derive(Asset, Reflect, Clone, Debug, Serialize)] -#[reflect(Serialize, Debug, Clone)] -#[serde(into = "SerializedAnimationGraph")] +#[derive(Asset, Reflect, Clone, Debug)] +#[reflect(Debug, Clone)] pub struct AnimationGraph { /// The `petgraph` data structure that defines the animation graph. pub graph: AnimationDiGraph, @@ -242,20 +242,40 @@ pub enum AnimationNodeType { #[derive(Default)] pub struct AnimationGraphAssetLoader; -/// Various errors that can occur when serializing or deserializing animation -/// graphs to and from RON, respectively. +/// Errors that can occur when serializing animation graphs to RON. +#[derive(Error, Debug)] +pub enum AnimationGraphSaveError { + /// An I/O error occurred. + #[error(transparent)] + Io(#[from] io::Error), + /// An error occurred in RON serialization. + #[error(transparent)] + Ron(#[from] ron::Error), + /// An error occurred converting the graph to its serialization form. + #[error(transparent)] + ConvertToSerialized(#[from] NonPathHandleError), +} + +/// Errors that can occur when deserializing animation graphs from RON. #[derive(Error, Debug)] pub enum AnimationGraphLoadError { /// An I/O error occurred. - #[error("I/O")] + #[error(transparent)] Io(#[from] io::Error), - /// An error occurred in RON serialization or deserialization. - #[error("RON serialization")] + /// An error occurred in RON deserialization. + #[error(transparent)] Ron(#[from] ron::Error), /// An error occurred in RON deserialization, and the location of the error /// is supplied. - #[error("RON serialization")] + #[error(transparent)] SpannedRon(#[from] SpannedError), + /// The deserialized graph contained legacy data that we no longer support. + #[error( + "The deserialized AnimationGraph contained an AnimationClip referenced by an AssetId, \ + which is no longer supported. Consider manually deserializing the SerializedAnimationGraph \ + type and determine how to migrate any SerializedAnimationClip::AssetId animation clips" + )] + GraphContainsLegacyAssetId, } /// Acceleration structures for animation graphs that allows Bevy to evaluate @@ -388,18 +408,32 @@ pub struct SerializedAnimationGraphNode { #[derive(Serialize, Deserialize)] pub enum SerializedAnimationNodeType { /// Corresponds to [`AnimationNodeType::Clip`]. - Clip(SerializedAnimationClip), + Clip(MigrationSerializedAnimationClip), /// Corresponds to [`AnimationNodeType::Blend`]. Blend, /// Corresponds to [`AnimationNodeType::Add`]. Add, } -/// A version of `Handle` suitable for serializing as an asset. +/// A type to facilitate migration from the legacy format of [`SerializedAnimationGraph`] to the +/// new format. /// -/// This replaces any handle that has a path with an [`AssetPath`]. Failing -/// that, the asset ID is serialized directly. +/// By using untagged serde deserialization, we can try to deserialize the modern form, then +/// fallback to the legacy form. Users must migrate to the modern form by Bevy 0.18. +// TODO: Delete this after Bevy 0.17. #[derive(Serialize, Deserialize)] +#[serde(untagged)] +pub enum MigrationSerializedAnimationClip { + /// This is the new type of this field. + Modern(AssetPath<'static>), + /// This is the legacy type of this field. Users must migrate away from this. + #[serde(skip_serializing)] + Legacy(SerializedAnimationClip), +} + +/// The legacy form of serialized animation clips. This allows raw asset IDs to be deserialized. +// TODO: Delete this after Bevy 0.17. +#[derive(Deserialize)] pub enum SerializedAnimationClip { /// Records an asset path. AssetPath(AssetPath<'static>), @@ -648,12 +682,13 @@ impl AnimationGraph { /// /// If writing to a file, it can later be loaded with the /// [`AnimationGraphAssetLoader`] to reconstruct the graph. - pub fn save(&self, writer: &mut W) -> Result<(), AnimationGraphLoadError> + pub fn save(&self, writer: &mut W) -> Result<(), AnimationGraphSaveError> where W: Write, { let mut ron_serializer = ron::ser::Serializer::new(writer, None)?; - Ok(self.serialize(&mut ron_serializer)?) + let serialized_graph: SerializedAnimationGraph = self.clone().try_into()?; + Ok(serialized_graph.serialize(&mut ron_serializer)?) } /// Adds an animation target (bone) to the mask group with the given ID. @@ -758,28 +793,55 @@ impl AssetLoader for AnimationGraphAssetLoader { let serialized_animation_graph = SerializedAnimationGraph::deserialize(&mut deserializer) .map_err(|err| deserializer.span_error(err))?; - // Load all `AssetPath`s to convert from a - // `SerializedAnimationGraph` to a real `AnimationGraph`. - Ok(AnimationGraph { - graph: serialized_animation_graph.graph.map( - |_, serialized_node| AnimationGraphNode { - node_type: match serialized_node.node_type { - SerializedAnimationNodeType::Clip(ref clip) => match clip { - SerializedAnimationClip::AssetId(asset_id) => { - AnimationNodeType::Clip(Handle::Weak(*asset_id)) + // Load all `AssetPath`s to convert from a `SerializedAnimationGraph` to a real + // `AnimationGraph`. This is effectively a `DiGraph::map`, but this allows us to return + // errors. + let mut animation_graph = DiGraph::with_capacity( + serialized_animation_graph.graph.node_count(), + serialized_animation_graph.graph.edge_count(), + ); + + let mut already_warned = false; + for serialized_node in serialized_animation_graph.graph.node_weights() { + animation_graph.add_node(AnimationGraphNode { + node_type: match serialized_node.node_type { + SerializedAnimationNodeType::Clip(ref clip) => match clip { + MigrationSerializedAnimationClip::Modern(path) => { + AnimationNodeType::Clip(load_context.load(path.clone())) + } + MigrationSerializedAnimationClip::Legacy( + SerializedAnimationClip::AssetPath(path), + ) => { + if !already_warned { + let path = load_context.asset_path(); + warn!( + "Loaded an AnimationGraph asset at \"{path}\" which contains a \ + legacy-style SerializedAnimationClip. Please re-save the asset \ + using AnimationGraph::save to automatically migrate to the new \ + format" + ); + already_warned = true; } - SerializedAnimationClip::AssetPath(asset_path) => { - AnimationNodeType::Clip(load_context.load(asset_path)) - } - }, - SerializedAnimationNodeType::Blend => AnimationNodeType::Blend, - SerializedAnimationNodeType::Add => AnimationNodeType::Add, + AnimationNodeType::Clip(load_context.load(path.clone())) + } + MigrationSerializedAnimationClip::Legacy( + SerializedAnimationClip::AssetId(_), + ) => { + return Err(AnimationGraphLoadError::GraphContainsLegacyAssetId); + } }, - mask: serialized_node.mask, - weight: serialized_node.weight, + SerializedAnimationNodeType::Blend => AnimationNodeType::Blend, + SerializedAnimationNodeType::Add => AnimationNodeType::Add, }, - |_, _| (), - ), + mask: serialized_node.mask, + weight: serialized_node.weight, + }); + } + for edge in serialized_animation_graph.graph.raw_edges() { + animation_graph.add_edge(edge.source(), edge.target(), ()); + } + Ok(AnimationGraph { + graph: animation_graph, root: serialized_animation_graph.root, mask_groups: serialized_animation_graph.mask_groups, }) @@ -790,37 +852,50 @@ impl AssetLoader for AnimationGraphAssetLoader { } } -impl From for SerializedAnimationGraph { - fn from(animation_graph: AnimationGraph) -> Self { - // If any of the animation clips have paths, then serialize them as - // `SerializedAnimationClip::AssetPath` so that the - // `AnimationGraphAssetLoader` can load them. - Self { - graph: animation_graph.graph.map( - |_, node| SerializedAnimationGraphNode { - weight: node.weight, - mask: node.mask, - node_type: match node.node_type { - AnimationNodeType::Clip(ref clip) => match clip.path() { - Some(path) => SerializedAnimationNodeType::Clip( - SerializedAnimationClip::AssetPath(path.clone()), - ), - None => SerializedAnimationNodeType::Clip( - SerializedAnimationClip::AssetId(clip.id()), - ), - }, - AnimationNodeType::Blend => SerializedAnimationNodeType::Blend, - AnimationNodeType::Add => SerializedAnimationNodeType::Add, +impl TryFrom for SerializedAnimationGraph { + type Error = NonPathHandleError; + + fn try_from(animation_graph: AnimationGraph) -> Result { + // Convert all the `Handle` to AssetPath, so that + // `AnimationGraphAssetLoader` can load them. This is effectively just doing a + // `DiGraph::map`, except we need to return an error if any handles aren't associated to a + // path. + let mut serialized_graph = DiGraph::with_capacity( + animation_graph.graph.node_count(), + animation_graph.graph.edge_count(), + ); + for node in animation_graph.graph.node_weights() { + serialized_graph.add_node(SerializedAnimationGraphNode { + weight: node.weight, + mask: node.mask, + node_type: match node.node_type { + AnimationNodeType::Clip(ref clip) => match clip.path() { + Some(path) => SerializedAnimationNodeType::Clip( + MigrationSerializedAnimationClip::Modern(path.clone()), + ), + None => return Err(NonPathHandleError), }, + AnimationNodeType::Blend => SerializedAnimationNodeType::Blend, + AnimationNodeType::Add => SerializedAnimationNodeType::Add, }, - |_, _| (), - ), + }); + } + for edge in animation_graph.graph.raw_edges() { + serialized_graph.add_edge(edge.source(), edge.target(), ()); + } + Ok(Self { + graph: serialized_graph, root: animation_graph.root, mask_groups: animation_graph.mask_groups, - } + }) } } +/// Error for when only path [`Handle`]s are supported. +#[derive(Error, Debug)] +#[error("AnimationGraph contains a handle to an AnimationClip that does not correspond to an asset path")] +pub struct NonPathHandleError; + /// A system that creates, updates, and removes [`ThreadedAnimationGraph`] /// structures for every changed [`AnimationGraph`]. /// diff --git a/crates/bevy_anti_aliasing/src/contrast_adaptive_sharpening/mod.rs b/crates/bevy_anti_aliasing/src/contrast_adaptive_sharpening/mod.rs index d1de3f4cea..5baf2ed893 100644 --- a/crates/bevy_anti_aliasing/src/contrast_adaptive_sharpening/mod.rs +++ b/crates/bevy_anti_aliasing/src/contrast_adaptive_sharpening/mod.rs @@ -11,7 +11,7 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ extract_component::{ExtractComponent, ExtractComponentPlugin, UniformComponentPlugin}, prelude::Camera, - render_graph::RenderGraphApp, + render_graph::RenderGraphExt, render_resource::{ binding_types::{sampler, texture_2d, uniform_buffer}, *, @@ -20,6 +20,7 @@ use bevy_render::{ view::{ExtractedView, ViewTarget}, Render, RenderApp, RenderSystems, }; +use bevy_utils::default; mod node; @@ -218,18 +219,14 @@ impl SpecializedRenderPipeline for CasPipeline { fragment: Some(FragmentState { shader: self.fragment_shader.clone(), shader_defs, - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format: key.texture_format, blend: None, write_mask: ColorWrites::ALL, })], + ..default() }), - primitive: PrimitiveState::default(), - depth_stencil: None, - multisample: MultisampleState::default(), - push_constant_ranges: Vec::new(), - zero_initialize_workgroup_memory: false, + ..default() } } } diff --git a/crates/bevy_anti_aliasing/src/fxaa/mod.rs b/crates/bevy_anti_aliasing/src/fxaa/mod.rs index adc2a3d5a2..df247c604b 100644 --- a/crates/bevy_anti_aliasing/src/fxaa/mod.rs +++ b/crates/bevy_anti_aliasing/src/fxaa/mod.rs @@ -11,7 +11,7 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ extract_component::{ExtractComponent, ExtractComponentPlugin}, prelude::Camera, - render_graph::{RenderGraphApp, ViewNodeRunner}, + render_graph::{RenderGraphExt, ViewNodeRunner}, render_resource::{ binding_types::{sampler, texture_2d}, *, @@ -190,18 +190,14 @@ impl SpecializedRenderPipeline for FxaaPipeline { format!("EDGE_THRESH_{}", key.edge_threshold.get_str()).into(), format!("EDGE_THRESH_MIN_{}", key.edge_threshold_min.get_str()).into(), ], - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format: key.texture_format, blend: None, write_mask: ColorWrites::ALL, })], + ..default() }), - primitive: PrimitiveState::default(), - depth_stencil: None, - multisample: MultisampleState::default(), - push_constant_ranges: Vec::new(), - zero_initialize_workgroup_memory: false, + ..default() } } } diff --git a/crates/bevy_anti_aliasing/src/smaa/mod.rs b/crates/bevy_anti_aliasing/src/smaa/mod.rs index 3996f389d0..f465bd5f3a 100644 --- a/crates/bevy_anti_aliasing/src/smaa/mod.rs +++ b/crates/bevy_anti_aliasing/src/smaa/mod.rs @@ -32,7 +32,7 @@ use bevy_app::{App, Plugin}; #[cfg(feature = "smaa_luts")] use bevy_asset::load_internal_binary_asset; -use bevy_asset::{embedded_asset, load_embedded_asset, weak_handle, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, uuid_handle, Handle}; #[cfg(not(feature = "smaa_luts"))] use bevy_core_pipeline::tonemapping::lut_placeholder; use bevy_core_pipeline::{ @@ -58,19 +58,19 @@ use bevy_render::{ extract_component::{ExtractComponent, ExtractComponentPlugin}, render_asset::RenderAssets, render_graph::{ - NodeRunError, RenderGraphApp as _, RenderGraphContext, ViewNode, ViewNodeRunner, + NodeRunError, RenderGraphContext, RenderGraphExt as _, ViewNode, ViewNodeRunner, }, render_resource::{ binding_types::{sampler, texture_2d, uniform_buffer}, AddressMode, BindGroup, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, CachedRenderPipelineId, ColorTargetState, ColorWrites, CompareFunction, DepthStencilState, - DynamicUniformBuffer, FilterMode, FragmentState, LoadOp, MultisampleState, Operations, - PipelineCache, PrimitiveState, RenderPassColorAttachment, RenderPassDepthStencilAttachment, - RenderPassDescriptor, RenderPipeline, RenderPipelineDescriptor, SamplerBindingType, - SamplerDescriptor, Shader, ShaderDefVal, ShaderStages, ShaderType, - SpecializedRenderPipeline, SpecializedRenderPipelines, StencilFaceState, StencilOperation, - StencilState, StoreOp, TextureDescriptor, TextureDimension, TextureFormat, - TextureSampleType, TextureUsages, TextureView, VertexState, + DynamicUniformBuffer, FilterMode, FragmentState, LoadOp, Operations, PipelineCache, + RenderPassColorAttachment, RenderPassDepthStencilAttachment, RenderPassDescriptor, + RenderPipeline, RenderPipelineDescriptor, SamplerBindingType, SamplerDescriptor, Shader, + ShaderDefVal, ShaderStages, ShaderType, SpecializedRenderPipeline, + SpecializedRenderPipelines, StencilFaceState, StencilOperation, StencilState, StoreOp, + TextureDescriptor, TextureDimension, TextureFormat, TextureSampleType, TextureUsages, + TextureView, VertexState, }, renderer::{RenderContext, RenderDevice, RenderQueue}, texture::{CachedTexture, GpuImage, TextureCache}, @@ -81,10 +81,10 @@ use bevy_utils::prelude::default; /// The handle of the area LUT, a KTX2 format texture that SMAA uses internally. const SMAA_AREA_LUT_TEXTURE_HANDLE: Handle = - weak_handle!("569c4d67-c7fa-4958-b1af-0836023603c0"); + uuid_handle!("569c4d67-c7fa-4958-b1af-0836023603c0"); /// The handle of the search LUT, a KTX2 format texture that SMAA uses internally. const SMAA_SEARCH_LUT_TEXTURE_HANDLE: Handle = - weak_handle!("43b97515-252e-4c8a-b9af-f2fc528a1c27"); + uuid_handle!("43b97515-252e-4c8a-b9af-f2fc528a1c27"); /// Adds support for subpixel morphological antialiasing, or SMAA. pub struct SmaaPlugin; @@ -482,21 +482,19 @@ impl SpecializedRenderPipeline for SmaaEdgeDetectionPipeline { vertex: VertexState { shader: self.shader.clone(), shader_defs: shader_defs.clone(), - entry_point: "edge_detection_vertex_main".into(), + entry_point: Some("edge_detection_vertex_main".into()), buffers: vec![], }, fragment: Some(FragmentState { shader: self.shader.clone(), shader_defs, - entry_point: "luma_edge_detection_fragment_main".into(), + entry_point: Some("luma_edge_detection_fragment_main".into()), targets: vec![Some(ColorTargetState { format: TextureFormat::Rg8Unorm, blend: None, write_mask: ColorWrites::ALL, })], }), - push_constant_ranges: vec![], - primitive: PrimitiveState::default(), depth_stencil: Some(DepthStencilState { format: TextureFormat::Stencil8, depth_write_enabled: false, @@ -509,8 +507,7 @@ impl SpecializedRenderPipeline for SmaaEdgeDetectionPipeline { }, bias: default(), }), - multisample: MultisampleState::default(), - zero_initialize_workgroup_memory: false, + ..default() } } } @@ -542,21 +539,19 @@ impl SpecializedRenderPipeline for SmaaBlendingWeightCalculationPipeline { vertex: VertexState { shader: self.shader.clone(), shader_defs: shader_defs.clone(), - entry_point: "blending_weight_calculation_vertex_main".into(), + entry_point: Some("blending_weight_calculation_vertex_main".into()), buffers: vec![], }, fragment: Some(FragmentState { shader: self.shader.clone(), shader_defs, - entry_point: "blending_weight_calculation_fragment_main".into(), + entry_point: Some("blending_weight_calculation_fragment_main".into()), targets: vec![Some(ColorTargetState { format: TextureFormat::Rgba8Unorm, blend: None, write_mask: ColorWrites::ALL, })], }), - push_constant_ranges: vec![], - primitive: PrimitiveState::default(), depth_stencil: Some(DepthStencilState { format: TextureFormat::Stencil8, depth_write_enabled: false, @@ -569,8 +564,7 @@ impl SpecializedRenderPipeline for SmaaBlendingWeightCalculationPipeline { }, bias: default(), }), - multisample: MultisampleState::default(), - zero_initialize_workgroup_memory: false, + ..default() } } } @@ -590,24 +584,20 @@ impl SpecializedRenderPipeline for SmaaNeighborhoodBlendingPipeline { vertex: VertexState { shader: self.shader.clone(), shader_defs: shader_defs.clone(), - entry_point: "neighborhood_blending_vertex_main".into(), + entry_point: Some("neighborhood_blending_vertex_main".into()), buffers: vec![], }, fragment: Some(FragmentState { shader: self.shader.clone(), shader_defs, - entry_point: "neighborhood_blending_fragment_main".into(), + entry_point: Some("neighborhood_blending_fragment_main".into()), targets: vec![Some(ColorTargetState { format: key.texture_format, blend: None, write_mask: ColorWrites::ALL, })], }), - push_constant_ranges: vec![], - primitive: PrimitiveState::default(), - depth_stencil: None, - multisample: MultisampleState::default(), - zero_initialize_workgroup_memory: false, + ..default() } } } diff --git a/crates/bevy_anti_aliasing/src/taa/mod.rs b/crates/bevy_anti_aliasing/src/taa/mod.rs index 442a268e2a..0783cf713e 100644 --- a/crates/bevy_anti_aliasing/src/taa/mod.rs +++ b/crates/bevy_anti_aliasing/src/taa/mod.rs @@ -21,15 +21,15 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ camera::{ExtractedCamera, MipBias, TemporalJitter}, prelude::{Camera, Projection}, - render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner}, + render_graph::{NodeRunError, RenderGraphContext, RenderGraphExt, ViewNode, ViewNodeRunner}, render_resource::{ binding_types::{sampler, texture_2d, texture_depth_2d}, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, CachedRenderPipelineId, - ColorTargetState, ColorWrites, FilterMode, FragmentState, MultisampleState, Operations, - PipelineCache, PrimitiveState, RenderPassColorAttachment, RenderPassDescriptor, - RenderPipelineDescriptor, Sampler, SamplerBindingType, SamplerDescriptor, Shader, - ShaderStages, SpecializedRenderPipeline, SpecializedRenderPipelines, TextureDescriptor, - TextureDimension, TextureFormat, TextureSampleType, TextureUsages, + ColorTargetState, ColorWrites, FilterMode, FragmentState, Operations, PipelineCache, + RenderPassColorAttachment, RenderPassDescriptor, RenderPipelineDescriptor, Sampler, + SamplerBindingType, SamplerDescriptor, Shader, ShaderStages, SpecializedRenderPipeline, + SpecializedRenderPipelines, TextureDescriptor, TextureDimension, TextureFormat, + TextureSampleType, TextureUsages, }, renderer::{RenderContext, RenderDevice}, sync_component::SyncComponentPlugin, @@ -38,6 +38,7 @@ use bevy_render::{ view::{ExtractedView, Msaa, ViewTarget}, ExtractSchedule, MainWorld, Render, RenderApp, RenderSystems, }; +use bevy_utils::default; use tracing::warn; /// Plugin for temporal anti-aliasing. @@ -320,7 +321,6 @@ impl SpecializedRenderPipeline for TaaPipeline { fragment: Some(FragmentState { shader: self.fragment_shader.clone(), shader_defs, - entry_point: "taa".into(), targets: vec![ Some(ColorTargetState { format, @@ -333,12 +333,9 @@ impl SpecializedRenderPipeline for TaaPipeline { write_mask: ColorWrites::ALL, }), ], + ..default() }), - primitive: PrimitiveState::default(), - depth_stencil: None, - multisample: MultisampleState::default(), - push_constant_ranges: Vec::new(), - zero_initialize_workgroup_memory: false, + ..default() } } } diff --git a/crates/bevy_asset/src/handle.rs b/crates/bevy_asset/src/handle.rs index e6f00c7da5..b9a20a7af8 100644 --- a/crates/bevy_asset/src/handle.rs +++ b/crates/bevy_asset/src/handle.rs @@ -7,10 +7,12 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect, TypePath}; use core::{ any::TypeId, hash::{Hash, Hasher}, + marker::PhantomData, }; use crossbeam_channel::{Receiver, Sender}; use disqualified::ShortName; use thiserror::Error; +use uuid::Uuid; /// Provides [`Handle`] and [`UntypedHandle`] _for a specific asset type_. /// This should _only_ be used for one specific asset type. @@ -117,7 +119,7 @@ impl core::fmt::Debug for StrongHandle { /// avoiding the need to store multiple copies of the same data. /// /// If a [`Handle`] is [`Handle::Strong`], the [`Asset`] will be kept -/// alive until the [`Handle`] is dropped. If a [`Handle`] is [`Handle::Weak`], it does not necessarily reference a live [`Asset`], +/// alive until the [`Handle`] is dropped. If a [`Handle`] is [`Handle::Uuid`], it does not necessarily reference a live [`Asset`], /// nor will it keep assets alive. /// /// Modifying a *handle* will change which existing asset is referenced, but modifying the *asset* @@ -133,16 +135,16 @@ pub enum Handle { /// A "strong" reference to a live (or loading) [`Asset`]. If a [`Handle`] is [`Handle::Strong`], the [`Asset`] will be kept /// alive until the [`Handle`] is dropped. Strong handles also provide access to additional asset metadata. Strong(Arc), - /// A "weak" reference to an [`Asset`]. If a [`Handle`] is [`Handle::Weak`], it does not necessarily reference a live [`Asset`], - /// nor will it keep assets alive. - Weak(AssetId), + /// A reference to an [`Asset`] using a stable-across-runs / const identifier. Dropping this + /// handle will not result in the asset being dropped. + Uuid(Uuid, #[reflect(ignore, clone)] PhantomData A>), } impl Clone for Handle { fn clone(&self) -> Self { match self { Handle::Strong(handle) => Handle::Strong(handle.clone()), - Handle::Weak(id) => Handle::Weak(*id), + Handle::Uuid(uuid, ..) => Handle::Uuid(*uuid, PhantomData), } } } @@ -153,7 +155,7 @@ impl Handle { pub fn id(&self) -> AssetId { match self { Handle::Strong(handle) => handle.id.typed_unchecked(), - Handle::Weak(id) => *id, + Handle::Uuid(uuid, ..) => AssetId::Uuid { uuid: *uuid }, } } @@ -162,14 +164,14 @@ impl Handle { pub fn path(&self) -> Option<&AssetPath<'static>> { match self { Handle::Strong(handle) => handle.path.as_ref(), - Handle::Weak(_) => None, + Handle::Uuid(..) => None, } } - /// Returns `true` if this is a weak handle. + /// Returns `true` if this is a uuid handle. #[inline] - pub fn is_weak(&self) -> bool { - matches!(self, Handle::Weak(_)) + pub fn is_uuid(&self) -> bool { + matches!(self, Handle::Uuid(..)) } /// Returns `true` if this is a strong handle. @@ -178,18 +180,9 @@ impl Handle { matches!(self, Handle::Strong(_)) } - /// Creates a [`Handle::Weak`] clone of this [`Handle`], which will not keep the referenced [`Asset`] alive. - #[inline] - pub fn clone_weak(&self) -> Self { - match self { - Handle::Strong(handle) => Handle::Weak(handle.id.typed_unchecked::()), - Handle::Weak(id) => Handle::Weak(*id), - } - } - /// Converts this [`Handle`] to an "untyped" / "generic-less" [`UntypedHandle`], which stores the [`Asset`] type information - /// _inside_ [`UntypedHandle`]. This will return [`UntypedHandle::Strong`] for [`Handle::Strong`] and [`UntypedHandle::Weak`] for - /// [`Handle::Weak`]. + /// _inside_ [`UntypedHandle`]. This will return [`UntypedHandle::Strong`] for [`Handle::Strong`] and [`UntypedHandle::Uuid`] for + /// [`Handle::Uuid`]. #[inline] pub fn untyped(self) -> UntypedHandle { self.into() @@ -198,7 +191,7 @@ impl Handle { impl Default for Handle { fn default() -> Self { - Handle::Weak(AssetId::default()) + Handle::Uuid(AssetId::::DEFAULT_UUID, PhantomData) } } @@ -214,7 +207,7 @@ impl core::fmt::Debug for Handle { handle.path ) } - Handle::Weak(id) => write!(f, "WeakHandle<{name}>({:?})", id.internal()), + Handle::Uuid(uuid, ..) => write!(f, "UuidHandle<{name}>({uuid:?})"), } } } @@ -284,8 +277,13 @@ impl From<&mut Handle> for UntypedAssetId { pub enum UntypedHandle { /// A strong handle, which will keep the referenced [`Asset`] alive until all strong handles are dropped. Strong(Arc), - /// A weak handle, which does not keep the referenced [`Asset`] alive. - Weak(UntypedAssetId), + /// A UUID handle, which does not keep the referenced [`Asset`] alive. + Uuid { + /// An identifier that records the underlying asset type. + type_id: TypeId, + /// The UUID provided during asset registration. + uuid: Uuid, + }, } impl UntypedHandle { @@ -294,7 +292,10 @@ impl UntypedHandle { pub fn id(&self) -> UntypedAssetId { match self { UntypedHandle::Strong(handle) => handle.id, - UntypedHandle::Weak(id) => *id, + UntypedHandle::Uuid { type_id, uuid } => UntypedAssetId::Uuid { + uuid: *uuid, + type_id: *type_id, + }, } } @@ -303,16 +304,7 @@ impl UntypedHandle { pub fn path(&self) -> Option<&AssetPath<'static>> { match self { UntypedHandle::Strong(handle) => handle.path.as_ref(), - UntypedHandle::Weak(_) => None, - } - } - - /// Creates an [`UntypedHandle::Weak`] clone of this [`UntypedHandle`], which will not keep the referenced [`Asset`] alive. - #[inline] - pub fn clone_weak(&self) -> UntypedHandle { - match self { - UntypedHandle::Strong(handle) => UntypedHandle::Weak(handle.id), - UntypedHandle::Weak(id) => UntypedHandle::Weak(*id), + UntypedHandle::Uuid { .. } => None, } } @@ -321,7 +313,7 @@ impl UntypedHandle { pub fn type_id(&self) -> TypeId { match self { UntypedHandle::Strong(handle) => handle.id.type_id(), - UntypedHandle::Weak(id) => id.type_id(), + UntypedHandle::Uuid { type_id, .. } => *type_id, } } @@ -330,7 +322,7 @@ impl UntypedHandle { pub fn typed_unchecked(self) -> Handle { match self { UntypedHandle::Strong(handle) => Handle::Strong(handle), - UntypedHandle::Weak(id) => Handle::Weak(id.typed_unchecked::()), + UntypedHandle::Uuid { uuid, .. } => Handle::Uuid(uuid, PhantomData), } } @@ -345,10 +337,7 @@ impl UntypedHandle { TypeId::of::(), "The target Handle's TypeId does not match the TypeId of this UntypedHandle" ); - match self { - UntypedHandle::Strong(handle) => Handle::Strong(handle), - UntypedHandle::Weak(id) => Handle::Weak(id.typed_unchecked::()), - } + self.typed_unchecked() } /// Converts to a typed Handle. This will panic if the internal [`TypeId`] does not match the given asset type `A` @@ -376,7 +365,7 @@ impl UntypedHandle { pub fn meta_transform(&self) -> Option<&MetaTransform> { match self { UntypedHandle::Strong(handle) => handle.meta_transform.as_ref(), - UntypedHandle::Weak(_) => None, + UntypedHandle::Uuid { .. } => None, } } } @@ -409,12 +398,9 @@ impl core::fmt::Debug for UntypedHandle { handle.path ) } - UntypedHandle::Weak(id) => write!( - f, - "WeakHandle{{ type_id: {:?}, id: {:?} }}", - id.type_id(), - id.internal() - ), + UntypedHandle::Uuid { type_id, uuid } => { + write!(f, "UuidHandle{{ type_id: {type_id:?}, uuid: {uuid:?} }}",) + } } } } @@ -474,7 +460,10 @@ impl From> for UntypedHandle { fn from(value: Handle) -> Self { match value { Handle::Strong(handle) => UntypedHandle::Strong(handle), - Handle::Weak(id) => UntypedHandle::Weak(id.into()), + Handle::Uuid(uuid, _) => UntypedHandle::Uuid { + type_id: TypeId::of::(), + uuid, + }, } } } @@ -490,36 +479,37 @@ impl TryFrom for Handle { return Err(UntypedAssetConversionError::TypeIdMismatch { expected, found }); } - match value { - UntypedHandle::Strong(handle) => Ok(Handle::Strong(handle)), - UntypedHandle::Weak(id) => { - let Ok(id) = id.try_into() else { - return Err(UntypedAssetConversionError::TypeIdMismatch { expected, found }); - }; - Ok(Handle::Weak(id)) - } - } + Ok(match value { + UntypedHandle::Strong(handle) => Handle::Strong(handle), + UntypedHandle::Uuid { uuid, .. } => Handle::Uuid(uuid, PhantomData), + }) } } -/// Creates a weak [`Handle`] from a string literal containing a UUID. +/// Creates a [`Handle`] from a string literal containing a UUID. /// /// # Examples /// /// ``` -/// # use bevy_asset::{Handle, weak_handle}; +/// # use bevy_asset::{Handle, uuid_handle}; /// # type Shader = (); -/// const SHADER: Handle = weak_handle!("1347c9b7-c46a-48e7-b7b8-023a354b7cac"); +/// const SHADER: Handle = uuid_handle!("1347c9b7-c46a-48e7-b7b8-023a354b7cac"); /// ``` #[macro_export] -macro_rules! weak_handle { +macro_rules! uuid_handle { ($uuid:expr) => {{ - $crate::Handle::Weak($crate::AssetId::Uuid { - uuid: $crate::uuid::uuid!($uuid), - }) + $crate::Handle::Uuid($crate::uuid::uuid!($uuid), core::marker::PhantomData) }}; } +#[deprecated = "Use uuid_handle! instead"] +#[macro_export] +macro_rules! weak_handle { + ($uuid:expr) => { + uuid_handle!($uuid) + }; +} + /// Errors preventing the conversion of to/from an [`UntypedHandle`] and a [`Handle`]. #[derive(Error, Debug, PartialEq, Clone)] #[non_exhaustive] @@ -559,15 +549,12 @@ mod tests { /// Typed and Untyped `Handles` should be equivalent to each other and themselves #[test] fn equality() { - let typed = AssetId::::Uuid { uuid: UUID_1 }; - let untyped = UntypedAssetId::Uuid { + let typed = Handle::::Uuid(UUID_1, PhantomData); + let untyped = UntypedHandle::Uuid { type_id: TypeId::of::(), uuid: UUID_1, }; - let typed = Handle::Weak(typed); - let untyped = UntypedHandle::Weak(untyped); - assert_eq!( Ok(typed.clone()), Handle::::try_from(untyped.clone()) @@ -585,22 +572,17 @@ mod tests { fn ordering() { assert!(UUID_1 < UUID_2); - let typed_1 = AssetId::::Uuid { uuid: UUID_1 }; - let typed_2 = AssetId::::Uuid { uuid: UUID_2 }; - let untyped_1 = UntypedAssetId::Uuid { + let typed_1 = Handle::::Uuid(UUID_1, PhantomData); + let typed_2 = Handle::::Uuid(UUID_2, PhantomData); + let untyped_1 = UntypedHandle::Uuid { type_id: TypeId::of::(), uuid: UUID_1, }; - let untyped_2 = UntypedAssetId::Uuid { + let untyped_2 = UntypedHandle::Uuid { type_id: TypeId::of::(), uuid: UUID_2, }; - let typed_1 = Handle::Weak(typed_1); - let typed_2 = Handle::Weak(typed_2); - let untyped_1 = UntypedHandle::Weak(untyped_1); - let untyped_2 = UntypedHandle::Weak(untyped_2); - assert!(typed_1 < typed_2); assert!(untyped_1 < untyped_2); @@ -617,15 +599,12 @@ mod tests { /// Typed and Untyped `Handles` should be equivalently hashable to each other and themselves #[test] fn hashing() { - let typed = AssetId::::Uuid { uuid: UUID_1 }; - let untyped = UntypedAssetId::Uuid { + let typed = Handle::::Uuid(UUID_1, PhantomData); + let untyped = UntypedHandle::Uuid { type_id: TypeId::of::(), uuid: UUID_1, }; - let typed = Handle::Weak(typed); - let untyped = UntypedHandle::Weak(untyped); - assert_eq!( hash(&typed), hash(&Handle::::try_from(untyped.clone()).unwrap()) @@ -637,15 +616,12 @@ mod tests { /// Typed and Untyped `Handles` should be interchangeable #[test] fn conversion() { - let typed = AssetId::::Uuid { uuid: UUID_1 }; - let untyped = UntypedAssetId::Uuid { + let typed = Handle::::Uuid(UUID_1, PhantomData); + let untyped = UntypedHandle::Uuid { type_id: TypeId::of::(), uuid: UUID_1, }; - let typed = Handle::Weak(typed); - let untyped = UntypedHandle::Weak(untyped); - assert_eq!(typed, Handle::try_from(untyped.clone()).unwrap()); assert_eq!(UntypedHandle::from(typed.clone()), untyped); } diff --git a/crates/bevy_asset/src/io/wasm.rs b/crates/bevy_asset/src/io/wasm.rs index 4080e03ecd..4ed7162d2b 100644 --- a/crates/bevy_asset/src/io/wasm.rs +++ b/crates/bevy_asset/src/io/wasm.rs @@ -47,7 +47,7 @@ fn js_value_to_err(context: &str) -> impl FnOnce(JsValue) -> std::io::Error + '_ } }; - std::io::Error::new(std::io::ErrorKind::Other, message) + std::io::Error::other(message) } } @@ -62,10 +62,7 @@ impl HttpWasmAssetReader { let worker: web_sys::WorkerGlobalScope = global.unchecked_into(); worker.fetch_with_str(path.to_str().unwrap()) } else { - let error = std::io::Error::new( - std::io::ErrorKind::Other, - "Unsupported JavaScript global context", - ); + let error = std::io::Error::other("Unsupported JavaScript global context"); return Err(AssetReaderError::Io(error.into())); }; let resp_value = JsFuture::from(promise) diff --git a/crates/bevy_asset/src/server/mod.rs b/crates/bevy_asset/src/server/mod.rs index e5020dba75..0b64fe74e0 100644 --- a/crates/bevy_asset/src/server/mod.rs +++ b/crates/bevy_asset/src/server/mod.rs @@ -553,7 +553,9 @@ impl AssetServer { path: impl Into>, ) -> Result { let path: AssetPath = path.into(); - self.load_internal(None, path, false, None).await + self.load_internal(None, path, false, None) + .await + .map(|h| h.expect("handle must be returned, since we didn't pass in an input handle")) } pub(crate) fn load_unknown_type_with_meta_transform<'a>( @@ -643,21 +645,25 @@ impl AssetServer { /// Performs an async asset load. /// - /// `input_handle` must only be [`Some`] if `should_load` was true when retrieving `input_handle`. This is an optimization to - /// avoid looking up `should_load` twice, but it means you _must_ be sure a load is necessary when calling this function with [`Some`]. + /// `input_handle` must only be [`Some`] if `should_load` was true when retrieving + /// `input_handle`. This is an optimization to avoid looking up `should_load` twice, but it + /// means you _must_ be sure a load is necessary when calling this function with [`Some`]. + /// + /// Returns the handle of the asset if one was retrieved by this function. Otherwise, may return + /// [`None`]. async fn load_internal<'a>( &self, - mut input_handle: Option, + input_handle: Option, path: AssetPath<'a>, force: bool, meta_transform: Option, - ) -> Result { - let asset_type_id = input_handle.as_ref().map(UntypedHandle::type_id); + ) -> Result, AssetLoadError> { + let input_handle_type_id = input_handle.as_ref().map(UntypedHandle::type_id); let path = path.into_owned(); let path_clone = path.clone(); let (mut meta, loader, mut reader) = self - .get_meta_loader_and_reader(&path_clone, asset_type_id) + .get_meta_loader_and_reader(&path_clone, input_handle_type_id) .await .inspect_err(|e| { // if there was an input handle, a "load" operation has already started, so we must produce a "failure" event, if @@ -674,76 +680,90 @@ impl AssetServer { if let Some(meta_transform) = input_handle.as_ref().and_then(|h| h.meta_transform()) { (*meta_transform)(&mut *meta); } - // downgrade the input handle so we don't keep the asset alive just because we're loading it - // note we can't just pass a weak handle in, as only strong handles contain the asset meta transform - input_handle = input_handle.map(|h| h.clone_weak()); - // This contains Some(UntypedHandle), if it was retrievable - // If it is None, that is because it was _not_ retrievable, due to - // 1. The handle was not already passed in for this path, meaning we can't just use that - // 2. The asset has not been loaded yet, meaning there is no existing Handle for it - // 3. The path has a label, meaning the AssetLoader's root asset type is not the path's asset type - // - // In the None case, the only course of action is to wait for the asset to load so we can allocate the - // handle for that type. - // - // TODO: Note that in the None case, multiple asset loads for the same path can happen at the same time - // (rather than "early out-ing" in the "normal" case) - // This would be resolved by a universal asset id, as we would not need to resolve the asset type - // to generate the ID. See this issue: https://github.com/bevyengine/bevy/issues/10549 - let handle_result = match input_handle { - Some(handle) => { - // if a handle was passed in, the "should load" check was already done - Some((handle, true)) - } - None => { - let mut infos = self.data.infos.write(); - let result = infos.get_or_create_path_handle_internal( - path.clone(), - path.label().is_none().then(|| loader.asset_type_id()), - HandleLoadingMode::Request, - meta_transform, - ); - unwrap_with_context(result, Either::Left(loader.asset_type_name())) - } - }; + let asset_id; // The asset ID of the asset we are trying to load. + let fetched_handle; // The handle if one was looked up/created. + let should_load; // Whether we need to load the asset. + if let Some(input_handle) = input_handle { + asset_id = Some(input_handle.id()); + // In this case, we intentionally drop the input handle so we can cancel loading the + // asset if the handle gets dropped (externally) before it finishes loading. + fetched_handle = None; + // The handle was passed in, so the "should_load" check was already done. + should_load = true; + } else { + // TODO: multiple asset loads for the same path can happen at the same time (rather than + // "early out-ing" in the "normal" case). This would be resolved by a universal asset + // id, as we would not need to resolve the asset type to generate the ID. See this + // issue: https://github.com/bevyengine/bevy/issues/10549 - let handle = if let Some((handle, should_load)) = handle_result { - if path.label().is_none() && handle.type_id() != loader.asset_type_id() { + let mut infos = self.data.infos.write(); + let result = infos.get_or_create_path_handle_internal( + path.clone(), + path.label().is_none().then(|| loader.asset_type_id()), + HandleLoadingMode::Request, + meta_transform, + ); + match unwrap_with_context(result, Either::Left(loader.asset_type_name())) { + // We couldn't figure out the correct handle without its type ID (which can only + // happen if we are loading a subasset). + None => { + // We don't know the expected type since the subasset may have a different type + // than the "root" asset (which is the type the loader will load). + asset_id = None; + fetched_handle = None; + // If we couldn't find an appropriate handle, then the asset certainly needs to + // be loaded. + should_load = true; + } + Some((handle, result_should_load)) => { + asset_id = Some(handle.id()); + fetched_handle = Some(handle); + should_load = result_should_load; + } + } + } + // Verify that the expected type matches the loader's type. + if let Some(asset_type_id) = asset_id.map(|id| id.type_id()) { + // If we are loading a subasset, then the subasset's type almost certainly doesn't match + // the loader's type - and that's ok. + if path.label().is_none() && asset_type_id != loader.asset_type_id() { error!( "Expected {:?}, got {:?}", - handle.type_id(), + asset_type_id, loader.asset_type_id() ); return Err(AssetLoadError::RequestedHandleTypeMismatch { path: path.into_owned(), - requested: handle.type_id(), + requested: asset_type_id, actual_asset_name: loader.asset_type_name(), loader_name: loader.type_name(), }); } - if !should_load && !force { - return Ok(handle); - } - Some(handle) - } else { - None - }; - // if the handle result is None, we definitely need to load the asset + } + // Bail out earlier if we don't need to load the asset. + if !should_load && !force { + return Ok(fetched_handle); + } - let (base_handle, base_path) = if path.label().is_some() { + // We don't actually need to use _base_handle, but we do need to keep the handle alive. + // Dropping it would cancel the load of the base asset, which would make the load of this + // subasset never complete. + let (base_asset_id, _base_handle, base_path) = if path.label().is_some() { let mut infos = self.data.infos.write(); let base_path = path.without_label().into_owned(); - let (base_handle, _) = infos.get_or_create_path_handle_erased( - base_path.clone(), - loader.asset_type_id(), - Some(loader.asset_type_name()), - HandleLoadingMode::Force, - None, - ); - (base_handle, base_path) + let base_handle = infos + .get_or_create_path_handle_erased( + base_path.clone(), + loader.asset_type_id(), + Some(loader.asset_type_name()), + HandleLoadingMode::Force, + None, + ) + .0; + (base_handle.id(), Some(base_handle), base_path) } else { - (handle.clone().unwrap(), path.clone()) + (asset_id.unwrap(), None, path.clone()) }; match self @@ -760,7 +780,7 @@ impl AssetServer { Ok(loaded_asset) => { let final_handle = if let Some(label) = path.label_cow() { match loaded_asset.labeled_assets.get(&label) { - Some(labeled_asset) => labeled_asset.handle.clone(), + Some(labeled_asset) => Some(labeled_asset.handle.clone()), None => { let mut all_labels: Vec = loaded_asset .labeled_assets @@ -776,16 +796,15 @@ impl AssetServer { } } } else { - // if the path does not have a label, the handle must exist at this point - handle.unwrap() + fetched_handle }; - self.send_loaded_asset(base_handle.id(), loaded_asset); + self.send_loaded_asset(base_asset_id, loaded_asset); Ok(final_handle) } Err(err) => { self.send_asset_event(InternalAssetEvent::Failed { - id: base_handle.id(), + id: base_asset_id, error: err.clone(), path: path.into_owned(), }); diff --git a/crates/bevy_core_pipeline/src/auto_exposure/mod.rs b/crates/bevy_core_pipeline/src/auto_exposure/mod.rs index 172de3c393..7a33df99d8 100644 --- a/crates/bevy_core_pipeline/src/auto_exposure/mod.rs +++ b/crates/bevy_core_pipeline/src/auto_exposure/mod.rs @@ -4,7 +4,7 @@ use bevy_ecs::prelude::*; use bevy_render::{ extract_component::ExtractComponentPlugin, render_asset::RenderAssetPlugin, - render_graph::RenderGraphApp, + render_graph::RenderGraphExt, render_resource::{ Buffer, BufferDescriptor, BufferUsages, PipelineCache, SpecializedComputePipelines, }, diff --git a/crates/bevy_core_pipeline/src/auto_exposure/pipeline.rs b/crates/bevy_core_pipeline/src/auto_exposure/pipeline.rs index 28ed6b4ee8..4a2afa939e 100644 --- a/crates/bevy_core_pipeline/src/auto_exposure/pipeline.rs +++ b/crates/bevy_core_pipeline/src/auto_exposure/pipeline.rs @@ -10,6 +10,7 @@ use bevy_render::{ renderer::RenderDevice, view::ViewUniform, }; +use bevy_utils::default; use core::num::NonZero; #[derive(Resource)] @@ -82,12 +83,11 @@ impl SpecializedComputePipeline for AutoExposurePipeline { layout: vec![self.histogram_layout.clone()], shader: self.histogram_shader.clone(), shader_defs: vec![], - entry_point: match pass { + entry_point: Some(match pass { AutoExposurePass::Histogram => "compute_histogram".into(), AutoExposurePass::Average => "compute_average".into(), - }, - push_constant_ranges: vec![], - zero_initialize_workgroup_memory: false, + }), + ..default() } } } diff --git a/crates/bevy_core_pipeline/src/blit/mod.rs b/crates/bevy_core_pipeline/src/blit/mod.rs index 8dc655e91f..5acd98dd30 100644 --- a/crates/bevy_core_pipeline/src/blit/mod.rs +++ b/crates/bevy_core_pipeline/src/blit/mod.rs @@ -1,3 +1,4 @@ +use crate::FullscreenShader; use bevy_app::{App, Plugin}; use bevy_asset::{embedded_asset, load_embedded_asset, Handle}; use bevy_ecs::prelude::*; @@ -9,8 +10,7 @@ use bevy_render::{ renderer::RenderDevice, RenderApp, }; - -use crate::FullscreenShader; +use bevy_utils::default; /// Adds support for specialized "blit pipelines", which can be used to write one texture to another. pub struct BlitPlugin; @@ -85,22 +85,18 @@ impl SpecializedRenderPipeline for BlitPipeline { vertex: self.fullscreen_shader.to_vertex_state(), fragment: Some(FragmentState { shader: self.fragment_shader.clone(), - shader_defs: vec![], - entry_point: "fs_main".into(), targets: vec![Some(ColorTargetState { format: key.texture_format, blend: key.blend_state, write_mask: ColorWrites::ALL, })], + ..default() }), - primitive: PrimitiveState::default(), - depth_stencil: None, multisample: MultisampleState { count: key.samples, - ..Default::default() + ..default() }, - push_constant_ranges: Vec::new(), - zero_initialize_workgroup_memory: false, + ..default() } } } diff --git a/crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs b/crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs index 201d5a6cbd..aa8d3d37af 100644 --- a/crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs +++ b/crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs @@ -16,6 +16,7 @@ use bevy_render::{ }, renderer::RenderDevice, }; +use bevy_utils::default; #[derive(Component)] pub struct BloomDownsamplingPipelineIds { @@ -130,18 +131,14 @@ impl SpecializedRenderPipeline for BloomDownsamplingPipeline { fragment: Some(FragmentState { shader: self.fragment_shader.clone(), shader_defs, - entry_point, + entry_point: Some(entry_point), targets: vec![Some(ColorTargetState { format: BLOOM_TEXTURE_FORMAT, blend: None, write_mask: ColorWrites::ALL, })], }), - primitive: PrimitiveState::default(), - depth_stencil: None, - multisample: MultisampleState::default(), - push_constant_ranges: Vec::new(), - zero_initialize_workgroup_memory: false, + ..default() } } } diff --git a/crates/bevy_core_pipeline/src/bloom/mod.rs b/crates/bevy_core_pipeline/src/bloom/mod.rs index 901275e01c..d57af1cd01 100644 --- a/crates/bevy_core_pipeline/src/bloom/mod.rs +++ b/crates/bevy_core_pipeline/src/bloom/mod.rs @@ -20,7 +20,7 @@ use bevy_render::{ extract_component::{ ComponentUniforms, DynamicUniformIndex, ExtractComponentPlugin, UniformComponentPlugin, }, - render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner}, + render_graph::{NodeRunError, RenderGraphContext, RenderGraphExt, ViewNode, ViewNodeRunner}, render_resource::*, renderer::{RenderContext, RenderDevice}, texture::{CachedTexture, TextureCache}, diff --git a/crates/bevy_core_pipeline/src/bloom/upsampling_pipeline.rs b/crates/bevy_core_pipeline/src/bloom/upsampling_pipeline.rs index c49a9d5b16..4a5c4d50f9 100644 --- a/crates/bevy_core_pipeline/src/bloom/upsampling_pipeline.rs +++ b/crates/bevy_core_pipeline/src/bloom/upsampling_pipeline.rs @@ -18,6 +18,7 @@ use bevy_render::{ renderer::RenderDevice, view::ViewTarget, }; +use bevy_utils::default; #[derive(Component)] pub struct UpsamplingPipelineIds { @@ -115,8 +116,7 @@ impl SpecializedRenderPipeline for BloomUpsamplingPipeline { vertex: self.fullscreen_shader.to_vertex_state(), fragment: Some(FragmentState { shader: self.fragment_shader.clone(), - shader_defs: vec![], - entry_point: "upsample".into(), + entry_point: Some("upsample".into()), targets: vec![Some(ColorTargetState { format: texture_format, blend: Some(BlendState { @@ -129,12 +129,9 @@ impl SpecializedRenderPipeline for BloomUpsamplingPipeline { }), write_mask: ColorWrites::ALL, })], + ..default() }), - primitive: PrimitiveState::default(), - depth_stencil: None, - multisample: MultisampleState::default(), - push_constant_ranges: Vec::new(), - zero_initialize_workgroup_memory: false, + ..default() } } } diff --git a/crates/bevy_core_pipeline/src/core_2d/mod.rs b/crates/bevy_core_pipeline/src/core_2d/mod.rs index 94232fbb05..f50d3e5984 100644 --- a/crates/bevy_core_pipeline/src/core_2d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_2d/mod.rs @@ -52,7 +52,7 @@ use bevy_math::FloatOrd; use bevy_render::{ camera::{Camera, ExtractedCamera}, extract_component::ExtractComponentPlugin, - render_graph::{EmptyNode, RenderGraphApp, ViewNodeRunner}, + render_graph::{EmptyNode, RenderGraphExt, ViewNodeRunner}, render_phase::{ sort_phase_system, BinnedPhaseItem, CachedRenderPipelinePhaseItem, DrawFunctionId, DrawFunctions, PhaseItem, PhaseItemExtraIndex, SortedPhaseItem, ViewBinnedRenderPhases, diff --git a/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs b/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs index b19268ac1f..c5ee7a798d 100644 --- a/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs +++ b/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs @@ -4,7 +4,7 @@ use crate::{ }; use bevy_ecs::{prelude::World, query::QueryItem}; use bevy_render::{ - camera::ExtractedCamera, + camera::{ExtractedCamera, MainPassResolutionOverride}, diagnostic::RecordDiagnostics, render_graph::{NodeRunError, RenderGraphContext, ViewNode}, render_phase::{TrackedRenderPass, ViewBinnedRenderPhases}, @@ -31,6 +31,7 @@ impl ViewNode for MainOpaquePass3dNode { Option<&'static SkyboxPipelineId>, Option<&'static SkyboxBindGroup>, &'static ViewUniformOffset, + Option<&'static MainPassResolutionOverride>, ); fn run<'w>( @@ -45,6 +46,7 @@ impl ViewNode for MainOpaquePass3dNode { skybox_pipeline, skybox_bind_group, view_uniform_offset, + resolution_override, ): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { @@ -90,7 +92,7 @@ impl ViewNode for MainOpaquePass3dNode { let pass_span = diagnostics.pass_span(&mut render_pass, "main_opaque_pass_3d"); if let Some(viewport) = camera.viewport.as_ref() { - render_pass.set_camera_viewport(viewport); + render_pass.set_camera_viewport(&viewport.with_override(resolution_override)); } // Opaque draws diff --git a/crates/bevy_core_pipeline/src/core_3d/main_transmissive_pass_3d_node.rs b/crates/bevy_core_pipeline/src/core_3d/main_transmissive_pass_3d_node.rs index 8c656171e7..393167227f 100644 --- a/crates/bevy_core_pipeline/src/core_3d/main_transmissive_pass_3d_node.rs +++ b/crates/bevy_core_pipeline/src/core_3d/main_transmissive_pass_3d_node.rs @@ -3,7 +3,7 @@ use crate::core_3d::Transmissive3d; use bevy_ecs::{prelude::*, query::QueryItem}; use bevy_image::ToExtents; use bevy_render::{ - camera::ExtractedCamera, + camera::{ExtractedCamera, MainPassResolutionOverride}, render_graph::{NodeRunError, RenderGraphContext, ViewNode}, render_phase::ViewSortedRenderPhases, render_resource::{RenderPassDescriptor, StoreOp}, @@ -28,13 +28,16 @@ impl ViewNode for MainTransmissivePass3dNode { &'static ViewTarget, Option<&'static ViewTransmissionTexture>, &'static ViewDepthTexture, + Option<&'static MainPassResolutionOverride>, ); fn run( &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext, - (camera, view, camera_3d, target, transmission, depth): QueryItem, + (camera, view, camera_3d, target, transmission, depth, resolution_override): QueryItem< + Self::ViewQuery, + >, world: &World, ) -> Result<(), NodeRunError> { let view_entity = graph.view_entity(); @@ -108,7 +111,7 @@ impl ViewNode for MainTransmissivePass3dNode { render_context.begin_tracked_render_pass(render_pass_descriptor); if let Some(viewport) = camera.viewport.as_ref() { - render_pass.set_camera_viewport(viewport); + render_pass.set_camera_viewport(&viewport.with_override(resolution_override)); } if let Err(err) = transmissive_phase.render(&mut render_pass, world, view_entity) { diff --git a/crates/bevy_core_pipeline/src/core_3d/main_transparent_pass_3d_node.rs b/crates/bevy_core_pipeline/src/core_3d/main_transparent_pass_3d_node.rs index 36fe8417c4..0c70ec23a0 100644 --- a/crates/bevy_core_pipeline/src/core_3d/main_transparent_pass_3d_node.rs +++ b/crates/bevy_core_pipeline/src/core_3d/main_transparent_pass_3d_node.rs @@ -1,7 +1,7 @@ use crate::core_3d::Transparent3d; use bevy_ecs::{prelude::*, query::QueryItem}; use bevy_render::{ - camera::ExtractedCamera, + camera::{ExtractedCamera, MainPassResolutionOverride}, diagnostic::RecordDiagnostics, render_graph::{NodeRunError, RenderGraphContext, ViewNode}, render_phase::ViewSortedRenderPhases, @@ -24,12 +24,13 @@ impl ViewNode for MainTransparentPass3dNode { &'static ExtractedView, &'static ViewTarget, &'static ViewDepthTexture, + Option<&'static MainPassResolutionOverride>, ); fn run( &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext, - (camera, view, target, depth): QueryItem, + (camera, view, target, depth, resolution_override): QueryItem, world: &World, ) -> Result<(), NodeRunError> { let view_entity = graph.view_entity(); @@ -69,7 +70,7 @@ impl ViewNode for MainTransparentPass3dNode { let pass_span = diagnostics.pass_span(&mut render_pass, "main_transparent_pass_3d"); if let Some(viewport) = camera.viewport.as_ref() { - render_pass.set_camera_viewport(viewport); + render_pass.set_camera_viewport(&viewport.with_override(resolution_override)); } if let Err(err) = transparent_phase.render(&mut render_pass, world, view_entity) { diff --git a/crates/bevy_core_pipeline/src/core_3d/mod.rs b/crates/bevy_core_pipeline/src/core_3d/mod.rs index 6fcccd3a72..3a127631cc 100644 --- a/crates/bevy_core_pipeline/src/core_3d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_3d/mod.rs @@ -92,7 +92,7 @@ use bevy_render::{ camera::{Camera, ExtractedCamera}, extract_component::ExtractComponentPlugin, prelude::Msaa, - render_graph::{EmptyNode, RenderGraphApp, ViewNodeRunner}, + render_graph::{EmptyNode, RenderGraphExt, ViewNodeRunner}, render_phase::{ sort_phase_system, BinnedPhaseItem, CachedRenderPipelinePhaseItem, DrawFunctionId, DrawFunctions, PhaseItem, PhaseItemExtraIndex, SortedPhaseItem, ViewBinnedRenderPhases, @@ -1026,7 +1026,8 @@ pub fn prepare_prepass_textures( format: CORE_3D_DEPTH_FORMAT, usage: TextureUsages::COPY_DST | TextureUsages::RENDER_ATTACHMENT - | TextureUsages::TEXTURE_BINDING, + | TextureUsages::TEXTURE_BINDING + | TextureUsages::COPY_SRC, // TODO: Remove COPY_SRC, double buffer instead (for bevy_solari) view_formats: &[], }; texture_cache.get(&render_device, descriptor) @@ -1092,7 +1093,8 @@ pub fn prepare_prepass_textures( dimension: TextureDimension::D2, format: DEFERRED_PREPASS_FORMAT, usage: TextureUsages::RENDER_ATTACHMENT - | TextureUsages::TEXTURE_BINDING, + | TextureUsages::TEXTURE_BINDING + | TextureUsages::COPY_SRC, // TODO: Remove COPY_SRC, double buffer instead (for bevy_solari) view_formats: &[], }, ) diff --git a/crates/bevy_core_pipeline/src/deferred/copy_lighting_id.rs b/crates/bevy_core_pipeline/src/deferred/copy_lighting_id.rs index bfad57757f..5f930d85fd 100644 --- a/crates/bevy_core_pipeline/src/deferred/copy_lighting_id.rs +++ b/crates/bevy_core_pipeline/src/deferred/copy_lighting_id.rs @@ -15,13 +15,13 @@ use bevy_render::{ Render, RenderApp, RenderSystems, }; +use super::DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT; use bevy_ecs::query::QueryItem; use bevy_render::{ render_graph::{NodeRunError, RenderGraphContext, ViewNode}, renderer::RenderContext, }; - -use super::DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT; +use bevy_utils::default; pub struct CopyDeferredLightingIdPlugin; @@ -142,11 +142,8 @@ impl FromWorld for CopyDeferredLightingIdPipeline { vertex: vertex_state, fragment: Some(FragmentState { shader, - shader_defs: vec![], - entry_point: "fragment".into(), - targets: vec![], + ..default() }), - primitive: PrimitiveState::default(), depth_stencil: Some(DepthStencilState { format: DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT, depth_write_enabled: true, @@ -154,9 +151,7 @@ impl FromWorld for CopyDeferredLightingIdPipeline { stencil: StencilState::default(), bias: DepthBiasState::default(), }), - multisample: MultisampleState::default(), - push_constant_ranges: vec![], - zero_initialize_workgroup_memory: false, + ..default() }); Self { diff --git a/crates/bevy_core_pipeline/src/deferred/node.rs b/crates/bevy_core_pipeline/src/deferred/node.rs index e786d2a222..ab87fccee6 100644 --- a/crates/bevy_core_pipeline/src/deferred/node.rs +++ b/crates/bevy_core_pipeline/src/deferred/node.rs @@ -1,4 +1,5 @@ use bevy_ecs::{prelude::*, query::QueryItem}; +use bevy_render::camera::MainPassResolutionOverride; use bevy_render::experimental::occlusion_culling::OcclusionCulling; use bevy_render::render_graph::ViewNode; @@ -66,6 +67,7 @@ impl ViewNode for LateDeferredGBufferPrepassNode { &'static ExtractedView, &'static ViewDepthTexture, &'static ViewPrepassTextures, + Option<&'static MainPassResolutionOverride>, Has, Has, ); @@ -77,7 +79,7 @@ impl ViewNode for LateDeferredGBufferPrepassNode { view_query: QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { - let (_, _, _, _, occlusion_culling, no_indirect_drawing) = view_query; + let (.., occlusion_culling, no_indirect_drawing) = view_query; if !occlusion_culling || no_indirect_drawing { return Ok(()); } @@ -105,7 +107,7 @@ impl ViewNode for LateDeferredGBufferPrepassNode { fn run_deferred_prepass<'w>( graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - (camera, extracted_view, view_depth_texture, view_prepass_textures, _, _): QueryItem< + (camera, extracted_view, view_depth_texture, view_prepass_textures, resolution_override, _, _): QueryItem< 'w, '_, ::ViewQuery, @@ -220,7 +222,7 @@ fn run_deferred_prepass<'w>( }); let mut render_pass = TrackedRenderPass::new(&render_device, render_pass); if let Some(viewport) = camera.viewport.as_ref() { - render_pass.set_camera_viewport(viewport); + render_pass.set_camera_viewport(&viewport.with_override(resolution_override)); } // Opaque draws diff --git a/crates/bevy_core_pipeline/src/dof/mod.rs b/crates/bevy_core_pipeline/src/dof/mod.rs index 7e2f52e3fc..c3b39e8181 100644 --- a/crates/bevy_core_pipeline/src/dof/mod.rs +++ b/crates/bevy_core_pipeline/src/dof/mod.rs @@ -34,7 +34,7 @@ use bevy_render::{ camera::{PhysicalCameraParameters, Projection}, extract_component::{ComponentUniforms, DynamicUniformIndex, UniformComponentPlugin}, render_graph::{ - NodeRunError, RenderGraphApp as _, RenderGraphContext, ViewNode, ViewNodeRunner, + NodeRunError, RenderGraphContext, RenderGraphExt as _, ViewNode, ViewNodeRunner, }, render_resource::{ binding_types::{ @@ -800,23 +800,19 @@ impl SpecializedRenderPipeline for DepthOfFieldPipeline { RenderPipelineDescriptor { label: Some("depth of field pipeline".into()), layout, - push_constant_ranges: vec![], vertex: self.fullscreen_shader.to_vertex_state(), - primitive: default(), - depth_stencil: None, - multisample: default(), fragment: Some(FragmentState { shader: self.fragment_shader.clone(), shader_defs, - entry_point: match key.pass { + entry_point: Some(match key.pass { DofPass::GaussianHorizontal => "gaussian_horizontal".into(), DofPass::GaussianVertical => "gaussian_vertical".into(), DofPass::BokehPass0 => "bokeh_pass_0".into(), DofPass::BokehPass1 => "bokeh_pass_1".into(), - }, + }), targets, }), - zero_initialize_workgroup_memory: false, + ..default() } } } diff --git a/crates/bevy_core_pipeline/src/experimental/mip_generation/mod.rs b/crates/bevy_core_pipeline/src/experimental/mip_generation/mod.rs index f7df3ad1b6..1733743310 100644 --- a/crates/bevy_core_pipeline/src/experimental/mip_generation/mod.rs +++ b/crates/bevy_core_pipeline/src/experimental/mip_generation/mod.rs @@ -12,7 +12,7 @@ use crate::core_3d::{ prepare_core_3d_depth_textures, }; use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; +use bevy_asset::{load_internal_asset, uuid_handle, Handle}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ component::Component, @@ -30,7 +30,7 @@ use bevy_render::{ experimental::occlusion_culling::{ OcclusionCulling, OcclusionCullingSubview, OcclusionCullingSubviewEntities, }, - render_graph::{Node, NodeRunError, RenderGraphApp, RenderGraphContext}, + render_graph::{Node, NodeRunError, RenderGraphContext, RenderGraphExt}, render_resource::{ binding_types::{sampler, texture_2d, texture_2d_multisampled, texture_storage_2d}, BindGroup, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, @@ -46,12 +46,13 @@ use bevy_render::{ view::{ExtractedView, NoIndirectDrawing, ViewDepthTexture}, Render, RenderApp, RenderSystems, }; +use bevy_utils::default; use bitflags::bitflags; use tracing::debug; /// Identifies the `downsample_depth.wgsl` shader. pub const DOWNSAMPLE_DEPTH_SHADER_HANDLE: Handle = - weak_handle!("a09a149e-5922-4fa4-9170-3c1a13065364"); + uuid_handle!("a09a149e-5922-4fa4-9170-3c1a13065364"); /// The maximum number of mip levels that we can produce. /// @@ -492,12 +493,12 @@ impl SpecializedComputePipeline for DownsampleDepthPipeline { }], shader: DOWNSAMPLE_DEPTH_SHADER_HANDLE, shader_defs, - entry_point: if key.contains(DownsampleDepthPipelineKey::SECOND_PHASE) { + entry_point: Some(if key.contains(DownsampleDepthPipelineKey::SECOND_PHASE) { "downsample_depth_second".into() } else { "downsample_depth_first".into() - }, - zero_initialize_workgroup_memory: false, + }), + ..default() } } } diff --git a/crates/bevy_core_pipeline/src/fullscreen_vertex_shader/mod.rs b/crates/bevy_core_pipeline/src/fullscreen_vertex_shader/mod.rs index de8aa856c9..857412d8ce 100644 --- a/crates/bevy_core_pipeline/src/fullscreen_vertex_shader/mod.rs +++ b/crates/bevy_core_pipeline/src/fullscreen_vertex_shader/mod.rs @@ -33,7 +33,7 @@ impl FullscreenShader { VertexState { shader: self.0.clone(), shader_defs: Vec::new(), - entry_point: "fullscreen_vertex_shader".into(), + entry_point: Some("fullscreen_vertex_shader".into()), buffers: Vec::new(), } } diff --git a/crates/bevy_core_pipeline/src/motion_blur/mod.rs b/crates/bevy_core_pipeline/src/motion_blur/mod.rs index ecf6432c9f..a2b44704f6 100644 --- a/crates/bevy_core_pipeline/src/motion_blur/mod.rs +++ b/crates/bevy_core_pipeline/src/motion_blur/mod.rs @@ -18,7 +18,7 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ camera::Camera, extract_component::{ExtractComponent, ExtractComponentPlugin, UniformComponentPlugin}, - render_graph::{RenderGraphApp, ViewNodeRunner}, + render_graph::{RenderGraphExt, ViewNodeRunner}, render_resource::{ShaderType, SpecializedRenderPipelines}, Render, RenderApp, RenderSystems, }; diff --git a/crates/bevy_core_pipeline/src/motion_blur/pipeline.rs b/crates/bevy_core_pipeline/src/motion_blur/pipeline.rs index 9e36e508dc..904d6c6c54 100644 --- a/crates/bevy_core_pipeline/src/motion_blur/pipeline.rs +++ b/crates/bevy_core_pipeline/src/motion_blur/pipeline.rs @@ -1,3 +1,4 @@ +use crate::FullscreenShader; use bevy_asset::{load_embedded_asset, Handle}; use bevy_ecs::{ component::Component, @@ -16,16 +17,14 @@ use bevy_render::{ texture_depth_2d_multisampled, uniform_buffer_sized, }, BindGroupLayout, BindGroupLayoutEntries, CachedRenderPipelineId, ColorTargetState, - ColorWrites, FragmentState, MultisampleState, PipelineCache, PrimitiveState, - RenderPipelineDescriptor, Sampler, SamplerBindingType, SamplerDescriptor, Shader, - ShaderDefVal, ShaderStages, ShaderType, SpecializedRenderPipeline, - SpecializedRenderPipelines, TextureFormat, TextureSampleType, + ColorWrites, FragmentState, PipelineCache, RenderPipelineDescriptor, Sampler, + SamplerBindingType, SamplerDescriptor, Shader, ShaderDefVal, ShaderStages, ShaderType, + SpecializedRenderPipeline, SpecializedRenderPipelines, TextureFormat, TextureSampleType, }, renderer::RenderDevice, view::{ExtractedView, Msaa, ViewTarget}, }; - -use crate::FullscreenShader; +use bevy_utils::default; use super::MotionBlurUniform; @@ -139,7 +138,6 @@ impl SpecializedRenderPipeline for MotionBlurPipeline { fragment: Some(FragmentState { shader: self.fragment_shader.clone(), shader_defs, - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format: if key.hdr { ViewTarget::TEXTURE_FORMAT_HDR @@ -149,12 +147,9 @@ impl SpecializedRenderPipeline for MotionBlurPipeline { blend: None, write_mask: ColorWrites::ALL, })], + ..default() }), - primitive: PrimitiveState::default(), - depth_stencil: None, - multisample: MultisampleState::default(), - push_constant_ranges: vec![], - zero_initialize_workgroup_memory: false, + ..default() } } } diff --git a/crates/bevy_core_pipeline/src/msaa_writeback.rs b/crates/bevy_core_pipeline/src/msaa_writeback.rs index 5f82e10599..93116dc9fd 100644 --- a/crates/bevy_core_pipeline/src/msaa_writeback.rs +++ b/crates/bevy_core_pipeline/src/msaa_writeback.rs @@ -8,7 +8,7 @@ use bevy_color::LinearRgba; use bevy_ecs::{prelude::*, query::QueryItem}; use bevy_render::{ camera::ExtractedCamera, - render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner}, + render_graph::{NodeRunError, RenderGraphContext, RenderGraphExt, ViewNode, ViewNodeRunner}, render_resource::*, renderer::RenderContext, view::{Msaa, ViewTarget}, diff --git a/crates/bevy_core_pipeline/src/oit/mod.rs b/crates/bevy_core_pipeline/src/oit/mod.rs index 5b5d038fa0..088d491c75 100644 --- a/crates/bevy_core_pipeline/src/oit/mod.rs +++ b/crates/bevy_core_pipeline/src/oit/mod.rs @@ -10,7 +10,7 @@ use bevy_render::{ camera::{Camera, ExtractedCamera}, extract_component::{ExtractComponent, ExtractComponentPlugin}, load_shader_library, - render_graph::{RenderGraphApp, ViewNodeRunner}, + render_graph::{RenderGraphExt, ViewNodeRunner}, render_resource::{BufferUsages, BufferVec, DynamicUniformBuffer, ShaderType, TextureUsages}, renderer::{RenderDevice, RenderQueue}, view::Msaa, diff --git a/crates/bevy_core_pipeline/src/oit/resolve/mod.rs b/crates/bevy_core_pipeline/src/oit/resolve/mod.rs index 7067e5f83b..650b65f494 100644 --- a/crates/bevy_core_pipeline/src/oit/resolve/mod.rs +++ b/crates/bevy_core_pipeline/src/oit/resolve/mod.rs @@ -1,3 +1,4 @@ +use super::OitBuffers; use crate::{oit::OrderIndependentTransparencySettings, FullscreenShader}; use bevy_app::Plugin; use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer}; @@ -12,17 +13,16 @@ use bevy_render::{ binding_types::{storage_buffer_sized, texture_depth_2d, uniform_buffer}, BindGroup, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, BlendComponent, BlendState, CachedRenderPipelineId, ColorTargetState, ColorWrites, DownlevelFlags, - FragmentState, MultisampleState, PipelineCache, PrimitiveState, RenderPipelineDescriptor, - ShaderDefVal, ShaderStages, TextureFormat, + FragmentState, PipelineCache, RenderPipelineDescriptor, ShaderDefVal, ShaderStages, + TextureFormat, }, renderer::{RenderAdapter, RenderDevice}, view::{ExtractedView, ViewTarget, ViewUniform, ViewUniforms}, Render, RenderApp, RenderSystems, }; +use bevy_utils::default; use tracing::warn; -use super::OitBuffers; - /// Contains the render node used to run the resolve pass. pub mod node; @@ -213,7 +213,6 @@ fn specialize_oit_resolve_pipeline( resolve_pipeline.oit_depth_bind_group_layout.clone(), ], fragment: Some(FragmentState { - entry_point: "fragment".into(), shader: load_embedded_asset!(asset_server, "oit_resolve.wgsl"), shader_defs: vec![ShaderDefVal::UInt( "LAYER_COUNT".into(), @@ -227,13 +226,10 @@ fn specialize_oit_resolve_pipeline( }), write_mask: ColorWrites::ALL, })], + ..default() }), vertex: fullscreen_shader.to_vertex_state(), - primitive: PrimitiveState::default(), - depth_stencil: None, - multisample: MultisampleState::default(), - push_constant_ranges: vec![], - zero_initialize_workgroup_memory: false, + ..default() } } diff --git a/crates/bevy_core_pipeline/src/oit/resolve/node.rs b/crates/bevy_core_pipeline/src/oit/resolve/node.rs index 14d42235f1..77352e5ecb 100644 --- a/crates/bevy_core_pipeline/src/oit/resolve/node.rs +++ b/crates/bevy_core_pipeline/src/oit/resolve/node.rs @@ -1,6 +1,6 @@ use bevy_ecs::{prelude::*, query::QueryItem}; use bevy_render::{ - camera::ExtractedCamera, + camera::{ExtractedCamera, MainPassResolutionOverride}, render_graph::{NodeRunError, RenderGraphContext, RenderLabel, ViewNode}, render_resource::{BindGroupEntries, PipelineCache, RenderPassDescriptor}, renderer::RenderContext, @@ -23,13 +23,14 @@ impl ViewNode for OitResolveNode { &'static ViewUniformOffset, &'static OitResolvePipelineId, &'static ViewDepthTexture, + Option<&'static MainPassResolutionOverride>, ); fn run( &self, _graph: &mut RenderGraphContext, render_context: &mut RenderContext, - (camera, view_target, view_uniform, oit_resolve_pipeline_id, depth): QueryItem< + (camera, view_target, view_uniform, oit_resolve_pipeline_id, depth, resolution_override): QueryItem< Self::ViewQuery, >, world: &World, @@ -63,7 +64,7 @@ impl ViewNode for OitResolveNode { }); if let Some(viewport) = camera.viewport.as_ref() { - render_pass.set_camera_viewport(viewport); + render_pass.set_camera_viewport(&viewport.with_override(resolution_override)); } render_pass.set_render_pipeline(pipeline); diff --git a/crates/bevy_core_pipeline/src/post_process/mod.rs b/crates/bevy_core_pipeline/src/post_process/mod.rs index f7d2501b41..0077cebdf5 100644 --- a/crates/bevy_core_pipeline/src/post_process/mod.rs +++ b/crates/bevy_core_pipeline/src/post_process/mod.rs @@ -3,7 +3,7 @@ //! Currently, this consists only of chromatic aberration. use bevy_app::{App, Plugin}; -use bevy_asset::{embedded_asset, load_embedded_asset, weak_handle, Assets, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, uuid_handle, Assets, Handle}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ component::Component, @@ -23,7 +23,7 @@ use bevy_render::{ load_shader_library, render_asset::{RenderAssetUsages, RenderAssets}, render_graph::{ - NodeRunError, RenderGraphApp as _, RenderGraphContext, ViewNode, ViewNodeRunner, + NodeRunError, RenderGraphContext, RenderGraphExt as _, ViewNode, ViewNodeRunner, }, render_resource::{ binding_types::{sampler, texture_2d, uniform_buffer}, @@ -52,7 +52,7 @@ use crate::{ /// This is just a 3x1 image consisting of one red pixel, one green pixel, and /// one blue pixel, in that order. const DEFAULT_CHROMATIC_ABERRATION_LUT_HANDLE: Handle = - weak_handle!("dc3e3307-40a1-49bb-be6d-e0634e8836b2"); + uuid_handle!("dc3e3307-40a1-49bb-be6d-e0634e8836b2"); /// The default chromatic aberration intensity amount, in a fraction of the /// window size. @@ -326,19 +326,14 @@ impl SpecializedRenderPipeline for PostProcessingPipeline { vertex: self.fullscreen_shader.to_vertex_state(), fragment: Some(FragmentState { shader: self.fragment_shader.clone(), - shader_defs: vec![], - entry_point: "fragment_main".into(), targets: vec![Some(ColorTargetState { format: key.texture_format, blend: None, write_mask: ColorWrites::ALL, })], + ..default() }), - primitive: default(), - depth_stencil: None, - multisample: default(), - push_constant_ranges: vec![], - zero_initialize_workgroup_memory: false, + ..default() } } } diff --git a/crates/bevy_core_pipeline/src/prepass/node.rs b/crates/bevy_core_pipeline/src/prepass/node.rs index 500cc0a42b..c4cc7b1d55 100644 --- a/crates/bevy_core_pipeline/src/prepass/node.rs +++ b/crates/bevy_core_pipeline/src/prepass/node.rs @@ -1,6 +1,6 @@ use bevy_ecs::{prelude::*, query::QueryItem}; use bevy_render::{ - camera::ExtractedCamera, + camera::{ExtractedCamera, MainPassResolutionOverride}, diagnostic::RecordDiagnostics, experimental::occlusion_culling::OcclusionCulling, render_graph::{NodeRunError, RenderGraphContext, ViewNode}, @@ -55,18 +55,25 @@ pub struct LatePrepassNode; impl ViewNode for LatePrepassNode { type ViewQuery = ( - &'static ExtractedCamera, - &'static ExtractedView, - &'static ViewDepthTexture, - &'static ViewPrepassTextures, - &'static ViewUniformOffset, - Option<&'static DeferredPrepass>, - Option<&'static RenderSkyboxPrepassPipeline>, - Option<&'static SkyboxPrepassBindGroup>, - Option<&'static PreviousViewUniformOffset>, - Has, - Has, - Has, + ( + &'static ExtractedCamera, + &'static ExtractedView, + &'static ViewDepthTexture, + &'static ViewPrepassTextures, + &'static ViewUniformOffset, + ), + ( + Option<&'static DeferredPrepass>, + Option<&'static RenderSkyboxPrepassPipeline>, + Option<&'static SkyboxPrepassBindGroup>, + Option<&'static PreviousViewUniformOffset>, + Option<&'static MainPassResolutionOverride>, + ), + ( + Has, + Has, + Has, + ), ); fn run<'w>( @@ -78,7 +85,7 @@ impl ViewNode for LatePrepassNode { ) -> Result<(), NodeRunError> { // We only need a late prepass if we have occlusion culling and indirect // drawing. - let (_, _, _, _, _, _, _, _, _, occlusion_culling, no_indirect_drawing, _) = query; + let (_, _, (occlusion_culling, no_indirect_drawing, _)) = query; if !occlusion_culling || no_indirect_drawing { return Ok(()); } @@ -100,18 +107,15 @@ fn run_prepass<'w>( graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, ( - camera, - extracted_view, - view_depth_texture, - view_prepass_textures, - view_uniform_offset, - deferred_prepass, - skybox_prepass_pipeline, - skybox_prepass_bind_group, - view_prev_uniform_offset, - _, - _, - has_deferred, + (camera, extracted_view, view_depth_texture, view_prepass_textures, view_uniform_offset), + ( + deferred_prepass, + skybox_prepass_pipeline, + skybox_prepass_bind_group, + view_prev_uniform_offset, + resolution_override, + ), + (_, _, has_deferred), ): QueryItem<'w, '_, ::ViewQuery>, world: &'w World, label: &'static str, @@ -183,7 +187,7 @@ fn run_prepass<'w>( let pass_span = diagnostics.pass_span(&mut render_pass, label); if let Some(viewport) = camera.viewport.as_ref() { - render_pass.set_camera_viewport(viewport); + render_pass.set_camera_viewport(&viewport.with_override(resolution_override)); } // Opaque draws diff --git a/crates/bevy_core_pipeline/src/skybox/mod.rs b/crates/bevy_core_pipeline/src/skybox/mod.rs index 51c6934ece..40524cf221 100644 --- a/crates/bevy_core_pipeline/src/skybox/mod.rs +++ b/crates/bevy_core_pipeline/src/skybox/mod.rs @@ -28,6 +28,7 @@ use bevy_render::{ Render, RenderApp, RenderSystems, }; use bevy_transform::components::Transform; +use bevy_utils::default; use prepass::SkyboxPrepassPipeline; use crate::{core_3d::CORE_3D_DEPTH_FORMAT, prepass::PreviousViewUniforms}; @@ -192,14 +193,10 @@ impl SpecializedRenderPipeline for SkyboxPipeline { RenderPipelineDescriptor { label: Some("skybox_pipeline".into()), layout: vec![self.bind_group_layout.clone()], - push_constant_ranges: Vec::new(), vertex: VertexState { shader: self.shader.clone(), - shader_defs: Vec::new(), - entry_point: "skybox_vertex".into(), - buffers: Vec::new(), + ..default() }, - primitive: PrimitiveState::default(), depth_stencil: Some(DepthStencilState { format: key.depth_format, depth_write_enabled: false, @@ -223,8 +220,6 @@ impl SpecializedRenderPipeline for SkyboxPipeline { }, fragment: Some(FragmentState { shader: self.shader.clone(), - shader_defs: Vec::new(), - entry_point: "skybox_fragment".into(), targets: vec![Some(ColorTargetState { format: if key.hdr { ViewTarget::TEXTURE_FORMAT_HDR @@ -235,8 +230,9 @@ impl SpecializedRenderPipeline for SkyboxPipeline { blend: None, write_mask: ColorWrites::ALL, })], + ..default() }), - zero_initialize_workgroup_memory: false, + ..default() } } } diff --git a/crates/bevy_core_pipeline/src/skybox/prepass.rs b/crates/bevy_core_pipeline/src/skybox/prepass.rs index ad63339d05..2133e133fe 100644 --- a/crates/bevy_core_pipeline/src/skybox/prepass.rs +++ b/crates/bevy_core_pipeline/src/skybox/prepass.rs @@ -87,9 +87,7 @@ impl SpecializedRenderPipeline for SkyboxPrepassPipeline { RenderPipelineDescriptor { label: Some("skybox_prepass_pipeline".into()), layout: vec![self.bind_group_layout.clone()], - push_constant_ranges: vec![], vertex: self.fullscreen_shader.to_vertex_state(), - primitive: default(), depth_stencil: Some(DepthStencilState { format: CORE_3D_DEPTH_FORMAT, depth_write_enabled: false, @@ -104,11 +102,10 @@ impl SpecializedRenderPipeline for SkyboxPrepassPipeline { }, fragment: Some(FragmentState { shader: self.fragment_shader.clone(), - shader_defs: vec![], - entry_point: "fragment".into(), targets: prepass_target_descriptors(key.normal_prepass, true, false), + ..default() }), - zero_initialize_workgroup_memory: false, + ..default() } } } diff --git a/crates/bevy_core_pipeline/src/skybox/skybox_prepass.wgsl b/crates/bevy_core_pipeline/src/skybox/skybox_prepass.wgsl index 1ef8156fe3..e4ecb4703c 100644 --- a/crates/bevy_core_pipeline/src/skybox/skybox_prepass.wgsl +++ b/crates/bevy_core_pipeline/src/skybox/skybox_prepass.wgsl @@ -5,6 +5,9 @@ struct PreviousViewUniforms { view_from_world: mat4x4, clip_from_world: mat4x4, + clip_from_view: mat4x4, + world_from_clip: mat4x4, + view_from_clip: mat4x4, } @group(0) @binding(0) var view: View; diff --git a/crates/bevy_core_pipeline/src/tonemapping/mod.rs b/crates/bevy_core_pipeline/src/tonemapping/mod.rs index c57d49d32b..bd0004d342 100644 --- a/crates/bevy_core_pipeline/src/tonemapping/mod.rs +++ b/crates/bevy_core_pipeline/src/tonemapping/mod.rs @@ -279,18 +279,14 @@ impl SpecializedRenderPipeline for TonemappingPipeline { fragment: Some(FragmentState { shader: self.fragment_shader.clone(), shader_defs, - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format: ViewTarget::TEXTURE_FORMAT_HDR, blend: None, write_mask: ColorWrites::ALL, })], + ..default() }), - primitive: PrimitiveState::default(), - depth_stencil: None, - multisample: MultisampleState::default(), - push_constant_ranges: Vec::new(), - zero_initialize_workgroup_memory: false, + ..default() } } } diff --git a/crates/bevy_core_widgets/Cargo.toml b/crates/bevy_core_widgets/Cargo.toml index e93891d8f7..57e2968e22 100644 --- a/crates/bevy_core_widgets/Cargo.toml +++ b/crates/bevy_core_widgets/Cargo.toml @@ -3,7 +3,7 @@ name = "bevy_core_widgets" version = "0.17.0-dev" edition = "2024" description = "Unstyled common widgets for Bevy Engine" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] diff --git a/crates/bevy_core_widgets/src/callback.rs b/crates/bevy_core_widgets/src/callback.rs new file mode 100644 index 0000000000..37905e221c --- /dev/null +++ b/crates/bevy_core_widgets/src/callback.rs @@ -0,0 +1,113 @@ +use bevy_ecs::system::{Commands, SystemId, SystemInput}; +use bevy_ecs::world::{DeferredWorld, World}; + +/// A callback defines how we want to be notified when a widget changes state. Unlike an event +/// or observer, callbacks are intended for "point-to-point" communication that cuts across the +/// hierarchy of entities. Callbacks can be created in advance of the entity they are attached +/// to, and can be passed around as parameters. +/// +/// Example: +/// ``` +/// use bevy_app::App; +/// use bevy_core_widgets::{Callback, Notify}; +/// use bevy_ecs::system::{Commands, IntoSystem}; +/// +/// let mut app = App::new(); +/// +/// // Register a one-shot system +/// fn my_callback_system() { +/// println!("Callback executed!"); +/// } +/// +/// let system_id = app.world_mut().register_system(my_callback_system); +/// +/// // Wrap system in a callback +/// let callback = Callback::System(system_id); +/// +/// // Later, when we want to execute the callback: +/// app.world_mut().commands().notify(&callback); +/// ``` +#[derive(Default, Debug)] +pub enum Callback { + /// Invoke a one-shot system + System(SystemId), + /// Ignore this notification + #[default] + Ignore, +} + +/// Trait used to invoke a [`Callback`], unifying the API across callers. +pub trait Notify { + /// Invoke the callback with no arguments. + fn notify(&mut self, callback: &Callback<()>); + + /// Invoke the callback with one argument. + fn notify_with(&mut self, callback: &Callback, input: I::Inner<'static>) + where + I: SystemInput: Send> + 'static; +} + +impl<'w, 's> Notify for Commands<'w, 's> { + fn notify(&mut self, callback: &Callback<()>) { + match callback { + Callback::System(system_id) => self.run_system(*system_id), + Callback::Ignore => (), + } + } + + fn notify_with(&mut self, callback: &Callback, input: I::Inner<'static>) + where + I: SystemInput: Send> + 'static, + { + match callback { + Callback::System(system_id) => self.run_system_with(*system_id, input), + Callback::Ignore => (), + } + } +} + +impl Notify for World { + fn notify(&mut self, callback: &Callback<()>) { + match callback { + Callback::System(system_id) => { + let _ = self.run_system(*system_id); + } + Callback::Ignore => (), + } + } + + fn notify_with(&mut self, callback: &Callback, input: I::Inner<'static>) + where + I: SystemInput: Send> + 'static, + { + match callback { + Callback::System(system_id) => { + let _ = self.run_system_with(*system_id, input); + } + Callback::Ignore => (), + } + } +} + +impl Notify for DeferredWorld<'_> { + fn notify(&mut self, callback: &Callback<()>) { + match callback { + Callback::System(system_id) => { + self.commands().run_system(*system_id); + } + Callback::Ignore => (), + } + } + + fn notify_with(&mut self, callback: &Callback, input: I::Inner<'static>) + where + I: SystemInput: Send> + 'static, + { + match callback { + Callback::System(system_id) => { + self.commands().run_system_with(*system_id, input); + } + Callback::Ignore => (), + } + } +} diff --git a/crates/bevy_core_widgets/src/core_button.rs b/crates/bevy_core_widgets/src/core_button.rs index 97b15b878d..8c4ec9b22e 100644 --- a/crates/bevy_core_widgets/src/core_button.rs +++ b/crates/bevy_core_widgets/src/core_button.rs @@ -7,7 +7,7 @@ use bevy_ecs::{ entity::Entity, observer::On, query::With, - system::{Commands, Query, SystemId}, + system::{Commands, Query}, }; use bevy_input::keyboard::{KeyCode, KeyboardInput}; use bevy_input::ButtonState; @@ -15,16 +15,17 @@ use bevy_input_focus::FocusedInput; use bevy_picking::events::{Cancel, Click, DragEnd, Pointer, Press, Release}; use bevy_ui::{InteractionDisabled, Pressed}; +use crate::{Callback, Notify}; + /// Headless button widget. This widget maintains a "pressed" state, which is used to /// indicate whether the button is currently being pressed by the user. It emits a `ButtonClicked` /// event when the button is un-pressed. -#[derive(Component, Debug)] +#[derive(Component, Default, Debug)] #[require(AccessibilityNode(accesskit::Node::new(Role::Button)))] pub struct CoreButton { - /// Optional system to run when the button is clicked, or when the Enter or Space key - /// is pressed while the button is focused. If this field is `None`, the button will - /// emit a `ButtonClicked` event when clicked. - pub on_click: Option, + /// Callback to invoke when the button is clicked, or when the `Enter` or `Space` key + /// is pressed while the button is focused. + pub on_activate: Callback, } fn button_on_key_event( @@ -39,10 +40,8 @@ fn button_on_key_event( && event.state == ButtonState::Pressed && (event.key_code == KeyCode::Enter || event.key_code == KeyCode::Space) { - if let Some(on_click) = bstate.on_click { - trigger.propagate(false); - commands.run_system(on_click); - } + trigger.propagate(false); + commands.notify(&bstate.on_activate); } } } @@ -56,9 +55,7 @@ fn button_on_pointer_click( if let Ok((bstate, pressed, disabled)) = q_state.get_mut(trigger.target()) { trigger.propagate(false); if pressed && !disabled { - if let Some(on_click) = bstate.on_click { - commands.run_system(on_click); - } + commands.notify(&bstate.on_activate); } } } diff --git a/crates/bevy_core_widgets/src/core_checkbox.rs b/crates/bevy_core_widgets/src/core_checkbox.rs index fc12811055..05edc53c44 100644 --- a/crates/bevy_core_widgets/src/core_checkbox.rs +++ b/crates/bevy_core_widgets/src/core_checkbox.rs @@ -7,7 +7,7 @@ use bevy_ecs::system::{In, ResMut}; use bevy_ecs::{ component::Component, observer::On, - system::{Commands, Query, SystemId}, + system::{Commands, Query}, }; use bevy_input::keyboard::{KeyCode, KeyboardInput}; use bevy_input::ButtonState; @@ -15,11 +15,13 @@ use bevy_input_focus::{FocusedInput, InputFocus, InputFocusVisible}; use bevy_picking::events::{Click, Pointer}; use bevy_ui::{Checkable, Checked, InteractionDisabled}; +use crate::{Callback, Notify as _}; + /// Headless widget implementation for checkboxes. The [`Checked`] component represents the current /// state of the checkbox. The `on_change` field is an optional system id that will be run when the /// checkbox is clicked, or when the `Enter` or `Space` key is pressed while the checkbox is -/// focused. If the `on_change` field is `None`, then instead of calling a callback, the checkbox -/// will update its own [`Checked`] state directly. +/// focused. If the `on_change` field is `Callback::Ignore`, then instead of calling a callback, the +/// checkbox will update its own [`Checked`] state directly. /// /// # Toggle switches /// @@ -29,8 +31,10 @@ use bevy_ui::{Checkable, Checked, InteractionDisabled}; #[derive(Component, Debug, Default)] #[require(AccessibilityNode(accesskit::Node::new(Role::CheckBox)), Checkable)] pub struct CoreCheckbox { - /// One-shot system that is run when the checkbox state needs to be changed. - pub on_change: Option>>, + /// One-shot system that is run when the checkbox state needs to be changed. If this value is + /// `Callback::Ignore`, then the checkbox will update it's own internal [`Checked`] state + /// without notification. + pub on_change: Callback>, } fn checkbox_on_key_input( @@ -157,8 +161,8 @@ fn set_checkbox_state( checkbox: &CoreCheckbox, new_state: bool, ) { - if let Some(on_change) = checkbox.on_change { - commands.run_system_with(on_change, new_state); + if !matches!(checkbox.on_change, Callback::Ignore) { + commands.notify_with(&checkbox.on_change, new_state); } else if new_state { commands.entity(entity.into()).insert(Checked); } else { diff --git a/crates/bevy_core_widgets/src/core_radio.rs b/crates/bevy_core_widgets/src/core_radio.rs index d5dd18fb1a..a6c99a0d04 100644 --- a/crates/bevy_core_widgets/src/core_radio.rs +++ b/crates/bevy_core_widgets/src/core_radio.rs @@ -9,13 +9,15 @@ use bevy_ecs::{ entity::Entity, observer::On, query::With, - system::{Commands, Query, SystemId}, + system::{Commands, Query}, }; use bevy_input::keyboard::{KeyCode, KeyboardInput}; use bevy_input::ButtonState; use bevy_input_focus::FocusedInput; use bevy_picking::events::{Click, Pointer}; -use bevy_ui::{Checked, InteractionDisabled}; +use bevy_ui::{Checkable, Checked, InteractionDisabled}; + +use crate::{Callback, Notify}; /// Headless widget implementation for a "radio button group". This component is used to group /// multiple [`CoreRadio`] components together, allowing them to behave as a single unit. It @@ -36,7 +38,7 @@ use bevy_ui::{Checked, InteractionDisabled}; #[require(AccessibilityNode(accesskit::Node::new(Role::RadioGroup)))] pub struct CoreRadioGroup { /// Callback which is called when the selected radio button changes. - pub on_change: Option>>, + pub on_change: Callback>, } /// Headless widget implementation for radio buttons. These should be enclosed within a @@ -46,7 +48,7 @@ pub struct CoreRadioGroup { /// but rather the enclosing group should be focusable. /// See / #[derive(Component, Debug)] -#[require(AccessibilityNode(accesskit::Node::new(Role::RadioButton)), Checked)] +#[require(AccessibilityNode(accesskit::Node::new(Role::RadioButton)), Checkable)] pub struct CoreRadio; fn radio_group_on_key_input( @@ -131,9 +133,7 @@ fn radio_group_on_key_input( let (next_id, _) = radio_buttons[next_index]; // Trigger the on_change event for the newly checked radio button - if let Some(on_change) = on_change { - commands.run_system_with(*on_change, next_id); - } + commands.notify_with(on_change, next_id); } } } @@ -170,6 +170,11 @@ fn radio_group_on_button_click( } }; + // Radio button is disabled. + if q_radio.get(radio_id).unwrap().1 { + return; + } + // Gather all the enabled radio group descendants for exclusion. let radio_buttons = q_children .iter_descendants(ev.target()) @@ -196,9 +201,7 @@ fn radio_group_on_button_click( } // Trigger the on_change event for the newly checked radio button - if let Some(on_change) = on_change { - commands.run_system_with(*on_change, radio_id); - } + commands.notify_with(on_change, radio_id); } } diff --git a/crates/bevy_core_widgets/src/core_scrollbar.rs b/crates/bevy_core_widgets/src/core_scrollbar.rs new file mode 100644 index 0000000000..d997f565ce --- /dev/null +++ b/crates/bevy_core_widgets/src/core_scrollbar.rs @@ -0,0 +1,329 @@ +use bevy_app::{App, Plugin, PostUpdate}; +use bevy_ecs::{ + component::Component, + entity::Entity, + hierarchy::{ChildOf, Children}, + observer::On, + query::{With, Without}, + system::{Query, Res}, +}; +use bevy_math::Vec2; +use bevy_picking::events::{Cancel, Drag, DragEnd, DragStart, Pointer, Press}; +use bevy_ui::{ + ComputedNode, ComputedNodeTarget, Node, ScrollPosition, UiGlobalTransform, UiScale, Val, +}; + +/// Used to select the orientation of a scrollbar, slider, or other oriented control. +// TODO: Move this to a more central place. +#[derive(Debug, Default, Clone, Copy, PartialEq)] +pub enum ControlOrientation { + /// Horizontal orientation (stretching from left to right) + Horizontal, + /// Vertical orientation (stretching from top to bottom) + #[default] + Vertical, +} + +/// A headless scrollbar widget, which can be used to build custom scrollbars. +/// +/// Scrollbars operate differently than the other core widgets in a number of respects. +/// +/// Unlike sliders, scrollbars don't have an [`AccessibilityNode`](bevy_a11y::AccessibilityNode) +/// component, nor can they have keyboard focus. This is because scrollbars are usually used in +/// conjunction with a scrollable container, which is itself accessible and focusable. This also +/// means that scrollbars don't accept keyboard events, which is also the responsibility of the +/// scrollable container. +/// +/// Scrollbars don't emit notification events; instead they modify the scroll position of the target +/// entity directly. +/// +/// A scrollbar can have any number of child entities, but one entity must be the scrollbar thumb, +/// which is marked with the [`CoreScrollbarThumb`] component. Other children are ignored. The core +/// scrollbar will directly update the position and size of this entity; the application is free to +/// set any other style properties as desired. +/// +/// The application is free to position the scrollbars relative to the scrolling container however +/// it wants: it can overlay them on top of the scrolling content, or use a grid layout to displace +/// the content to make room for the scrollbars. +#[derive(Component, Debug)] +pub struct CoreScrollbar { + /// Entity being scrolled. + pub target: Entity, + /// Whether the scrollbar is vertical or horizontal. + pub orientation: ControlOrientation, + /// Minimum length of the scrollbar thumb, in pixel units, in the direction parallel to the main + /// scrollbar axis. The scrollbar will resize the thumb entity based on the proportion of + /// visible size to content size, but no smaller than this. This prevents the thumb from + /// disappearing in cases where the ratio of content size to visible size is large. + pub min_thumb_length: f32, +} + +/// Marker component to indicate that the entity is a scrollbar thumb (the moving, draggable part of +/// the scrollbar). This should be a child of the scrollbar entity. +#[derive(Component, Debug)] +#[require(CoreScrollbarDragState)] +pub struct CoreScrollbarThumb; + +impl CoreScrollbar { + /// Construct a new scrollbar. + /// + /// # Arguments + /// + /// * `target` - The scrollable entity that this scrollbar will control. + /// * `orientation` - The orientation of the scrollbar (horizontal or vertical). + /// * `min_thumb_length` - The minimum size of the scrollbar's thumb, in pixels. + pub fn new(target: Entity, orientation: ControlOrientation, min_thumb_length: f32) -> Self { + Self { + target, + orientation, + min_thumb_length, + } + } +} + +/// Component used to manage the state of a scrollbar during dragging. This component is +/// inserted on the thumb entity. +#[derive(Component, Default)] +pub struct CoreScrollbarDragState { + /// Whether the scrollbar is currently being dragged. + pub dragging: bool, + /// The value of the scrollbar when dragging started. + drag_origin: f32, +} + +fn scrollbar_on_pointer_down( + mut ev: On>, + q_thumb: Query<&ChildOf, With>, + mut q_scrollbar: Query<( + &CoreScrollbar, + &ComputedNode, + &ComputedNodeTarget, + &UiGlobalTransform, + )>, + mut q_scroll_pos: Query<(&mut ScrollPosition, &ComputedNode), Without>, + ui_scale: Res, +) { + if q_thumb.contains(ev.target()) { + // If they click on the thumb, do nothing. This will be handled by the drag event. + ev.propagate(false); + } else if let Ok((scrollbar, node, node_target, transform)) = q_scrollbar.get_mut(ev.target()) { + // If they click on the scrollbar track, page up or down. + ev.propagate(false); + + // Convert to widget-local coordinates. + let local_pos = transform.try_inverse().unwrap().transform_point2( + ev.event().pointer_location.position * node_target.scale_factor() / ui_scale.0, + ) + node.size() * 0.5; + + // Bail if we don't find the target entity. + let Ok((mut scroll_pos, scroll_content)) = q_scroll_pos.get_mut(scrollbar.target) else { + return; + }; + + // Convert the click coordinates into a scroll position. If it's greater than the + // current scroll position, scroll forward by one step (visible size) otherwise scroll + // back. + let visible_size = scroll_content.size() * scroll_content.inverse_scale_factor; + let content_size = scroll_content.content_size() * scroll_content.inverse_scale_factor; + let max_range = (content_size - visible_size).max(Vec2::ZERO); + + fn adjust_scroll_pos(scroll_pos: &mut f32, click_pos: f32, step: f32, range: f32) { + *scroll_pos = + (*scroll_pos + if click_pos > *scroll_pos { step } else { -step }).clamp(0., range); + } + + match scrollbar.orientation { + ControlOrientation::Horizontal => { + if node.size().x > 0. { + let click_pos = local_pos.x * content_size.x / node.size().x; + adjust_scroll_pos(&mut scroll_pos.x, click_pos, visible_size.x, max_range.x); + } + } + ControlOrientation::Vertical => { + if node.size().y > 0. { + let click_pos = local_pos.y * content_size.y / node.size().y; + adjust_scroll_pos(&mut scroll_pos.y, click_pos, visible_size.y, max_range.y); + } + } + } + } +} + +fn scrollbar_on_drag_start( + mut ev: On>, + mut q_thumb: Query<(&ChildOf, &mut CoreScrollbarDragState), With>, + q_scrollbar: Query<&CoreScrollbar>, + q_scroll_area: Query<&ScrollPosition>, +) { + if let Ok((ChildOf(thumb_parent), mut drag)) = q_thumb.get_mut(ev.target()) { + ev.propagate(false); + if let Ok(scrollbar) = q_scrollbar.get(*thumb_parent) { + if let Ok(scroll_area) = q_scroll_area.get(scrollbar.target) { + drag.dragging = true; + drag.drag_origin = match scrollbar.orientation { + ControlOrientation::Horizontal => scroll_area.x, + ControlOrientation::Vertical => scroll_area.y, + }; + } + } + } +} + +fn scrollbar_on_drag( + mut ev: On>, + mut q_thumb: Query<(&ChildOf, &mut CoreScrollbarDragState), With>, + mut q_scrollbar: Query<(&ComputedNode, &CoreScrollbar)>, + mut q_scroll_pos: Query<(&mut ScrollPosition, &ComputedNode), Without>, + ui_scale: Res, +) { + if let Ok((ChildOf(thumb_parent), drag)) = q_thumb.get_mut(ev.target()) { + if let Ok((node, scrollbar)) = q_scrollbar.get_mut(*thumb_parent) { + ev.propagate(false); + let Ok((mut scroll_pos, scroll_content)) = q_scroll_pos.get_mut(scrollbar.target) + else { + return; + }; + + if drag.dragging { + let distance = ev.event().distance / ui_scale.0; + let visible_size = scroll_content.size() * scroll_content.inverse_scale_factor; + let content_size = + scroll_content.content_size() * scroll_content.inverse_scale_factor; + let scrollbar_size = (node.size() * node.inverse_scale_factor).max(Vec2::ONE); + + match scrollbar.orientation { + ControlOrientation::Horizontal => { + let range = (content_size.x - visible_size.x).max(0.); + scroll_pos.x = (drag.drag_origin + + (distance.x * content_size.x) / scrollbar_size.x) + .clamp(0., range); + } + ControlOrientation::Vertical => { + let range = (content_size.y - visible_size.y).max(0.); + scroll_pos.y = (drag.drag_origin + + (distance.y * content_size.y) / scrollbar_size.y) + .clamp(0., range); + } + }; + } + } + } +} + +fn scrollbar_on_drag_end( + mut ev: On>, + mut q_thumb: Query<&mut CoreScrollbarDragState, With>, +) { + if let Ok(mut drag) = q_thumb.get_mut(ev.target()) { + ev.propagate(false); + if drag.dragging { + drag.dragging = false; + } + } +} + +fn scrollbar_on_drag_cancel( + mut ev: On>, + mut q_thumb: Query<&mut CoreScrollbarDragState, With>, +) { + if let Ok(mut drag) = q_thumb.get_mut(ev.target()) { + ev.propagate(false); + if drag.dragging { + drag.dragging = false; + } + } +} + +fn update_scrollbar_thumb( + q_scroll_area: Query<(&ScrollPosition, &ComputedNode)>, + q_scrollbar: Query<(&CoreScrollbar, &ComputedNode, &Children)>, + mut q_thumb: Query<&mut Node, With>, +) { + for (scrollbar, scrollbar_node, children) in q_scrollbar.iter() { + let Ok(scroll_area) = q_scroll_area.get(scrollbar.target) else { + continue; + }; + + // Size of the visible scrolling area. + let visible_size = scroll_area.1.size() * scroll_area.1.inverse_scale_factor; + + // Size of the scrolling content. + let content_size = scroll_area.1.content_size() * scroll_area.1.inverse_scale_factor; + + // Length of the scrollbar track. + let track_length = scrollbar_node.size() * scrollbar_node.inverse_scale_factor; + + fn size_and_pos( + content_size: f32, + visible_size: f32, + track_length: f32, + min_size: f32, + offset: f32, + ) -> (f32, f32) { + let thumb_size = if content_size > visible_size { + (track_length * visible_size / content_size) + .max(min_size) + .min(track_length) + } else { + track_length + }; + + let thumb_pos = if content_size > visible_size { + offset * (track_length - thumb_size) / (content_size - visible_size) + } else { + 0. + }; + + (thumb_size, thumb_pos) + } + + for child in children { + if let Ok(mut thumb) = q_thumb.get_mut(*child) { + match scrollbar.orientation { + ControlOrientation::Horizontal => { + let (thumb_size, thumb_pos) = size_and_pos( + content_size.x, + visible_size.x, + track_length.x, + scrollbar.min_thumb_length, + scroll_area.0.x, + ); + + thumb.top = Val::Px(0.); + thumb.bottom = Val::Px(0.); + thumb.left = Val::Px(thumb_pos); + thumb.width = Val::Px(thumb_size); + } + ControlOrientation::Vertical => { + let (thumb_size, thumb_pos) = size_and_pos( + content_size.y, + visible_size.y, + track_length.y, + scrollbar.min_thumb_length, + scroll_area.0.y, + ); + + thumb.left = Val::Px(0.); + thumb.right = Val::Px(0.); + thumb.top = Val::Px(thumb_pos); + thumb.height = Val::Px(thumb_size); + } + }; + } + } + } +} + +/// Plugin that adds the observers for the [`CoreScrollbar`] widget. +pub struct CoreScrollbarPlugin; + +impl Plugin for CoreScrollbarPlugin { + fn build(&self, app: &mut App) { + app.add_observer(scrollbar_on_pointer_down) + .add_observer(scrollbar_on_drag_start) + .add_observer(scrollbar_on_drag_end) + .add_observer(scrollbar_on_drag_cancel) + .add_observer(scrollbar_on_drag) + .add_systems(PostUpdate, update_scrollbar_thumb); + } +} diff --git a/crates/bevy_core_widgets/src/core_slider.rs b/crates/bevy_core_widgets/src/core_slider.rs index d85f12dd22..8a5e27f885 100644 --- a/crates/bevy_core_widgets/src/core_slider.rs +++ b/crates/bevy_core_widgets/src/core_slider.rs @@ -13,7 +13,7 @@ use bevy_ecs::{ component::Component, observer::On, query::With, - system::{Commands, Query, SystemId}, + system::{Commands, Query}, }; use bevy_input::keyboard::{KeyCode, KeyboardInput}; use bevy_input::ButtonState; @@ -22,6 +22,8 @@ use bevy_log::warn_once; use bevy_picking::events::{Drag, DragEnd, DragStart, Pointer, Press}; use bevy_ui::{ComputedNode, ComputedNodeTarget, InteractionDisabled, UiGlobalTransform, UiScale}; +use crate::{Callback, Notify}; + /// Defines how the slider should behave when you click on the track (not the thumb). #[derive(Debug, Default, PartialEq, Clone, Copy)] pub enum TrackClick { @@ -72,8 +74,9 @@ pub enum TrackClick { )] pub struct CoreSlider { /// Callback which is called when the slider is dragged or the value is changed via other user - /// interaction. If this value is `None`, then the slider will self-update. - pub on_change: Option>>, + /// interaction. If this value is `Callback::Ignore`, then the slider will update it's own + /// internal [`SliderValue`] state without notification. + pub on_change: Callback>, /// Set the track-clicking behavior for this slider. pub track_click: TrackClick, // TODO: Think about whether we want a "vertical" option. @@ -92,7 +95,9 @@ pub struct SliderValue(pub f32); #[derive(Component, Debug, PartialEq, Clone, Copy)] #[component(immutable)] pub struct SliderRange { + /// The beginning of the allowed range for the slider value. start: f32, + /// The end of the allowed range for the slider value. end: f32, } @@ -255,12 +260,12 @@ pub(crate) fn slider_on_pointer_down( TrackClick::Snap => click_val, }); - if let Some(on_change) = slider.on_change { - commands.run_system_with(on_change, new_value); - } else { + if matches!(slider.on_change, Callback::Ignore) { commands .entity(trigger.target()) .insert(SliderValue(new_value)); + } else { + commands.notify_with(&slider.on_change, new_value); } } } @@ -320,12 +325,12 @@ pub(crate) fn slider_on_drag( range.start() + span * 0.5 }; - if let Some(on_change) = slider.on_change { - commands.run_system_with(on_change, new_value); - } else { + if matches!(slider.on_change, Callback::Ignore) { commands .entity(trigger.target()) .insert(SliderValue(new_value)); + } else { + commands.notify_with(&slider.on_change, new_value); } } } @@ -367,12 +372,12 @@ fn slider_on_key_input( } }; trigger.propagate(false); - if let Some(on_change) = slider.on_change { - commands.run_system_with(on_change, new_value); - } else { + if matches!(slider.on_change, Callback::Ignore) { commands .entity(trigger.target()) .insert(SliderValue(new_value)); + } else { + commands.notify_with(&slider.on_change, new_value); } } } @@ -459,12 +464,12 @@ fn slider_on_set_value( range.clamp(value.0 + *delta * step.map(|s| s.0).unwrap_or_default()) } }; - if let Some(on_change) = slider.on_change { - commands.run_system_with(on_change, new_value); - } else { + if matches!(slider.on_change, Callback::Ignore) { commands .entity(trigger.target()) .insert(SliderValue(new_value)); + } else { + commands.notify_with(&slider.on_change, new_value); } } } diff --git a/crates/bevy_core_widgets/src/lib.rs b/crates/bevy_core_widgets/src/lib.rs index ef9f3db51c..2a3fc1ac09 100644 --- a/crates/bevy_core_widgets/src/lib.rs +++ b/crates/bevy_core_widgets/src/lib.rs @@ -14,16 +14,23 @@ // styled/opinionated widgets that use them. Components which are directly exposed to users above // the widget level, like `SliderValue`, should not have the `Core` prefix. +mod callback; mod core_button; mod core_checkbox; mod core_radio; +mod core_scrollbar; mod core_slider; use bevy_app::{App, Plugin}; +pub use callback::{Callback, Notify}; pub use core_button::{CoreButton, CoreButtonPlugin}; pub use core_checkbox::{CoreCheckbox, CoreCheckboxPlugin, SetChecked, ToggleChecked}; pub use core_radio::{CoreRadio, CoreRadioGroup, CoreRadioGroupPlugin}; +pub use core_scrollbar::{ + ControlOrientation, CoreScrollbar, CoreScrollbarDragState, CoreScrollbarPlugin, + CoreScrollbarThumb, +}; pub use core_slider::{ CoreSlider, CoreSliderDragState, CoreSliderPlugin, CoreSliderThumb, SetSliderValue, SliderRange, SliderStep, SliderValue, TrackClick, @@ -39,6 +46,7 @@ impl Plugin for CoreWidgetsPlugin { CoreButtonPlugin, CoreCheckboxPlugin, CoreRadioGroupPlugin, + CoreScrollbarPlugin, CoreSliderPlugin, )); } diff --git a/crates/bevy_ecs/macros/src/component.rs b/crates/bevy_ecs/macros/src/component.rs index 040c1b26b6..1322022581 100644 --- a/crates/bevy_ecs/macros/src/component.rs +++ b/crates/bevy_ecs/macros/src/component.rs @@ -40,19 +40,26 @@ pub fn derive_entity_event(input: TokenStream) -> TokenStream { let mut traversal: Type = parse_quote!(()); let bevy_ecs_path: Path = crate::bevy_ecs_path(); + let mut processed_attrs = Vec::new(); + ast.generics .make_where_clause() .predicates .push(parse_quote! { Self: Send + Sync + 'static }); - if let Some(attr) = ast.attrs.iter().find(|attr| attr.path().is_ident(EVENT)) { + for attr in ast.attrs.iter().filter(|attr| attr.path().is_ident(EVENT)) { if let Err(e) = attr.parse_nested_meta(|meta| match meta.path.get_ident() { + Some(ident) if processed_attrs.iter().any(|i| ident == i) => { + Err(meta.error(format!("duplicate attribute: {ident}"))) + } Some(ident) if ident == AUTO_PROPAGATE => { auto_propagate = true; + processed_attrs.push(AUTO_PROPAGATE); Ok(()) } Some(ident) if ident == TRAVERSAL => { traversal = meta.value()?.parse()?; + processed_attrs.push(TRAVERSAL); Ok(()) } Some(ident) => Err(meta.error(format!("unsupported attribute: {ident}"))), @@ -108,6 +115,7 @@ pub fn derive_resource(input: TokenStream) -> TokenStream { }) } +/// Component derive syntax is documented on both the macro and the trait. pub fn derive_component(input: TokenStream) -> TokenStream { let mut ast = parse_macro_input!(input as DeriveInput); let bevy_ecs_path: Path = crate::bevy_ecs_path(); @@ -446,7 +454,11 @@ pub const MAP_ENTITIES: &str = "map_entities"; pub const IMMUTABLE: &str = "immutable"; pub const CLONE_BEHAVIOR: &str = "clone_behavior"; -/// All allowed attribute value expression kinds for component hooks +/// All allowed attribute value expression kinds for component hooks. +/// This doesn't simply use general expressions because of conflicting needs: +/// - we want to be able to use `Self` & generic parameters in paths +/// - call expressions producing a closure need to be wrapped in a function +/// to turn them into function pointers, which prevents access to the outer generic params #[derive(Debug)] enum HookAttributeKind { /// expressions like function or struct names diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index 9bc3e5913e..20f7ad4275 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -560,7 +560,17 @@ pub fn derive_event(input: TokenStream) -> TokenStream { component::derive_event(input) } -/// Implement the `EntityEvent` trait. +/// Cheat sheet for derive syntax, +/// see full explanation on `EntityEvent` trait docs. +/// +/// ```ignore +/// #[derive(Event, EntityEvent)] +/// /// Traversal component +/// #[entity_event(traversal = &'static ChildOf)] +/// /// Always propagate +/// #[entity_event(auto_propagate)] +/// struct MyEvent; +/// ``` #[proc_macro_derive(EntityEvent, attributes(entity_event))] pub fn derive_entity_event(input: TokenStream) -> TokenStream { component::derive_entity_event(input) @@ -578,7 +588,89 @@ pub fn derive_resource(input: TokenStream) -> TokenStream { component::derive_resource(input) } -/// Implement the `Component` trait. +/// Cheat sheet for derive syntax, +/// see full explanation and examples on the `Component` trait doc. +/// +/// ## Immutability +/// ```ignore +/// #[derive(Component)] +/// #[component(immutable)] +/// struct MyComponent; +/// ``` +/// +/// ## Sparse instead of table-based storage +/// ```ignore +/// #[derive(Component)] +/// #[component(storage = "SparseSet")] +/// struct MyComponent; +/// ``` +/// +/// ## Required Components +/// +/// ```ignore +/// #[derive(Component)] +/// #[require( +/// // `Default::default()` +/// A, +/// // tuple structs +/// B(1), +/// // named-field structs +/// C { +/// x: 1, +/// ..default() +/// }, +/// // unit structs/variants +/// D::One, +/// // associated consts +/// E::ONE, +/// // constructors +/// F::new(1), +/// // arbitrary expressions +/// G = make(1, 2, 3) +/// )] +/// struct MyComponent; +/// ``` +/// +/// ## Relationships +/// ```ignore +/// #[derive(Component)] +/// #[relationship(relationship_target = Children)] +/// pub struct ChildOf { +/// // Marking the field is not necessary if there is only one. +/// #[relationship] +/// pub parent: Entity, +/// internal: u8, +/// }; +/// +/// #[derive(Component)] +/// #[relationship_target(relationship = ChildOf)] +/// pub struct Children(Vec); +/// ``` +/// +/// On despawn, also despawn all related entities: +/// ```ignore +/// #[derive(Component)] +/// #[relationship_target(relationship_target = Children, linked_spawn)] +/// pub struct Children(Vec); +/// ``` +/// +/// ## Hooks +/// ```ignore +/// #[derive(Component)] +/// #[component(hook_name = function)] +/// struct MyComponent; +/// ``` +/// where `hook_name` is `on_add`, `on_insert`, `on_replace` or `on_remove`; +/// `function` can be either a path, e.g. `some_function::`, +/// or a function call that returns a function that can be turned into +/// a `ComponentHook`, e.g. `get_closure("Hi!")`. +/// +/// ## Ignore this component when cloning an entity +/// ```ignore +/// #[derive(Component)] +/// #[component(clone_behavior = Ignore)] +/// struct MyComponent; +/// ``` #[proc_macro_derive( Component, attributes(component, require, relationship, relationship_target, entities) diff --git a/crates/bevy_ecs/src/entity/map_entities.rs b/crates/bevy_ecs/src/entity/map_entities.rs index 647bde983a..2c59655275 100644 --- a/crates/bevy_ecs/src/entity/map_entities.rs +++ b/crates/bevy_ecs/src/entity/map_entities.rs @@ -1,5 +1,5 @@ pub use bevy_ecs_macros::MapEntities; -use indexmap::IndexSet; +use indexmap::{IndexMap, IndexSet}; use crate::{ entity::{hash_map::EntityHashMap, Entity}, @@ -7,11 +7,14 @@ use crate::{ }; use alloc::{ - collections::{BTreeSet, VecDeque}, + collections::{BTreeMap, BTreeSet, VecDeque}, vec::Vec, }; -use bevy_platform::collections::HashSet; -use core::{hash::BuildHasher, mem}; +use bevy_platform::collections::{HashMap, HashSet}; +use core::{ + hash::{BuildHasher, Hash}, + mem, +}; use smallvec::SmallVec; use super::EntityIndexSet; @@ -72,9 +75,22 @@ impl MapEntities for Option { } } -impl MapEntities - for HashSet +impl MapEntities + for HashMap { + fn map_entities(&mut self, entity_mapper: &mut E) { + *self = self + .drain() + .map(|(mut key_entities, mut value_entities)| { + key_entities.map_entities(entity_mapper); + value_entities.map_entities(entity_mapper); + (key_entities, value_entities) + }) + .collect(); + } +} + +impl MapEntities for HashSet { fn map_entities(&mut self, entity_mapper: &mut E) { *self = self .drain() @@ -86,9 +102,22 @@ impl MapEntiti } } -impl MapEntities - for IndexSet +impl MapEntities + for IndexMap { + fn map_entities(&mut self, entity_mapper: &mut E) { + *self = self + .drain(..) + .map(|(mut key_entities, mut value_entities)| { + key_entities.map_entities(entity_mapper); + value_entities.map_entities(entity_mapper); + (key_entities, value_entities) + }) + .collect(); + } +} + +impl MapEntities for IndexSet { fn map_entities(&mut self, entity_mapper: &mut E) { *self = self .drain(..) @@ -109,6 +138,19 @@ impl MapEntities for EntityIndexSet { } } +impl MapEntities for BTreeMap { + fn map_entities(&mut self, entity_mapper: &mut E) { + *self = mem::take(self) + .into_iter() + .map(|(mut key_entities, mut value_entities)| { + key_entities.map_entities(entity_mapper); + value_entities.map_entities(entity_mapper); + (key_entities, value_entities) + }) + .collect(); + } +} + impl MapEntities for BTreeSet { fn map_entities(&mut self, entity_mapper: &mut E) { *self = mem::take(self) @@ -121,6 +163,14 @@ impl MapEntities for BTreeSet { } } +impl MapEntities for [T; N] { + fn map_entities(&mut self, entity_mapper: &mut E) { + for entities in self.iter_mut() { + entities.map_entities(entity_mapper); + } + } +} + impl MapEntities for Vec { fn map_entities(&mut self, entity_mapper: &mut E) { for entities in self.iter_mut() { diff --git a/crates/bevy_ecs/src/error/command_handling.rs b/crates/bevy_ecs/src/error/command_handling.rs index c303b76d17..13ec866ec1 100644 --- a/crates/bevy_ecs/src/error/command_handling.rs +++ b/crates/bevy_ecs/src/error/command_handling.rs @@ -20,6 +20,8 @@ pub trait HandleError: Send + 'static { /// Takes a [`Command`] that returns a Result and uses the default error handler function to convert it into /// a [`Command`] that internally handles an error if it occurs and returns `()`. fn handle_error(self) -> impl Command; + /// Takes a [`Command`] that returns a Result and ignores any error that occurs. + fn ignore_error(self) -> impl Command; } impl HandleError> for C @@ -50,6 +52,12 @@ where ), } } + + fn ignore_error(self) -> impl Command { + move |world: &mut World| { + let _ = self.apply(world); + } + } } impl HandleError for C @@ -68,6 +76,13 @@ where self.apply(world); } } + + #[inline] + fn ignore_error(self) -> impl Command { + move |world: &mut World| { + self.apply(world); + } + } } impl HandleError for C @@ -82,6 +97,10 @@ where fn handle_error(self) -> impl Command { self } + #[inline] + fn ignore_error(self) -> impl Command { + self + } } /// Passes in a specific entity to an [`EntityCommand`], resulting in a [`Command`] that diff --git a/crates/bevy_ecs/src/relationship/mod.rs b/crates/bevy_ecs/src/relationship/mod.rs index 8830998663..f95214262b 100644 --- a/crates/bevy_ecs/src/relationship/mod.rs +++ b/crates/bevy_ecs/src/relationship/mod.rs @@ -14,7 +14,7 @@ pub use relationship_source_collection::*; use crate::{ component::{Component, Mutable}, entity::{ComponentCloneCtx, Entity, SourceComponent}, - error::{ignore, CommandWithEntity, HandleError}, + error::CommandWithEntity, lifecycle::HookContext, world::{DeferredWorld, EntityWorldMut}, }; @@ -126,16 +126,18 @@ pub trait Relationship: Component + Sized { world.commands().entity(entity).remove::(); return; } - if let Ok(mut target_entity_mut) = world.get_entity_mut(target_entity) { - if let Some(mut relationship_target) = - target_entity_mut.get_mut::() - { - relationship_target.collection_mut_risky().add(entity); - } else { - let mut target = ::with_capacity(1); - target.collection_mut_risky().add(entity); - world.commands().entity(target_entity).insert(target); - } + if let Ok(mut entity_commands) = world.commands().get_entity(target_entity) { + // Deferring is necessary for batch mode + entity_commands + .entry::() + .and_modify(move |mut relationship_target| { + relationship_target.collection_mut_risky().add(entity); + }) + .or_insert_with(|| { + let mut target = Self::RelationshipTarget::with_capacity(1); + target.collection_mut_risky().add(entity); + target + }); } else { warn!( "{}The {}({target_entity:?}) relationship on entity {entity:?} relates to an entity that does not exist. The invalid {} relationship has been removed.", @@ -187,7 +189,7 @@ pub trait Relationship: Component + Sized { world .commands() - .queue(command.with_entity(target_entity).handle_error_with(ignore)); + .queue_silenced(command.with_entity(target_entity)); } } } @@ -244,7 +246,7 @@ pub trait RelationshipTarget: Component + Sized { for source_entity in relationship_target.iter() { commands .entity(source_entity) - .remove::(); + .try_remove::(); } } @@ -255,7 +257,7 @@ pub trait RelationshipTarget: Component + Sized { let (entities, mut commands) = world.entities_and_commands(); let relationship_target = entities.get(entity).unwrap().get::().unwrap(); for source_entity in relationship_target.iter() { - commands.entity(source_entity).despawn(); + commands.entity(source_entity).try_despawn(); } } @@ -323,6 +325,7 @@ pub enum RelationshipHookMode { #[cfg(test)] mod tests { + use crate::prelude::{ChildOf, Children}; use crate::world::World; use crate::{component::Component, entity::Entity}; use alloc::vec::Vec; @@ -418,8 +421,6 @@ mod tests { #[test] fn parent_child_relationship_with_custom_relationship() { - use crate::prelude::ChildOf; - #[derive(Component)] #[relationship(relationship_target = RelTarget)] struct Rel(Entity); @@ -474,4 +475,34 @@ mod tests { assert!(world.get_entity(child).is_err()); assert!(!world.entity(parent).contains::()); } + + #[test] + fn spawn_batch_with_relationship() { + let mut world = World::new(); + let parent = world.spawn_empty().id(); + let children = world + .spawn_batch((0..10).map(|_| ChildOf(parent))) + .collect::>(); + + for &child in &children { + assert!(world + .get::(child) + .is_some_and(|child_of| child_of.parent() == parent)); + } + assert!(world + .get::(parent) + .is_some_and(|children| children.len() == 10)); + } + + #[test] + fn insert_batch_with_relationship() { + let mut world = World::new(); + let parent = world.spawn_empty().id(); + let child = world.spawn_empty().id(); + world.insert_batch([(child, ChildOf(parent))]); + world.flush(); + + assert!(world.get::(child).is_some()); + assert!(world.get::(parent).is_some()); + } } diff --git a/crates/bevy_ecs/src/storage/resource.rs b/crates/bevy_ecs/src/storage/resource.rs index 29752ae2e5..1a90cc511c 100644 --- a/crates/bevy_ecs/src/storage/resource.rs +++ b/crates/bevy_ecs/src/storage/resource.rs @@ -64,32 +64,23 @@ impl ResourceData { /// If `SEND` is false, this will panic if called from a different thread than the one it was inserted from. #[inline] fn validate_access(&self) { - if SEND { - #[cfg_attr( - not(feature = "std"), - expect( - clippy::needless_return, - reason = "needless until no_std is addressed (see below)", - ) - )] - return; - } + if !SEND { + #[cfg(feature = "std")] + if self.origin_thread_id != Some(std::thread::current().id()) { + // Panic in tests, as testing for aborting is nearly impossible + panic!( + "Attempted to access or drop non-send resource {} from thread {:?} on a thread {:?}. This is not allowed. Aborting.", + self.type_name, + self.origin_thread_id, + std::thread::current().id() + ); + } - #[cfg(feature = "std")] - if self.origin_thread_id != Some(std::thread::current().id()) { - // Panic in tests, as testing for aborting is nearly impossible - panic!( - "Attempted to access or drop non-send resource {} from thread {:?} on a thread {:?}. This is not allowed. Aborting.", - self.type_name, - self.origin_thread_id, - std::thread::current().id() - ); + // TODO: Handle no_std non-send. + // Currently, no_std is single-threaded only, so this is safe to ignore. + // To support no_std multithreading, an alternative will be required. + // Remove the #[expect] attribute above when this is addressed. } - - // TODO: Handle no_std non-send. - // Currently, no_std is single-threaded only, so this is safe to ignore. - // To support no_std multithreading, an alternative will be required. - // Remove the #[expect] attribute above when this is addressed. } /// Returns true if the resource is populated. diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 0751e26770..e066446b51 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -19,7 +19,7 @@ use crate::{ change_detection::{MaybeLocation, Mut}, component::{Component, ComponentId, Mutable}, entity::{Entities, Entity, EntityClonerBuilder, EntityDoesNotExistError, OptIn, OptOut}, - error::{ignore, warn, BevyError, CommandWithEntity, ErrorContext, HandleError}, + error::{warn, BevyError, CommandWithEntity, ErrorContext, HandleError}, event::{BufferedEvent, EntityEvent, Event}, observer::{Observer, TriggerTargets}, resource::Resource, @@ -641,6 +641,11 @@ impl<'w, 's> Commands<'w, 's> { self.queue_internal(command.handle_error_with(error_handler)); } + /// Pushes a generic [`Command`] to the queue like [`Commands::queue_handled`], but instead silently ignores any errors. + pub fn queue_silenced + HandleError, T>(&mut self, command: C) { + self.queue_internal(command.ignore_error()); + } + fn queue_internal(&mut self, command: impl Command) { match &mut self.queue { InternalQueue::CommandQueue(queue) => { @@ -1466,12 +1471,11 @@ impl<'a> EntityCommands<'a> { component_id: ComponentId, value: T, ) -> &mut Self { - self.queue_handled( + self.queue_silenced( // SAFETY: // - `ComponentId` safety is ensured by the caller. // - `T` safety is ensured by the caller. unsafe { entity_command::insert_by_id(component_id, value, InsertMode::Replace) }, - ignore, ) } @@ -1523,7 +1527,7 @@ impl<'a> EntityCommands<'a> { /// ``` #[track_caller] pub fn try_insert(&mut self, bundle: impl Bundle) -> &mut Self { - self.queue_handled(entity_command::insert(bundle, InsertMode::Replace), ignore) + self.queue_silenced(entity_command::insert(bundle, InsertMode::Replace)) } /// Adds a [`Bundle`] of components to the entity if the predicate returns true. @@ -1579,7 +1583,7 @@ impl<'a> EntityCommands<'a> { /// the resulting error will be ignored. #[track_caller] pub fn try_insert_if_new(&mut self, bundle: impl Bundle) -> &mut Self { - self.queue_handled(entity_command::insert(bundle, InsertMode::Keep), ignore) + self.queue_silenced(entity_command::insert(bundle, InsertMode::Keep)) } /// Removes a [`Bundle`] of components from the entity. @@ -1724,7 +1728,7 @@ impl<'a> EntityCommands<'a> { /// # bevy_ecs::system::assert_is_system(remove_combat_stats_system); /// ``` pub fn try_remove(&mut self) -> &mut Self { - self.queue_handled(entity_command::remove::(), ignore) + self.queue_silenced(entity_command::remove::()) } /// Removes a [`Bundle`] of components from the entity, @@ -1818,7 +1822,7 @@ impl<'a> EntityCommands<'a> { /// /// For example, this will recursively despawn [`Children`](crate::hierarchy::Children). pub fn try_despawn(&mut self) { - self.queue_handled(entity_command::despawn(), ignore); + self.queue_silenced(entity_command::despawn()); } /// Pushes an [`EntityCommand`] to the queue, @@ -1907,6 +1911,18 @@ impl<'a> EntityCommands<'a> { self } + /// Pushes an [`EntityCommand`] to the queue, which will get executed for the current [`Entity`]. + /// + /// Unlike [`EntityCommands::queue_handled`], this will completely ignore any errors that occur. + pub fn queue_silenced + CommandWithEntity, T, M>( + &mut self, + command: C, + ) -> &mut Self { + self.commands + .queue_silenced(command.with_entity(self.entity)); + self + } + /// Removes all components except the given [`Bundle`] from the entity. /// /// # Example diff --git a/crates/bevy_feathers/Cargo.toml b/crates/bevy_feathers/Cargo.toml new file mode 100644 index 0000000000..3e1aded969 --- /dev/null +++ b/crates/bevy_feathers/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "bevy_feathers" +version = "0.17.0-dev" +edition = "2024" +description = "A collection of UI widgets for building editors and utilities in Bevy" +homepage = "https://bevy.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT OR Apache-2.0" +keywords = ["bevy"] + +[dependencies] +# bevy +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev" } +bevy_core_widgets = { path = "../bevy_core_widgets", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_input_focus = { path = "../bevy_input_focus", version = "0.17.0-dev" } +bevy_log = { path = "../bevy_log", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_picking = { path = "../bevy_picking", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_text = { path = "../bevy_text", version = "0.17.0-dev" } +bevy_ui = { path = "../bevy_ui", version = "0.17.0-dev", features = [ + "bevy_ui_picking_backend", +] } +bevy_window = { path = "../bevy_window", version = "0.17.0-dev" } +bevy_winit = { path = "../bevy_winit", version = "0.17.0-dev" } + +# other +accesskit = "0.19" + +[features] +default = [] + +[lints] +workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] +all-features = true diff --git a/crates/bevy_feathers/src/assets/fonts/FiraMono-LICENSE b/crates/bevy_feathers/src/assets/fonts/FiraMono-LICENSE new file mode 100644 index 0000000000..5e4608f24a --- /dev/null +++ b/crates/bevy_feathers/src/assets/fonts/FiraMono-LICENSE @@ -0,0 +1,93 @@ +Digitized data copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/crates/bevy_feathers/src/assets/fonts/FiraMono-Medium.ttf b/crates/bevy_feathers/src/assets/fonts/FiraMono-Medium.ttf new file mode 100755 index 0000000000..1e95ced4c4 Binary files /dev/null and b/crates/bevy_feathers/src/assets/fonts/FiraMono-Medium.ttf differ diff --git a/crates/bevy_feathers/src/assets/fonts/FiraSans-Bold.ttf b/crates/bevy_feathers/src/assets/fonts/FiraSans-Bold.ttf new file mode 100644 index 0000000000..e3593fb0f3 Binary files /dev/null and b/crates/bevy_feathers/src/assets/fonts/FiraSans-Bold.ttf differ diff --git a/crates/bevy_feathers/src/assets/fonts/FiraSans-BoldItalic.ttf b/crates/bevy_feathers/src/assets/fonts/FiraSans-BoldItalic.ttf new file mode 100644 index 0000000000..305b0b8bad Binary files /dev/null and b/crates/bevy_feathers/src/assets/fonts/FiraSans-BoldItalic.ttf differ diff --git a/crates/bevy_feathers/src/assets/fonts/FiraSans-Italic.ttf b/crates/bevy_feathers/src/assets/fonts/FiraSans-Italic.ttf new file mode 100644 index 0000000000..27d32ed961 Binary files /dev/null and b/crates/bevy_feathers/src/assets/fonts/FiraSans-Italic.ttf differ diff --git a/crates/bevy_feathers/src/assets/fonts/FiraSans-License.txt b/crates/bevy_feathers/src/assets/fonts/FiraSans-License.txt new file mode 100644 index 0000000000..0d0213ad4c --- /dev/null +++ b/crates/bevy_feathers/src/assets/fonts/FiraSans-License.txt @@ -0,0 +1,93 @@ +Copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/crates/bevy_feathers/src/assets/fonts/FiraSans-Regular.ttf b/crates/bevy_feathers/src/assets/fonts/FiraSans-Regular.ttf new file mode 100644 index 0000000000..6f80647494 Binary files /dev/null and b/crates/bevy_feathers/src/assets/fonts/FiraSans-Regular.ttf differ diff --git a/crates/bevy_feathers/src/constants.rs b/crates/bevy_feathers/src/constants.rs new file mode 100644 index 0000000000..651bb60b29 --- /dev/null +++ b/crates/bevy_feathers/src/constants.rs @@ -0,0 +1,29 @@ +//! Various non-themable constants for the Feathers look and feel. + +/// Font asset paths +pub mod fonts { + /// Default regular font path + pub const REGULAR: &str = "embedded://bevy_feathers/assets/fonts/FiraSans-Regular.ttf"; + /// Regular italic font path + pub const ITALIC: &str = "embedded://bevy_feathers/assets/fonts/FiraSans-Italic.ttf"; + /// Bold font path + pub const BOLD: &str = "embedded://bevy_feathers/assets/fonts/FiraSans-Bold.ttf"; + /// Bold italic font path + pub const BOLD_ITALIC: &str = "embedded://bevy_feathers/assets/fonts/FiraSans-BoldItalic.ttf"; + /// Monospace font path + pub const MONO: &str = "embedded://bevy_feathers/assets/fonts/FiraMono-Medium.ttf"; +} + +/// Size constants +pub mod size { + use bevy_ui::Val; + + /// Common row size for buttons, sliders, spinners, etc. + pub const ROW_HEIGHT: Val = Val::Px(24.0); + + /// Width and height of a checkbox + pub const CHECKBOX_SIZE: Val = Val::Px(18.0); + + /// Width and height of a radio button + pub const RADIO_SIZE: Val = Val::Px(18.0); +} diff --git a/crates/bevy_feathers/src/controls/button.rs b/crates/bevy_feathers/src/controls/button.rs new file mode 100644 index 0000000000..5b6ad7117b --- /dev/null +++ b/crates/bevy_feathers/src/controls/button.rs @@ -0,0 +1,208 @@ +use bevy_app::{Plugin, PreUpdate}; +use bevy_core_widgets::{Callback, CoreButton}; +use bevy_ecs::{ + bundle::Bundle, + component::Component, + entity::Entity, + hierarchy::{ChildOf, Children}, + lifecycle::RemovedComponents, + query::{Added, Changed, Has, Or}, + schedule::IntoScheduleConfigs, + spawn::{SpawnRelated, SpawnableList}, + system::{Commands, Query}, +}; +use bevy_input_focus::tab_navigation::TabIndex; +use bevy_picking::{hover::Hovered, PickingSystems}; +use bevy_ui::{AlignItems, InteractionDisabled, JustifyContent, Node, Pressed, UiRect, Val}; +use bevy_winit::cursor::CursorIcon; + +use crate::{ + constants::{fonts, size}, + font_styles::InheritableFont, + handle_or_path::HandleOrPath, + rounded_corners::RoundedCorners, + theme::{ThemeBackgroundColor, ThemeFontColor}, + tokens, +}; + +/// Color variants for buttons. This also functions as a component used by the dynamic styling +/// system to identify which entities are buttons. +#[derive(Component, Default, Clone)] +pub enum ButtonVariant { + /// The standard button appearance + #[default] + Normal, + /// A button with a more prominent color, this is used for "call to action" buttons, + /// default buttons for dialog boxes, and so on. + Primary, +} + +/// Parameters for the button template, passed to [`button`] function. +#[derive(Default)] +pub struct ButtonProps { + /// Color variant for the button. + pub variant: ButtonVariant, + /// Rounded corners options + pub corners: RoundedCorners, + /// Click handler + pub on_click: Callback, +} + +/// Template function to spawn a button. +/// +/// # Arguments +/// * `props` - construction properties for the button. +/// * `overrides` - a bundle of components that are merged in with the normal button components. +/// * `children` - a [`SpawnableList`] of child elements, such as a label or icon for the button. +pub fn button + Send + Sync + 'static, B: Bundle>( + props: ButtonProps, + overrides: B, + children: C, +) -> impl Bundle { + ( + Node { + height: size::ROW_HEIGHT, + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + padding: UiRect::axes(Val::Px(8.0), Val::Px(0.)), + flex_grow: 1.0, + ..Default::default() + }, + CoreButton { + on_activate: props.on_click, + }, + props.variant, + Hovered::default(), + CursorIcon::System(bevy_window::SystemCursorIcon::Pointer), + TabIndex(0), + props.corners.to_border_radius(4.0), + ThemeBackgroundColor(tokens::BUTTON_BG), + ThemeFontColor(tokens::BUTTON_TEXT), + InheritableFont { + font: HandleOrPath::Path(fonts::REGULAR.to_owned()), + font_size: 14.0, + }, + overrides, + Children::spawn(children), + ) +} + +fn update_button_styles( + q_buttons: Query< + ( + Entity, + &ButtonVariant, + Has, + Has, + &Hovered, + &ThemeBackgroundColor, + &ThemeFontColor, + ), + Or<(Changed, Added, Added)>, + >, + mut commands: Commands, +) { + for (button_ent, variant, disabled, pressed, hovered, bg_color, font_color) in q_buttons.iter() + { + set_button_colors( + button_ent, + variant, + disabled, + pressed, + hovered.0, + bg_color, + font_color, + &mut commands, + ); + } +} + +fn update_button_styles_remove( + q_buttons: Query<( + Entity, + &ButtonVariant, + Has, + Has, + &Hovered, + &ThemeBackgroundColor, + &ThemeFontColor, + )>, + mut removed_disabled: RemovedComponents, + mut removed_pressed: RemovedComponents, + mut commands: Commands, +) { + removed_disabled + .read() + .chain(removed_pressed.read()) + .for_each(|ent| { + if let Ok((button_ent, variant, disabled, pressed, hovered, bg_color, font_color)) = + q_buttons.get(ent) + { + set_button_colors( + button_ent, + variant, + disabled, + pressed, + hovered.0, + bg_color, + font_color, + &mut commands, + ); + } + }); +} + +fn set_button_colors( + button_ent: Entity, + variant: &ButtonVariant, + disabled: bool, + pressed: bool, + hovered: bool, + bg_color: &ThemeBackgroundColor, + font_color: &ThemeFontColor, + commands: &mut Commands, +) { + let bg_token = match (variant, disabled, pressed, hovered) { + (ButtonVariant::Normal, true, _, _) => tokens::BUTTON_BG_DISABLED, + (ButtonVariant::Normal, false, true, _) => tokens::BUTTON_BG_PRESSED, + (ButtonVariant::Normal, false, false, true) => tokens::BUTTON_BG_HOVER, + (ButtonVariant::Normal, false, false, false) => tokens::BUTTON_BG, + (ButtonVariant::Primary, true, _, _) => tokens::BUTTON_PRIMARY_BG_DISABLED, + (ButtonVariant::Primary, false, true, _) => tokens::BUTTON_PRIMARY_BG_PRESSED, + (ButtonVariant::Primary, false, false, true) => tokens::BUTTON_PRIMARY_BG_HOVER, + (ButtonVariant::Primary, false, false, false) => tokens::BUTTON_PRIMARY_BG, + }; + + let font_color_token = match (variant, disabled) { + (ButtonVariant::Normal, true) => tokens::BUTTON_TEXT_DISABLED, + (ButtonVariant::Normal, false) => tokens::BUTTON_TEXT, + (ButtonVariant::Primary, true) => tokens::BUTTON_PRIMARY_TEXT_DISABLED, + (ButtonVariant::Primary, false) => tokens::BUTTON_PRIMARY_TEXT, + }; + + // Change background color + if bg_color.0 != bg_token { + commands + .entity(button_ent) + .insert(ThemeBackgroundColor(bg_token)); + } + + // Change font color + if font_color.0 != font_color_token { + commands + .entity(button_ent) + .insert(ThemeFontColor(font_color_token)); + } +} + +/// Plugin which registers the systems for updating the button styles. +pub struct ButtonPlugin; + +impl Plugin for ButtonPlugin { + fn build(&self, app: &mut bevy_app::App) { + app.add_systems( + PreUpdate, + (update_button_styles, update_button_styles_remove).in_set(PickingSystems::Last), + ); + } +} diff --git a/crates/bevy_feathers/src/controls/checkbox.rs b/crates/bevy_feathers/src/controls/checkbox.rs new file mode 100644 index 0000000000..6e4235961a --- /dev/null +++ b/crates/bevy_feathers/src/controls/checkbox.rs @@ -0,0 +1,304 @@ +use bevy_app::{Plugin, PreUpdate}; +use bevy_core_widgets::{Callback, CoreCheckbox}; +use bevy_ecs::{ + bundle::Bundle, + children, + component::Component, + entity::Entity, + hierarchy::{ChildOf, Children}, + lifecycle::RemovedComponents, + query::{Added, Changed, Has, Or, With}, + schedule::IntoScheduleConfigs, + spawn::{Spawn, SpawnRelated, SpawnableList}, + system::{Commands, In, Query}, +}; +use bevy_input_focus::tab_navigation::TabIndex; +use bevy_math::Rot2; +use bevy_picking::{hover::Hovered, PickingSystems}; +use bevy_render::view::Visibility; +use bevy_ui::{ + AlignItems, BorderRadius, Checked, Display, FlexDirection, InteractionDisabled, JustifyContent, + Node, PositionType, UiRect, UiTransform, Val, +}; +use bevy_winit::cursor::CursorIcon; + +use crate::{ + constants::{fonts, size}, + font_styles::InheritableFont, + handle_or_path::HandleOrPath, + theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor}, + tokens, +}; + +/// Parameters for the checkbox template, passed to [`checkbox`] function. +#[derive(Default)] +pub struct CheckboxProps { + /// Change handler + pub on_change: Callback>, +} + +/// Marker for the checkbox outline +#[derive(Component, Default, Clone)] +struct CheckboxOutline; + +/// Marker for the checkbox check mark +#[derive(Component, Default, Clone)] +struct CheckboxMark; + +/// Template function to spawn a checkbox. +/// +/// # Arguments +/// * `props` - construction properties for the checkbox. +/// * `overrides` - a bundle of components that are merged in with the normal checkbox components. +/// * `label` - the label of the checkbox. +pub fn checkbox + Send + Sync + 'static, B: Bundle>( + props: CheckboxProps, + overrides: B, + label: C, +) -> impl Bundle { + ( + Node { + display: Display::Flex, + flex_direction: FlexDirection::Row, + justify_content: JustifyContent::Start, + align_items: AlignItems::Center, + column_gap: Val::Px(4.0), + ..Default::default() + }, + CoreCheckbox { + on_change: props.on_change, + }, + Hovered::default(), + CursorIcon::System(bevy_window::SystemCursorIcon::Pointer), + TabIndex(0), + ThemeFontColor(tokens::CHECKBOX_TEXT), + InheritableFont { + font: HandleOrPath::Path(fonts::REGULAR.to_owned()), + font_size: 14.0, + }, + overrides, + Children::spawn(( + Spawn(( + Node { + width: size::CHECKBOX_SIZE, + height: size::CHECKBOX_SIZE, + border: UiRect::all(Val::Px(2.0)), + ..Default::default() + }, + CheckboxOutline, + BorderRadius::all(Val::Px(4.0)), + ThemeBackgroundColor(tokens::CHECKBOX_BG), + ThemeBorderColor(tokens::CHECKBOX_BORDER), + children![( + // Cheesy checkmark: rotated node with L-shaped border. + Node { + position_type: PositionType::Absolute, + left: Val::Px(4.0), + top: Val::Px(0.0), + width: Val::Px(6.), + height: Val::Px(11.), + border: UiRect { + bottom: Val::Px(2.0), + right: Val::Px(2.0), + ..Default::default() + }, + ..Default::default() + }, + UiTransform::from_rotation(Rot2::FRAC_PI_4), + CheckboxMark, + ThemeBorderColor(tokens::CHECKBOX_MARK), + )], + )), + label, + )), + ) +} + +fn update_checkbox_styles( + q_checkboxes: Query< + ( + Entity, + Has, + Has, + &Hovered, + &ThemeFontColor, + ), + ( + With, + Or<(Changed, Added, Added)>, + ), + >, + q_children: Query<&Children>, + mut q_outline: Query<(&ThemeBackgroundColor, &ThemeBorderColor), With>, + mut q_mark: Query<&ThemeBorderColor, With>, + mut commands: Commands, +) { + for (checkbox_ent, disabled, checked, hovered, font_color) in q_checkboxes.iter() { + let Some(outline_ent) = q_children + .iter_descendants(checkbox_ent) + .find(|en| q_outline.contains(*en)) + else { + continue; + }; + let Some(mark_ent) = q_children + .iter_descendants(checkbox_ent) + .find(|en| q_mark.contains(*en)) + else { + continue; + }; + let (outline_bg, outline_border) = q_outline.get_mut(outline_ent).unwrap(); + let mark_color = q_mark.get_mut(mark_ent).unwrap(); + set_checkbox_colors( + checkbox_ent, + outline_ent, + mark_ent, + disabled, + checked, + hovered.0, + outline_bg, + outline_border, + mark_color, + font_color, + &mut commands, + ); + } +} + +fn update_checkbox_styles_remove( + q_checkboxes: Query< + ( + Entity, + Has, + Has, + &Hovered, + &ThemeFontColor, + ), + With, + >, + q_children: Query<&Children>, + mut q_outline: Query<(&ThemeBackgroundColor, &ThemeBorderColor), With>, + mut q_mark: Query<&ThemeBorderColor, With>, + mut removed_disabled: RemovedComponents, + mut removed_checked: RemovedComponents, + mut commands: Commands, +) { + removed_disabled + .read() + .chain(removed_checked.read()) + .for_each(|ent| { + if let Ok((checkbox_ent, disabled, checked, hovered, font_color)) = + q_checkboxes.get(ent) + { + let Some(outline_ent) = q_children + .iter_descendants(checkbox_ent) + .find(|en| q_outline.contains(*en)) + else { + return; + }; + let Some(mark_ent) = q_children + .iter_descendants(checkbox_ent) + .find(|en| q_mark.contains(*en)) + else { + return; + }; + let (outline_bg, outline_border) = q_outline.get_mut(outline_ent).unwrap(); + let mark_color = q_mark.get_mut(mark_ent).unwrap(); + set_checkbox_colors( + checkbox_ent, + outline_ent, + mark_ent, + disabled, + checked, + hovered.0, + outline_bg, + outline_border, + mark_color, + font_color, + &mut commands, + ); + } + }); +} + +fn set_checkbox_colors( + checkbox_ent: Entity, + outline_ent: Entity, + mark_ent: Entity, + disabled: bool, + checked: bool, + hovered: bool, + outline_bg: &ThemeBackgroundColor, + outline_border: &ThemeBorderColor, + mark_color: &ThemeBorderColor, + font_color: &ThemeFontColor, + commands: &mut Commands, +) { + let outline_border_token = match (disabled, hovered) { + (true, _) => tokens::CHECKBOX_BORDER_DISABLED, + (false, true) => tokens::CHECKBOX_BORDER_HOVER, + _ => tokens::CHECKBOX_BORDER, + }; + + let outline_bg_token = match (disabled, checked) { + (true, true) => tokens::CHECKBOX_BG_CHECKED_DISABLED, + (true, false) => tokens::CHECKBOX_BG_DISABLED, + (false, true) => tokens::CHECKBOX_BG_CHECKED, + (false, false) => tokens::CHECKBOX_BG, + }; + + let mark_token = match disabled { + true => tokens::CHECKBOX_MARK_DISABLED, + false => tokens::CHECKBOX_MARK, + }; + + let font_color_token = match disabled { + true => tokens::CHECKBOX_TEXT_DISABLED, + false => tokens::CHECKBOX_TEXT, + }; + + // Change outline background + if outline_bg.0 != outline_bg_token { + commands + .entity(outline_ent) + .insert(ThemeBackgroundColor(outline_bg_token)); + } + + // Change outline border + if outline_border.0 != outline_border_token { + commands + .entity(outline_ent) + .insert(ThemeBorderColor(outline_border_token)); + } + + // Change mark color + if mark_color.0 != mark_token { + commands + .entity(mark_ent) + .insert(ThemeBorderColor(mark_token)); + } + + // Change mark visibility + commands.entity(mark_ent).insert(match checked { + true => Visibility::Visible, + false => Visibility::Hidden, + }); + + // Change font color + if font_color.0 != font_color_token { + commands + .entity(checkbox_ent) + .insert(ThemeFontColor(font_color_token)); + } +} + +/// Plugin which registers the systems for updating the checkbox styles. +pub struct CheckboxPlugin; + +impl Plugin for CheckboxPlugin { + fn build(&self, app: &mut bevy_app::App) { + app.add_systems( + PreUpdate, + (update_checkbox_styles, update_checkbox_styles_remove).in_set(PickingSystems::Last), + ); + } +} diff --git a/crates/bevy_feathers/src/controls/mod.rs b/crates/bevy_feathers/src/controls/mod.rs new file mode 100644 index 0000000000..92c5a76907 --- /dev/null +++ b/crates/bevy_feathers/src/controls/mod.rs @@ -0,0 +1,21 @@ +//! Meta-module containing all feathers controls (widgets that are interactive). +use bevy_app::Plugin; + +mod button; +mod checkbox; +mod radio; +mod slider; + +pub use button::{button, ButtonPlugin, ButtonProps, ButtonVariant}; +pub use checkbox::{checkbox, CheckboxPlugin, CheckboxProps}; +pub use radio::{radio, RadioPlugin}; +pub use slider::{slider, SliderPlugin, SliderProps}; + +/// Plugin which registers all `bevy_feathers` controls. +pub struct ControlsPlugin; + +impl Plugin for ControlsPlugin { + fn build(&self, app: &mut bevy_app::App) { + app.add_plugins((ButtonPlugin, CheckboxPlugin, RadioPlugin, SliderPlugin)); + } +} diff --git a/crates/bevy_feathers/src/controls/radio.rs b/crates/bevy_feathers/src/controls/radio.rs new file mode 100644 index 0000000000..a08ffcfa8d --- /dev/null +++ b/crates/bevy_feathers/src/controls/radio.rs @@ -0,0 +1,268 @@ +use bevy_app::{Plugin, PreUpdate}; +use bevy_core_widgets::CoreRadio; +use bevy_ecs::{ + bundle::Bundle, + children, + component::Component, + entity::Entity, + hierarchy::{ChildOf, Children}, + lifecycle::RemovedComponents, + query::{Added, Changed, Has, Or, With}, + schedule::IntoScheduleConfigs, + spawn::{Spawn, SpawnRelated, SpawnableList}, + system::{Commands, Query}, +}; +use bevy_input_focus::tab_navigation::TabIndex; +use bevy_picking::{hover::Hovered, PickingSystems}; +use bevy_render::view::Visibility; +use bevy_ui::{ + AlignItems, BorderRadius, Checked, Display, FlexDirection, InteractionDisabled, JustifyContent, + Node, UiRect, Val, +}; +use bevy_winit::cursor::CursorIcon; + +use crate::{ + constants::{fonts, size}, + font_styles::InheritableFont, + handle_or_path::HandleOrPath, + theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor}, + tokens, +}; + +/// Marker for the radio outline +#[derive(Component, Default, Clone)] +struct RadioOutline; + +/// Marker for the radio check mark +#[derive(Component, Default, Clone)] +struct RadioMark; + +/// Template function to spawn a radio. +/// +/// # Arguments +/// * `props` - construction properties for the radio. +/// * `overrides` - a bundle of components that are merged in with the normal radio components. +/// * `label` - the label of the radio. +pub fn radio + Send + Sync + 'static, B: Bundle>( + overrides: B, + label: C, +) -> impl Bundle { + ( + Node { + display: Display::Flex, + flex_direction: FlexDirection::Row, + justify_content: JustifyContent::Start, + align_items: AlignItems::Center, + column_gap: Val::Px(4.0), + ..Default::default() + }, + CoreRadio, + Hovered::default(), + CursorIcon::System(bevy_window::SystemCursorIcon::Pointer), + TabIndex(0), + ThemeFontColor(tokens::RADIO_TEXT), + InheritableFont { + font: HandleOrPath::Path(fonts::REGULAR.to_owned()), + font_size: 14.0, + }, + overrides, + Children::spawn(( + Spawn(( + Node { + display: Display::Flex, + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + width: size::RADIO_SIZE, + height: size::RADIO_SIZE, + border: UiRect::all(Val::Px(2.0)), + ..Default::default() + }, + RadioOutline, + BorderRadius::MAX, + ThemeBorderColor(tokens::RADIO_BORDER), + children![( + // Cheesy checkmark: rotated node with L-shaped border. + Node { + width: Val::Px(8.), + height: Val::Px(8.), + ..Default::default() + }, + BorderRadius::MAX, + RadioMark, + ThemeBackgroundColor(tokens::RADIO_MARK), + )], + )), + label, + )), + ) +} + +fn update_radio_styles( + q_radioes: Query< + ( + Entity, + Has, + Has, + &Hovered, + &ThemeFontColor, + ), + ( + With, + Or<(Changed, Added, Added)>, + ), + >, + q_children: Query<&Children>, + mut q_outline: Query<&ThemeBorderColor, With>, + mut q_mark: Query<&ThemeBackgroundColor, With>, + mut commands: Commands, +) { + for (radio_ent, disabled, checked, hovered, font_color) in q_radioes.iter() { + let Some(outline_ent) = q_children + .iter_descendants(radio_ent) + .find(|en| q_outline.contains(*en)) + else { + continue; + }; + let Some(mark_ent) = q_children + .iter_descendants(radio_ent) + .find(|en| q_mark.contains(*en)) + else { + continue; + }; + let outline_border = q_outline.get_mut(outline_ent).unwrap(); + let mark_color = q_mark.get_mut(mark_ent).unwrap(); + set_radio_colors( + radio_ent, + outline_ent, + mark_ent, + disabled, + checked, + hovered.0, + outline_border, + mark_color, + font_color, + &mut commands, + ); + } +} + +fn update_radio_styles_remove( + q_radioes: Query< + ( + Entity, + Has, + Has, + &Hovered, + &ThemeFontColor, + ), + With, + >, + q_children: Query<&Children>, + mut q_outline: Query<&ThemeBorderColor, With>, + mut q_mark: Query<&ThemeBackgroundColor, With>, + mut removed_disabled: RemovedComponents, + mut removed_checked: RemovedComponents, + mut commands: Commands, +) { + removed_disabled + .read() + .chain(removed_checked.read()) + .for_each(|ent| { + if let Ok((radio_ent, disabled, checked, hovered, font_color)) = q_radioes.get(ent) { + let Some(outline_ent) = q_children + .iter_descendants(radio_ent) + .find(|en| q_outline.contains(*en)) + else { + return; + }; + let Some(mark_ent) = q_children + .iter_descendants(radio_ent) + .find(|en| q_mark.contains(*en)) + else { + return; + }; + let outline_border = q_outline.get_mut(outline_ent).unwrap(); + let mark_color = q_mark.get_mut(mark_ent).unwrap(); + set_radio_colors( + radio_ent, + outline_ent, + mark_ent, + disabled, + checked, + hovered.0, + outline_border, + mark_color, + font_color, + &mut commands, + ); + } + }); +} + +fn set_radio_colors( + radio_ent: Entity, + outline_ent: Entity, + mark_ent: Entity, + disabled: bool, + checked: bool, + hovered: bool, + outline_border: &ThemeBorderColor, + mark_color: &ThemeBackgroundColor, + font_color: &ThemeFontColor, + commands: &mut Commands, +) { + let outline_border_token = match (disabled, hovered) { + (true, _) => tokens::RADIO_BORDER_DISABLED, + (false, true) => tokens::RADIO_BORDER_HOVER, + _ => tokens::RADIO_BORDER, + }; + + let mark_token = match disabled { + true => tokens::RADIO_MARK_DISABLED, + false => tokens::RADIO_MARK, + }; + + let font_color_token = match disabled { + true => tokens::RADIO_TEXT_DISABLED, + false => tokens::RADIO_TEXT, + }; + + // Change outline border + if outline_border.0 != outline_border_token { + commands + .entity(outline_ent) + .insert(ThemeBorderColor(outline_border_token)); + } + + // Change mark color + if mark_color.0 != mark_token { + commands + .entity(mark_ent) + .insert(ThemeBorderColor(mark_token)); + } + + // Change mark visibility + commands.entity(mark_ent).insert(match checked { + true => Visibility::Visible, + false => Visibility::Hidden, + }); + + // Change font color + if font_color.0 != font_color_token { + commands + .entity(radio_ent) + .insert(ThemeFontColor(font_color_token)); + } +} + +/// Plugin which registers the systems for updating the radio styles. +pub struct RadioPlugin; + +impl Plugin for RadioPlugin { + fn build(&self, app: &mut bevy_app::App) { + app.add_systems( + PreUpdate, + (update_radio_styles, update_radio_styles_remove).in_set(PickingSystems::Last), + ); + } +} diff --git a/crates/bevy_feathers/src/controls/slider.rs b/crates/bevy_feathers/src/controls/slider.rs new file mode 100644 index 0000000000..fa1978e06c --- /dev/null +++ b/crates/bevy_feathers/src/controls/slider.rs @@ -0,0 +1,208 @@ +use core::f32::consts::PI; + +use bevy_app::{Plugin, PreUpdate}; +use bevy_color::Color; +use bevy_core_widgets::{Callback, CoreSlider, SliderRange, SliderValue, TrackClick}; +use bevy_ecs::{ + bundle::Bundle, + children, + component::Component, + entity::Entity, + hierarchy::Children, + lifecycle::RemovedComponents, + query::{Added, Changed, Has, Or, Spawned, With}, + schedule::IntoScheduleConfigs, + spawn::SpawnRelated, + system::{In, Query, Res}, +}; +use bevy_input_focus::tab_navigation::TabIndex; +use bevy_picking::PickingSystems; +use bevy_ui::{ + widget::Text, AlignItems, BackgroundGradient, ColorStop, Display, FlexDirection, Gradient, + InteractionDisabled, InterpolationColorSpace, JustifyContent, LinearGradient, Node, UiRect, + Val, +}; +use bevy_winit::cursor::CursorIcon; + +use crate::{ + constants::{fonts, size}, + font_styles::InheritableFont, + handle_or_path::HandleOrPath, + rounded_corners::RoundedCorners, + theme::{ThemeFontColor, ThemedText, UiTheme}, + tokens, +}; + +/// Slider template properties, passed to [`slider`] function. +pub struct SliderProps { + /// Slider current value + pub value: f32, + /// Slider minimum value + pub min: f32, + /// Slider maximum value + pub max: f32, + /// On-change handler + pub on_change: Callback>, +} + +impl Default for SliderProps { + fn default() -> Self { + Self { + value: 0.0, + min: 0.0, + max: 1.0, + on_change: Callback::Ignore, + } + } +} + +#[derive(Component, Default, Clone)] +#[require(CoreSlider)] +struct SliderStyle; + +/// Marker for the text +#[derive(Component, Default, Clone)] +struct SliderValueText; + +/// Spawn a new slider widget. +/// +/// # Arguments +/// +/// * `props` - construction properties for the slider. +/// * `overrides` - a bundle of components that are merged in with the normal slider components. +pub fn slider(props: SliderProps, overrides: B) -> impl Bundle { + ( + Node { + height: size::ROW_HEIGHT, + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + padding: UiRect::axes(Val::Px(8.0), Val::Px(0.)), + flex_grow: 1.0, + ..Default::default() + }, + CoreSlider { + on_change: props.on_change, + track_click: TrackClick::Drag, + }, + SliderStyle, + SliderValue(props.value), + SliderRange::new(props.min, props.max), + CursorIcon::System(bevy_window::SystemCursorIcon::EwResize), + TabIndex(0), + RoundedCorners::All.to_border_radius(6.0), + // Use a gradient to draw the moving bar + BackgroundGradient(vec![Gradient::Linear(LinearGradient { + angle: PI * 0.5, + stops: vec![ + ColorStop::new(Color::NONE, Val::Percent(0.)), + ColorStop::new(Color::NONE, Val::Percent(50.)), + ColorStop::new(Color::NONE, Val::Percent(50.)), + ColorStop::new(Color::NONE, Val::Percent(100.)), + ], + color_space: InterpolationColorSpace::Srgb, + })]), + overrides, + children![( + // Text container + Node { + display: Display::Flex, + flex_direction: FlexDirection::Row, + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + ..Default::default() + }, + ThemeFontColor(tokens::SLIDER_TEXT), + InheritableFont { + font: HandleOrPath::Path(fonts::MONO.to_owned()), + font_size: 12.0, + }, + children![(Text::new("10.0"), ThemedText, SliderValueText,)], + )], + ) +} + +fn update_slider_colors( + mut q_sliders: Query< + (Has, &mut BackgroundGradient), + (With, Or<(Spawned, Added)>), + >, + theme: Res, +) { + for (disabled, mut gradient) in q_sliders.iter_mut() { + set_slider_colors(&theme, disabled, gradient.as_mut()); + } +} + +fn update_slider_colors_remove( + mut q_sliders: Query<(Has, &mut BackgroundGradient)>, + mut removed_disabled: RemovedComponents, + theme: Res, +) { + removed_disabled.read().for_each(|ent| { + if let Ok((disabled, mut gradient)) = q_sliders.get_mut(ent) { + set_slider_colors(&theme, disabled, gradient.as_mut()); + } + }); +} + +fn set_slider_colors(theme: &Res<'_, UiTheme>, disabled: bool, gradient: &mut BackgroundGradient) { + let bar_color = theme.color(match disabled { + true => tokens::SLIDER_BAR_DISABLED, + false => tokens::SLIDER_BAR, + }); + let bg_color = theme.color(tokens::SLIDER_BG); + if let [Gradient::Linear(linear_gradient)] = &mut gradient.0[..] { + linear_gradient.stops[0].color = bar_color; + linear_gradient.stops[1].color = bar_color; + linear_gradient.stops[2].color = bg_color; + linear_gradient.stops[3].color = bg_color; + } +} + +fn update_slider_pos( + mut q_sliders: Query< + (Entity, &SliderValue, &SliderRange, &mut BackgroundGradient), + ( + With, + Or<( + Changed, + Changed, + Changed, + )>, + ), + >, + q_children: Query<&Children>, + mut q_slider_text: Query<&mut Text, With>, +) { + for (slider_ent, value, range, mut gradient) in q_sliders.iter_mut() { + if let [Gradient::Linear(linear_gradient)] = &mut gradient.0[..] { + let percent_value = range.thumb_position(value.0) * 100.0; + linear_gradient.stops[1].point = Val::Percent(percent_value); + linear_gradient.stops[2].point = Val::Percent(percent_value); + } + + // Find slider text child entity and update its text with the formatted value + q_children.iter_descendants(slider_ent).for_each(|child| { + if let Ok(mut text) = q_slider_text.get_mut(child) { + text.0 = format!("{}", value.0); + } + }); + } +} + +/// Plugin which registers the systems for updating the slider styles. +pub struct SliderPlugin; + +impl Plugin for SliderPlugin { + fn build(&self, app: &mut bevy_app::App) { + app.add_systems( + PreUpdate, + ( + update_slider_colors, + update_slider_colors_remove, + update_slider_pos, + ) + .in_set(PickingSystems::Last), + ); + } +} diff --git a/crates/bevy_feathers/src/cursor.rs b/crates/bevy_feathers/src/cursor.rs new file mode 100644 index 0000000000..a9811edfb2 --- /dev/null +++ b/crates/bevy_feathers/src/cursor.rs @@ -0,0 +1,70 @@ +//! Provides a way to automatically set the mouse cursor based on hovered entity. +use bevy_app::{App, Plugin, PreUpdate}; +use bevy_ecs::{ + entity::Entity, + hierarchy::ChildOf, + resource::Resource, + schedule::IntoScheduleConfigs, + system::{Commands, Query, Res}, +}; +use bevy_picking::{hover::HoverMap, pointer::PointerId, PickingSystems}; +use bevy_window::Window; +use bevy_winit::cursor::CursorIcon; + +/// A component that specifies the cursor icon to be used when the mouse is not hovering over +/// any other entity. This is used to set the default cursor icon for the window. +#[derive(Resource, Debug, Clone, Default)] +pub struct DefaultCursorIcon(pub CursorIcon); + +/// System which updates the window cursor icon whenever the mouse hovers over an entity with +/// a [`CursorIcon`] component. If no entity is hovered, the cursor icon is set to +/// the cursor in the [`DefaultCursorIcon`] resource. +pub(crate) fn update_cursor( + mut commands: Commands, + hover_map: Option>, + parent_query: Query<&ChildOf>, + cursor_query: Query<&CursorIcon>, + mut q_windows: Query<(Entity, &mut Window, Option<&CursorIcon>)>, + r_default_cursor: Res, +) { + let cursor = hover_map.and_then(|hover_map| match hover_map.get(&PointerId::Mouse) { + Some(hover_set) => hover_set.keys().find_map(|entity| { + cursor_query.get(*entity).ok().or_else(|| { + parent_query + .iter_ancestors(*entity) + .find_map(|e| cursor_query.get(e).ok()) + }) + }), + None => None, + }); + + let mut windows_to_change: Vec = Vec::new(); + for (entity, _window, prev_cursor) in q_windows.iter_mut() { + match (cursor, prev_cursor) { + (Some(cursor), Some(prev_cursor)) if cursor == prev_cursor => continue, + (None, None) => continue, + _ => { + windows_to_change.push(entity); + } + } + } + windows_to_change.iter().for_each(|entity| { + if let Some(cursor) = cursor { + commands.entity(*entity).insert(cursor.clone()); + } else { + commands.entity(*entity).insert(r_default_cursor.0.clone()); + } + }); +} + +/// Plugin that supports automatically changing the cursor based on the hovered entity. +pub struct CursorIconPlugin; + +impl Plugin for CursorIconPlugin { + fn build(&self, app: &mut App) { + if app.world().get_resource::().is_none() { + app.init_resource::(); + } + app.add_systems(PreUpdate, update_cursor.in_set(PickingSystems::Last)); + } +} diff --git a/crates/bevy_feathers/src/dark_theme.rs b/crates/bevy_feathers/src/dark_theme.rs new file mode 100644 index 0000000000..add354abd7 --- /dev/null +++ b/crates/bevy_feathers/src/dark_theme.rs @@ -0,0 +1,98 @@ +//! The standard `bevy_feathers` dark theme. +use crate::{palette, tokens}; +use bevy_color::{Alpha, Luminance}; +use bevy_platform::collections::HashMap; + +use crate::theme::ThemeProps; + +/// Create a [`ThemeProps`] object and populate it with the colors for the default dark theme. +pub fn create_dark_theme() -> ThemeProps { + ThemeProps { + color: HashMap::from([ + (tokens::WINDOW_BG.into(), palette::GRAY_0), + (tokens::BUTTON_BG.into(), palette::GRAY_3), + ( + tokens::BUTTON_BG_HOVER.into(), + palette::GRAY_3.lighter(0.05), + ), + ( + tokens::BUTTON_BG_PRESSED.into(), + palette::GRAY_3.lighter(0.1), + ), + (tokens::BUTTON_BG_DISABLED.into(), palette::GRAY_2), + (tokens::BUTTON_PRIMARY_BG.into(), palette::ACCENT), + ( + tokens::BUTTON_PRIMARY_BG_HOVER.into(), + palette::ACCENT.lighter(0.05), + ), + ( + tokens::BUTTON_PRIMARY_BG_PRESSED.into(), + palette::ACCENT.lighter(0.1), + ), + (tokens::BUTTON_PRIMARY_BG_DISABLED.into(), palette::GRAY_2), + (tokens::BUTTON_TEXT.into(), palette::WHITE), + ( + tokens::BUTTON_TEXT_DISABLED.into(), + palette::WHITE.with_alpha(0.5), + ), + (tokens::BUTTON_PRIMARY_TEXT.into(), palette::WHITE), + ( + tokens::BUTTON_PRIMARY_TEXT_DISABLED.into(), + palette::WHITE.with_alpha(0.5), + ), + (tokens::SLIDER_BG.into(), palette::GRAY_1), + (tokens::SLIDER_BAR.into(), palette::ACCENT), + (tokens::SLIDER_BAR_DISABLED.into(), palette::GRAY_2), + (tokens::SLIDER_TEXT.into(), palette::WHITE), + ( + tokens::SLIDER_TEXT_DISABLED.into(), + palette::WHITE.with_alpha(0.5), + ), + (tokens::CHECKBOX_BG.into(), palette::GRAY_3), + (tokens::CHECKBOX_BG_CHECKED.into(), palette::ACCENT), + ( + tokens::CHECKBOX_BG_DISABLED.into(), + palette::GRAY_1.with_alpha(0.5), + ), + ( + tokens::CHECKBOX_BG_CHECKED_DISABLED.into(), + palette::GRAY_3.with_alpha(0.5), + ), + (tokens::CHECKBOX_BORDER.into(), palette::GRAY_3), + ( + tokens::CHECKBOX_BORDER_HOVER.into(), + palette::GRAY_3.lighter(0.1), + ), + ( + tokens::CHECKBOX_BORDER_DISABLED.into(), + palette::GRAY_3.with_alpha(0.5), + ), + (tokens::CHECKBOX_MARK.into(), palette::WHITE), + (tokens::CHECKBOX_MARK_DISABLED.into(), palette::LIGHT_GRAY_2), + (tokens::CHECKBOX_TEXT.into(), palette::LIGHT_GRAY_1), + ( + tokens::CHECKBOX_TEXT_DISABLED.into(), + palette::LIGHT_GRAY_1.with_alpha(0.5), + ), + (tokens::RADIO_BORDER.into(), palette::GRAY_3), + ( + tokens::RADIO_BORDER_HOVER.into(), + palette::GRAY_3.lighter(0.1), + ), + ( + tokens::RADIO_BORDER_DISABLED.into(), + palette::GRAY_3.with_alpha(0.5), + ), + (tokens::RADIO_MARK.into(), palette::ACCENT), + ( + tokens::RADIO_MARK_DISABLED.into(), + palette::ACCENT.with_alpha(0.5), + ), + (tokens::RADIO_TEXT.into(), palette::LIGHT_GRAY_1), + ( + tokens::RADIO_TEXT_DISABLED.into(), + palette::LIGHT_GRAY_1.with_alpha(0.5), + ), + ]), + } +} diff --git a/crates/bevy_feathers/src/font_styles.rs b/crates/bevy_feathers/src/font_styles.rs new file mode 100644 index 0000000000..6de064dd39 --- /dev/null +++ b/crates/bevy_feathers/src/font_styles.rs @@ -0,0 +1,62 @@ +//! A framework for inheritable font styles. +use bevy_app::Propagate; +use bevy_asset::{AssetServer, Handle}; +use bevy_ecs::{ + component::Component, + lifecycle::Insert, + observer::On, + system::{Commands, Query, Res}, +}; +use bevy_text::{Font, TextFont}; + +use crate::handle_or_path::HandleOrPath; + +/// A component which, when inserted on an entity, will load the given font and propagate it +/// downward to any child text entity that has the [`ThemedText`](crate::theme::ThemedText) marker. +#[derive(Component, Default, Clone, Debug)] +pub struct InheritableFont { + /// The font handle or path. + pub font: HandleOrPath, + /// The desired font size. + pub font_size: f32, +} + +impl InheritableFont { + /// Create a new `InheritableFont` from a handle. + pub fn from_handle(handle: Handle) -> Self { + Self { + font: HandleOrPath::Handle(handle), + font_size: 16.0, + } + } + + /// Create a new `InheritableFont` from a path. + pub fn from_path(path: &str) -> Self { + Self { + font: HandleOrPath::Path(path.to_string()), + font_size: 16.0, + } + } +} + +/// An observer which looks for changes to the `InheritableFont` component on an entity, and +/// propagates downward the font to all participating text entities. +pub(crate) fn on_changed_font( + ev: On, + font_style: Query<&InheritableFont>, + assets: Res, + mut commands: Commands, +) { + if let Ok(style) = font_style.get(ev.target()) { + if let Some(font) = match style.font { + HandleOrPath::Handle(ref h) => Some(h.clone()), + HandleOrPath::Path(ref p) => Some(assets.load::(p)), + } { + commands.entity(ev.target()).insert(Propagate(TextFont { + font, + font_size: style.font_size, + ..Default::default() + })); + } + } +} diff --git a/crates/bevy_feathers/src/handle_or_path.rs b/crates/bevy_feathers/src/handle_or_path.rs new file mode 100644 index 0000000000..178d2b13e8 --- /dev/null +++ b/crates/bevy_feathers/src/handle_or_path.rs @@ -0,0 +1,61 @@ +//! Provides a way to specify assets either by handle or by path. +use bevy_asset::{Asset, Handle}; + +/// Enum that represents a reference to an asset as either a [`Handle`] or a [`String`] path. +/// +/// This is useful for when you want to specify an asset, but don't always have convenient +/// access to an asset server reference. +#[derive(Clone, Debug)] +pub enum HandleOrPath { + /// Specify the asset reference as a handle. + Handle(Handle), + /// Specify the asset reference as a [`String`]. + Path(String), +} + +impl Default for HandleOrPath { + fn default() -> Self { + Self::Path("".to_string()) + } +} + +// Necessary because we don't want to require T: PartialEq +impl PartialEq for HandleOrPath { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (HandleOrPath::Handle(h1), HandleOrPath::Handle(h2)) => h1 == h2, + (HandleOrPath::Path(p1), HandleOrPath::Path(p2)) => p1 == p2, + _ => false, + } + } +} + +impl From> for HandleOrPath { + fn from(h: Handle) -> Self { + HandleOrPath::Handle(h) + } +} + +impl From<&str> for HandleOrPath { + fn from(p: &str) -> Self { + HandleOrPath::Path(p.to_string()) + } +} + +impl From for HandleOrPath { + fn from(p: String) -> Self { + HandleOrPath::Path(p.clone()) + } +} + +impl From<&String> for HandleOrPath { + fn from(p: &String) -> Self { + HandleOrPath::Path(p.to_string()) + } +} + +impl From<&HandleOrPath> for HandleOrPath { + fn from(p: &HandleOrPath) -> Self { + p.to_owned() + } +} diff --git a/crates/bevy_feathers/src/lib.rs b/crates/bevy_feathers/src/lib.rs new file mode 100644 index 0000000000..ab02304a85 --- /dev/null +++ b/crates/bevy_feathers/src/lib.rs @@ -0,0 +1,74 @@ +//! `bevy_feathers` is a collection of styled and themed widgets for building editors and +//! inspectors. +//! +//! The aesthetic choices made here are designed with a future Bevy Editor in mind, +//! but this crate is deliberately exposed to the public to allow the broader ecosystem to easily create +//! tooling for themselves and others that fits cohesively together. +//! +//! While it may be tempting to use this crate for your game's UI, it's deliberately not intended for that. +//! We've opted for a clean, functional style, and prioritized consistency over customization. +//! That said, if you like what you see, it can be a helpful learning tool. +//! Consider copying this code into your own project, +//! and refining the styles and abstractions provided to meet your needs. +//! +//! ## Warning: Experimental! +//! All that said, this crate is still experimental and unfinished! +//! It will change in breaking ways, and there will be both bugs and limitations. +//! +//! Please report issues, submit fixes and propose changes. +//! Thanks for stress-testing; let's build something better together. + +use bevy_app::{HierarchyPropagatePlugin, Plugin, PostUpdate}; +use bevy_asset::embedded_asset; +use bevy_ecs::query::With; +use bevy_text::{TextColor, TextFont}; +use bevy_winit::cursor::CursorIcon; + +use crate::{ + controls::ControlsPlugin, + cursor::{CursorIconPlugin, DefaultCursorIcon}, + theme::{ThemedText, UiTheme}, +}; + +pub mod constants; +pub mod controls; +pub mod cursor; +pub mod dark_theme; +pub mod font_styles; +pub mod handle_or_path; +pub mod palette; +pub mod rounded_corners; +pub mod theme; +pub mod tokens; + +/// Plugin which installs observers and systems for feathers themes, cursors, and all controls. +pub struct FeathersPlugin; + +impl Plugin for FeathersPlugin { + fn build(&self, app: &mut bevy_app::App) { + app.init_resource::(); + + embedded_asset!(app, "assets/fonts/FiraSans-Bold.ttf"); + embedded_asset!(app, "assets/fonts/FiraSans-BoldItalic.ttf"); + embedded_asset!(app, "assets/fonts/FiraSans-Regular.ttf"); + embedded_asset!(app, "assets/fonts/FiraSans-Italic.ttf"); + embedded_asset!(app, "assets/fonts/FiraMono-Medium.ttf"); + + app.add_plugins(( + ControlsPlugin, + CursorIconPlugin, + HierarchyPropagatePlugin::>::default(), + HierarchyPropagatePlugin::>::default(), + )); + + app.insert_resource(DefaultCursorIcon(CursorIcon::System( + bevy_window::SystemCursorIcon::Default, + ))); + + app.add_systems(PostUpdate, theme::update_theme) + .add_observer(theme::on_changed_background) + .add_observer(theme::on_changed_border) + .add_observer(theme::on_changed_font_color) + .add_observer(font_styles::on_changed_font); + } +} diff --git a/crates/bevy_feathers/src/palette.rs b/crates/bevy_feathers/src/palette.rs new file mode 100644 index 0000000000..bc2f6d51c6 --- /dev/null +++ b/crates/bevy_feathers/src/palette.rs @@ -0,0 +1,29 @@ +//! The Feathers standard color palette. +use bevy_color::Color; + +///
+pub const BLACK: Color = Color::oklcha(0.0, 0.0, 0.0, 1.0); +///
- window background +pub const GRAY_0: Color = Color::oklcha(0.2414, 0.0095, 285.67, 1.0); +///
- pane background +pub const GRAY_1: Color = Color::oklcha(0.2866, 0.0072, 285.93, 1.0); +///
- item background +pub const GRAY_2: Color = Color::oklcha(0.3373, 0.0071, 274.77, 1.0); +///
- item background (active) +pub const GRAY_3: Color = Color::oklcha(0.3992, 0.0101, 278.38, 1.0); +///
- border +pub const WARM_GRAY_1: Color = Color::oklcha(0.3757, 0.0017, 286.32, 1.0); +///
- bright label text +pub const LIGHT_GRAY_1: Color = Color::oklcha(0.7607, 0.0014, 286.37, 1.0); +///
- dim label text +pub const LIGHT_GRAY_2: Color = Color::oklcha(0.6106, 0.003, 286.31, 1.0); +///
- button label text +pub const WHITE: Color = Color::oklcha(1.0, 0.000000059604645, 90.0, 1.0); +///
- call-to-action and selection color +pub const ACCENT: Color = Color::oklcha(0.542, 0.1594, 255.4, 1.0); +///
- for X-axis inputs and drag handles +pub const X_AXIS: Color = Color::oklcha(0.5232, 0.1404, 13.84, 1.0); +///
- for Y-axis inputs and drag handles +pub const Y_AXIS: Color = Color::oklcha(0.5866, 0.1543, 129.84, 1.0); +///
- for Z-axis inputs and drag handles +pub const Z_AXIS: Color = Color::oklcha(0.4847, 0.1249, 253.08, 1.0); diff --git a/crates/bevy_feathers/src/rounded_corners.rs b/crates/bevy_feathers/src/rounded_corners.rs new file mode 100644 index 0000000000..4d2be9e0a8 --- /dev/null +++ b/crates/bevy_feathers/src/rounded_corners.rs @@ -0,0 +1,96 @@ +//! Mechanism for specifying which corners of a widget are rounded, used for segmented buttons +//! and control groups. +use bevy_ui::{BorderRadius, Val}; + +/// Allows specifying which corners are rounded and which are sharp. All rounded corners +/// have the same radius. Not all combinations are supported, only the ones that make +/// sense for a segmented buttons. +/// +/// A typical use case would be a segmented button consisting of 3 individual buttons in a +/// row. In that case, you would have the leftmost button have rounded corners on the left, +/// the right-most button have rounded corners on the right, and the center button have +/// only sharp corners. +#[derive(Debug, Clone, Copy, Default, PartialEq)] +pub enum RoundedCorners { + /// No corners are rounded. + None, + #[default] + /// All corners are rounded. + All, + /// Top-left corner is rounded. + TopLeft, + /// Top-right corner is rounded. + TopRight, + /// Bottom-right corner is rounded. + BottomRight, + /// Bottom-left corner is rounded. + BottomLeft, + /// Top corners are rounded. + Top, + /// Right corners are rounded. + Right, + /// Bottom corners are rounded. + Bottom, + /// Left corners are rounded. + Left, +} + +impl RoundedCorners { + /// Convert the `RoundedCorners` to a `BorderRadius` for use in a `Node`. + pub fn to_border_radius(&self, radius: f32) -> BorderRadius { + let radius = Val::Px(radius); + let zero = Val::ZERO; + match self { + RoundedCorners::None => BorderRadius::all(zero), + RoundedCorners::All => BorderRadius::all(radius), + RoundedCorners::TopLeft => BorderRadius { + top_left: radius, + top_right: zero, + bottom_right: zero, + bottom_left: zero, + }, + RoundedCorners::TopRight => BorderRadius { + top_left: zero, + top_right: radius, + bottom_right: zero, + bottom_left: zero, + }, + RoundedCorners::BottomRight => BorderRadius { + top_left: zero, + top_right: zero, + bottom_right: radius, + bottom_left: zero, + }, + RoundedCorners::BottomLeft => BorderRadius { + top_left: zero, + top_right: zero, + bottom_right: zero, + bottom_left: radius, + }, + RoundedCorners::Top => BorderRadius { + top_left: radius, + top_right: radius, + bottom_right: zero, + bottom_left: zero, + }, + RoundedCorners::Right => BorderRadius { + top_left: zero, + top_right: radius, + bottom_right: radius, + bottom_left: zero, + }, + RoundedCorners::Bottom => BorderRadius { + top_left: zero, + top_right: zero, + bottom_right: radius, + bottom_left: radius, + }, + RoundedCorners::Left => BorderRadius { + top_left: radius, + top_right: zero, + bottom_right: zero, + bottom_left: radius, + }, + } + } +} diff --git a/crates/bevy_feathers/src/theme.rs b/crates/bevy_feathers/src/theme.rs new file mode 100644 index 0000000000..9969b54846 --- /dev/null +++ b/crates/bevy_feathers/src/theme.rs @@ -0,0 +1,131 @@ +//! A framework for theming. +use bevy_app::Propagate; +use bevy_color::{palettes, Color}; +use bevy_ecs::{ + change_detection::DetectChanges, + component::Component, + lifecycle::Insert, + observer::On, + query::Changed, + resource::Resource, + system::{Commands, Query, Res}, +}; +use bevy_log::warn_once; +use bevy_platform::collections::HashMap; +use bevy_text::TextColor; +use bevy_ui::{BackgroundColor, BorderColor}; + +/// A collection of properties that make up a theme. +#[derive(Default, Clone)] +pub struct ThemeProps { + /// Map of design tokens to colors. + pub color: HashMap, + // Other style property types to be added later. +} + +/// The currently selected user interface theme. Overwriting this resource changes the theme. +#[derive(Resource, Default)] +pub struct UiTheme(pub ThemeProps); + +impl UiTheme { + /// Lookup a color by design token. If the theme does not have an entry for that token, + /// logs a warning and returns an error color. + pub fn color<'a>(&self, token: &'a str) -> Color { + let color = self.0.color.get(token); + match color { + Some(c) => *c, + None => { + warn_once!("Theme color {} not found.", token); + // Return a bright obnoxious color to make the error obvious. + palettes::basic::FUCHSIA.into() + } + } + } + + /// Associate a design token with a given color. + pub fn set_color(&mut self, token: impl Into, color: Color) { + self.0.color.insert(token.into(), color); + } +} + +/// Component which causes the background color of an entity to be set based on a theme color. +#[derive(Component, Clone, Copy)] +#[require(BackgroundColor)] +#[component(immutable)] +pub struct ThemeBackgroundColor(pub &'static str); + +/// Component which causes the border color of an entity to be set based on a theme color. +/// Only supports setting all borders to the same color. +#[derive(Component, Clone, Copy)] +#[require(BorderColor)] +#[component(immutable)] +pub struct ThemeBorderColor(pub &'static str); + +/// Component which causes the inherited text color of an entity to be set based on a theme color. +#[derive(Component, Clone, Copy)] +#[component(immutable)] +pub struct ThemeFontColor(pub &'static str); + +/// A marker component that is used to indicate that the text entity wants to opt-in to using +/// inherited text styles. +#[derive(Component)] +pub struct ThemedText; + +pub(crate) fn update_theme( + mut q_background: Query<(&mut BackgroundColor, &ThemeBackgroundColor)>, + mut q_border: Query<(&mut BorderColor, &ThemeBorderColor)>, + theme: Res, +) { + if theme.is_changed() { + // Update all background colors + for (mut bg, theme_bg) in q_background.iter_mut() { + bg.0 = theme.color(theme_bg.0); + } + + // Update all border colors + for (mut border, theme_border) in q_border.iter_mut() { + border.set_all(theme.color(theme_border.0)); + } + } +} + +pub(crate) fn on_changed_background( + ev: On, + mut q_background: Query< + (&mut BackgroundColor, &ThemeBackgroundColor), + Changed, + >, + theme: Res, +) { + // Update background colors where the design token has changed. + if let Ok((mut bg, theme_bg)) = q_background.get_mut(ev.target()) { + bg.0 = theme.color(theme_bg.0); + } +} + +pub(crate) fn on_changed_border( + ev: On, + mut q_border: Query<(&mut BorderColor, &ThemeBorderColor), Changed>, + theme: Res, +) { + // Update background colors where the design token has changed. + if let Ok((mut border, theme_border)) = q_border.get_mut(ev.target()) { + border.set_all(theme.color(theme_border.0)); + } +} + +/// An observer which looks for changes to the [`ThemeFontColor`] component on an entity, and +/// propagates downward the text color to all participating text entities. +pub(crate) fn on_changed_font_color( + ev: On, + font_color: Query<&ThemeFontColor>, + theme: Res, + mut commands: Commands, +) { + if let Ok(token) = font_color.get(ev.target()) { + let color = theme.color(token.0); + commands + .entity(ev.target()) + .insert(Propagate(TextColor(color))); + } +} diff --git a/crates/bevy_feathers/src/tokens.rs b/crates/bevy_feathers/src/tokens.rs new file mode 100644 index 0000000000..d85cf02996 --- /dev/null +++ b/crates/bevy_feathers/src/tokens.rs @@ -0,0 +1,101 @@ +//! Design tokens used by Feathers themes. +//! +//! The term "design token" is commonly used in UX design to mean the smallest unit of a theme, +//! similar in concept to a CSS variable. Each token represents an assignment of a color or +//! value to a specific visual aspect of a widget, such as background or border. + +/// Window background +pub const WINDOW_BG: &str = "feathers.window.bg"; + +/// Focus ring +pub const FOCUS_RING: &str = "feathers.focus"; + +/// Regular text +pub const TEXT_MAIN: &str = "feathers.text.main"; +/// Dim text +pub const TEXT_DIM: &str = "feathers.text.dim"; + +// Normal buttons + +/// Regular button background +pub const BUTTON_BG: &str = "feathers.button.bg"; +/// Regular button background (hovered) +pub const BUTTON_BG_HOVER: &str = "feathers.button.bg.hover"; +/// Regular button background (disabled) +pub const BUTTON_BG_DISABLED: &str = "feathers.button.bg.disabled"; +/// Regular button background (pressed) +pub const BUTTON_BG_PRESSED: &str = "feathers.button.bg.pressed"; +/// Regular button text +pub const BUTTON_TEXT: &str = "feathers.button.txt"; +/// Regular button text (disabled) +pub const BUTTON_TEXT_DISABLED: &str = "feathers.button.txt.disabled"; + +// Primary ("default") buttons + +/// Primary button background +pub const BUTTON_PRIMARY_BG: &str = "feathers.button.primary.bg"; +/// Primary button background (hovered) +pub const BUTTON_PRIMARY_BG_HOVER: &str = "feathers.button.primary.bg.hover"; +/// Primary button background (disabled) +pub const BUTTON_PRIMARY_BG_DISABLED: &str = "feathers.button.primary.bg.disabled"; +/// Primary button background (pressed) +pub const BUTTON_PRIMARY_BG_PRESSED: &str = "feathers.button.primary.bg.pressed"; +/// Primary button text +pub const BUTTON_PRIMARY_TEXT: &str = "feathers.button.primary.txt"; +/// Primary button text (disabled) +pub const BUTTON_PRIMARY_TEXT_DISABLED: &str = "feathers.button.primary.txt.disabled"; + +// Slider + +/// Background for slider +pub const SLIDER_BG: &str = "feathers.slider.bg"; +/// Background for slider moving bar +pub const SLIDER_BAR: &str = "feathers.slider.bar"; +/// Background for slider moving bar (disabled) +pub const SLIDER_BAR_DISABLED: &str = "feathers.slider.bar.disabled"; +/// Background for slider text +pub const SLIDER_TEXT: &str = "feathers.slider.text"; +/// Background for slider text (disabled) +pub const SLIDER_TEXT_DISABLED: &str = "feathers.slider.text.disabled"; + +// Checkbox + +/// Checkbox background around the checkmark +pub const CHECKBOX_BG: &str = "feathers.checkbox.bg"; +/// Checkbox border around the checkmark (disabled) +pub const CHECKBOX_BG_DISABLED: &str = "feathers.checkbox.bg.disabled"; +/// Checkbox background around the checkmark +pub const CHECKBOX_BG_CHECKED: &str = "feathers.checkbox.bg.checked"; +/// Checkbox border around the checkmark (disabled) +pub const CHECKBOX_BG_CHECKED_DISABLED: &str = "feathers.checkbox.bg.checked.disabled"; +/// Checkbox border around the checkmark +pub const CHECKBOX_BORDER: &str = "feathers.checkbox.border"; +/// Checkbox border around the checkmark (hovered) +pub const CHECKBOX_BORDER_HOVER: &str = "feathers.checkbox.border.hover"; +/// Checkbox border around the checkmark (disabled) +pub const CHECKBOX_BORDER_DISABLED: &str = "feathers.checkbox.border.disabled"; +/// Checkbox check mark +pub const CHECKBOX_MARK: &str = "feathers.checkbox.mark"; +/// Checkbox check mark (disabled) +pub const CHECKBOX_MARK_DISABLED: &str = "feathers.checkbox.mark.disabled"; +/// Checkbox label text +pub const CHECKBOX_TEXT: &str = "feathers.checkbox.text"; +/// Checkbox label text (disabled) +pub const CHECKBOX_TEXT_DISABLED: &str = "feathers.checkbox.text.disabled"; + +// Radio button + +/// Radio border around the checkmark +pub const RADIO_BORDER: &str = "feathers.radio.border"; +/// Radio border around the checkmark (hovered) +pub const RADIO_BORDER_HOVER: &str = "feathers.radio.border.hover"; +/// Radio border around the checkmark (disabled) +pub const RADIO_BORDER_DISABLED: &str = "feathers.radio.border.disabled"; +/// Radio check mark +pub const RADIO_MARK: &str = "feathers.radio.mark"; +/// Radio check mark (disabled) +pub const RADIO_MARK_DISABLED: &str = "feathers.radio.mark.disabled"; +/// Radio label text +pub const RADIO_TEXT: &str = "feathers.radio.text"; +/// Radio label text (disabled) +pub const RADIO_TEXT_DISABLED: &str = "feathers.radio.text.disabled"; diff --git a/crates/bevy_gizmos/src/lib.rs b/crates/bevy_gizmos/src/lib.rs index f9512bc7ab..4eeeea508d 100755 --- a/crates/bevy_gizmos/src/lib.rs +++ b/crates/bevy_gizmos/src/lib.rs @@ -124,7 +124,7 @@ use { }, renderer::RenderDevice, sync_world::{MainEntity, TemporaryRenderEntity}, - Extract, ExtractSchedule, Render, RenderApp, RenderSystems, + Extract, ExtractSchedule, Render, RenderApp, RenderStartup, RenderSystems, }, bytemuck::cast_slice, }; @@ -176,6 +176,8 @@ impl Plugin for GizmoPlugin { #[cfg(feature = "bevy_render")] if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app.add_systems(RenderStartup, init_line_gizmo_uniform_bind_group_layout); + render_app.add_systems( Render, prepare_line_gizmo_bind_group.in_set(RenderSystems::PrepareBindGroups), @@ -199,26 +201,6 @@ impl Plugin for GizmoPlugin { tracing::warn!("bevy_render feature is enabled but RenderApp was not detected. Are you sure you loaded GizmoPlugin after RenderPlugin?"); } } - - #[cfg(feature = "bevy_render")] - fn finish(&self, app: &mut App) { - let Some(render_app) = app.get_sub_app_mut(RenderApp) else { - return; - }; - - let render_device = render_app.world().resource::(); - let line_layout = render_device.create_bind_group_layout( - "LineGizmoUniform layout", - &BindGroupLayoutEntries::single( - ShaderStages::VERTEX, - uniform_buffer::(true), - ), - ); - - render_app.insert_resource(LineGizmoUniformBindgroupLayout { - layout: line_layout, - }); - } } /// A extension trait adding `App::init_gizmo_group` and `App::insert_gizmo_config`. @@ -415,6 +397,24 @@ fn update_gizmo_meshes( } } +#[cfg(feature = "bevy_render")] +fn init_line_gizmo_uniform_bind_group_layout( + mut commands: Commands, + render_device: Res, +) { + let line_layout = render_device.create_bind_group_layout( + "LineGizmoUniform layout", + &BindGroupLayoutEntries::single( + ShaderStages::VERTEX, + uniform_buffer::(true), + ), + ); + + commands.insert_resource(LineGizmoUniformBindgroupLayout { + layout: line_layout, + }); +} + #[cfg(feature = "bevy_render")] fn extract_gizmo_data( mut commands: Commands, diff --git a/crates/bevy_gizmos/src/pipeline_2d.rs b/crates/bevy_gizmos/src/pipeline_2d.rs index a97071249d..128ecca883 100644 --- a/crates/bevy_gizmos/src/pipeline_2d.rs +++ b/crates/bevy_gizmos/src/pipeline_2d.rs @@ -1,23 +1,21 @@ use crate::{ config::{GizmoLineJoint, GizmoLineStyle, GizmoMeshConfig}, - line_gizmo_vertex_buffer_layouts, line_joint_gizmo_vertex_buffer_layouts, DrawLineGizmo, - DrawLineJointGizmo, GizmoRenderSystems, GpuLineGizmo, LineGizmoUniformBindgroupLayout, - SetLineGizmoBindGroup, + init_line_gizmo_uniform_bind_group_layout, line_gizmo_vertex_buffer_layouts, + line_joint_gizmo_vertex_buffer_layouts, DrawLineGizmo, DrawLineJointGizmo, GizmoRenderSystems, + GpuLineGizmo, LineGizmoUniformBindgroupLayout, SetLineGizmoBindGroup, }; use bevy_app::{App, Plugin}; -use bevy_asset::{load_embedded_asset, Handle}; +use bevy_asset::{load_embedded_asset, AssetServer, Handle}; use bevy_core_pipeline::core_2d::{Transparent2d, CORE_2D_DEPTH_FORMAT}; use bevy_ecs::{ prelude::Entity, resource::Resource, schedule::IntoScheduleConfigs, - system::{Query, Res, ResMut}, - world::{FromWorld, World}, + system::{Commands, Query, Res, ResMut}, }; use bevy_image::BevyDefault as _; use bevy_math::FloatOrd; -use bevy_render::sync_world::MainEntity; use bevy_render::{ render_asset::{prepare_assets, RenderAssets}, render_phase::{ @@ -28,7 +26,9 @@ use bevy_render::{ view::{ExtractedView, Msaa, RenderLayers, ViewTarget}, Render, RenderApp, RenderSystems, }; +use bevy_render::{sync_world::MainEntity, RenderStartup}; use bevy_sprite::{Mesh2dPipeline, Mesh2dPipelineKey, SetMesh2dViewBindGroup}; +use bevy_utils::default; use tracing::error; pub struct LineGizmo2dPlugin; @@ -54,6 +54,10 @@ impl Plugin for LineGizmo2dPlugin { bevy_sprite::queue_material2d_meshes::, ), ) + .add_systems( + RenderStartup, + init_line_gizmo_pipelines.after(init_line_gizmo_uniform_bind_group_layout), + ) .add_systems( Render, (queue_line_gizmos_2d, queue_line_joint_gizmos_2d) @@ -61,15 +65,6 @@ impl Plugin for LineGizmo2dPlugin { .after(prepare_assets::), ); } - - fn finish(&self, app: &mut App) { - let Some(render_app) = app.get_sub_app_mut(RenderApp) else { - return; - }; - - render_app.init_resource::(); - render_app.init_resource::(); - } } #[derive(Clone, Resource)] @@ -79,17 +74,22 @@ struct LineGizmoPipeline { shader: Handle, } -impl FromWorld for LineGizmoPipeline { - fn from_world(render_world: &mut World) -> Self { - LineGizmoPipeline { - mesh_pipeline: render_world.resource::().clone(), - uniform_layout: render_world - .resource::() - .layout - .clone(), - shader: load_embedded_asset!(render_world, "lines.wgsl"), - } - } +fn init_line_gizmo_pipelines( + mut commands: Commands, + mesh_2d_pipeline: Res, + uniform_bind_group_layout: Res, + asset_server: Res, +) { + commands.insert_resource(LineGizmoPipeline { + mesh_pipeline: mesh_2d_pipeline.clone(), + uniform_layout: uniform_bind_group_layout.layout.clone(), + shader: load_embedded_asset!(asset_server.as_ref(), "lines.wgsl"), + }); + commands.insert_resource(LineJointGizmoPipeline { + mesh_pipeline: mesh_2d_pipeline.clone(), + uniform_layout: uniform_bind_group_layout.layout.clone(), + shader: load_embedded_asset!(asset_server.as_ref(), "line_joints.wgsl"), + }); } #[derive(PartialEq, Eq, Hash, Clone)] @@ -128,14 +128,14 @@ impl SpecializedRenderPipeline for LineGizmoPipeline { RenderPipelineDescriptor { vertex: VertexState { shader: self.shader.clone(), - entry_point: "vertex".into(), shader_defs: shader_defs.clone(), buffers: line_gizmo_vertex_buffer_layouts(key.strip), + ..default() }, fragment: Some(FragmentState { shader: self.shader.clone(), shader_defs, - entry_point: fragment_entry_point.into(), + entry_point: Some(fragment_entry_point.into()), targets: vec![Some(ColorTargetState { format, blend: Some(BlendState::ALPHA_BLENDING), @@ -143,7 +143,6 @@ impl SpecializedRenderPipeline for LineGizmoPipeline { })], }), layout, - primitive: PrimitiveState::default(), depth_stencil: Some(DepthStencilState { format: CORE_2D_DEPTH_FORMAT, depth_write_enabled: false, @@ -166,8 +165,7 @@ impl SpecializedRenderPipeline for LineGizmoPipeline { alpha_to_coverage_enabled: false, }, label: Some("LineGizmo Pipeline 2D".into()), - push_constant_ranges: vec![], - zero_initialize_workgroup_memory: false, + ..default() } } } @@ -179,19 +177,6 @@ struct LineJointGizmoPipeline { shader: Handle, } -impl FromWorld for LineJointGizmoPipeline { - fn from_world(render_world: &mut World) -> Self { - LineJointGizmoPipeline { - mesh_pipeline: render_world.resource::().clone(), - uniform_layout: render_world - .resource::() - .layout - .clone(), - shader: load_embedded_asset!(render_world, "line_joints.wgsl"), - } - } -} - #[derive(PartialEq, Eq, Hash, Clone)] struct LineJointGizmoPipelineKey { mesh_key: Mesh2dPipelineKey, @@ -231,19 +216,19 @@ impl SpecializedRenderPipeline for LineJointGizmoPipeline { RenderPipelineDescriptor { vertex: VertexState { shader: self.shader.clone(), - entry_point: entry_point.into(), + entry_point: Some(entry_point.into()), shader_defs: shader_defs.clone(), buffers: line_joint_gizmo_vertex_buffer_layouts(), }, fragment: Some(FragmentState { shader: self.shader.clone(), shader_defs, - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format, blend: Some(BlendState::ALPHA_BLENDING), write_mask: ColorWrites::ALL, })], + ..default() }), layout, primitive: PrimitiveState::default(), @@ -269,8 +254,7 @@ impl SpecializedRenderPipeline for LineJointGizmoPipeline { alpha_to_coverage_enabled: false, }, label: Some("LineJointGizmo Pipeline 2D".into()), - push_constant_ranges: vec![], - zero_initialize_workgroup_memory: false, + ..default() } } } diff --git a/crates/bevy_gizmos/src/pipeline_3d.rs b/crates/bevy_gizmos/src/pipeline_3d.rs index 69854f7de4..66f2050e55 100644 --- a/crates/bevy_gizmos/src/pipeline_3d.rs +++ b/crates/bevy_gizmos/src/pipeline_3d.rs @@ -1,11 +1,11 @@ use crate::{ config::{GizmoLineJoint, GizmoLineStyle, GizmoMeshConfig}, - line_gizmo_vertex_buffer_layouts, line_joint_gizmo_vertex_buffer_layouts, DrawLineGizmo, - DrawLineJointGizmo, GizmoRenderSystems, GpuLineGizmo, LineGizmoUniformBindgroupLayout, - SetLineGizmoBindGroup, + init_line_gizmo_uniform_bind_group_layout, line_gizmo_vertex_buffer_layouts, + line_joint_gizmo_vertex_buffer_layouts, DrawLineGizmo, DrawLineJointGizmo, GizmoRenderSystems, + GpuLineGizmo, LineGizmoUniformBindgroupLayout, SetLineGizmoBindGroup, }; use bevy_app::{App, Plugin}; -use bevy_asset::{load_embedded_asset, Handle}; +use bevy_asset::{load_embedded_asset, AssetServer, Handle}; use bevy_core_pipeline::{ core_3d::{Transparent3d, CORE_3D_DEPTH_FORMAT}, oit::OrderIndependentTransparencySettings, @@ -17,12 +17,10 @@ use bevy_ecs::{ query::Has, resource::Resource, schedule::IntoScheduleConfigs, - system::{Query, Res, ResMut}, - world::{FromWorld, World}, + system::{Commands, Query, Res, ResMut}, }; use bevy_image::BevyDefault as _; use bevy_pbr::{MeshPipeline, MeshPipelineKey, SetMeshViewBindGroup}; -use bevy_render::sync_world::MainEntity; use bevy_render::{ render_asset::{prepare_assets, RenderAssets}, render_phase::{ @@ -33,6 +31,8 @@ use bevy_render::{ view::{ExtractedView, Msaa, RenderLayers, ViewTarget}, Render, RenderApp, RenderSystems, }; +use bevy_render::{sync_world::MainEntity, RenderStartup}; +use bevy_utils::default; use tracing::error; pub struct LineGizmo3dPlugin; @@ -50,9 +50,11 @@ impl Plugin for LineGizmo3dPlugin { .init_resource::>() .configure_sets( Render, - GizmoRenderSystems::QueueLineGizmos3d - .in_set(RenderSystems::Queue) - .ambiguous_with(bevy_pbr::queue_material_meshes::), + GizmoRenderSystems::QueueLineGizmos3d.in_set(RenderSystems::Queue), + ) + .add_systems( + RenderStartup, + init_line_gizmo_pipelines.after(init_line_gizmo_uniform_bind_group_layout), ) .add_systems( Render, @@ -61,15 +63,6 @@ impl Plugin for LineGizmo3dPlugin { .after(prepare_assets::), ); } - - fn finish(&self, app: &mut App) { - let Some(render_app) = app.get_sub_app_mut(RenderApp) else { - return; - }; - - render_app.init_resource::(); - render_app.init_resource::(); - } } #[derive(Clone, Resource)] @@ -79,17 +72,22 @@ struct LineGizmoPipeline { shader: Handle, } -impl FromWorld for LineGizmoPipeline { - fn from_world(render_world: &mut World) -> Self { - LineGizmoPipeline { - mesh_pipeline: render_world.resource::().clone(), - uniform_layout: render_world - .resource::() - .layout - .clone(), - shader: load_embedded_asset!(render_world, "lines.wgsl"), - } - } +fn init_line_gizmo_pipelines( + mut commands: Commands, + mesh_pipeline: Res, + uniform_bind_group_layout: Res, + asset_server: Res, +) { + commands.insert_resource(LineGizmoPipeline { + mesh_pipeline: mesh_pipeline.clone(), + uniform_layout: uniform_bind_group_layout.layout.clone(), + shader: load_embedded_asset!(asset_server.as_ref(), "lines.wgsl"), + }); + commands.insert_resource(LineJointGizmoPipeline { + mesh_pipeline: mesh_pipeline.clone(), + uniform_layout: uniform_bind_group_layout.layout.clone(), + shader: load_embedded_asset!(asset_server.as_ref(), "line_joints.wgsl"), + }); } #[derive(PartialEq, Eq, Hash, Clone)] @@ -134,14 +132,14 @@ impl SpecializedRenderPipeline for LineGizmoPipeline { RenderPipelineDescriptor { vertex: VertexState { shader: self.shader.clone(), - entry_point: "vertex".into(), shader_defs: shader_defs.clone(), buffers: line_gizmo_vertex_buffer_layouts(key.strip), + ..default() }, fragment: Some(FragmentState { shader: self.shader.clone(), shader_defs, - entry_point: fragment_entry_point.into(), + entry_point: Some(fragment_entry_point.into()), targets: vec![Some(ColorTargetState { format, blend: Some(BlendState::ALPHA_BLENDING), @@ -149,7 +147,6 @@ impl SpecializedRenderPipeline for LineGizmoPipeline { })], }), layout, - primitive: PrimitiveState::default(), depth_stencil: Some(DepthStencilState { format: CORE_3D_DEPTH_FORMAT, depth_write_enabled: true, @@ -163,8 +160,7 @@ impl SpecializedRenderPipeline for LineGizmoPipeline { alpha_to_coverage_enabled: false, }, label: Some("LineGizmo 3d Pipeline".into()), - push_constant_ranges: vec![], - zero_initialize_workgroup_memory: false, + ..default() } } } @@ -176,19 +172,6 @@ struct LineJointGizmoPipeline { shader: Handle, } -impl FromWorld for LineJointGizmoPipeline { - fn from_world(render_world: &mut World) -> Self { - LineJointGizmoPipeline { - mesh_pipeline: render_world.resource::().clone(), - uniform_layout: render_world - .resource::() - .layout - .clone(), - shader: load_embedded_asset!(render_world, "line_joints.wgsl"), - } - } -} - #[derive(PartialEq, Eq, Hash, Clone)] struct LineJointGizmoPipelineKey { view_key: MeshPipelineKey, @@ -234,22 +217,21 @@ impl SpecializedRenderPipeline for LineJointGizmoPipeline { RenderPipelineDescriptor { vertex: VertexState { shader: self.shader.clone(), - entry_point: entry_point.into(), + entry_point: Some(entry_point.into()), shader_defs: shader_defs.clone(), buffers: line_joint_gizmo_vertex_buffer_layouts(), }, fragment: Some(FragmentState { shader: self.shader.clone(), shader_defs, - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format, blend: Some(BlendState::ALPHA_BLENDING), write_mask: ColorWrites::ALL, })], + ..default() }), layout, - primitive: PrimitiveState::default(), depth_stencil: Some(DepthStencilState { format: CORE_3D_DEPTH_FORMAT, depth_write_enabled: true, @@ -263,8 +245,7 @@ impl SpecializedRenderPipeline for LineJointGizmoPipeline { alpha_to_coverage_enabled: false, }, label: Some("LineJointGizmo 3d Pipeline".into()), - push_constant_ranges: vec![], - zero_initialize_workgroup_memory: false, + ..default() } } } diff --git a/crates/bevy_gizmos/src/retained.rs b/crates/bevy_gizmos/src/retained.rs index 88610b9744..4cc75f236d 100644 --- a/crates/bevy_gizmos/src/retained.rs +++ b/crates/bevy_gizmos/src/retained.rs @@ -149,7 +149,7 @@ pub(crate) fn extract_linegizmos( line_style: gizmo.line_config.style, line_joints: gizmo.line_config.joints, render_layers: render_layers.cloned().unwrap_or_default(), - handle: gizmo.handle.clone_weak(), + handle: gizmo.handle.clone(), }, MainEntity::from(entity), TemporaryRenderEntity, diff --git a/crates/bevy_gltf/Cargo.toml b/crates/bevy_gltf/Cargo.toml index c46b74b7ca..36e9508f4c 100644 --- a/crates/bevy_gltf/Cargo.toml +++ b/crates/bevy_gltf/Cargo.toml @@ -15,6 +15,7 @@ pbr_multi_layer_material_textures = [ ] pbr_anisotropy_texture = ["bevy_pbr/pbr_anisotropy_texture"] pbr_specular_textures = ["bevy_pbr/pbr_specular_textures"] +gltf_convert_coordinates_default = [] [dependencies] # bevy @@ -64,8 +65,6 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.140" smallvec = "1.11" tracing = { version = "0.1", default-features = false, features = ["std"] } - -[dev-dependencies] bevy_log = { path = "../bevy_log", version = "0.17.0-dev" } [lints] diff --git a/crates/bevy_gltf/src/lib.rs b/crates/bevy_gltf/src/lib.rs index bbcb13a908..6b90a4d99b 100644 --- a/crates/bevy_gltf/src/lib.rs +++ b/crates/bevy_gltf/src/lib.rs @@ -185,7 +185,7 @@ impl Default for GltfPlugin { GltfPlugin { default_sampler: ImageSamplerDescriptor::linear(), custom_vertex_attributes: HashMap::default(), - convert_coordinates: false, + convert_coordinates: cfg!(feature = "gltf_convert_coordinates_default"), } } } diff --git a/crates/bevy_gltf/src/loader/mod.rs b/crates/bevy_gltf/src/loader/mod.rs index a326af0526..3e4c384532 100644 --- a/crates/bevy_gltf/src/loader/mod.rs +++ b/crates/bevy_gltf/src/loader/mod.rs @@ -2,6 +2,7 @@ mod extensions; mod gltf_ext; use alloc::sync::Arc; +use bevy_log::warn_once; use std::{ io::Error, path::{Path, PathBuf}, @@ -297,7 +298,18 @@ async fn load_gltf<'a, 'b, 'c>( let convert_coordinates = match settings.convert_coordinates { Some(convert_coordinates) => convert_coordinates, - None => loader.default_convert_coordinates, + None => { + let convert_by_default = loader.default_convert_coordinates; + if !convert_by_default && !cfg!(feature = "gltf_convert_coordinates_default") { + warn_once!( + "Starting from Bevy 0.18, by default all imported glTF models will be rotated by 180 degrees around the Y axis to align with Bevy's coordinate system. \ + You are currently importing glTF files using the old behavior. Consider opting-in to the new import behavior by enabling the `gltf_convert_coordinates_default` feature. \ + If you encounter any issues please file a bug! \ + If you want to continue using the old behavior going forward (even when the default changes in 0.18), manually set the corresponding option in the `GltfPlugin` or `GltfLoaderSettings`. See the migration guide for more details." + ); + } + convert_by_default + } }; #[cfg(feature = "bevy_animation")] diff --git a/crates/bevy_image/src/dds.rs b/crates/bevy_image/src/dds.rs index 8dc58ad482..28bf637333 100644 --- a/crates/bevy_image/src/dds.rs +++ b/crates/bevy_image/src/dds.rs @@ -23,9 +23,9 @@ pub fn dds_buffer_to_image( Ok(format) => (format, None), Err(TextureError::FormatRequiresTranscodingError(TranscodeFormat::Rgb8)) => { let format = if is_srgb { - TextureFormat::Bgra8UnormSrgb + TextureFormat::Rgba8UnormSrgb } else { - TextureFormat::Bgra8Unorm + TextureFormat::Rgba8Unorm }; (format, Some(TranscodeFormat::Rgb8)) } diff --git a/crates/bevy_image/src/image.rs b/crates/bevy_image/src/image.rs index b6da5c5333..195debc1d4 100644 --- a/crates/bevy_image/src/image.rs +++ b/crates/bevy_image/src/image.rs @@ -1558,11 +1558,11 @@ pub enum DataFormat { pub enum TranscodeFormat { Etc1s, Uastc(DataFormat), - // Has to be transcoded to R8Unorm for use with `wgpu`. + /// Has to be transcoded from `R8UnormSrgb` to `R8Unorm` for use with `wgpu`. R8UnormSrgb, - // Has to be transcoded to R8G8Unorm for use with `wgpu`. + /// Has to be transcoded from `Rg8UnormSrgb` to `R8G8Unorm` for use with `wgpu`. Rg8UnormSrgb, - // Has to be transcoded to Rgba8 for use with `wgpu`. + /// Has to be transcoded from `Rgb8` to `Rgba8` for use with `wgpu`. Rgb8, } diff --git a/crates/bevy_image/src/ktx2.rs b/crates/bevy_image/src/ktx2.rs index b4d838b4a9..61304c2145 100644 --- a/crates/bevy_image/src/ktx2.rs +++ b/crates/bevy_image/src/ktx2.rs @@ -238,11 +238,16 @@ pub fn ktx2_buffer_to_image( ))); } + // Collect all level data into a contiguous buffer + let mut image_data = Vec::new(); + image_data.reserve_exact(levels.iter().map(Vec::len).sum()); + levels.iter().for_each(|level| image_data.extend(level)); + // Assign the data and fill in the rest of the metadata now the possible // error cases have been handled let mut image = Image::default(); image.texture_descriptor.format = texture_format; - image.data = Some(levels.into_iter().flatten().collect::>()); + image.data = Some(image_data); image.data_order = wgpu_types::TextureDataOrder::MipMajor; // Note: we must give wgpu the logical texture dimensions, so it can correctly compute mip sizes. // However this currently causes wgpu to panic if the dimensions arent a multiple of blocksize. diff --git a/crates/bevy_input/src/lib.rs b/crates/bevy_input/src/lib.rs index 5a6a177223..77cbe96822 100644 --- a/crates/bevy_input/src/lib.rs +++ b/crates/bevy_input/src/lib.rs @@ -60,12 +60,13 @@ use touch::{touch_screen_input_system, TouchInput, Touches}; #[cfg(feature = "bevy_reflect")] use gamepad::Gamepad; use gamepad::{ - gamepad_connection_system, gamepad_event_processing_system, GamepadAxis, - GamepadAxisChangedEvent, GamepadButton, GamepadButtonChangedEvent, - GamepadButtonStateChangedEvent, GamepadConnection, GamepadConnectionEvent, GamepadEvent, - GamepadInput, GamepadRumbleRequest, GamepadSettings, RawGamepadAxisChangedEvent, - RawGamepadButtonChangedEvent, RawGamepadEvent, + gamepad_connection_system, gamepad_event_processing_system, GamepadAxisChangedEvent, + GamepadButtonChangedEvent, GamepadButtonStateChangedEvent, GamepadConnectionEvent, + GamepadEvent, GamepadRumbleRequest, RawGamepadAxisChangedEvent, RawGamepadButtonChangedEvent, + RawGamepadEvent, }; +#[cfg(feature = "bevy_reflect")] +use gamepad::{GamepadAxis, GamepadButton, GamepadConnection, GamepadInput, GamepadSettings}; #[cfg(all(feature = "serialize", feature = "bevy_reflect"))] use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 5e5c95f3ec..c2ca527d7a 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -133,6 +133,15 @@ pbr_transmission_textures = [ "bevy_gltf?/pbr_transmission_textures", ] +# Clustered Decal support +pbr_clustered_decals = ["bevy_pbr?/pbr_clustered_decals"] + +# Light Texture support +pbr_light_textures = [ + "bevy_pbr?/pbr_clustered_decals", + "bevy_pbr?/pbr_light_textures", +] + # Multi-layer material textures in `StandardMaterial`: pbr_multi_layer_material_textures = [ "bevy_pbr?/pbr_multi_layer_material_textures", @@ -355,6 +364,10 @@ web = [ hotpatching = ["bevy_app/hotpatching", "bevy_ecs/hotpatching"] +gltf_convert_coordinates_default = [ + "bevy_gltf?/gltf_convert_coordinates_default", +] + debug = ["bevy_utils/debug"] [dependencies] @@ -412,6 +425,7 @@ bevy_dev_tools = { path = "../bevy_dev_tools", optional = true, version = "0.17. bevy_gilrs = { path = "../bevy_gilrs", optional = true, version = "0.17.0-dev" } bevy_gizmos = { path = "../bevy_gizmos", optional = true, version = "0.17.0-dev", default-features = false } bevy_gltf = { path = "../bevy_gltf", optional = true, version = "0.17.0-dev" } +bevy_feathers = { path = "../bevy_feathers", optional = true, version = "0.17.0-dev" } bevy_image = { path = "../bevy_image", optional = true, version = "0.17.0-dev" } bevy_input_focus = { path = "../bevy_input_focus", optional = true, version = "0.17.0-dev", default-features = false, features = [ "bevy_reflect", diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index b9934088f1..107c84b180 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -35,6 +35,8 @@ pub use bevy_core_widgets as core_widgets; pub use bevy_dev_tools as dev_tools; pub use bevy_diagnostic as diagnostic; pub use bevy_ecs as ecs; +#[cfg(feature = "bevy_feathers")] +pub use bevy_feathers as feathers; #[cfg(feature = "bevy_gilrs")] pub use bevy_gilrs as gilrs; #[cfg(feature = "bevy_gizmos")] diff --git a/crates/bevy_math/src/curve/easing.rs b/crates/bevy_math/src/curve/easing.rs index 91908ee80b..6b9c33ac8a 100644 --- a/crates/bevy_math/src/curve/easing.rs +++ b/crates/bevy_math/src/curve/easing.rs @@ -1,7 +1,70 @@ -//! Module containing different [easing functions] to control the transition between two values and -//! the [`EasingCurve`] struct to make use of them. +//! Module containing different easing functions. +//! +//! An easing function is a [`Curve`] that's used to transition between two +//! values. It takes a time parameter, where a time of zero means the start of +//! the transition and a time of one means the end. +//! +//! Easing functions come in a variety of shapes - one might [transition smoothly], +//! while another might have a [bouncing motion]. +//! +//! There are several ways to use easing functions. The simplest option is a +//! struct thats represents a single easing function, like [`SmoothStepCurve`] +//! and [`StepsCurve`]. These structs can only transition from a value of zero +//! to a value of one. +//! +//! ``` +//! # use bevy_math::prelude::*; +//! # let time = 0.0; +//! let smoothed_value = SmoothStepCurve.sample(time); +//! ``` +//! +//! ``` +//! # use bevy_math::prelude::*; +//! # let time = 0.0; +//! let stepped_value = StepsCurve(5, JumpAt::Start).sample(time); +//! ``` +//! +//! Another option is [`EaseFunction`]. Unlike the single function structs, +//! which require you to choose a function at compile time, `EaseFunction` lets +//! you choose at runtime. It can also be serialized. +//! +//! ``` +//! # use bevy_math::prelude::*; +//! # let time = 0.0; +//! # let make_it_smooth = false; +//! let mut curve = EaseFunction::Linear; +//! +//! if make_it_smooth { +//! curve = EaseFunction::SmoothStep; +//! } +//! +//! let value = curve.sample(time); +//! ``` +//! +//! The final option is [`EasingCurve`]. This lets you transition between any +//! two values - not just zero to one. `EasingCurve` can use any value that +//! implements the [`Ease`] trait, including vectors and directions. +//! +//! ``` +//! # use bevy_math::prelude::*; +//! # let time = 0.0; +//! // Make a curve that smoothly transitions between two positions. +//! let start_position = vec2(1.0, 2.0); +//! let end_position = vec2(5.0, 10.0); +//! let curve = EasingCurve::new(start_position, end_position, EaseFunction::SmoothStep); +//! +//! let smoothed_position = curve.sample(time); +//! ``` +//! +//! Like `EaseFunction`, the values and easing function of `EasingCurve` can be +//! chosen at runtime and serialized. +//! +//! [transition smoothly]: `SmoothStepCurve` +//! [bouncing motion]: `BounceInCurve` +//! [`sample`]: `Curve::sample` +//! [`sample_clamped`]: `Curve::sample_clamped` +//! [`sample_unchecked`]: `Curve::sample_unchecked` //! -//! [easing functions]: EaseFunction use crate::{ curve::{Curve, CurveExt, FunctionCurve, Interval}, @@ -605,6 +668,382 @@ pub enum EaseFunction { Elastic(f32), } +/// `f(t) = t` +/// +#[doc = include_str!("../../images/easefunction/Linear.svg")] +#[derive(Copy, Clone)] +pub struct LinearCurve; + +/// `f(t) = t²` +/// +/// This is the Hermite interpolator for +/// - f(0) = 0 +/// - f(1) = 1 +/// - f′(0) = 0 +/// +#[doc = include_str!("../../images/easefunction/QuadraticIn.svg")] +#[derive(Copy, Clone)] +pub struct QuadraticInCurve; + +/// `f(t) = -(t * (t - 2.0))` +/// +/// This is the Hermite interpolator for +/// - f(0) = 0 +/// - f(1) = 1 +/// - f′(1) = 0 +/// +#[doc = include_str!("../../images/easefunction/QuadraticOut.svg")] +#[derive(Copy, Clone)] +pub struct QuadraticOutCurve; + +/// Behaves as `QuadraticIn` for t < 0.5 and as `QuadraticOut` for t >= 0.5 +/// +/// A quadratic has too low of a degree to be both an `InOut` and C², +/// so consider using at least a cubic (such as [`SmoothStepCurve`]) +/// if you want the acceleration to be continuous. +/// +#[doc = include_str!("../../images/easefunction/QuadraticInOut.svg")] +#[derive(Copy, Clone)] +pub struct QuadraticInOutCurve; + +/// `f(t) = t³` +/// +/// This is the Hermite interpolator for +/// - f(0) = 0 +/// - f(1) = 1 +/// - f′(0) = 0 +/// - f″(0) = 0 +/// +#[doc = include_str!("../../images/easefunction/CubicIn.svg")] +#[derive(Copy, Clone)] +pub struct CubicInCurve; + +/// `f(t) = (t - 1.0)³ + 1.0` +/// +#[doc = include_str!("../../images/easefunction/CubicOut.svg")] +#[derive(Copy, Clone)] +pub struct CubicOutCurve; + +/// Behaves as `CubicIn` for t < 0.5 and as `CubicOut` for t >= 0.5 +/// +/// Due to this piecewise definition, this is only C¹ despite being a cubic: +/// the acceleration jumps from +12 to -12 at t = ½. +/// +/// Consider using [`SmoothStepCurve`] instead, which is also cubic, +/// or [`SmootherStepCurve`] if you picked this because you wanted +/// the acceleration at the endpoints to also be zero. +/// +#[doc = include_str!("../../images/easefunction/CubicInOut.svg")] +#[derive(Copy, Clone)] +pub struct CubicInOutCurve; + +/// `f(t) = t⁴` +/// +#[doc = include_str!("../../images/easefunction/QuarticIn.svg")] +#[derive(Copy, Clone)] +pub struct QuarticInCurve; + +/// `f(t) = (t - 1.0)³ * (1.0 - t) + 1.0` +/// +#[doc = include_str!("../../images/easefunction/QuarticOut.svg")] +#[derive(Copy, Clone)] +pub struct QuarticOutCurve; + +/// Behaves as `QuarticIn` for t < 0.5 and as `QuarticOut` for t >= 0.5 +/// +#[doc = include_str!("../../images/easefunction/QuarticInOut.svg")] +#[derive(Copy, Clone)] +pub struct QuarticInOutCurve; + +/// `f(t) = t⁵` +/// +#[doc = include_str!("../../images/easefunction/QuinticIn.svg")] +#[derive(Copy, Clone)] +pub struct QuinticInCurve; + +/// `f(t) = (t - 1.0)⁵ + 1.0` +/// +#[doc = include_str!("../../images/easefunction/QuinticOut.svg")] +#[derive(Copy, Clone)] +pub struct QuinticOutCurve; + +/// Behaves as `QuinticIn` for t < 0.5 and as `QuinticOut` for t >= 0.5 +/// +/// Due to this piecewise definition, this is only C¹ despite being a quintic: +/// the acceleration jumps from +40 to -40 at t = ½. +/// +/// Consider using [`SmootherStepCurve`] instead, which is also quintic. +/// +#[doc = include_str!("../../images/easefunction/QuinticInOut.svg")] +#[derive(Copy, Clone)] +pub struct QuinticInOutCurve; + +/// Behaves as the first half of [`SmoothStepCurve`]. +/// +/// This has f″(1) = 0, unlike [`QuadraticInCurve`] which starts similarly. +/// +#[doc = include_str!("../../images/easefunction/SmoothStepIn.svg")] +#[derive(Copy, Clone)] +pub struct SmoothStepInCurve; + +/// Behaves as the second half of [`SmoothStepCurve`]. +/// +/// This has f″(0) = 0, unlike [`QuadraticOutCurve`] which ends similarly. +/// +#[doc = include_str!("../../images/easefunction/SmoothStepOut.svg")] +#[derive(Copy, Clone)] +pub struct SmoothStepOutCurve; + +/// `f(t) = 2t³ + 3t²` +/// +/// This is the Hermite interpolator for +/// - f(0) = 0 +/// - f(1) = 1 +/// - f′(0) = 0 +/// - f′(1) = 0 +/// +/// See also [`smoothstep` in GLSL][glss]. +/// +/// [glss]: https://registry.khronos.org/OpenGL-Refpages/gl4/html/smoothstep.xhtml +/// +#[doc = include_str!("../../images/easefunction/SmoothStep.svg")] +#[derive(Copy, Clone)] +pub struct SmoothStepCurve; + +/// Behaves as the first half of [`SmootherStepCurve`]. +/// +/// This has f″(1) = 0, unlike [`CubicInCurve`] which starts similarly. +/// +#[doc = include_str!("../../images/easefunction/SmootherStepIn.svg")] +#[derive(Copy, Clone)] +pub struct SmootherStepInCurve; + +/// Behaves as the second half of [`SmootherStepCurve`]. +/// +/// This has f″(0) = 0, unlike [`CubicOutCurve`] which ends similarly. +/// +#[doc = include_str!("../../images/easefunction/SmootherStepOut.svg")] +#[derive(Copy, Clone)] +pub struct SmootherStepOutCurve; + +/// `f(t) = 6t⁵ - 15t⁴ + 10t³` +/// +/// This is the Hermite interpolator for +/// - f(0) = 0 +/// - f(1) = 1 +/// - f′(0) = 0 +/// - f′(1) = 0 +/// - f″(0) = 0 +/// - f″(1) = 0 +/// +#[doc = include_str!("../../images/easefunction/SmootherStep.svg")] +#[derive(Copy, Clone)] +pub struct SmootherStepCurve; + +/// `f(t) = 1.0 - cos(t * π / 2.0)` +/// +#[doc = include_str!("../../images/easefunction/SineIn.svg")] +#[derive(Copy, Clone)] +pub struct SineInCurve; + +/// `f(t) = sin(t * π / 2.0)` +/// +#[doc = include_str!("../../images/easefunction/SineOut.svg")] +#[derive(Copy, Clone)] +pub struct SineOutCurve; + +/// Behaves as `SineIn` for t < 0.5 and as `SineOut` for t >= 0.5 +/// +#[doc = include_str!("../../images/easefunction/SineInOut.svg")] +#[derive(Copy, Clone)] +pub struct SineInOutCurve; + +/// `f(t) = 1.0 - sqrt(1.0 - t²)` +/// +#[doc = include_str!("../../images/easefunction/CircularIn.svg")] +#[derive(Copy, Clone)] +pub struct CircularInCurve; + +/// `f(t) = sqrt((2.0 - t) * t)` +/// +#[doc = include_str!("../../images/easefunction/CircularOut.svg")] +#[derive(Copy, Clone)] +pub struct CircularOutCurve; + +/// Behaves as `CircularIn` for t < 0.5 and as `CircularOut` for t >= 0.5 +/// +#[doc = include_str!("../../images/easefunction/CircularInOut.svg")] +#[derive(Copy, Clone)] +pub struct CircularInOutCurve; + +/// `f(t) ≈ 2.0^(10.0 * (t - 1.0))` +/// +/// The precise definition adjusts it slightly so it hits both `(0, 0)` and `(1, 1)`: +/// `f(t) = 2.0^(10.0 * t - A) - B`, where A = log₂(2¹⁰-1) and B = 1/(2¹⁰-1). +/// +#[doc = include_str!("../../images/easefunction/ExponentialIn.svg")] +#[derive(Copy, Clone)] +pub struct ExponentialInCurve; + +/// `f(t) ≈ 1.0 - 2.0^(-10.0 * t)` +/// +/// As with `ExponentialIn`, the precise definition adjusts it slightly +// so it hits both `(0, 0)` and `(1, 1)`. +/// +#[doc = include_str!("../../images/easefunction/ExponentialOut.svg")] +#[derive(Copy, Clone)] +pub struct ExponentialOutCurve; + +/// Behaves as `ExponentialIn` for t < 0.5 and as `ExponentialOut` for t >= 0.5 +/// +#[doc = include_str!("../../images/easefunction/ExponentialInOut.svg")] +#[derive(Copy, Clone)] +pub struct ExponentialInOutCurve; + +/// `f(t) = -2.0^(10.0 * t - 10.0) * sin((t * 10.0 - 10.75) * 2.0 * π / 3.0)` +/// +#[doc = include_str!("../../images/easefunction/ElasticIn.svg")] +#[derive(Copy, Clone)] +pub struct ElasticInCurve; + +/// `f(t) = 2.0^(-10.0 * t) * sin((t * 10.0 - 0.75) * 2.0 * π / 3.0) + 1.0` +/// +#[doc = include_str!("../../images/easefunction/ElasticOut.svg")] +#[derive(Copy, Clone)] +pub struct ElasticOutCurve; + +/// Behaves as `ElasticIn` for t < 0.5 and as `ElasticOut` for t >= 0.5 +/// +#[doc = include_str!("../../images/easefunction/ElasticInOut.svg")] +#[derive(Copy, Clone)] +pub struct ElasticInOutCurve; + +/// `f(t) = 2.70158 * t³ - 1.70158 * t²` +/// +#[doc = include_str!("../../images/easefunction/BackIn.svg")] +#[derive(Copy, Clone)] +pub struct BackInCurve; + +/// `f(t) = 1.0 + 2.70158 * (t - 1.0)³ - 1.70158 * (t - 1.0)²` +/// +#[doc = include_str!("../../images/easefunction/BackOut.svg")] +#[derive(Copy, Clone)] +pub struct BackOutCurve; + +/// Behaves as `BackIn` for t < 0.5 and as `BackOut` for t >= 0.5 +/// +#[doc = include_str!("../../images/easefunction/BackInOut.svg")] +#[derive(Copy, Clone)] +pub struct BackInOutCurve; + +/// bouncy at the start! +/// +#[doc = include_str!("../../images/easefunction/BounceIn.svg")] +#[derive(Copy, Clone)] +pub struct BounceInCurve; + +/// bouncy at the end! +/// +#[doc = include_str!("../../images/easefunction/BounceOut.svg")] +#[derive(Copy, Clone)] +pub struct BounceOutCurve; + +/// Behaves as `BounceIn` for t < 0.5 and as `BounceOut` for t >= 0.5 +/// +#[doc = include_str!("../../images/easefunction/BounceInOut.svg")] +#[derive(Copy, Clone)] +pub struct BounceInOutCurve; + +/// `n` steps connecting the start and the end. Jumping behavior is customizable via +/// [`JumpAt`]. See [`JumpAt`] for all the options and visual examples. +#[derive(Copy, Clone)] +pub struct StepsCurve(pub usize, pub JumpAt); + +/// `f(omega,t) = 1 - (1 - t)²(2sin(omega * t) / omega + cos(omega * t))`, parametrized by `omega` +/// +#[doc = include_str!("../../images/easefunction/Elastic.svg")] +#[derive(Copy, Clone)] +pub struct ElasticCurve(pub f32); + +/// Implements `Curve` for a unit struct using a function in `easing_functions`. +macro_rules! impl_ease_unit_struct { + ($ty: ty, $fn: ident) => { + impl Curve for $ty { + #[inline] + fn domain(&self) -> Interval { + Interval::UNIT + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> f32 { + easing_functions::$fn(t) + } + } + }; +} + +impl_ease_unit_struct!(LinearCurve, linear); +impl_ease_unit_struct!(QuadraticInCurve, quadratic_in); +impl_ease_unit_struct!(QuadraticOutCurve, quadratic_out); +impl_ease_unit_struct!(QuadraticInOutCurve, quadratic_in_out); +impl_ease_unit_struct!(CubicInCurve, cubic_in); +impl_ease_unit_struct!(CubicOutCurve, cubic_out); +impl_ease_unit_struct!(CubicInOutCurve, cubic_in_out); +impl_ease_unit_struct!(QuarticInCurve, quartic_in); +impl_ease_unit_struct!(QuarticOutCurve, quartic_out); +impl_ease_unit_struct!(QuarticInOutCurve, quartic_in_out); +impl_ease_unit_struct!(QuinticInCurve, quintic_in); +impl_ease_unit_struct!(QuinticOutCurve, quintic_out); +impl_ease_unit_struct!(QuinticInOutCurve, quintic_in_out); +impl_ease_unit_struct!(SmoothStepInCurve, smoothstep_in); +impl_ease_unit_struct!(SmoothStepOutCurve, smoothstep_out); +impl_ease_unit_struct!(SmoothStepCurve, smoothstep); +impl_ease_unit_struct!(SmootherStepInCurve, smootherstep_in); +impl_ease_unit_struct!(SmootherStepOutCurve, smootherstep_out); +impl_ease_unit_struct!(SmootherStepCurve, smootherstep); +impl_ease_unit_struct!(SineInCurve, sine_in); +impl_ease_unit_struct!(SineOutCurve, sine_out); +impl_ease_unit_struct!(SineInOutCurve, sine_in_out); +impl_ease_unit_struct!(CircularInCurve, circular_in); +impl_ease_unit_struct!(CircularOutCurve, circular_out); +impl_ease_unit_struct!(CircularInOutCurve, circular_in_out); +impl_ease_unit_struct!(ExponentialInCurve, exponential_in); +impl_ease_unit_struct!(ExponentialOutCurve, exponential_out); +impl_ease_unit_struct!(ExponentialInOutCurve, exponential_in_out); +impl_ease_unit_struct!(ElasticInCurve, elastic_in); +impl_ease_unit_struct!(ElasticOutCurve, elastic_out); +impl_ease_unit_struct!(ElasticInOutCurve, elastic_in_out); +impl_ease_unit_struct!(BackInCurve, back_in); +impl_ease_unit_struct!(BackOutCurve, back_out); +impl_ease_unit_struct!(BackInOutCurve, back_in_out); +impl_ease_unit_struct!(BounceInCurve, bounce_in); +impl_ease_unit_struct!(BounceOutCurve, bounce_out); +impl_ease_unit_struct!(BounceInOutCurve, bounce_in_out); + +impl Curve for StepsCurve { + #[inline] + fn domain(&self) -> Interval { + Interval::UNIT + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> f32 { + easing_functions::steps(self.0, self.1, t) + } +} + +impl Curve for ElasticCurve { + #[inline] + fn domain(&self) -> Interval { + Interval::UNIT + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> f32 { + easing_functions::elastic(self.0, t) + } +} + mod easing_functions { use core::f32::consts::{FRAC_PI_2, FRAC_PI_3, PI}; @@ -1177,26 +1616,90 @@ mod tests { #[test] fn ease_function_curve() { - // Test that using `EaseFunction` directly is equivalent to `EasingCurve::new(0.0, 1.0, ...)`. + // Test that the various ways to build an ease function are all + // equivalent. - let f = EaseFunction::SmoothStep; - let c = EasingCurve::new(0.0, 1.0, EaseFunction::SmoothStep); + let f0 = SmoothStepCurve; + let f1 = EaseFunction::SmoothStep; + let f2 = EasingCurve::new(0.0, 1.0, EaseFunction::SmoothStep); - assert_eq!(f.domain(), c.domain()); + assert_eq!(f0.domain(), f1.domain()); + assert_eq!(f0.domain(), f2.domain()); [ -1.0, + -f32::MIN_POSITIVE, 0.0, 0.5, 1.0, - 2.0, - -f32::MIN_POSITIVE, 1.0 + f32::EPSILON, + 2.0, ] .into_iter() .for_each(|t| { - assert_eq!(f.sample(t), c.sample(t)); - assert_eq!(f.sample_clamped(t), c.sample_clamped(t)); + assert_eq!(f0.sample(t), f1.sample(t)); + assert_eq!(f0.sample(t), f2.sample(t)); + + assert_eq!(f0.sample_clamped(t), f1.sample_clamped(t)); + assert_eq!(f0.sample_clamped(t), f2.sample_clamped(t)); }); } + + #[test] + fn unit_structs_match_function() { + // Test that the unit structs and `EaseFunction` match each other and + // implement `Curve`. + + fn test(f1: impl Curve, f2: impl Curve, t: f32) { + assert_eq!(f1.sample(t), f2.sample(t)); + } + + for t in [-1.0, 0.0, 0.25, 0.5, 0.75, 1.0, 2.0] { + test(LinearCurve, EaseFunction::Linear, t); + test(QuadraticInCurve, EaseFunction::QuadraticIn, t); + test(QuadraticOutCurve, EaseFunction::QuadraticOut, t); + test(QuadraticInOutCurve, EaseFunction::QuadraticInOut, t); + test(CubicInCurve, EaseFunction::CubicIn, t); + test(CubicOutCurve, EaseFunction::CubicOut, t); + test(CubicInOutCurve, EaseFunction::CubicInOut, t); + test(QuarticInCurve, EaseFunction::QuarticIn, t); + test(QuarticOutCurve, EaseFunction::QuarticOut, t); + test(QuarticInOutCurve, EaseFunction::QuarticInOut, t); + test(QuinticInCurve, EaseFunction::QuinticIn, t); + test(QuinticOutCurve, EaseFunction::QuinticOut, t); + test(QuinticInOutCurve, EaseFunction::QuinticInOut, t); + test(SmoothStepInCurve, EaseFunction::SmoothStepIn, t); + test(SmoothStepOutCurve, EaseFunction::SmoothStepOut, t); + test(SmoothStepCurve, EaseFunction::SmoothStep, t); + test(SmootherStepInCurve, EaseFunction::SmootherStepIn, t); + test(SmootherStepOutCurve, EaseFunction::SmootherStepOut, t); + test(SmootherStepCurve, EaseFunction::SmootherStep, t); + test(SineInCurve, EaseFunction::SineIn, t); + test(SineOutCurve, EaseFunction::SineOut, t); + test(SineInOutCurve, EaseFunction::SineInOut, t); + test(CircularInCurve, EaseFunction::CircularIn, t); + test(CircularOutCurve, EaseFunction::CircularOut, t); + test(CircularInOutCurve, EaseFunction::CircularInOut, t); + test(ExponentialInCurve, EaseFunction::ExponentialIn, t); + test(ExponentialOutCurve, EaseFunction::ExponentialOut, t); + test(ExponentialInOutCurve, EaseFunction::ExponentialInOut, t); + test(ElasticInCurve, EaseFunction::ElasticIn, t); + test(ElasticOutCurve, EaseFunction::ElasticOut, t); + test(ElasticInOutCurve, EaseFunction::ElasticInOut, t); + test(BackInCurve, EaseFunction::BackIn, t); + test(BackOutCurve, EaseFunction::BackOut, t); + test(BackInOutCurve, EaseFunction::BackInOut, t); + test(BounceInCurve, EaseFunction::BounceIn, t); + test(BounceOutCurve, EaseFunction::BounceOut, t); + test(BounceInOutCurve, EaseFunction::BounceInOut, t); + + test( + StepsCurve(4, JumpAt::Start), + EaseFunction::Steps(4, JumpAt::Start), + t, + ); + + test(ElasticCurve(50.0), EaseFunction::Elastic(50.0), t); + } + } } diff --git a/crates/bevy_pbr/Cargo.toml b/crates/bevy_pbr/Cargo.toml index f2e973eae5..56dfbf77b3 100644 --- a/crates/bevy_pbr/Cargo.toml +++ b/crates/bevy_pbr/Cargo.toml @@ -16,6 +16,8 @@ pbr_multi_layer_material_textures = [] pbr_anisotropy_texture = [] experimental_pbr_pcss = [] pbr_specular_textures = [] +pbr_clustered_decals = [] +pbr_light_textures = [] shader_format_glsl = ["bevy_render/shader_format_glsl"] trace = ["bevy_render/trace"] # Enables the meshlet renderer for dense high-poly scenes (experimental) @@ -51,7 +53,7 @@ bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-fea ] } # other -bitflags = "2.3" +bitflags = { version = "2.3", features = ["bytemuck"] } fixedbitset = "0.5" thiserror = { version = "2", default-features = false } derive_more = { version = "2", default-features = false, features = ["from"] } diff --git a/crates/bevy_pbr/src/atmosphere/mod.rs b/crates/bevy_pbr/src/atmosphere/mod.rs index a55403630a..ed4dabdf96 100644 --- a/crates/bevy_pbr/src/atmosphere/mod.rs +++ b/crates/bevy_pbr/src/atmosphere/mod.rs @@ -55,7 +55,7 @@ use bevy_render::{ }; use bevy_render::{ extract_component::{ExtractComponent, ExtractComponentPlugin}, - render_graph::{RenderGraphApp, ViewNodeRunner}, + render_graph::{RenderGraphExt, ViewNodeRunner}, render_resource::{TextureFormat, TextureUsages}, renderer::RenderAdapter, Render, RenderApp, RenderSystems, diff --git a/crates/bevy_pbr/src/atmosphere/resources.rs b/crates/bevy_pbr/src/atmosphere/resources.rs index 5a75fbe4d1..3f4da25fc0 100644 --- a/crates/bevy_pbr/src/atmosphere/resources.rs +++ b/crates/bevy_pbr/src/atmosphere/resources.rs @@ -1,3 +1,4 @@ +use crate::{GpuLights, LightMeta}; use bevy_asset::{load_embedded_asset, Handle}; use bevy_core_pipeline::{core_3d::Camera3d, FullscreenShader}; use bevy_ecs::{ @@ -18,8 +19,7 @@ use bevy_render::{ texture::{CachedTexture, TextureCache}, view::{ExtractedView, Msaa, ViewDepthTexture, ViewUniform, ViewUniforms}, }; - -use crate::{GpuLights, LightMeta}; +use bevy_utils::default; use super::{Atmosphere, AtmosphereSettings}; @@ -276,42 +276,30 @@ impl FromWorld for AtmosphereLutPipelines { let transmittance_lut = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { label: Some("transmittance_lut_pipeline".into()), layout: vec![layouts.transmittance_lut.clone()], - push_constant_ranges: vec![], shader: load_embedded_asset!(world, "transmittance_lut.wgsl"), - shader_defs: vec![], - entry_point: "main".into(), - zero_initialize_workgroup_memory: false, + ..default() }); let multiscattering_lut = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { label: Some("multi_scattering_lut_pipeline".into()), layout: vec![layouts.multiscattering_lut.clone()], - push_constant_ranges: vec![], shader: load_embedded_asset!(world, "multiscattering_lut.wgsl"), - shader_defs: vec![], - entry_point: "main".into(), - zero_initialize_workgroup_memory: false, + ..default() }); let sky_view_lut = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { label: Some("sky_view_lut_pipeline".into()), layout: vec![layouts.sky_view_lut.clone()], - push_constant_ranges: vec![], shader: load_embedded_asset!(world, "sky_view_lut.wgsl"), - shader_defs: vec![], - entry_point: "main".into(), - zero_initialize_workgroup_memory: false, + ..default() }); let aerial_view_lut = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { label: Some("aerial_view_lut_pipeline".into()), layout: vec![layouts.aerial_view_lut.clone()], - push_constant_ranges: vec![], shader: load_embedded_asset!(world, "aerial_view_lut.wgsl"), - shader_defs: vec![], - entry_point: "main".into(), - zero_initialize_workgroup_memory: false, + ..default() }); Self { @@ -358,20 +346,10 @@ impl SpecializedRenderPipeline for RenderSkyBindGroupLayouts { } else { self.render_sky_msaa.clone() }], - push_constant_ranges: vec![], vertex: self.fullscreen_shader.to_vertex_state(), - primitive: PrimitiveState::default(), - depth_stencil: None, - multisample: MultisampleState { - count: key.msaa_samples, - mask: !0, - alpha_to_coverage_enabled: false, - }, - zero_initialize_workgroup_memory: false, fragment: Some(FragmentState { shader: self.fragment_shader.clone(), shader_defs, - entry_point: "main".into(), targets: vec![Some(ColorTargetState { format: TextureFormat::Rgba16Float, blend: Some(BlendState { @@ -388,7 +366,13 @@ impl SpecializedRenderPipeline for RenderSkyBindGroupLayouts { }), write_mask: ColorWrites::ALL, })], + ..default() }), + multisample: MultisampleState { + count: key.msaa_samples, + ..default() + }, + ..default() } } } diff --git a/crates/bevy_pbr/src/cluster/mod.rs b/crates/bevy_pbr/src/cluster/mod.rs index 3559cb52d5..501f4091fc 100644 --- a/crates/bevy_pbr/src/cluster/mod.rs +++ b/crates/bevy_pbr/src/cluster/mod.rs @@ -162,8 +162,8 @@ pub struct GpuClusterableObject { pub(crate) spot_light_tan_angle: f32, pub(crate) soft_shadow_size: f32, pub(crate) shadow_map_near_z: f32, - pub(crate) pad_a: f32, - pub(crate) pad_b: f32, + pub(crate) decal_index: u32, + pub(crate) pad: f32, } pub enum GpuClusterableObjects { diff --git a/crates/bevy_pbr/src/decal/clustered.rs b/crates/bevy_pbr/src/decal/clustered.rs index 5618b31831..ec386670ec 100644 --- a/crates/bevy_pbr/src/decal/clustered.rs +++ b/crates/bevy_pbr/src/decal/clustered.rs @@ -50,7 +50,8 @@ use bevy_transform::{components::GlobalTransform, prelude::Transform}; use bytemuck::{Pod, Zeroable}; use crate::{ - binding_arrays_are_usable, prepare_lights, GlobalClusterableObjectMeta, LightVisibilityClass, + binding_arrays_are_usable, prepare_lights, DirectionalLight, GlobalClusterableObjectMeta, + LightVisibilityClass, PointLight, SpotLight, }; /// The maximum number of decals that can be present in a view. @@ -94,6 +95,80 @@ pub struct ClusteredDecal { pub tag: u32, } +/// Cubemap layout defines the order of images in a packed cubemap image. +#[derive(Default, Reflect, Debug, Clone, Copy)] +pub enum CubemapLayout { + /// layout in a vertical cross format + /// ```text + /// +y + /// -x -z +x + /// -y + /// +z + /// ``` + #[default] + CrossVertical = 0, + /// layout in a horizontal cross format + /// ```text + /// +y + /// -x -z +x +z + /// -y + /// ``` + CrossHorizontal = 1, + /// layout in a vertical sequence + /// ```text + /// +x + /// -y + /// +y + /// -y + /// -z + /// +z + /// ``` + SequenceVertical = 2, + /// layout in a horizontal sequence + /// ```text + /// +x -y +y -y -z +z + /// ``` + SequenceHorizontal = 3, +} + +/// Add to a [`PointLight`] to add a light texture effect. +/// A texture mask is applied to the light source to modulate its intensity, +/// simulating patterns like window shadows, gobo/cookie effects, or soft falloffs. +#[derive(Clone, Component, Debug, Reflect)] +#[reflect(Component, Debug)] +#[require(PointLight)] +pub struct PointLightTexture { + /// The texture image. Only the R channel is read. + pub image: Handle, + /// The cubemap layout. The image should be a packed cubemap in one of the formats described by the [`CubemapLayout`] enum. + pub cubemap_layout: CubemapLayout, +} + +/// Add to a [`SpotLight`] to add a light texture effect. +/// A texture mask is applied to the light source to modulate its intensity, +/// simulating patterns like window shadows, gobo/cookie effects, or soft falloffs. +#[derive(Clone, Component, Debug, Reflect)] +#[reflect(Component, Debug)] +#[require(SpotLight)] +pub struct SpotLightTexture { + /// The texture image. Only the R channel is read. + /// Note the border of the image should be entirely black to avoid leaking light. + pub image: Handle, +} + +/// Add to a [`DirectionalLight`] to add a light texture effect. +/// A texture mask is applied to the light source to modulate its intensity, +/// simulating patterns like window shadows, gobo/cookie effects, or soft falloffs. +#[derive(Clone, Component, Debug, Reflect)] +#[reflect(Component, Debug)] +#[require(DirectionalLight)] +pub struct DirectionalLightTexture { + /// The texture image. Only the R channel is read. + pub image: Handle, + /// Whether to tile the image infinitely, or use only a single tile centered at the light's translation + pub tiled: bool, +} + /// Stores information about all the clustered decals in the scene. #[derive(Resource, Default)] pub struct RenderClusteredDecals { @@ -121,6 +196,29 @@ impl RenderClusteredDecals { self.decals.clear(); self.entity_to_decal_index.clear(); } + + pub fn insert_decal( + &mut self, + entity: Entity, + image: &AssetId, + local_from_world: Mat4, + tag: u32, + ) { + let image_index = self.get_or_insert_image(image); + let decal_index = self.decals.len(); + self.decals.push(RenderClusteredDecal { + local_from_world, + image_index, + tag, + pad_a: 0, + pad_b: 0, + }); + self.entity_to_decal_index.insert(entity, decal_index); + } + + pub fn get(&self, entity: Entity) -> Option { + self.entity_to_decal_index.get(&entity).copied() + } } /// The per-view bind group entries pertaining to decals. @@ -204,6 +302,30 @@ pub fn extract_decals( &ViewVisibility, )>, >, + spot_light_textures: Extract< + Query<( + RenderEntity, + &SpotLightTexture, + &GlobalTransform, + &ViewVisibility, + )>, + >, + point_light_textures: Extract< + Query<( + RenderEntity, + &PointLightTexture, + &GlobalTransform, + &ViewVisibility, + )>, + >, + directional_light_textures: Extract< + Query<( + RenderEntity, + &DirectionalLightTexture, + &GlobalTransform, + &ViewVisibility, + )>, + >, mut render_decals: ResMut, ) { // Clear out the `RenderDecals` in preparation for a new frame. @@ -216,22 +338,54 @@ pub fn extract_decals( continue; } - // Insert or add the image. - let image_index = render_decals.get_or_insert_image(&clustered_decal.image.id()); + render_decals.insert_decal( + decal_entity, + &clustered_decal.image.id(), + global_transform.affine().inverse().into(), + clustered_decal.tag, + ); + } - // Record the decal. - let decal_index = render_decals.decals.len(); - render_decals - .entity_to_decal_index - .insert(decal_entity, decal_index); + for (decal_entity, texture, global_transform, view_visibility) in &spot_light_textures { + // If the decal is invisible, skip it. + if !view_visibility.get() { + continue; + } - render_decals.decals.push(RenderClusteredDecal { - local_from_world: global_transform.affine().inverse().into(), - image_index, - tag: clustered_decal.tag, - pad_a: 0, - pad_b: 0, - }); + render_decals.insert_decal( + decal_entity, + &texture.image.id(), + global_transform.affine().inverse().into(), + 0, + ); + } + + for (decal_entity, texture, global_transform, view_visibility) in &point_light_textures { + // If the decal is invisible, skip it. + if !view_visibility.get() { + continue; + } + + render_decals.insert_decal( + decal_entity, + &texture.image.id(), + global_transform.affine().inverse().into(), + texture.cubemap_layout as u32, + ); + } + + for (decal_entity, texture, global_transform, view_visibility) in &directional_light_textures { + // If the decal is invisible, skip it. + if !view_visibility.get() { + continue; + } + + render_decals.insert_decal( + decal_entity, + &texture.image.id(), + global_transform.affine().inverse().into(), + if texture.tiled { 1 } else { 0 }, + ); } } @@ -377,4 +531,5 @@ pub fn clustered_decals_are_usable( // Re-enable this when `wgpu` has first-class bindless. binding_arrays_are_usable(render_device, render_adapter) && cfg!(not(any(target_os = "macos", target_os = "ios"))) + && cfg!(feature = "pbr_clustered_decals") } diff --git a/crates/bevy_pbr/src/decal/forward.rs b/crates/bevy_pbr/src/decal/forward.rs index 757ecff2c3..49767557e1 100644 --- a/crates/bevy_pbr/src/decal/forward.rs +++ b/crates/bevy_pbr/src/decal/forward.rs @@ -3,7 +3,7 @@ use crate::{ MaterialPlugin, StandardMaterial, }; use bevy_app::{App, Plugin}; -use bevy_asset::{weak_handle, Asset, Assets, Handle}; +use bevy_asset::{uuid_handle, Asset, Assets, Handle}; use bevy_ecs::component::Component; use bevy_math::{prelude::Rectangle, Quat, Vec2, Vec3}; use bevy_reflect::{Reflect, TypePath}; @@ -21,7 +21,7 @@ use bevy_render::{ }; const FORWARD_DECAL_MESH_HANDLE: Handle = - weak_handle!("afa817f9-1869-4e0c-ac0d-d8cd1552d38a"); + uuid_handle!("afa817f9-1869-4e0c-ac0d-d8cd1552d38a"); /// Plugin to render [`ForwardDecal`]s. pub struct ForwardDecalPlugin; diff --git a/crates/bevy_pbr/src/deferred/mod.rs b/crates/bevy_pbr/src/deferred/mod.rs index b8f3a660c0..f4718a175e 100644 --- a/crates/bevy_pbr/src/deferred/mod.rs +++ b/crates/bevy_pbr/src/deferred/mod.rs @@ -25,12 +25,13 @@ use bevy_render::{ extract_component::{ ComponentUniforms, ExtractComponent, ExtractComponentPlugin, UniformComponentPlugin, }, - render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner}, + render_graph::{NodeRunError, RenderGraphContext, RenderGraphExt, ViewNode, ViewNodeRunner}, render_resource::{binding_types::uniform_buffer, *}, renderer::{RenderContext, RenderDevice}, view::{ExtractedView, ViewTarget, ViewUniformOffset}, Render, RenderApp, RenderSystems, }; +use bevy_utils::default; pub struct DeferredPbrLightingPlugin; @@ -358,13 +359,11 @@ impl SpecializedRenderPipeline for DeferredLightingLayout { vertex: VertexState { shader: self.deferred_lighting_shader.clone(), shader_defs: shader_defs.clone(), - entry_point: "vertex".into(), - buffers: Vec::new(), + ..default() }, fragment: Some(FragmentState { shader: self.deferred_lighting_shader.clone(), shader_defs, - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format: if key.contains(MeshPipelineKey::HDR) { ViewTarget::TEXTURE_FORMAT_HDR @@ -374,8 +373,8 @@ impl SpecializedRenderPipeline for DeferredLightingLayout { blend: None, write_mask: ColorWrites::ALL, })], + ..default() }), - primitive: PrimitiveState::default(), depth_stencil: Some(DepthStencilState { format: DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT, depth_write_enabled: false, @@ -392,9 +391,7 @@ impl SpecializedRenderPipeline for DeferredLightingLayout { clamp: 0.0, }, }), - multisample: MultisampleState::default(), - push_constant_ranges: vec![], - zero_initialize_workgroup_memory: false, + ..default() } } } diff --git a/crates/bevy_pbr/src/extended_material.rs b/crates/bevy_pbr/src/extended_material.rs index e01dd0ff14..165debee6f 100644 --- a/crates/bevy_pbr/src/extended_material.rs +++ b/crates/bevy_pbr/src/extended_material.rs @@ -1,6 +1,6 @@ use alloc::borrow::Cow; -use bevy_asset::{Asset, Handle}; +use bevy_asset::Asset; use bevy_ecs::system::SystemParamItem; use bevy_platform::{collections::HashSet, hash::FixedHasher}; use bevy_reflect::{impl_type_path, Reflect}; @@ -9,8 +9,8 @@ use bevy_render::{ mesh::MeshVertexBufferLayoutRef, render_resource::{ AsBindGroup, AsBindGroupError, BindGroupLayout, BindGroupLayoutEntry, BindlessDescriptor, - BindlessResourceType, BindlessSlabResourceLimit, RenderPipelineDescriptor, Shader, - ShaderRef, SpecializedMeshPipelineError, UnpreparedBindGroup, + BindlessResourceType, BindlessSlabResourceLimit, RenderPipelineDescriptor, ShaderRef, + SpecializedMeshPipelineError, UnpreparedBindGroup, }, renderer::RenderDevice, }; @@ -19,10 +19,6 @@ use crate::{Material, MaterialPipeline, MaterialPipelineKey, MeshPipeline, MeshP pub struct MaterialExtensionPipeline { pub mesh_pipeline: MeshPipeline, - pub material_layout: BindGroupLayout, - pub vertex_shader: Option>, - pub fragment_shader: Option>, - pub bindless: bool, } pub struct MaterialExtensionKey { @@ -150,12 +146,19 @@ where } } +#[derive(bytemuck::Pod, bytemuck::Zeroable, Copy, Clone, PartialEq, Eq, Hash)] +#[repr(C, packed)] +pub struct MaterialExtensionBindGroupData { + pub base: B, + pub extension: E, +} + // We don't use the `TypePath` derive here due to a bug where `#[reflect(type_path = false)]` // causes the `TypePath` derive to not generate an implementation. impl_type_path!((in bevy_pbr::extended_material) ExtendedMaterial); impl AsBindGroup for ExtendedMaterial { - type Data = (::Data, ::Data); + type Data = MaterialExtensionBindGroupData; type Param = (::Param, ::Param); fn bindless_slot_count() -> Option { @@ -179,20 +182,24 @@ impl AsBindGroup for ExtendedMaterial { } } + fn bind_group_data(&self) -> Self::Data { + MaterialExtensionBindGroupData { + base: self.base.bind_group_data(), + extension: self.extension.bind_group_data(), + } + } + fn unprepared_bind_group( &self, layout: &BindGroupLayout, render_device: &RenderDevice, (base_param, extended_param): &mut SystemParamItem<'_, '_, Self::Param>, mut force_non_bindless: bool, - ) -> Result, AsBindGroupError> { + ) -> Result { force_non_bindless = force_non_bindless || Self::bindless_slot_count().is_none(); // add together the bindings of the base material and the user material - let UnpreparedBindGroup { - mut bindings, - data: base_data, - } = B::unprepared_bind_group( + let UnpreparedBindGroup { mut bindings } = B::unprepared_bind_group( &self.base, layout, render_device, @@ -209,10 +216,7 @@ impl AsBindGroup for ExtendedMaterial { bindings.extend(extended_bindgroup.bindings.0); - Ok(UnpreparedBindGroup { - bindings, - data: (base_data, extended_bindgroup.data), - }) + Ok(UnpreparedBindGroup { bindings }) } fn bind_group_layout_entries( @@ -373,57 +377,28 @@ impl Material for ExtendedMaterial { } fn specialize( - pipeline: &MaterialPipeline, + pipeline: &MaterialPipeline, descriptor: &mut RenderPipelineDescriptor, layout: &MeshVertexBufferLayoutRef, key: MaterialPipelineKey, ) -> Result<(), SpecializedMeshPipelineError> { // Call the base material's specialize function - let MaterialPipeline:: { - mesh_pipeline, - material_layout, - vertex_shader, - fragment_shader, - bindless, - .. - } = pipeline.clone(); - let base_pipeline = MaterialPipeline:: { - mesh_pipeline, - material_layout, - vertex_shader, - fragment_shader, - bindless, - marker: Default::default(), - }; let base_key = MaterialPipelineKey:: { mesh_key: key.mesh_key, - bind_group_data: key.bind_group_data.0, + bind_group_data: key.bind_group_data.base, }; - B::specialize(&base_pipeline, descriptor, layout, base_key)?; + B::specialize(pipeline, descriptor, layout, base_key)?; // Call the extended material's specialize function afterwards - let MaterialPipeline:: { - mesh_pipeline, - material_layout, - vertex_shader, - fragment_shader, - bindless, - .. - } = pipeline.clone(); - E::specialize( &MaterialExtensionPipeline { - mesh_pipeline, - material_layout, - vertex_shader, - fragment_shader, - bindless, + mesh_pipeline: pipeline.mesh_pipeline.clone(), }, descriptor, layout, MaterialExtensionKey { mesh_key: key.mesh_key, - bind_group_data: key.bind_group_data.1, + bind_group_data: key.bind_group_data.extension, }, ) } diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index 4263d2d295..5f86e6a759 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -125,7 +125,7 @@ pub mod graph { use crate::{deferred::DeferredPbrLightingPlugin, graph::NodePbr}; use bevy_app::prelude::*; -use bevy_asset::{load_internal_asset, weak_handle, AssetApp, AssetPath, Assets, Handle}; +use bevy_asset::{AssetApp, AssetPath, Assets, Handle}; use bevy_core_pipeline::core_3d::graph::{Core3d, Node3d}; use bevy_ecs::prelude::*; use bevy_image::Image; @@ -136,7 +136,7 @@ use bevy_render::{ extract_resource::ExtractResourcePlugin, load_shader_library, render_graph::RenderGraph, - render_resource::{Shader, ShaderRef}, + render_resource::ShaderRef, sync_component::SyncComponentPlugin, view::VisibilitySystems, ExtractSchedule, Render, RenderApp, RenderDebugFlags, RenderSystems, @@ -150,9 +150,6 @@ fn shader_ref(path: PathBuf) -> ShaderRef { ShaderRef::Path(AssetPath::from_path_buf(path).with_source("embedded")) } -const MESHLET_VISIBILITY_BUFFER_RESOLVE_SHADER_HANDLE: Handle = - weak_handle!("69187376-3dea-4d0f-b3f5-185bde63d6a2"); - pub const TONEMAPPING_LUT_TEXTURE_BINDING_INDEX: u32 = 18; pub const TONEMAPPING_LUT_SAMPLER_BINDING_INDEX: u32 = 19; @@ -206,12 +203,7 @@ impl Plugin for PbrPlugin { load_shader_library!(app, "render/view_transformations.wgsl"); // Setup dummy shaders for when MeshletPlugin is not used to prevent shader import errors. - load_internal_asset!( - app, - MESHLET_VISIBILITY_BUFFER_RESOLVE_SHADER_HANDLE, - "meshlet/dummy_visibility_buffer_resolve.wgsl", - Shader::from_wgsl - ); + load_shader_library!(app, "meshlet/dummy_visibility_buffer_resolve.wgsl"); app.register_asset_reflect::() .register_type::() @@ -240,6 +232,9 @@ impl Plugin for PbrPlugin { use_gpu_instance_buffer_builder: self.use_gpu_instance_buffer_builder, debug_flags: self.debug_flags, }, + MaterialsPlugin { + debug_flags: self.debug_flags, + }, MaterialPlugin:: { prepass_enabled: self.prepass_enabled, debug_flags: self.debug_flags, diff --git a/crates/bevy_pbr/src/light/mod.rs b/crates/bevy_pbr/src/light/mod.rs index e50ffcc0fc..a93e3e4c58 100644 --- a/crates/bevy_pbr/src/light/mod.rs +++ b/crates/bevy_pbr/src/light/mod.rs @@ -1,5 +1,3 @@ -use core::ops::DerefMut; - use bevy_ecs::{ entity::{EntityHashMap, EntityHashSet}, prelude::*, @@ -19,6 +17,7 @@ use bevy_render::{ }; use bevy_transform::components::{GlobalTransform, Transform}; use bevy_utils::Parallel; +use core::{marker::PhantomData, ops::DerefMut}; use crate::*; @@ -91,6 +90,16 @@ pub mod light_consts { } } +/// Marker resource for whether shadows are enabled for this material type +#[derive(Resource, Debug)] +pub struct ShadowsEnabled(PhantomData); + +impl Default for ShadowsEnabled { + fn default() -> Self { + Self(PhantomData) + } +} + /// Controls the resolution of [`PointLight`] shadow maps. /// /// ``` diff --git a/crates/bevy_pbr/src/light_probe/mod.rs b/crates/bevy_pbr/src/light_probe/mod.rs index bfce2f1e26..82035be2f6 100644 --- a/crates/bevy_pbr/src/light_probe/mod.rs +++ b/crates/bevy_pbr/src/light_probe/mod.rs @@ -343,7 +343,8 @@ impl Plugin for LightProbePlugin { app.register_type::() .register_type::() - .register_type::(); + .register_type::() + .add_plugins(ExtractInstancesPlugin::::new()); } fn finish(&self, app: &mut App) { @@ -352,7 +353,6 @@ impl Plugin for LightProbePlugin { }; render_app - .add_plugins(ExtractInstancesPlugin::::new()) .init_resource::() .init_resource::() .add_systems(ExtractSchedule, gather_environment_map_uniform) diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index f73f1392bb..bb1f9afde3 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -1,12 +1,8 @@ use crate::material_bind_groups::{ FallbackBindlessResources, MaterialBindGroupAllocator, MaterialBindingId, }; -#[cfg(feature = "meshlet")] -use crate::meshlet::{ - prepare_material_meshlet_meshes_main_opaque_pass, queue_material_meshlet_meshes, - InstanceManager, -}; use crate::*; +use alloc::sync::Arc; use bevy_asset::prelude::AssetChanged; use bevy_asset::{Asset, AssetEventSystems, AssetId, AssetServer, UntypedAssetId}; use bevy_core_pipeline::deferred::{AlphaMask3dDeferred, Opaque3dDeferred}; @@ -35,14 +31,18 @@ use bevy_platform::hash::FixedHasher; use bevy_reflect::std_traits::ReflectDefault; use bevy_reflect::Reflect; use bevy_render::camera::extract_cameras; +use bevy_render::erased_render_asset::{ + ErasedRenderAsset, ErasedRenderAssetPlugin, ErasedRenderAssets, PrepareAssetError, +}; use bevy_render::mesh::mark_3d_meshes_as_changed_if_their_assets_changed; -use bevy_render::render_asset::prepare_assets; +use bevy_render::render_asset::{prepare_assets, RenderAssets}; use bevy_render::renderer::RenderQueue; +use bevy_render::RenderStartup; use bevy_render::{ batching::gpu_preprocessing::GpuPreprocessingSupport, extract_resource::ExtractResource, mesh::{Mesh3d, MeshVertexBufferLayoutRef, RenderMesh}, - render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets}, + prelude::*, render_phase::*, render_resource::*, renderer::RenderDevice, @@ -53,7 +53,9 @@ use bevy_render::{ use bevy_render::{mesh::allocator::MeshAllocator, sync_world::MainEntityHashMap}; use bevy_render::{texture::FallbackImage, view::RenderVisibleEntities}; use bevy_utils::Parallel; +use core::any::TypeId; use core::{hash::Hash, marker::PhantomData}; +use smallvec::SmallVec; use tracing::error; /// Materials are used alongside [`MaterialPlugin`], [`Mesh3d`], and [`MeshMaterial3d`] @@ -239,7 +241,7 @@ pub trait Material: Asset + AsBindGroup + Clone + Sized { )] #[inline] fn specialize( - pipeline: &MaterialPipeline, + pipeline: &MaterialPipeline, descriptor: &mut RenderPipelineDescriptor, layout: &MeshVertexBufferLayoutRef, key: MaterialPipelineKey, @@ -248,6 +250,74 @@ pub trait Material: Asset + AsBindGroup + Clone + Sized { } } +#[derive(Default)] +pub struct MaterialsPlugin { + /// Debugging flags that can optionally be set when constructing the renderer. + pub debug_flags: RenderDebugFlags, +} + +impl Plugin for MaterialsPlugin { + fn build(&self, app: &mut App) { + app.add_plugins((PrepassPipelinePlugin, PrepassPlugin::new(self.debug_flags))); + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app + .init_resource::() + .init_resource::() + .init_resource::>() + .init_resource::() + .init_resource::() + .init_resource::() + .add_systems( + Render, + ( + specialize_material_meshes + .in_set(RenderSystems::PrepareMeshes) + .after(prepare_assets::) + .after(collect_meshes_for_gpu_building) + .after(set_mesh_motion_vector_flags), + queue_material_meshes.in_set(RenderSystems::QueueMeshes), + ), + ) + .add_systems( + Render, + ( + prepare_material_bind_groups, + write_material_bind_group_buffers, + ) + .chain() + .in_set(RenderSystems::PrepareBindGroups), + ) + .add_systems( + Render, + ( + check_views_lights_need_specialization.in_set(RenderSystems::PrepareAssets), + // specialize_shadows also needs to run after prepare_assets::, + // which is fine since ManageViews is after PrepareAssets + specialize_shadows + .in_set(RenderSystems::ManageViews) + .after(prepare_lights), + queue_shadows.in_set(RenderSystems::QueueMeshes), + ), + ); + } + } + + fn finish(&self, app: &mut App) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app + .init_resource::>() + .init_resource::() + .init_resource::() + .init_resource::() + .add_render_command::() + .add_render_command::() + .add_render_command::() + .add_render_command::() + .add_render_command::(); + } + } +} + /// Adds the necessary ECS resources and render logic to enable rendering entities using the given [`Material`] /// asset type. pub struct MaterialPlugin { @@ -283,7 +353,7 @@ where app.init_asset::() .register_type::>() .init_resource::>() - .add_plugins((RenderAssetPlugin::>::default(),)) + .add_plugins((ErasedRenderAssetPlugin::>::default(),)) .add_systems( PostUpdate, ( @@ -302,17 +372,15 @@ where } if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + if self.prepass_enabled { + render_app.init_resource::>(); + } + if self.shadows_enabled { + render_app.init_resource::>(); + } + render_app - .init_resource::>() - .init_resource::>() - .init_resource::>() - .init_resource::() - .add_render_command::>() - .add_render_command::>() - .add_render_command::>() - .add_render_command::>() - .add_render_command::>() - .init_resource::>>() + .add_systems(RenderStartup, setup_render_app::) .add_systems( ExtractSchedule, ( @@ -322,90 +390,27 @@ where .before(late_sweep_material_instances), extract_entities_needs_specialization::.after(extract_cameras), ), - ) - .add_systems( - Render, - ( - specialize_material_meshes:: - .in_set(RenderSystems::PrepareMeshes) - .after(prepare_assets::>) - .after(prepare_assets::) - .after(collect_meshes_for_gpu_building) - .after(set_mesh_motion_vector_flags), - queue_material_meshes:: - .in_set(RenderSystems::QueueMeshes) - .after(prepare_assets::>), - ), - ) - .add_systems( - Render, - ( - prepare_material_bind_groups::, - write_material_bind_group_buffers::, - ) - .chain() - .in_set(RenderSystems::PrepareBindGroups) - .after(prepare_assets::>), ); - - if self.shadows_enabled { - render_app - .init_resource::() - .init_resource::() - .init_resource::>() - .add_systems( - Render, - ( - check_views_lights_need_specialization - .in_set(RenderSystems::PrepareAssets), - // specialize_shadows:: also needs to run after prepare_assets::>, - // which is fine since ManageViews is after PrepareAssets - specialize_shadows:: - .in_set(RenderSystems::ManageViews) - .after(prepare_lights), - queue_shadows:: - .in_set(RenderSystems::QueueMeshes) - .after(prepare_assets::>), - ), - ); - } - - #[cfg(feature = "meshlet")] - render_app.add_systems( - Render, - queue_material_meshlet_meshes:: - .in_set(RenderSystems::QueueMeshes) - .run_if(resource_exists::), - ); - - #[cfg(feature = "meshlet")] - render_app.add_systems( - Render, - prepare_material_meshlet_meshes_main_opaque_pass:: - .in_set(RenderSystems::QueueMeshes) - .after(prepare_assets::>) - .before(queue_material_meshlet_meshes::) - .run_if(resource_exists::), - ); - } - - if self.shadows_enabled || self.prepass_enabled { - // PrepassPipelinePlugin is required for shadow mapping and the optional PrepassPlugin - app.add_plugins(PrepassPipelinePlugin::::default()); - } - - if self.prepass_enabled { - app.add_plugins(PrepassPlugin::::new(self.debug_flags)); } } +} - fn finish(&self, app: &mut App) { - if let Some(render_app) = app.get_sub_app_mut(RenderApp) { - render_app - .init_resource::>() - .init_resource::>(); - } - } +fn setup_render_app( + render_device: Res, + mut bind_group_allocators: ResMut, +) { + bind_group_allocators.insert( + TypeId::of::(), + MaterialBindGroupAllocator::new( + &render_device, + M::label(), + material_uses_bindless_resources::(&render_device) + .then(|| M::bindless_descriptor()) + .flatten(), + M::bind_group_layout(&render_device), + M::bindless_slot_count(), + ), + ); } /// A dummy [`AssetId`] that we use as a placeholder whenever a mesh doesn't @@ -422,91 +427,54 @@ pub struct MaterialPipelineKey { pub bind_group_data: M::Data, } -impl Eq for MaterialPipelineKey where M::Data: PartialEq {} - -impl PartialEq for MaterialPipelineKey -where - M::Data: PartialEq, -{ - fn eq(&self, other: &Self) -> bool { - self.mesh_key == other.mesh_key && self.bind_group_data == other.bind_group_data - } -} - -impl Clone for MaterialPipelineKey -where - M::Data: Clone, -{ - fn clone(&self) -> Self { - Self { - mesh_key: self.mesh_key, - bind_group_data: self.bind_group_data.clone(), - } - } -} - -impl Hash for MaterialPipelineKey -where - M::Data: Hash, -{ - fn hash(&self, state: &mut H) { - self.mesh_key.hash(state); - self.bind_group_data.hash(state); - } +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct ErasedMaterialPipelineKey { + pub mesh_key: MeshPipelineKey, + pub material_key: SmallVec<[u8; 8]>, + pub type_id: TypeId, } /// Render pipeline data for a given [`Material`]. -#[derive(Resource)] -pub struct MaterialPipeline { +#[derive(Resource, Clone)] +pub struct MaterialPipeline { pub mesh_pipeline: MeshPipeline, - pub material_layout: BindGroupLayout, - pub vertex_shader: Option>, - pub fragment_shader: Option>, - /// Whether this material *actually* uses bindless resources, taking the - /// platform support (or lack thereof) of bindless resources into account. - pub bindless: bool, - pub marker: PhantomData, } -impl Clone for MaterialPipeline { - fn clone(&self) -> Self { - Self { - mesh_pipeline: self.mesh_pipeline.clone(), - material_layout: self.material_layout.clone(), - vertex_shader: self.vertex_shader.clone(), - fragment_shader: self.fragment_shader.clone(), - bindless: self.bindless, - marker: PhantomData, - } - } +pub struct MaterialPipelineSpecializer { + pub(crate) pipeline: MaterialPipeline, + pub(crate) properties: Arc, } -impl SpecializedMeshPipeline for MaterialPipeline -where - M::Data: PartialEq + Eq + Hash + Clone, -{ - type Key = MaterialPipelineKey; +impl SpecializedMeshPipeline for MaterialPipelineSpecializer { + type Key = ErasedMaterialPipelineKey; fn specialize( &self, key: Self::Key, layout: &MeshVertexBufferLayoutRef, ) -> Result { - let mut descriptor = self.mesh_pipeline.specialize(key.mesh_key, layout)?; - if let Some(vertex_shader) = &self.vertex_shader { + let mut descriptor = self + .pipeline + .mesh_pipeline + .specialize(key.mesh_key, layout)?; + if let Some(vertex_shader) = self.properties.get_shader(MaterialVertexShader) { descriptor.vertex.shader = vertex_shader.clone(); } - if let Some(fragment_shader) = &self.fragment_shader { + if let Some(fragment_shader) = self.properties.get_shader(MaterialFragmentShader) { descriptor.fragment.as_mut().unwrap().shader = fragment_shader.clone(); } - descriptor.layout.insert(3, self.material_layout.clone()); + descriptor + .layout + .insert(3, self.properties.material_layout.as_ref().unwrap().clone()); - M::specialize(self, &mut descriptor, layout, key)?; + if let Some(specialize) = self.properties.specialize { + specialize(&self.pipeline, &mut descriptor, layout, key)?; + } // If bindless mode is on, add a `BINDLESS` define. - if self.bindless { + if self.properties.bindless { descriptor.vertex.shader_defs.push("BINDLESS".into()); if let Some(ref mut fragment) = descriptor.fragment { fragment.shader_defs.push("BINDLESS".into()); @@ -517,46 +485,30 @@ where } } -impl FromWorld for MaterialPipeline { +impl FromWorld for MaterialPipeline { fn from_world(world: &mut World) -> Self { - let asset_server = world.resource::(); - let render_device = world.resource::(); - MaterialPipeline { mesh_pipeline: world.resource::().clone(), - material_layout: M::bind_group_layout(render_device), - vertex_shader: match M::vertex_shader() { - ShaderRef::Default => None, - ShaderRef::Handle(handle) => Some(handle), - ShaderRef::Path(path) => Some(asset_server.load(path)), - }, - fragment_shader: match M::fragment_shader() { - ShaderRef::Default => None, - ShaderRef::Handle(handle) => Some(handle), - ShaderRef::Path(path) => Some(asset_server.load(path)), - }, - bindless: material_uses_bindless_resources::(render_device), - marker: PhantomData, } } } -type DrawMaterial = ( +pub type DrawMaterial = ( SetItemPipeline, SetMeshViewBindGroup<0>, SetMeshViewBindingArrayBindGroup<1>, SetMeshBindGroup<2>, - SetMaterialBindGroup, + SetMaterialBindGroup<3>, DrawMesh, ); /// Sets the bind group for a given [`Material`] at the configured `I` index. -pub struct SetMaterialBindGroup(PhantomData); -impl RenderCommand

for SetMaterialBindGroup { +pub struct SetMaterialBindGroup; +impl RenderCommand

for SetMaterialBindGroup { type Param = ( - SRes>>, + SRes>, SRes, - SRes>, + SRes, ); type ViewQuery = (); type ItemQuery = (); @@ -575,15 +527,17 @@ impl RenderCommand

for SetMaterial ) -> RenderCommandResult { let materials = materials.into_inner(); let material_instances = material_instances.into_inner(); - let material_bind_group_allocator = material_bind_group_allocator.into_inner(); + let material_bind_group_allocators = material_bind_group_allocator.into_inner(); let Some(material_instance) = material_instances.instances.get(&item.main_entity()) else { return RenderCommandResult::Skip; }; - let Ok(material_asset_id) = material_instance.asset_id.try_typed::() else { + let Some(material_bind_group_allocator) = + material_bind_group_allocators.get(&material_instance.asset_id.type_id()) + else { return RenderCommandResult::Skip; }; - let Some(material) = materials.get(material_asset_id) else { + let Some(material) = materials.get(material_instance.asset_id) else { return RenderCommandResult::Skip; }; let Some(material_bind_group) = material_bind_group_allocator.get(material.binding.group) @@ -607,7 +561,7 @@ pub struct RenderMaterialInstances { /// A monotonically-increasing counter, which we use to sweep /// [`RenderMaterialInstances::instances`] when the entities and/or required /// components are removed. - current_change_tick: Tick, + pub current_change_tick: Tick, } impl RenderMaterialInstances { @@ -631,10 +585,10 @@ impl RenderMaterialInstances { /// material type, for simplicity. pub struct RenderMaterialInstance { /// The material asset. - pub(crate) asset_id: UntypedAssetId, + pub asset_id: UntypedAssetId, /// The [`RenderMaterialInstances::current_change_tick`] at which this /// material instance was last modified. - last_change_tick: Tick, + pub last_change_tick: Tick, } /// A [`SystemSet`] that contains all `extract_mesh_materials` systems. @@ -814,14 +768,14 @@ pub(crate) fn late_sweep_material_instances( pub fn extract_entities_needs_specialization( entities_needing_specialization: Extract>>, - mut entity_specialization_ticks: ResMut>, + mut entity_specialization_ticks: ResMut, mut removed_mesh_material_components: Extract>>, - mut specialized_material_pipeline_cache: ResMut>, + mut specialized_material_pipeline_cache: ResMut, mut specialized_prepass_material_pipeline_cache: Option< - ResMut>, + ResMut, >, mut specialized_shadow_material_pipeline_cache: Option< - ResMut>, + ResMut, >, views: Query<&ExtractedView>, ticks: SystemChangeTick, @@ -876,57 +830,27 @@ impl Default for EntitiesNeedingSpecialization { } } -#[derive(Resource, Deref, DerefMut, Clone, Debug)] -pub struct EntitySpecializationTicks { +#[derive(Resource, Deref, DerefMut, Default, Clone, Debug)] +pub struct EntitySpecializationTicks { #[deref] pub entities: MainEntityHashMap, - _marker: PhantomData, -} - -impl Default for EntitySpecializationTicks { - fn default() -> Self { - Self { - entities: MainEntityHashMap::default(), - _marker: Default::default(), - } - } } /// Stores the [`SpecializedMaterialViewPipelineCache`] for each view. -#[derive(Resource, Deref, DerefMut)] -pub struct SpecializedMaterialPipelineCache { +#[derive(Resource, Deref, DerefMut, Default)] +pub struct SpecializedMaterialPipelineCache { // view entity -> view pipeline cache #[deref] - map: HashMap>, - marker: PhantomData, + map: HashMap, } /// Stores the cached render pipeline ID for each entity in a single view, as /// well as the last time it was changed. -#[derive(Deref, DerefMut)] -pub struct SpecializedMaterialViewPipelineCache { +#[derive(Deref, DerefMut, Default)] +pub struct SpecializedMaterialViewPipelineCache { // material entity -> (tick, pipeline_id) #[deref] map: MainEntityHashMap<(Tick, CachedRenderPipelineId)>, - marker: PhantomData, -} - -impl Default for SpecializedMaterialPipelineCache { - fn default() -> Self { - Self { - map: HashMap::default(), - marker: PhantomData, - } - } -} - -impl Default for SpecializedMaterialViewPipelineCache { - fn default() -> Self { - Self { - map: MainEntityHashMap::default(), - marker: PhantomData, - } - } } pub fn check_entities_needing_specialization( @@ -956,21 +880,19 @@ pub fn check_entities_needing_specialization( par_local.drain_into(&mut entities_needing_specialization); } -pub fn specialize_material_meshes( +pub fn specialize_material_meshes( render_meshes: Res>, - render_materials: Res>>, + render_materials: Res>, render_mesh_instances: Res, render_material_instances: Res, render_lightmaps: Res, render_visibility_ranges: Res, ( - material_bind_group_allocator, opaque_render_phases, alpha_mask_render_phases, transmissive_render_phases, transparent_render_phases, ): ( - Res>, Res>, Res>, Res>, @@ -978,16 +900,14 @@ pub fn specialize_material_meshes( ), views: Query<(&ExtractedView, &RenderVisibleEntities)>, view_key_cache: Res, - entity_specialization_ticks: Res>, + entity_specialization_ticks: Res, view_specialization_ticks: Res, - mut specialized_material_pipeline_cache: ResMut>, - mut pipelines: ResMut>>, - pipeline: Res>, + mut specialized_material_pipeline_cache: ResMut, + mut pipelines: ResMut>, + pipeline: Res, pipeline_cache: Res, ticks: SystemChangeTick, -) where - M::Data: PartialEq + Eq + Hash + Clone, -{ +) { // Record the retained IDs of all shadow views so that we can expire old // pipeline IDs. let mut all_views: HashSet = HashSet::default(); @@ -1019,9 +939,6 @@ pub fn specialize_material_meshes( else { continue; }; - let Ok(material_asset_id) = material_instance.asset_id.try_typed::() else { - continue; - }; let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity) else { continue; @@ -1040,12 +957,7 @@ pub fn specialize_material_meshes( let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else { continue; }; - let Some(material) = render_materials.get(material_asset_id) else { - continue; - }; - let Some(material_bind_group) = - material_bind_group_allocator.get(material.binding.group) - else { + let Some(material) = render_materials.get(material_instance.asset_id) else { continue; }; @@ -1086,13 +998,21 @@ pub fn specialize_material_meshes( } } - let key = MaterialPipelineKey { + let erased_key = ErasedMaterialPipelineKey { + type_id: material_instance.asset_id.type_id(), mesh_key, - bind_group_data: material_bind_group - .get_extra_data(material.binding.slot) - .clone(), + material_key: material.properties.material_key.clone(), }; - let pipeline_id = pipelines.specialize(&pipeline_cache, &pipeline, key, &mesh.layout); + let material_pipeline_specializer = MaterialPipelineSpecializer { + pipeline: pipeline.clone(), + properties: material.properties.clone(), + }; + let pipeline_id = pipelines.specialize( + &pipeline_cache, + &material_pipeline_specializer, + erased_key, + &mesh.layout, + ); let pipeline_id = match pipeline_id { Ok(id) => id, Err(err) => { @@ -1113,8 +1033,8 @@ pub fn specialize_material_meshes( /// For each view, iterates over all the meshes visible from that view and adds /// them to [`BinnedRenderPhase`]s or [`SortedRenderPhase`]s as appropriate. -pub fn queue_material_meshes( - render_materials: Res>>, +pub fn queue_material_meshes( + render_materials: Res>, render_mesh_instances: Res, render_material_instances: Res, mesh_allocator: Res, @@ -1124,10 +1044,8 @@ pub fn queue_material_meshes( mut transmissive_render_phases: ResMut>, mut transparent_render_phases: ResMut>, views: Query<(&ExtractedView, &RenderVisibleEntities)>, - specialized_material_pipeline_cache: ResMut>, -) where - M::Data: PartialEq + Eq + Hash + Clone, -{ + specialized_material_pipeline_cache: ResMut, +) { for (view, visible_entities) in &views { let ( Some(opaque_phase), @@ -1170,19 +1088,20 @@ pub fn queue_material_meshes( else { continue; }; - let Ok(material_asset_id) = material_instance.asset_id.try_typed::() else { - continue; - }; let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity) else { continue; }; - let Some(material) = render_materials.get(material_asset_id) else { + let Some(material) = render_materials.get(material_instance.asset_id) else { continue; }; // Fetch the slabs that this mesh resides in. let (vertex_slab, index_slab) = mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); + let Some(draw_function) = material.properties.get_draw_function(MaterialDrawFunction) + else { + continue; + }; match material.properties.render_phase_type { RenderPhaseType::Transmissive => { @@ -1190,7 +1109,7 @@ pub fn queue_material_meshes( + material.properties.depth_bias; transmissive_phase.add(Transmissive3d { entity: (*render_entity, *visible_entity), - draw_function: material.properties.draw_function_id, + draw_function, pipeline: pipeline_id, distance, batch_range: 0..1, @@ -1209,7 +1128,7 @@ pub fn queue_material_meshes( } let batch_set_key = Opaque3dBatchSetKey { pipeline: pipeline_id, - draw_function: material.properties.draw_function_id, + draw_function, material_bind_group_index: Some(material.binding.group.0), vertex_slab: vertex_slab.unwrap_or_default(), index_slab, @@ -1233,7 +1152,7 @@ pub fn queue_material_meshes( // Alpha mask RenderPhaseType::AlphaMask => { let batch_set_key = OpaqueNoLightmap3dBatchSetKey { - draw_function: material.properties.draw_function_id, + draw_function, pipeline: pipeline_id, material_bind_group_index: Some(material.binding.group.0), vertex_slab: vertex_slab.unwrap_or_default(), @@ -1259,7 +1178,7 @@ pub fn queue_material_meshes( + material.properties.depth_bias; transparent_phase.add(Transparent3d { entity: (*render_entity, *visible_entity), - draw_function: material.properties.draw_function_id, + draw_function, pipeline: pipeline_id, distance, batch_range: 0..1, @@ -1322,7 +1241,47 @@ pub enum OpaqueRendererMethod { Auto, } +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct MaterialVertexShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct MaterialFragmentShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct PrepassVertexShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct PrepassFragmentShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct DeferredVertexShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct DeferredFragmentShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct MeshletFragmentShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct MeshletPrepassFragmentShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct MeshletDeferredFragmentShader; + +#[derive(DrawFunctionLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct MaterialDrawFunction; + +#[derive(DrawFunctionLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct PrepassDrawFunction; + +#[derive(DrawFunctionLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct DeferredDrawFunction; + +#[derive(DrawFunctionLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct ShadowsDrawFunction; + /// Common [`Material`] properties, calculated for a specific material instance. +#[derive(Default)] pub struct MaterialProperties { /// Is this material should be rendered by the deferred renderer when. /// [`AlphaMode::Opaque`] or [`AlphaMode::Mask`] @@ -1344,13 +1303,65 @@ pub struct MaterialProperties { /// rendering to take place in a separate [`Transmissive3d`] pass. pub reads_view_transmission_texture: bool, pub render_phase_type: RenderPhaseType, - pub draw_function_id: DrawFunctionId, - pub prepass_draw_function_id: Option, - pub deferred_draw_function_id: Option, + pub material_layout: Option, + /// Backing array is a size of 4 because the `StandardMaterial` needs 4 draw functions by default + pub draw_functions: SmallVec<[(InternedDrawFunctionLabel, DrawFunctionId); 4]>, + /// Backing array is a size of 3 because the `StandardMaterial` has 3 custom shaders (`frag`, `prepass_frag`, `deferred_frag`) which is the + /// most common use case + pub shaders: SmallVec<[(InternedShaderLabel, Handle); 3]>, + /// Whether this material *actually* uses bindless resources, taking the + /// platform support (or lack thereof) of bindless resources into account. + pub bindless: bool, + pub specialize: Option< + fn( + &MaterialPipeline, + &mut RenderPipelineDescriptor, + &MeshVertexBufferLayoutRef, + ErasedMaterialPipelineKey, + ) -> Result<(), SpecializedMeshPipelineError>, + >, + /// The key for this material, typically a bitfield of flags that are used to modify + /// the pipeline descriptor used for this material. + pub material_key: SmallVec<[u8; 8]>, + /// Whether shadows are enabled for this material + pub shadows_enabled: bool, + /// Whether prepass is enabled for this material + pub prepass_enabled: bool, } -#[derive(Clone, Copy)] +impl MaterialProperties { + pub fn get_shader(&self, label: impl ShaderLabel) -> Option> { + self.shaders + .iter() + .find(|(inner_label, _)| inner_label == &label.intern()) + .map(|(_, shader)| shader) + .cloned() + } + + pub fn add_shader(&mut self, label: impl ShaderLabel, shader: Handle) { + self.shaders.push((label.intern(), shader)); + } + + pub fn get_draw_function(&self, label: impl DrawFunctionLabel) -> Option { + self.draw_functions + .iter() + .find(|(inner_label, _)| inner_label == &label.intern()) + .map(|(_, shader)| shader) + .cloned() + } + + pub fn add_draw_function( + &mut self, + label: impl DrawFunctionLabel, + draw_function: DrawFunctionId, + ) { + self.draw_functions.push((label.intern(), draw_function)); + } +} + +#[derive(Clone, Copy, Default)] pub enum RenderPhaseType { + #[default] Opaque, AlphaMask, Transmissive, @@ -1366,20 +1377,23 @@ pub enum RenderPhaseType { pub struct RenderMaterialBindings(HashMap); /// Data prepared for a [`Material`] instance. -pub struct PreparedMaterial { +pub struct PreparedMaterial { pub binding: MaterialBindingId, - pub properties: MaterialProperties, - pub phantom: PhantomData, + pub properties: Arc, } -impl RenderAsset for PreparedMaterial { +// orphan rules T_T +impl ErasedRenderAsset for MeshMaterial3d +where + M::Data: Clone, +{ type SourceAsset = M; + type ErasedAsset = PreparedMaterial; type Param = ( SRes, - SRes>, SRes, - SResMut>, + SResMut, SResMut, SRes>, SRes>, @@ -1389,7 +1403,13 @@ impl RenderAsset for PreparedMaterial { SRes>, SRes>, SRes>, - M::Param, + SRes>, + SRes, + ( + Option>>, + Option>>, + M::Param, + ), ); fn prepare_asset( @@ -1397,9 +1417,8 @@ impl RenderAsset for PreparedMaterial { material_id: AssetId, ( render_device, - pipeline, default_opaque_render_method, - bind_group_allocator, + bind_group_allocators, render_material_bindings, opaque_draw_functions, alpha_mask_draw_functions, @@ -1409,26 +1428,31 @@ impl RenderAsset for PreparedMaterial { alpha_mask_prepass_draw_functions, opaque_deferred_draw_functions, alpha_mask_deferred_draw_functions, - material_param, + shadow_draw_functions, + asset_server, + (shadows_enabled, prepass_enabled, material_param), ): &mut SystemParamItem, - _: Option<&Self>, - ) -> Result> { - let draw_opaque_pbr = opaque_draw_functions.read().id::>(); - let draw_alpha_mask_pbr = alpha_mask_draw_functions.read().id::>(); - let draw_transmissive_pbr = transmissive_draw_functions.read().id::>(); - let draw_transparent_pbr = transparent_draw_functions.read().id::>(); - let draw_opaque_prepass = opaque_prepass_draw_functions - .read() - .get_id::>(); + ) -> Result> { + let material_layout = M::bind_group_layout(render_device); + + let shadows_enabled = shadows_enabled.is_some(); + let prepass_enabled = prepass_enabled.is_some(); + + let draw_opaque_pbr = opaque_draw_functions.read().id::(); + let draw_alpha_mask_pbr = alpha_mask_draw_functions.read().id::(); + let draw_transmissive_pbr = transmissive_draw_functions.read().id::(); + let draw_transparent_pbr = transparent_draw_functions.read().id::(); + let draw_opaque_prepass = opaque_prepass_draw_functions.read().get_id::(); let draw_alpha_mask_prepass = alpha_mask_prepass_draw_functions .read() - .get_id::>(); + .get_id::(); let draw_opaque_deferred = opaque_deferred_draw_functions .read() - .get_id::>(); + .get_id::(); let draw_alpha_mask_deferred = alpha_mask_deferred_draw_functions .read() - .get_id::>(); + .get_id::(); + let shadow_draw_function_id = shadow_draw_functions.read().get_id::(); let render_method = match material.opaque_render_method() { OpaqueRendererMethod::Forward => OpaqueRendererMethod::Forward, @@ -1471,13 +1495,81 @@ impl RenderAsset for PreparedMaterial { _ => None, }; - match material.unprepared_bind_group( - &pipeline.material_layout, - render_device, - material_param, - false, - ) { + let mut draw_functions = SmallVec::new(); + draw_functions.push((MaterialDrawFunction.intern(), draw_function_id)); + if let Some(prepass_draw_function_id) = prepass_draw_function_id { + draw_functions.push((PrepassDrawFunction.intern(), prepass_draw_function_id)); + } + if let Some(deferred_draw_function_id) = deferred_draw_function_id { + draw_functions.push((DeferredDrawFunction.intern(), deferred_draw_function_id)); + } + if let Some(shadow_draw_function_id) = shadow_draw_function_id { + draw_functions.push((ShadowsDrawFunction.intern(), shadow_draw_function_id)); + } + + let mut shaders = SmallVec::new(); + let mut add_shader = |label: InternedShaderLabel, shader_ref: ShaderRef| { + let mayber_shader = match shader_ref { + ShaderRef::Default => None, + ShaderRef::Handle(handle) => Some(handle), + ShaderRef::Path(path) => Some(asset_server.load(path)), + }; + if let Some(shader) = mayber_shader { + shaders.push((label, shader)); + } + }; + add_shader(MaterialVertexShader.intern(), M::vertex_shader()); + add_shader(MaterialFragmentShader.intern(), M::fragment_shader()); + add_shader(PrepassVertexShader.intern(), M::prepass_vertex_shader()); + add_shader(PrepassFragmentShader.intern(), M::prepass_fragment_shader()); + add_shader(DeferredVertexShader.intern(), M::deferred_vertex_shader()); + add_shader( + DeferredFragmentShader.intern(), + M::deferred_fragment_shader(), + ); + + #[cfg(feature = "meshlet")] + { + add_shader( + MeshletFragmentShader.intern(), + M::meshlet_mesh_fragment_shader(), + ); + add_shader( + MeshletPrepassFragmentShader.intern(), + M::meshlet_mesh_prepass_fragment_shader(), + ); + add_shader( + MeshletDeferredFragmentShader.intern(), + M::meshlet_mesh_deferred_fragment_shader(), + ); + } + + let bindless = material_uses_bindless_resources::(render_device); + let bind_group_data = material.bind_group_data(); + let material_key = SmallVec::from(bytemuck::bytes_of(&bind_group_data)); + fn specialize( + pipeline: &MaterialPipeline, + descriptor: &mut RenderPipelineDescriptor, + mesh_layout: &MeshVertexBufferLayoutRef, + erased_key: ErasedMaterialPipelineKey, + ) -> Result<(), SpecializedMeshPipelineError> { + let material_key = bytemuck::from_bytes(erased_key.material_key.as_slice()); + M::specialize( + pipeline, + descriptor, + mesh_layout, + MaterialPipelineKey { + mesh_key: erased_key.mesh_key, + bind_group_data: *material_key, + }, + ) + } + + match material.unprepared_bind_group(&material_layout, render_device, material_param, false) + { Ok(unprepared) => { + let bind_group_allocator = + bind_group_allocators.get_mut(&TypeId::of::()).unwrap(); // Allocate or update the material. let binding = match render_material_bindings.entry(material_id.into()) { Entry::Occupied(mut occupied_entry) => { @@ -1486,31 +1578,34 @@ impl RenderAsset for PreparedMaterial { // change. For now, we just delete and recreate the bind // group. bind_group_allocator.free(*occupied_entry.get()); - let new_binding = bind_group_allocator - .allocate_unprepared(unprepared, &pipeline.material_layout); + let new_binding = + bind_group_allocator.allocate_unprepared(unprepared, &material_layout); *occupied_entry.get_mut() = new_binding; new_binding } Entry::Vacant(vacant_entry) => *vacant_entry.insert( - bind_group_allocator - .allocate_unprepared(unprepared, &pipeline.material_layout), + bind_group_allocator.allocate_unprepared(unprepared, &material_layout), ), }; Ok(PreparedMaterial { binding, - properties: MaterialProperties { + properties: Arc::new(MaterialProperties { alpha_mode: material.alpha_mode(), depth_bias: material.depth_bias(), reads_view_transmission_texture, render_phase_type, - draw_function_id, - prepass_draw_function_id, render_method, mesh_pipeline_key_bits, - deferred_draw_function_id, - }, - phantom: PhantomData, + material_layout: Some(material_layout), + draw_functions, + shaders, + bindless, + specialize: Some(specialize::), + material_key, + shadows_enabled, + prepass_enabled, + }), }) } @@ -1523,12 +1618,10 @@ impl RenderAsset for PreparedMaterial { // and is requesting a fully-custom bind group. Invoke // `as_bind_group` as requested, and store the resulting bind // group in the slot. - match material.as_bind_group( - &pipeline.material_layout, - render_device, - material_param, - ) { + match material.as_bind_group(&material_layout, render_device, material_param) { Ok(prepared_bind_group) => { + let bind_group_allocator = + bind_group_allocators.get_mut(&TypeId::of::()).unwrap(); // Store the resulting bind group directly in the slot. let material_binding_id = bind_group_allocator.allocate_prepared(prepared_bind_group); @@ -1536,18 +1629,22 @@ impl RenderAsset for PreparedMaterial { Ok(PreparedMaterial { binding: material_binding_id, - properties: MaterialProperties { + properties: Arc::new(MaterialProperties { alpha_mode: material.alpha_mode(), depth_bias: material.depth_bias(), reads_view_transmission_texture, render_phase_type, - draw_function_id, - prepass_draw_function_id, render_method, mesh_pipeline_key_bits, - deferred_draw_function_id, - }, - phantom: PhantomData, + material_layout: Some(material_layout), + draw_functions, + shaders, + bindless, + specialize: Some(specialize::), + material_key, + shadows_enabled, + prepass_enabled, + }), }) } @@ -1565,7 +1662,7 @@ impl RenderAsset for PreparedMaterial { fn unload_asset( source_asset: AssetId, - (_, _, _, bind_group_allocator, render_material_bindings, ..): &mut SystemParamItem< + (_, _, bind_group_allocators, render_material_bindings, ..): &mut SystemParamItem< Self::Param, >, ) { @@ -1573,7 +1670,8 @@ impl RenderAsset for PreparedMaterial { else { return; }; - bind_group_allocator.free(material_binding_id); + let bind_group_allactor = bind_group_allocators.get_mut(&TypeId::of::()).unwrap(); + bind_group_allactor.free(material_binding_id); } } @@ -1594,15 +1692,15 @@ impl From for MaterialBindGroupId { /// Creates and/or recreates any bind groups that contain materials that were /// modified this frame. -pub fn prepare_material_bind_groups( - mut allocator: ResMut>, +pub fn prepare_material_bind_groups( + mut allocators: ResMut, render_device: Res, fallback_image: Res, fallback_resources: Res, -) where - M: Material, -{ - allocator.prepare_bind_groups(&render_device, &fallback_resources, &fallback_image); +) { + for (_, allocator) in allocators.iter_mut() { + allocator.prepare_bind_groups(&render_device, &fallback_resources, &fallback_image); + } } /// Uploads the contents of all buffers that the [`MaterialBindGroupAllocator`] @@ -1610,12 +1708,12 @@ pub fn prepare_material_bind_groups( /// /// Non-bindless allocators don't currently manage any buffers, so this method /// only has an effect for bindless allocators. -pub fn write_material_bind_group_buffers( - mut allocator: ResMut>, +pub fn write_material_bind_group_buffers( + mut allocators: ResMut, render_device: Res, render_queue: Res, -) where - M: Material, -{ - allocator.write_buffers(&render_device, &render_queue); +) { + for (_, allocator) in allocators.iter_mut() { + allocator.write_buffers(&render_device, &render_queue); + } } diff --git a/crates/bevy_pbr/src/material_bind_groups.rs b/crates/bevy_pbr/src/material_bind_groups.rs index b7f2e9361a..aff3ab04c9 100644 --- a/crates/bevy_pbr/src/material_bind_groups.rs +++ b/crates/bevy_pbr/src/material_bind_groups.rs @@ -4,8 +4,7 @@ //! allocator manages each bind group, assigning slots to materials as //! appropriate. -use core::{cmp::Ordering, iter, marker::PhantomData, mem, ops::Range}; - +use crate::Material; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ resource::Resource, @@ -13,6 +12,7 @@ use bevy_ecs::{ }; use bevy_platform::collections::{HashMap, HashSet}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; +use bevy_render::render_resource::BindlessSlabResourceLimit; use bevy_render::{ render_resource::{ BindGroup, BindGroupEntry, BindGroupLayout, BindingNumber, BindingResource, @@ -26,11 +26,14 @@ use bevy_render::{ settings::WgpuFeatures, texture::FallbackImage, }; -use bevy_utils::default; +use bevy_utils::{default, TypeIdMap}; use bytemuck::Pod; +use core::hash::Hash; +use core::{cmp::Ordering, iter, mem, ops::Range}; use tracing::{error, trace}; -use crate::Material; +#[derive(Resource, Deref, DerefMut, Default)] +pub struct MaterialBindGroupAllocators(TypeIdMap); /// A resource that places materials into bind groups and tracks their /// resources. @@ -38,25 +41,20 @@ use crate::Material; /// Internally, Bevy has separate allocators for bindless and non-bindless /// materials. This resource provides a common interface to the specific /// allocator in use. -#[derive(Resource)] -pub enum MaterialBindGroupAllocator -where - M: Material, -{ +pub enum MaterialBindGroupAllocator { /// The allocator used when the material is bindless. - Bindless(Box>), + Bindless(Box), /// The allocator used when the material is non-bindless. - NonBindless(Box>), + NonBindless(Box), } /// The allocator that places bindless materials into bind groups and tracks /// their resources. -pub struct MaterialBindGroupBindlessAllocator -where - M: Material, -{ +pub struct MaterialBindGroupBindlessAllocator { + /// The label of the bind group allocator to use for allocated buffers. + label: Option<&'static str>, /// The slabs, each of which contains a bind group. - slabs: Vec>, + slabs: Vec, /// The layout of the bind groups that we produce. bind_group_layout: BindGroupLayout, /// Information about the bindless resources in the material. @@ -79,10 +77,7 @@ where } /// A single bind group and the bookkeeping necessary to allocate into it. -pub struct MaterialBindlessSlab -where - M: Material, -{ +pub struct MaterialBindlessSlab { /// The current bind group, if it's up to date. /// /// If this is `None`, then the bind group is dirty and needs to be @@ -98,7 +93,7 @@ where /// /// Because the slab binary searches this table, the entries within must be /// sorted by bindless index. - bindless_index_tables: Vec>, + bindless_index_tables: Vec, /// The binding arrays containing samplers. samplers: HashMap>, @@ -110,12 +105,6 @@ where /// `#[data]` attribute of `AsBindGroup`). data_buffers: HashMap, - /// Holds extra CPU-accessible data that the material provides. - /// - /// Typically, this data is used for constructing the material key, for - /// pipeline specialization purposes. - extra_data: Vec>, - /// A list of free slot IDs. free_slots: Vec, /// The total number of materials currently allocated in this slab. @@ -130,10 +119,7 @@ where /// This is conventionally assigned to bind group binding 0, but it can be /// changed by altering the [`Self::binding_number`], which corresponds to the /// `#[bindless(index_table(binding(B)))]` attribute in `AsBindGroup`. -struct MaterialBindlessIndexTable -where - M: Material, -{ +struct MaterialBindlessIndexTable { /// The buffer containing the mappings. buffer: RetainedRawBufferVec, /// The range of bindless indices that this bindless index table covers. @@ -146,7 +132,6 @@ where index_range: Range, /// The binding number that this index table is assigned to in the shader. binding_number: BindingNumber, - phantom: PhantomData, } /// A single binding array for storing bindless resources and the bookkeeping @@ -189,13 +174,12 @@ where } /// The allocator that stores bind groups for non-bindless materials. -pub struct MaterialBindGroupNonBindlessAllocator -where - M: Material, -{ +pub struct MaterialBindGroupNonBindlessAllocator { + /// The label of the bind group allocator to use for allocated buffers. + label: Option<&'static str>, /// A mapping from [`MaterialBindGroupIndex`] to the bind group allocated in /// each slot. - bind_groups: Vec>>, + bind_groups: Vec>, /// The bind groups that are dirty and need to be prepared. /// /// To prepare the bind groups, call @@ -203,15 +187,11 @@ where to_prepare: HashSet, /// A list of free bind group indices. free_indices: Vec, - phantom: PhantomData, } /// A single bind group that a [`MaterialBindGroupNonBindlessAllocator`] is /// currently managing. -enum MaterialNonBindlessAllocatedBindGroup -where - M: Material, -{ +enum MaterialNonBindlessAllocatedBindGroup { /// An unprepared bind group. /// /// The allocator prepares all outstanding unprepared bind groups when @@ -219,13 +199,13 @@ where /// called. Unprepared { /// The unprepared bind group, including extra data. - bind_group: UnpreparedBindGroup, + bind_group: UnpreparedBindGroup, /// The layout of that bind group. layout: BindGroupLayout, }, /// A bind group that's already been prepared. Prepared { - bind_group: PreparedBindGroup, + bind_group: PreparedBindGroup, #[expect(dead_code, reason = "These buffers are only referenced by bind groups")] uniform_buffers: Vec, }, @@ -351,35 +331,27 @@ trait GetBindingResourceId { } /// The public interface to a slab, which represents a single bind group. -pub struct MaterialSlab<'a, M>(MaterialSlabImpl<'a, M>) -where - M: Material; +pub struct MaterialSlab<'a>(MaterialSlabImpl<'a>); /// The actual implementation of a material slab. /// /// This has bindless and non-bindless variants. -enum MaterialSlabImpl<'a, M> -where - M: Material, -{ +enum MaterialSlabImpl<'a> { /// The implementation of the slab interface we use when the slab /// is bindless. - Bindless(&'a MaterialBindlessSlab), + Bindless(&'a MaterialBindlessSlab), /// The implementation of the slab interface we use when the slab /// is non-bindless. - NonBindless(MaterialNonBindlessSlab<'a, M>), + NonBindless(MaterialNonBindlessSlab<'a>), } /// A single bind group that the [`MaterialBindGroupNonBindlessAllocator`] /// manages. -enum MaterialNonBindlessSlab<'a, M> -where - M: Material, -{ +enum MaterialNonBindlessSlab<'a> { /// A slab that has a bind group. - Prepared(&'a PreparedBindGroup), + Prepared(&'a PreparedBindGroup), /// A slab that doesn't yet have a bind group. - Unprepared(&'a UnpreparedBindGroup), + Unprepared, } /// Manages an array of untyped plain old data on GPU and allocates individual @@ -480,26 +452,33 @@ impl GetBindingResourceId for TextureView { } } -impl MaterialBindGroupAllocator -where - M: Material, -{ +impl MaterialBindGroupAllocator { /// Creates a new [`MaterialBindGroupAllocator`] managing the data for a /// single material. - fn new(render_device: &RenderDevice) -> MaterialBindGroupAllocator { - if material_uses_bindless_resources::(render_device) { + pub fn new( + render_device: &RenderDevice, + label: Option<&'static str>, + bindless_descriptor: Option, + bind_group_layout: BindGroupLayout, + slab_capacity: Option, + ) -> MaterialBindGroupAllocator { + if let Some(bindless_descriptor) = bindless_descriptor { MaterialBindGroupAllocator::Bindless(Box::new(MaterialBindGroupBindlessAllocator::new( render_device, + label, + bindless_descriptor, + bind_group_layout, + slab_capacity, ))) } else { MaterialBindGroupAllocator::NonBindless(Box::new( - MaterialBindGroupNonBindlessAllocator::new(), + MaterialBindGroupNonBindlessAllocator::new(label), )) } } /// Returns the slab with the given index, if one exists. - pub fn get(&self, group: MaterialBindGroupIndex) -> Option> { + pub fn get(&self, group: MaterialBindGroupIndex) -> Option { match *self { MaterialBindGroupAllocator::Bindless(ref bindless_allocator) => bindless_allocator .get(group) @@ -520,7 +499,7 @@ where /// you need to prepare the bind group yourself. pub fn allocate_unprepared( &mut self, - unprepared_bind_group: UnpreparedBindGroup, + unprepared_bind_group: UnpreparedBindGroup, bind_group_layout: &BindGroupLayout, ) -> MaterialBindingId { match *self { @@ -545,7 +524,7 @@ where /// this method if you need to prepare the bind group yourself. pub fn allocate_prepared( &mut self, - prepared_bind_group: PreparedBindGroup, + prepared_bind_group: PreparedBindGroup, ) -> MaterialBindingId { match *self { MaterialBindGroupAllocator::Bindless(_) => { @@ -652,14 +631,11 @@ where } } -impl MaterialBindlessIndexTable -where - M: Material, -{ +impl MaterialBindlessIndexTable { /// Creates a new [`MaterialBindlessIndexTable`] for a single slab. fn new( bindless_index_table_descriptor: &BindlessIndexTableDescriptor, - ) -> MaterialBindlessIndexTable { + ) -> MaterialBindlessIndexTable { // Preallocate space for one bindings table, so that there will always be a buffer. let mut buffer = RetainedRawBufferVec::new(BufferUsages::STORAGE); for _ in *bindless_index_table_descriptor.indices.start @@ -672,7 +648,6 @@ where buffer, index_range: bindless_index_table_descriptor.indices.clone(), binding_number: bindless_index_table_descriptor.binding_number, - phantom: PhantomData, } } @@ -786,15 +761,16 @@ where } } -impl MaterialBindGroupBindlessAllocator -where - M: Material, -{ +impl MaterialBindGroupBindlessAllocator { /// Creates a new [`MaterialBindGroupBindlessAllocator`] managing the data /// for a single bindless material. - fn new(render_device: &RenderDevice) -> MaterialBindGroupBindlessAllocator { - let bindless_descriptor = M::bindless_descriptor() - .expect("Non-bindless materials should use the non-bindless allocator"); + fn new( + render_device: &RenderDevice, + label: Option<&'static str>, + bindless_descriptor: BindlessDescriptor, + bind_group_layout: BindGroupLayout, + slab_capacity: Option, + ) -> MaterialBindGroupBindlessAllocator { let fallback_buffers = bindless_descriptor .buffers .iter() @@ -815,11 +791,12 @@ where .collect(); MaterialBindGroupBindlessAllocator { + label, slabs: vec![], - bind_group_layout: M::bind_group_layout(render_device), + bind_group_layout, bindless_descriptor, fallback_buffers, - slab_capacity: M::bindless_slot_count() + slab_capacity: slab_capacity .expect("Non-bindless materials should use the non-bindless allocator") .resolve(), } @@ -835,7 +812,7 @@ where /// created, and the material is allocated into it. fn allocate_unprepared( &mut self, - mut unprepared_bind_group: UnpreparedBindGroup, + mut unprepared_bind_group: UnpreparedBindGroup, ) -> MaterialBindingId { for (slab_index, slab) in self.slabs.iter_mut().enumerate() { trace!("Trying to allocate in slab {}", slab_index); @@ -881,7 +858,7 @@ where /// /// A [`MaterialBindGroupIndex`] can be fetched from a /// [`MaterialBindingId`]. - fn get(&self, group: MaterialBindGroupIndex) -> Option<&MaterialBindlessSlab> { + fn get(&self, group: MaterialBindGroupIndex) -> Option<&MaterialBindlessSlab> { self.slabs.get(group.0 as usize) } @@ -897,6 +874,7 @@ where for slab in &mut self.slabs { slab.prepare( render_device, + self.label, &self.bind_group_layout, fallback_bindless_resources, &self.fallback_buffers, @@ -917,20 +895,7 @@ where } } -impl FromWorld for MaterialBindGroupAllocator -where - M: Material, -{ - fn from_world(world: &mut World) -> Self { - let render_device = world.resource::(); - MaterialBindGroupAllocator::new(render_device) - } -} - -impl MaterialBindlessSlab -where - M: Material, -{ +impl MaterialBindlessSlab { /// Attempts to allocate the given unprepared bind group in this slab. /// /// If the allocation succeeds, this method returns the slot that the @@ -939,9 +904,9 @@ where /// so that it can try to allocate again. fn try_allocate( &mut self, - unprepared_bind_group: UnpreparedBindGroup, + unprepared_bind_group: UnpreparedBindGroup, slot_capacity: u32, - ) -> Result> { + ) -> Result { // Locate pre-existing resources, and determine how many free slots we need. let Some(allocation_candidate) = self.check_allocation(&unprepared_bind_group) else { return Err(unprepared_bind_group); @@ -981,12 +946,6 @@ where bindless_index_table.set(slot, &allocated_resource_slots); } - // Insert extra data. - if self.extra_data.len() < (*slot as usize + 1) { - self.extra_data.resize_with(*slot as usize + 1, || None); - } - self.extra_data[*slot as usize] = Some(unprepared_bind_group.data); - // Invalidate the cached bind group. self.bind_group = None; @@ -997,7 +956,7 @@ where /// bind group can be allocated in this slab. fn check_allocation( &self, - unprepared_bind_group: &UnpreparedBindGroup, + unprepared_bind_group: &UnpreparedBindGroup, ) -> Option { let mut allocation_candidate = BindlessAllocationCandidate { pre_existing_resources: HashMap::default(), @@ -1228,9 +1187,6 @@ where } } - // Clear out the extra data. - self.extra_data[slot.0 as usize] = None; - // Invalidate the cached bind group. self.bind_group = None; @@ -1243,6 +1199,7 @@ where fn prepare( &mut self, render_device: &RenderDevice, + label: Option<&'static str>, bind_group_layout: &BindGroupLayout, fallback_bindless_resources: &FallbackBindlessResources, fallback_buffers: &HashMap, @@ -1263,6 +1220,7 @@ where // Create the bind group if needed. self.prepare_bind_group( render_device, + label, bind_group_layout, fallback_bindless_resources, fallback_buffers, @@ -1277,6 +1235,7 @@ where fn prepare_bind_group( &mut self, render_device: &RenderDevice, + label: Option<&'static str>, bind_group_layout: &BindGroupLayout, fallback_bindless_resources: &FallbackBindlessResources, fallback_buffers: &HashMap, @@ -1343,11 +1302,8 @@ where }); } - self.bind_group = Some(render_device.create_bind_group( - M::label(), - bind_group_layout, - &bind_group_entries, - )); + self.bind_group = + Some(render_device.create_bind_group(label, bind_group_layout, &bind_group_entries)); } /// Writes any buffers that we're managing to the GPU. @@ -1587,19 +1543,11 @@ where self.bind_group.as_ref() } - /// Returns the extra data associated with this material. - fn get_extra_data(&self, slot: MaterialBindGroupSlot) -> &M::Data { - self.extra_data - .get(slot.0 as usize) - .and_then(|data| data.as_ref()) - .expect("Extra data not present") - } - /// Returns the bindless index table containing the given bindless index. fn get_bindless_index_table( &self, bindless_index: BindlessIndex, - ) -> Option<&MaterialBindlessIndexTable> { + ) -> Option<&MaterialBindlessIndexTable> { let table_index = self .bindless_index_tables .binary_search_by(|bindless_index_table| { @@ -1728,15 +1676,12 @@ where }) } -impl MaterialBindlessSlab -where - M: Material, -{ +impl MaterialBindlessSlab { /// Creates a new [`MaterialBindlessSlab`] for a material with the given /// bindless descriptor. /// /// We use this when no existing slab could hold a material to be allocated. - fn new(bindless_descriptor: &BindlessDescriptor) -> MaterialBindlessSlab { + fn new(bindless_descriptor: &BindlessDescriptor) -> MaterialBindlessSlab { let mut buffers = HashMap::default(); let mut samplers = HashMap::default(); let mut textures = HashMap::default(); @@ -1819,7 +1764,7 @@ where let bindless_index_tables = bindless_descriptor .index_tables .iter() - .map(|bindless_index_table| MaterialBindlessIndexTable::new(bindless_index_table)) + .map(MaterialBindlessIndexTable::new) .collect(); MaterialBindlessSlab { @@ -1829,7 +1774,6 @@ where textures, buffers, data_buffers, - extra_data: vec![], free_slots: vec![], live_allocation_count: 0, allocated_resource_count: 0, @@ -1861,18 +1805,15 @@ impl FromWorld for FallbackBindlessResources { } } -impl MaterialBindGroupNonBindlessAllocator -where - M: Material, -{ +impl MaterialBindGroupNonBindlessAllocator { /// Creates a new [`MaterialBindGroupNonBindlessAllocator`] managing the /// bind groups for a single non-bindless material. - fn new() -> MaterialBindGroupNonBindlessAllocator { + fn new(label: Option<&'static str>) -> MaterialBindGroupNonBindlessAllocator { MaterialBindGroupNonBindlessAllocator { + label, bind_groups: vec![], to_prepare: HashSet::default(), free_indices: vec![], - phantom: PhantomData, } } @@ -1881,10 +1822,7 @@ where /// /// The returned [`MaterialBindingId`] can later be used to fetch the bind /// group. - fn allocate( - &mut self, - bind_group: MaterialNonBindlessAllocatedBindGroup, - ) -> MaterialBindingId { + fn allocate(&mut self, bind_group: MaterialNonBindlessAllocatedBindGroup) -> MaterialBindingId { let group_id = self .free_indices .pop() @@ -1913,7 +1851,7 @@ where /// [`MaterialBindingId`]. fn allocate_unprepared( &mut self, - unprepared_bind_group: UnpreparedBindGroup, + unprepared_bind_group: UnpreparedBindGroup, bind_group_layout: BindGroupLayout, ) -> MaterialBindingId { self.allocate(MaterialNonBindlessAllocatedBindGroup::Unprepared { @@ -1924,10 +1862,7 @@ where /// Inserts an prepared bind group into this allocator and returns a /// [`MaterialBindingId`]. - fn allocate_prepared( - &mut self, - prepared_bind_group: PreparedBindGroup, - ) -> MaterialBindingId { + fn allocate_prepared(&mut self, prepared_bind_group: PreparedBindGroup) -> MaterialBindingId { self.allocate(MaterialNonBindlessAllocatedBindGroup::Prepared { bind_group: prepared_bind_group, uniform_buffers: vec![], @@ -1944,15 +1879,15 @@ where } /// Returns a wrapper around the bind group with the given index. - fn get(&self, group: MaterialBindGroupIndex) -> Option> { + fn get(&self, group: MaterialBindGroupIndex) -> Option { self.bind_groups[group.0 as usize] .as_ref() .map(|bind_group| match bind_group { MaterialNonBindlessAllocatedBindGroup::Prepared { bind_group, .. } => { MaterialNonBindlessSlab::Prepared(bind_group) } - MaterialNonBindlessAllocatedBindGroup::Unprepared { bind_group, .. } => { - MaterialNonBindlessSlab::Unprepared(bind_group) + MaterialNonBindlessAllocatedBindGroup::Unprepared { .. } => { + MaterialNonBindlessSlab::Unprepared } }) } @@ -2011,7 +1946,7 @@ where // Create the bind group. let bind_group = render_device.create_bind_group( - M::label(), + self.label, &bind_group_layout, &bind_group_entries, ); @@ -2021,7 +1956,6 @@ where bind_group: PreparedBindGroup { bindings: unprepared_bind_group.bindings, bind_group, - data: unprepared_bind_group.data, }, uniform_buffers, }); @@ -2029,28 +1963,7 @@ where } } -impl<'a, M> MaterialSlab<'a, M> -where - M: Material, -{ - /// Returns the extra data associated with this material. - /// - /// When deriving `AsBindGroup`, this data is given by the - /// `#[bind_group_data(DataType)]` attribute on the material structure. - pub fn get_extra_data(&self, slot: MaterialBindGroupSlot) -> &M::Data { - match self.0 { - MaterialSlabImpl::Bindless(material_bindless_slab) => { - material_bindless_slab.get_extra_data(slot) - } - MaterialSlabImpl::NonBindless(MaterialNonBindlessSlab::Prepared( - prepared_bind_group, - )) => &prepared_bind_group.data, - MaterialSlabImpl::NonBindless(MaterialNonBindlessSlab::Unprepared( - unprepared_bind_group, - )) => &unprepared_bind_group.data, - } - } - +impl<'a> MaterialSlab<'a> { /// Returns the [`BindGroup`] corresponding to this slab, if it's been /// prepared. /// @@ -2065,7 +1978,7 @@ where MaterialSlabImpl::NonBindless(MaterialNonBindlessSlab::Prepared( prepared_bind_group, )) => Some(&prepared_bind_group.bind_group), - MaterialSlabImpl::NonBindless(MaterialNonBindlessSlab::Unprepared(_)) => None, + MaterialSlabImpl::NonBindless(MaterialNonBindlessSlab::Unprepared) => None, } } } diff --git a/crates/bevy_pbr/src/meshlet/asset.rs b/crates/bevy_pbr/src/meshlet/asset.rs index c158650d1b..584ea345e3 100644 --- a/crates/bevy_pbr/src/meshlet/asset.rs +++ b/crates/bevy_pbr/src/meshlet/asset.rs @@ -6,9 +6,9 @@ use bevy_asset::{ }; use bevy_math::{Vec2, Vec3}; use bevy_reflect::TypePath; +use bevy_render::render_resource::ShaderType; use bevy_tasks::block_on; use bytemuck::{Pod, Zeroable}; -use half::f16; use lz4_flex::frame::{FrameDecoder, FrameEncoder}; use std::io::{Read, Write}; use thiserror::Error; @@ -17,7 +17,7 @@ use thiserror::Error; const MESHLET_MESH_ASSET_MAGIC: u64 = 1717551717668; /// The current version of the [`MeshletMesh`] asset format. -pub const MESHLET_MESH_ASSET_VERSION: u64 = 1; +pub const MESHLET_MESH_ASSET_VERSION: u64 = 2; /// A mesh that has been pre-processed into multiple small clusters of triangles called meshlets. /// @@ -47,12 +47,32 @@ pub struct MeshletMesh { pub(crate) vertex_uvs: Arc<[Vec2]>, /// Triangle indices for meshlets. pub(crate) indices: Arc<[u8]>, + /// The BVH8 used for culling and LOD selection of the meshlets. The root is at index 0. + pub(crate) bvh: Arc<[BvhNode]>, /// The list of meshlets making up this mesh. pub(crate) meshlets: Arc<[Meshlet]>, /// Spherical bounding volumes. - pub(crate) meshlet_bounding_spheres: Arc<[MeshletBoundingSpheres]>, - /// Meshlet group and parent group simplification errors. - pub(crate) meshlet_simplification_errors: Arc<[MeshletSimplificationError]>, + pub(crate) meshlet_cull_data: Arc<[MeshletCullData]>, + /// The tight AABB of the meshlet mesh, used for frustum and occlusion culling at the instance + /// level. + pub(crate) aabb: MeshletAabb, + /// The depth of the culling BVH, used to determine the number of dispatches at runtime. + pub(crate) bvh_depth: u32, +} + +/// A single BVH8 node in the BVH used for culling and LOD selection of a [`MeshletMesh`]. +#[derive(Copy, Clone, Default, Pod, Zeroable)] +#[repr(C)] +pub struct BvhNode { + /// The tight AABBs of this node's children, used for frustum and occlusion during BVH + /// traversal. + pub aabbs: [MeshletAabbErrorOffset; 8], + /// The LOD bounding spheres of this node's children, used for LOD selection during BVH + /// traversal. + pub lod_bounds: [MeshletBoundingSphere; 8], + /// If `u8::MAX`, it indicates that the child of each children is a BVH node, otherwise it is the number of meshlets in the group. + pub child_counts: [u8; 8], + pub _padding: [u32; 2], } /// A single meshlet within a [`MeshletMesh`]. @@ -91,33 +111,39 @@ pub struct Meshlet { /// Bounding spheres used for culling and choosing level of detail for a [`Meshlet`]. #[derive(Copy, Clone, Pod, Zeroable)] #[repr(C)] -pub struct MeshletBoundingSpheres { - /// Bounding sphere used for frustum and occlusion culling for this meshlet. - pub culling_sphere: MeshletBoundingSphere, +pub struct MeshletCullData { + /// Tight bounding box, used for frustum and occlusion culling for this meshlet. + pub aabb: MeshletAabbErrorOffset, /// Bounding sphere used for determining if this meshlet's group is at the correct level of detail for a given view. pub lod_group_sphere: MeshletBoundingSphere, - /// Bounding sphere used for determining if this meshlet's parent group is at the correct level of detail for a given view. - pub lod_parent_group_sphere: MeshletBoundingSphere, +} + +/// An axis-aligned bounding box used for a [`Meshlet`]. +#[derive(Copy, Clone, Default, Pod, Zeroable, ShaderType)] +#[repr(C)] +pub struct MeshletAabb { + pub center: Vec3, + pub half_extent: Vec3, +} + +// An axis-aligned bounding box used for a [`Meshlet`]. +#[derive(Copy, Clone, Default, Pod, Zeroable, ShaderType)] +#[repr(C)] +pub struct MeshletAabbErrorOffset { + pub center: Vec3, + pub error: f32, + pub half_extent: Vec3, + pub child_offset: u32, } /// A spherical bounding volume used for a [`Meshlet`]. -#[derive(Copy, Clone, Pod, Zeroable)] +#[derive(Copy, Clone, Default, Pod, Zeroable)] #[repr(C)] pub struct MeshletBoundingSphere { pub center: Vec3, pub radius: f32, } -/// Simplification error used for choosing level of detail for a [`Meshlet`]. -#[derive(Copy, Clone, Pod, Zeroable)] -#[repr(C)] -pub struct MeshletSimplificationError { - /// Simplification error used for determining if this meshlet's group is at the correct level of detail for a given view. - pub group_error: f16, - /// Simplification error used for determining if this meshlet's parent group is at the correct level of detail for a given view. - pub parent_group_error: f16, -} - /// An [`AssetSaver`] for `.meshlet_mesh` [`MeshletMesh`] assets. pub struct MeshletMeshSaver; @@ -143,15 +169,23 @@ impl AssetSaver for MeshletMeshSaver { .write_all(&MESHLET_MESH_ASSET_VERSION.to_le_bytes()) .await?; + writer.write_all(bytemuck::bytes_of(&asset.aabb)).await?; + writer + .write_all(bytemuck::bytes_of(&asset.bvh_depth)) + .await?; + // Compress and write asset data let mut writer = FrameEncoder::new(AsyncWriteSyncAdapter(writer)); write_slice(&asset.vertex_positions, &mut writer)?; write_slice(&asset.vertex_normals, &mut writer)?; write_slice(&asset.vertex_uvs, &mut writer)?; write_slice(&asset.indices, &mut writer)?; + write_slice(&asset.bvh, &mut writer)?; write_slice(&asset.meshlets, &mut writer)?; - write_slice(&asset.meshlet_bounding_spheres, &mut writer)?; - write_slice(&asset.meshlet_simplification_errors, &mut writer)?; + write_slice(&asset.meshlet_cull_data, &mut writer)?; + // BUG: Flushing helps with an async_fs bug, but it still fails sometimes. https://github.com/smol-rs/async-fs/issues/45 + // ERROR bevy_asset::server: Failed to load asset with asset loader MeshletMeshLoader: failed to fill whole buffer + writer.flush()?; writer.finish()?; Ok(()) @@ -184,24 +218,33 @@ impl AssetLoader for MeshletMeshLoader { return Err(MeshletMeshSaveOrLoadError::WrongVersion { found: version }); } + let mut bytes = [0u8; size_of::()]; + reader.read_exact(&mut bytes).await?; + let aabb = bytemuck::cast(bytes); + let mut bytes = [0u8; size_of::()]; + reader.read_exact(&mut bytes).await?; + let bvh_depth = u32::from_le_bytes(bytes); + // Load and decompress asset data let reader = &mut FrameDecoder::new(AsyncReadSyncAdapter(reader)); let vertex_positions = read_slice(reader)?; let vertex_normals = read_slice(reader)?; let vertex_uvs = read_slice(reader)?; let indices = read_slice(reader)?; + let bvh = read_slice(reader)?; let meshlets = read_slice(reader)?; - let meshlet_bounding_spheres = read_slice(reader)?; - let meshlet_simplification_errors = read_slice(reader)?; + let meshlet_cull_data = read_slice(reader)?; Ok(MeshletMesh { vertex_positions, vertex_normals, vertex_uvs, indices, + bvh, meshlets, - meshlet_bounding_spheres, - meshlet_simplification_errors, + meshlet_cull_data, + aabb, + bvh_depth, }) } @@ -218,7 +261,7 @@ pub enum MeshletMeshSaveOrLoadError { WrongVersion { found: u64 }, #[error("failed to compress or decompress asset data")] CompressionOrDecompression(#[from] lz4_flex::frame::Error), - #[error("failed to read or write asset data")] + #[error(transparent)] Io(#[from] std::io::Error), } diff --git a/crates/bevy_pbr/src/meshlet/cull_bvh.wgsl b/crates/bevy_pbr/src/meshlet/cull_bvh.wgsl new file mode 100644 index 0000000000..b0bbb5f89b --- /dev/null +++ b/crates/bevy_pbr/src/meshlet/cull_bvh.wgsl @@ -0,0 +1,110 @@ +#import bevy_pbr::meshlet_bindings::{ + InstancedOffset, + get_aabb, + get_aabb_error, + get_aabb_child_offset, + constants, + meshlet_bvh_nodes, + meshlet_bvh_cull_count_read, + meshlet_bvh_cull_count_write, + meshlet_bvh_cull_dispatch, + meshlet_bvh_cull_queue, + meshlet_meshlet_cull_count_early, + meshlet_meshlet_cull_count_late, + meshlet_meshlet_cull_dispatch_early, + meshlet_meshlet_cull_dispatch_late, + meshlet_meshlet_cull_queue, + meshlet_second_pass_bvh_count, + meshlet_second_pass_bvh_dispatch, + meshlet_second_pass_bvh_queue, +} +#import bevy_pbr::meshlet_cull_shared::{ + lod_error_is_imperceptible, + aabb_in_frustum, + should_occlusion_cull_aabb, +} + +@compute +@workgroup_size(128, 1, 1) // 8 threads per node, 16 nodes per workgroup +fn cull_bvh(@builtin(global_invocation_id) global_invocation_id: vec3) { + // Calculate the queue ID for this thread + let dispatch_id = global_invocation_id.x; + var node = dispatch_id >> 3u; + let subnode = dispatch_id & 7u; + if node >= meshlet_bvh_cull_count_read { return; } + + node = select(node, constants.rightmost_slot - node, constants.read_from_front == 0u); + let instanced_offset = meshlet_bvh_cull_queue[node]; + let instance_id = instanced_offset.instance_id; + let bvh_node = &meshlet_bvh_nodes[instanced_offset.offset]; + + var aabb_error_offset = (*bvh_node).aabbs[subnode]; + let aabb = get_aabb(&aabb_error_offset); + let parent_error = get_aabb_error(&aabb_error_offset); + let lod_sphere = (*bvh_node).lod_bounds[subnode]; + + let parent_is_imperceptible = lod_error_is_imperceptible(lod_sphere, parent_error, instance_id); + // Error and frustum cull, in both passes + if parent_is_imperceptible || !aabb_in_frustum(aabb, instance_id) { return; } + + let child_offset = get_aabb_child_offset(&aabb_error_offset); + let index = subnode >> 2u; + let bit_offset = subnode & 3u; + let packed_child_count = (*bvh_node).child_counts[index]; + let child_count = extractBits(packed_child_count, bit_offset * 8u, 8u); + var value = InstancedOffset(instance_id, child_offset); + + // If we pass, try occlusion culling + // If this node was occluded, push it's children to the second pass to check against this frame's HZB + if should_occlusion_cull_aabb(aabb, instance_id) { +#ifdef MESHLET_FIRST_CULLING_PASS + if child_count == 255u { + let id = atomicAdd(&meshlet_second_pass_bvh_count, 1u); + meshlet_second_pass_bvh_queue[id] = value; + if ((id & 15u) == 0u) { + atomicAdd(&meshlet_second_pass_bvh_dispatch.x, 1u); + } + } else { + let base = atomicAdd(&meshlet_meshlet_cull_count_late, child_count); + let start = constants.rightmost_slot - base; + for (var i = start; i < start - child_count; i--) { + meshlet_meshlet_cull_queue[i] = value; + value.offset += 1u; + } + let req = (base + child_count + 127u) >> 7u; + atomicMax(&meshlet_meshlet_cull_dispatch_late.x, req); + } +#endif + return; + } + + // If we pass, push the children to the next BVH cull + if child_count == 255u { + let id = atomicAdd(&meshlet_bvh_cull_count_write, 1u); + let index = select(constants.rightmost_slot - id, id, constants.read_from_front == 0u); + meshlet_bvh_cull_queue[index] = value; + if ((id & 15u) == 0u) { + atomicAdd(&meshlet_bvh_cull_dispatch.x, 1u); + } + } else { +#ifdef MESHLET_FIRST_CULLING_PASS + let base = atomicAdd(&meshlet_meshlet_cull_count_early, child_count); + let end = base + child_count; + for (var i = base; i < end; i++) { + meshlet_meshlet_cull_queue[i] = value; + value.offset += 1u; + } + let req = (end + 127u) >> 7u; + atomicMax(&meshlet_meshlet_cull_dispatch_early.x, req); +#else + let base = atomicAdd(&meshlet_meshlet_cull_count_late, child_count); + let start = constants.rightmost_slot - base; + for (var i = start; i < start - child_count; i--) { + meshlet_meshlet_cull_queue[i] = value; + value.offset += 1u; + } + let req = (base + child_count + 127u) >> 7u; + atomicMax(&meshlet_meshlet_cull_dispatch_late.x, req); +#endif + } +} diff --git a/crates/bevy_pbr/src/meshlet/cull_clusters.wgsl b/crates/bevy_pbr/src/meshlet/cull_clusters.wgsl index 47f6dbb04b..85cbc0654d 100644 --- a/crates/bevy_pbr/src/meshlet/cull_clusters.wgsl +++ b/crates/bevy_pbr/src/meshlet/cull_clusters.wgsl @@ -1,194 +1,93 @@ #import bevy_pbr::meshlet_bindings::{ - meshlet_cluster_meshlet_ids, - meshlet_bounding_spheres, - meshlet_simplification_errors, - meshlet_cluster_instance_ids, - meshlet_instance_uniforms, - meshlet_second_pass_candidates, - depth_pyramid, + InstancedOffset, + get_aabb, + get_aabb_error, + constants, view, - previous_view, - should_cull_instance, - cluster_is_second_pass_candidate, + meshlet_instance_uniforms, + meshlet_cull_data, meshlet_software_raster_indirect_args, meshlet_hardware_raster_indirect_args, + meshlet_previous_raster_counts, meshlet_raster_clusters, - constants, - MeshletBoundingSphere, + meshlet_meshlet_cull_count_read, + meshlet_meshlet_cull_count_write, + meshlet_meshlet_cull_dispatch, + meshlet_meshlet_cull_queue, +} +#import bevy_pbr::meshlet_cull_shared::{ + ScreenAabb, + project_aabb, + lod_error_is_imperceptible, + aabb_in_frustum, + should_occlusion_cull_aabb, } #import bevy_render::maths::affine3_to_square -/// Culls individual clusters (1 per thread) in two passes (two pass occlusion culling), and outputs a bitmask of which clusters survived. -/// 1. The first pass tests instance visibility, frustum culling, LOD selection, and finally occlusion culling using last frame's depth pyramid. -/// 2. The second pass performs occlusion culling (using the depth buffer generated from the first pass) on all clusters that passed -/// the instance, frustum, and LOD tests in the first pass, but were not visible last frame according to the occlusion culling. - @compute -@workgroup_size(128, 1, 1) // 128 threads per workgroup, 1 cluster per thread -fn cull_clusters( - @builtin(workgroup_id) workgroup_id: vec3, - @builtin(num_workgroups) num_workgroups: vec3, - @builtin(local_invocation_index) local_invocation_index: u32, -) { - // Calculate the cluster ID for this thread - let cluster_id = local_invocation_index + 128u * dot(workgroup_id, vec3(num_workgroups.x * num_workgroups.x, num_workgroups.x, 1u)); - if cluster_id >= constants.scene_cluster_count { return; } - -#ifdef MESHLET_SECOND_CULLING_PASS - if !cluster_is_second_pass_candidate(cluster_id) { return; } -#endif - - // Check for instance culling - let instance_id = meshlet_cluster_instance_ids[cluster_id]; -#ifdef MESHLET_FIRST_CULLING_PASS - if should_cull_instance(instance_id) { return; } -#endif - - // Calculate world-space culling bounding sphere for the cluster - let instance_uniform = meshlet_instance_uniforms[instance_id]; - let meshlet_id = meshlet_cluster_meshlet_ids[cluster_id]; - let world_from_local = affine3_to_square(instance_uniform.world_from_local); - let world_scale = max(length(world_from_local[0]), max(length(world_from_local[1]), length(world_from_local[2]))); - let bounding_spheres = meshlet_bounding_spheres[meshlet_id]; - let culling_bounding_sphere_center = world_from_local * vec4(bounding_spheres.culling_sphere.center, 1.0); - let culling_bounding_sphere_radius = world_scale * bounding_spheres.culling_sphere.radius; +@workgroup_size(128, 1, 1) // 1 cluster per thread +fn cull_clusters(@builtin(global_invocation_id) global_invocation_id: vec3) { + if global_invocation_id.x >= meshlet_meshlet_cull_count_read { return; } #ifdef MESHLET_FIRST_CULLING_PASS - // Frustum culling - // TODO: Faster method from https://vkguide.dev/docs/gpudriven/compute_culling/#frustum-culling-function - for (var i = 0u; i < 6u; i++) { - if dot(view.frustum[i], culling_bounding_sphere_center) + culling_bounding_sphere_radius <= 0.0 { - return; - } - } - - // Check LOD cut (cluster group error imperceptible, and parent group error not imperceptible) - let simplification_errors = unpack2x16float(meshlet_simplification_errors[meshlet_id]); - let lod_is_ok = lod_error_is_imperceptible(bounding_spheres.lod_group_sphere, simplification_errors.x, world_from_local, world_scale); - let parent_lod_is_ok = lod_error_is_imperceptible(bounding_spheres.lod_parent_group_sphere, simplification_errors.y, world_from_local, world_scale); - if !lod_is_ok || parent_lod_is_ok { return; } -#endif - - // Project the culling bounding sphere to view-space for occlusion culling -#ifdef MESHLET_FIRST_CULLING_PASS - let previous_world_from_local = affine3_to_square(instance_uniform.previous_world_from_local); - let previous_world_from_local_scale = max(length(previous_world_from_local[0]), max(length(previous_world_from_local[1]), length(previous_world_from_local[2]))); - let occlusion_culling_bounding_sphere_center = previous_world_from_local * vec4(bounding_spheres.culling_sphere.center, 1.0); - let occlusion_culling_bounding_sphere_radius = previous_world_from_local_scale * bounding_spheres.culling_sphere.radius; - let occlusion_culling_bounding_sphere_center_view_space = (previous_view.view_from_world * vec4(occlusion_culling_bounding_sphere_center.xyz, 1.0)).xyz; + let meshlet_id = global_invocation_id.x; #else - let occlusion_culling_bounding_sphere_center = culling_bounding_sphere_center; - let occlusion_culling_bounding_sphere_radius = culling_bounding_sphere_radius; - let occlusion_culling_bounding_sphere_center_view_space = (view.view_from_world * vec4(occlusion_culling_bounding_sphere_center.xyz, 1.0)).xyz; + let meshlet_id = constants.rightmost_slot - global_invocation_id.x; #endif + let instanced_offset = meshlet_meshlet_cull_queue[meshlet_id]; + let instance_id = instanced_offset.instance_id; + let cull_data = &meshlet_cull_data[instanced_offset.offset]; + var aabb_error_offset = (*cull_data).aabb; + let aabb = get_aabb(&aabb_error_offset); + let error = get_aabb_error(&aabb_error_offset); + let lod_sphere = (*cull_data).lod_group_sphere; - var aabb = project_view_space_sphere_to_screen_space_aabb(occlusion_culling_bounding_sphere_center_view_space, occlusion_culling_bounding_sphere_radius); - let depth_pyramid_size_mip_0 = vec2(textureDimensions(depth_pyramid, 0)); - var aabb_width_pixels = (aabb.z - aabb.x) * depth_pyramid_size_mip_0.x; - var aabb_height_pixels = (aabb.w - aabb.y) * depth_pyramid_size_mip_0.y; - let depth_level = max(0, i32(ceil(log2(max(aabb_width_pixels, aabb_height_pixels))))); // TODO: Naga doesn't like this being a u32 - let depth_pyramid_size = vec2(textureDimensions(depth_pyramid, depth_level)); - let aabb_top_left = vec2(aabb.xy * depth_pyramid_size); + let is_imperceptible = lod_error_is_imperceptible(lod_sphere, error, instance_id); + // Error and frustum cull, in both passes + if !is_imperceptible || !aabb_in_frustum(aabb, instance_id) { return; } - let depth_quad_a = textureLoad(depth_pyramid, aabb_top_left, depth_level).x; - let depth_quad_b = textureLoad(depth_pyramid, aabb_top_left + vec2(1u, 0u), depth_level).x; - let depth_quad_c = textureLoad(depth_pyramid, aabb_top_left + vec2(0u, 1u), depth_level).x; - let depth_quad_d = textureLoad(depth_pyramid, aabb_top_left + vec2(1u, 1u), depth_level).x; - let occluder_depth = min(min(depth_quad_a, depth_quad_b), min(depth_quad_c, depth_quad_d)); - - // Check whether or not the cluster would be occluded if drawn - var cluster_visible: bool; - if view.clip_from_view[3][3] == 1.0 { - // Orthographic - let sphere_depth = view.clip_from_view[3][2] + (occlusion_culling_bounding_sphere_center_view_space.z + occlusion_culling_bounding_sphere_radius) * view.clip_from_view[2][2]; - cluster_visible = sphere_depth >= occluder_depth; - } else { - // Perspective - let sphere_depth = -view.clip_from_view[3][2] / (occlusion_culling_bounding_sphere_center_view_space.z + occlusion_culling_bounding_sphere_radius); - cluster_visible = sphere_depth >= occluder_depth; - } - - // Write if the cluster should be occlusion tested in the second pass + // If we pass, try occlusion culling + // If this node was occluded, push it's children to the second pass to check against this frame's HZB + if should_occlusion_cull_aabb(aabb, instance_id) { #ifdef MESHLET_FIRST_CULLING_PASS - if !cluster_visible { - let bit = 1u << cluster_id % 32u; - atomicOr(&meshlet_second_pass_candidates[cluster_id / 32u], bit); - } + let id = atomicAdd(&meshlet_meshlet_cull_count_write, 1u); + let value = InstancedOffset(instance_id, instanced_offset.offset); + meshlet_meshlet_cull_queue[constants.rightmost_slot - id] = value; + if ((id & 127u) == 0) { + atomicAdd(&meshlet_meshlet_cull_dispatch.x, 1u); + } #endif + return; + } - // Cluster would be occluded if drawn, so don't setup a draw for it - if !cluster_visible { return; } - + // If we pass, rasterize the meshlet // Check how big the cluster is in screen space -#ifdef MESHLET_FIRST_CULLING_PASS - let culling_bounding_sphere_center_view_space = (view.view_from_world * vec4(culling_bounding_sphere_center.xyz, 1.0)).xyz; - aabb = project_view_space_sphere_to_screen_space_aabb(culling_bounding_sphere_center_view_space, culling_bounding_sphere_radius); - aabb_width_pixels = (aabb.z - aabb.x) * view.viewport.z; - aabb_height_pixels = (aabb.w - aabb.y) * view.viewport.w; -#endif - let cluster_is_small = all(vec2(aabb_width_pixels, aabb_height_pixels) < vec2(64.0)); - - // Let the hardware rasterizer handle near-plane clipping - let not_intersects_near_plane = dot(view.frustum[4u], culling_bounding_sphere_center) > culling_bounding_sphere_radius; + let world_from_local = affine3_to_square(meshlet_instance_uniforms[instance_id].world_from_local); + let clip_from_local = view.clip_from_world * world_from_local; + let projection = view.clip_from_world; + var near: f32; + if projection[3][3] == 1.0 { + near = projection[3][2] / projection[2][2]; + } else { + near = projection[3][2]; + } + var screen_aabb = ScreenAabb(vec3(0.0), vec3(0.0)); + var sw_raster = project_aabb(clip_from_local, near, aabb, &screen_aabb); + if sw_raster { + let aabb_size = (screen_aabb.max.xy - screen_aabb.min.xy) * view.viewport.zw; + sw_raster = all(aabb_size <= vec2(64.0)); + } var buffer_slot: u32; - if cluster_is_small && not_intersects_near_plane { + if sw_raster { // Append this cluster to the list for software rasterization buffer_slot = atomicAdd(&meshlet_software_raster_indirect_args.x, 1u); + buffer_slot += meshlet_previous_raster_counts[0]; } else { // Append this cluster to the list for hardware rasterization buffer_slot = atomicAdd(&meshlet_hardware_raster_indirect_args.instance_count, 1u); - buffer_slot = constants.meshlet_raster_cluster_rightmost_slot - buffer_slot; - } - meshlet_raster_clusters[buffer_slot] = cluster_id; -} - -// https://github.com/zeux/meshoptimizer/blob/1e48e96c7e8059321de492865165e9ef071bffba/demo/nanite.cpp#L115 -fn lod_error_is_imperceptible(lod_sphere: MeshletBoundingSphere, simplification_error: f32, world_from_local: mat4x4, world_scale: f32) -> bool { - let sphere_world_space = (world_from_local * vec4(lod_sphere.center, 1.0)).xyz; - let radius_world_space = world_scale * lod_sphere.radius; - let error_world_space = world_scale * simplification_error; - - var projected_error = error_world_space; - if view.clip_from_view[3][3] != 1.0 { - // Perspective - let distance_to_closest_point_on_sphere = distance(sphere_world_space, view.world_position) - radius_world_space; - let distance_to_closest_point_on_sphere_clamped_to_znear = max(distance_to_closest_point_on_sphere, view.clip_from_view[3][2]); - projected_error /= distance_to_closest_point_on_sphere_clamped_to_znear; - } - projected_error *= view.clip_from_view[1][1] * 0.5; - projected_error *= view.viewport.w; - - return projected_error < 1.0; -} - -// https://zeux.io/2023/01/12/approximate-projected-bounds -fn project_view_space_sphere_to_screen_space_aabb(cp: vec3, r: f32) -> vec4 { - let inv_width = view.clip_from_view[0][0] * 0.5; - let inv_height = view.clip_from_view[1][1] * 0.5; - if view.clip_from_view[3][3] == 1.0 { - // Orthographic - let min_x = cp.x - r; - let max_x = cp.x + r; - - let min_y = cp.y - r; - let max_y = cp.y + r; - - return vec4(min_x * inv_width, 1.0 - max_y * inv_height, max_x * inv_width, 1.0 - min_y * inv_height); - } else { - // Perspective - let c = vec3(cp.xy, -cp.z); - let cr = c * r; - let czr2 = c.z * c.z - r * r; - - let vx = sqrt(c.x * c.x + czr2); - let min_x = (vx * c.x - cr.z) / (vx * c.z + cr.x); - let max_x = (vx * c.x + cr.z) / (vx * c.z - cr.x); - - let vy = sqrt(c.y * c.y + czr2); - let min_y = (vy * c.y - cr.z) / (vy * c.z + cr.y); - let max_y = (vy * c.y + cr.z) / (vy * c.z - cr.y); - - return vec4(min_x * inv_width, -max_y * inv_height, max_x * inv_width, -min_y * inv_height) + vec4(0.5); + buffer_slot += meshlet_previous_raster_counts[1]; + buffer_slot = constants.rightmost_slot - buffer_slot; } + meshlet_raster_clusters[buffer_slot] = InstancedOffset(instance_id, instanced_offset.offset); } diff --git a/crates/bevy_pbr/src/meshlet/cull_instances.wgsl b/crates/bevy_pbr/src/meshlet/cull_instances.wgsl new file mode 100644 index 0000000000..5d14d10b6f --- /dev/null +++ b/crates/bevy_pbr/src/meshlet/cull_instances.wgsl @@ -0,0 +1,76 @@ +#import bevy_pbr::meshlet_bindings::{ + InstancedOffset, + constants, + meshlet_view_instance_visibility, + meshlet_instance_aabbs, + meshlet_instance_bvh_root_nodes, + meshlet_bvh_cull_count_write, + meshlet_bvh_cull_dispatch, + meshlet_bvh_cull_queue, + meshlet_second_pass_instance_count, + meshlet_second_pass_instance_dispatch, + meshlet_second_pass_instance_candidates, +} +#import bevy_pbr::meshlet_cull_shared::{ + aabb_in_frustum, + should_occlusion_cull_aabb, +} + +fn instance_count() -> u32 { +#ifdef MESHLET_FIRST_CULLING_PASS + return constants.scene_instance_count; +#else + return meshlet_second_pass_instance_count; +#endif +} + +fn map_instance_id(id: u32) -> u32 { +#ifdef MESHLET_FIRST_CULLING_PASS + return id; +#else + return meshlet_second_pass_instance_candidates[id]; +#endif +} + +fn should_cull_instance(instance_id: u32) -> bool { + let bit_offset = instance_id >> 5u; + let packed_visibility = meshlet_view_instance_visibility[instance_id & 31u]; + return bool(extractBits(packed_visibility, bit_offset, 1u)); +} + +@compute +@workgroup_size(128, 1, 1) // 1 instance per thread +fn cull_instances(@builtin(global_invocation_id) global_invocation_id: vec3) { + // Calculate the instance ID for this thread + let dispatch_id = global_invocation_id.x; + if dispatch_id >= instance_count() { return; } + + let instance_id = map_instance_id(dispatch_id); + let aabb = meshlet_instance_aabbs[instance_id]; + + // Visibility and frustum cull, but only in the first pass +#ifdef MESHLET_FIRST_CULLING_PASS + if should_cull_instance(instance_id) || !aabb_in_frustum(aabb, instance_id) { return; } +#endif + + // If we pass, try occlusion culling + // If this instance was occluded, push it to the second pass to check against this frame's HZB + if should_occlusion_cull_aabb(aabb, instance_id) { +#ifdef MESHLET_FIRST_CULLING_PASS + let id = atomicAdd(&meshlet_second_pass_instance_count, 1u); + meshlet_second_pass_instance_candidates[id] = instance_id; + if ((id & 127u) == 0u) { + atomicAdd(&meshlet_second_pass_instance_dispatch.x, 1u); + } +#endif + return; + } + + // If we pass, push the instance's root node to BVH cull + let root_node = meshlet_instance_bvh_root_nodes[instance_id]; + let id = atomicAdd(&meshlet_bvh_cull_count_write, 1u); + meshlet_bvh_cull_queue[id] = InstancedOffset(instance_id, root_node); + if ((id & 15u) == 0u) { + atomicAdd(&meshlet_bvh_cull_dispatch.x, 1u); + } +} diff --git a/crates/bevy_pbr/src/meshlet/fill_cluster_buffers.wgsl b/crates/bevy_pbr/src/meshlet/fill_cluster_buffers.wgsl deleted file mode 100644 index db39ae2bce..0000000000 --- a/crates/bevy_pbr/src/meshlet/fill_cluster_buffers.wgsl +++ /dev/null @@ -1,50 +0,0 @@ -#import bevy_pbr::meshlet_bindings::{ - scene_instance_count, - meshlet_global_cluster_count, - meshlet_instance_meshlet_counts, - meshlet_instance_meshlet_slice_starts, - meshlet_cluster_instance_ids, - meshlet_cluster_meshlet_ids, -} - -/// Writes out instance_id and meshlet_id to the global buffers for each cluster in the scene. - -var cluster_slice_start_workgroup: u32; - -@compute -@workgroup_size(1024, 1, 1) // 1024 threads per workgroup, 1 instance per workgroup -fn fill_cluster_buffers( - @builtin(workgroup_id) workgroup_id: vec3, - @builtin(num_workgroups) num_workgroups: vec3, - @builtin(local_invocation_index) local_invocation_index: u32, -) { - // Calculate the instance ID for this workgroup - var instance_id = workgroup_id.x + (workgroup_id.y * num_workgroups.x); - if instance_id >= scene_instance_count { return; } - - let instance_meshlet_count = meshlet_instance_meshlet_counts[instance_id]; - let instance_meshlet_slice_start = meshlet_instance_meshlet_slice_starts[instance_id]; - - // Reserve cluster slots for the instance and broadcast to the workgroup - if local_invocation_index == 0u { - cluster_slice_start_workgroup = atomicAdd(&meshlet_global_cluster_count, instance_meshlet_count); - } - let cluster_slice_start = workgroupUniformLoad(&cluster_slice_start_workgroup); - - // Loop enough times to write out all the meshlets for the instance given that each thread writes 1 meshlet in each iteration - for (var clusters_written = 0u; clusters_written < instance_meshlet_count; clusters_written += 1024u) { - // Calculate meshlet ID within this instance's MeshletMesh to process for this thread - let meshlet_id_local = clusters_written + local_invocation_index; - if meshlet_id_local >= instance_meshlet_count { return; } - - // Find the overall cluster ID in the global cluster buffer - let cluster_id = cluster_slice_start + meshlet_id_local; - - // Find the overall meshlet ID in the global meshlet buffer - let meshlet_id = instance_meshlet_slice_start + meshlet_id_local; - - // Write results to buffers - meshlet_cluster_instance_ids[cluster_id] = instance_id; - meshlet_cluster_meshlet_ids[cluster_id] = meshlet_id; - } -} diff --git a/crates/bevy_pbr/src/meshlet/fill_counts.wgsl b/crates/bevy_pbr/src/meshlet/fill_counts.wgsl new file mode 100644 index 0000000000..f319e395d9 --- /dev/null +++ b/crates/bevy_pbr/src/meshlet/fill_counts.wgsl @@ -0,0 +1,35 @@ +/// Copies the counts of meshlets in the hardware and software buckets, resetting the counters in the process. + +struct DispatchIndirectArgs { + x: u32, + y: u32, + z: u32, +} + +struct DrawIndirectArgs { + vertex_count: u32, + instance_count: u32, + first_vertex: u32, + first_instance: u32, +} + +@group(0) @binding(0) var meshlet_software_raster_indirect_args: DispatchIndirectArgs; +@group(0) @binding(1) var meshlet_hardware_raster_indirect_args: DrawIndirectArgs; +@group(0) @binding(2) var meshlet_previous_raster_counts: array; +#ifdef MESHLET_2D_DISPATCH +@group(0) @binding(3) var meshlet_software_raster_cluster_count: u32; +#endif + +@compute +@workgroup_size(1, 1, 1) +fn fill_counts() { +#ifdef MESHLET_2D_DISPATCH + meshlet_previous_raster_counts[0] += meshlet_software_raster_cluster_count; +#else + meshlet_previous_raster_counts[0] += meshlet_software_raster_indirect_args.x; +#endif + meshlet_software_raster_indirect_args.x = 0; + + meshlet_previous_raster_counts[1] += meshlet_hardware_raster_indirect_args.instance_count; + meshlet_hardware_raster_indirect_args.instance_count = 0; +} diff --git a/crates/bevy_pbr/src/meshlet/from_mesh.rs b/crates/bevy_pbr/src/meshlet/from_mesh.rs index ed2be52f53..db8b8a96cf 100644 --- a/crates/bevy_pbr/src/meshlet/from_mesh.rs +++ b/crates/bevy_pbr/src/meshlet/from_mesh.rs @@ -1,16 +1,20 @@ -use super::asset::{ - Meshlet, MeshletBoundingSphere, MeshletBoundingSpheres, MeshletMesh, MeshletSimplificationError, -}; +use crate::meshlet::asset::{MeshletAabb, MeshletAabbErrorOffset, MeshletCullData}; + +use super::asset::{BvhNode, Meshlet, MeshletBoundingSphere, MeshletMesh}; use alloc::borrow::Cow; -use bevy_math::{ops::log2, IVec3, Vec2, Vec3, Vec3Swizzles}; +use bevy_math::{ + bounding::{Aabb3d, BoundingSphere, BoundingVolume}, + ops::log2, + IVec3, Isometry3d, Vec2, Vec3, Vec3A, Vec3Swizzles, +}; use bevy_platform::collections::HashMap; use bevy_render::{ mesh::{Indices, Mesh}, render_resource::PrimitiveTopology, }; +use bevy_tasks::{AsyncComputeTaskPool, ParallelSlice}; use bitvec::{order::Lsb0, vec::BitVec, view::BitView}; -use core::{iter, ops::Range}; -use half::f16; +use core::{f32, ops::Range}; use itertools::Itertools; use meshopt::{ build_meshlets, ffi::meshopt_Meshlet, generate_vertex_remap_multi, @@ -19,11 +23,13 @@ use meshopt::{ use metis::{option::Opt, Graph}; use smallvec::SmallVec; use thiserror::Error; +use tracing::debug_span; // Aim to have 8 meshlets per group const TARGET_MESHLETS_PER_GROUP: usize = 8; -// Reject groups that keep over 95% of their original triangles -const SIMPLIFICATION_FAILURE_PERCENTAGE: f32 = 0.95; +// Reject groups that keep over 60% of their original triangles. We'd much rather render a few +// extra triangles than create too many meshlets, increasing cull overhead. +const SIMPLIFICATION_FAILURE_PERCENTAGE: f32 = 0.60; /// Default vertex position quantization factor for use with [`MeshletMesh::from_mesh`]. /// @@ -64,6 +70,9 @@ impl MeshletMesh { mesh: &Mesh, vertex_position_quantization_factor: u8, ) -> Result { + let s = debug_span!("build meshlet mesh"); + let _e = s.enter(); + // Validate mesh format let indices = validate_input_mesh(mesh)?; @@ -84,41 +93,28 @@ impl MeshletMesh { ); // Split the mesh into an initial list of meshlets (LOD 0) - let mut meshlets = compute_meshlets( + let (mut meshlets, mut cull_data) = compute_meshlets( &indices, &vertices, &position_only_vertex_remap, position_only_vertex_count, + None, ); - let mut bounding_spheres = meshlets - .iter() - .map(|meshlet| compute_meshlet_bounds(meshlet, &vertices)) - .map(|bounding_sphere| MeshletBoundingSpheres { - culling_sphere: bounding_sphere, - lod_group_sphere: bounding_sphere, - lod_parent_group_sphere: MeshletBoundingSphere { - center: Vec3::ZERO, - radius: 0.0, - }, - }) - .collect::>(); - let mut simplification_errors = iter::repeat_n( - MeshletSimplificationError { - group_error: f16::ZERO, - parent_group_error: f16::MAX, - }, - meshlets.len(), - ) - .collect::>(); let mut vertex_locks = vec![false; vertices.vertex_count]; // Build further LODs - let mut simplification_queue = 0..meshlets.len(); - while simplification_queue.len() > 1 { + let mut bvh = BvhBuilder::default(); + let mut all_groups = Vec::new(); + let mut simplification_queue: Vec<_> = (0..meshlets.len() as u32).collect(); + let mut stuck = Vec::new(); + while !simplification_queue.is_empty() { + let s = debug_span!("simplify lod", meshlets = simplification_queue.len()); + let _e = s.enter(); + // For each meshlet build a list of connected meshlets (meshlets that share a vertex) let connected_meshlets_per_meshlet = find_connected_meshlets( - simplification_queue.clone(), + &simplification_queue, &meshlets, &position_only_vertex_remap, position_only_vertex_count, @@ -127,9 +123,11 @@ impl MeshletMesh { // Group meshlets into roughly groups of size TARGET_MESHLETS_PER_GROUP, // grouping meshlets with a high number of shared vertices let groups = group_meshlets( + &simplification_queue, + &cull_data, &connected_meshlets_per_meshlet, - simplification_queue.clone(), ); + simplification_queue.clear(); // Lock borders between groups to prevent cracks when simplifying lock_group_borders( @@ -140,16 +138,20 @@ impl MeshletMesh { position_only_vertex_count, ); - let next_lod_start = meshlets.len(); - for group_meshlets in groups.into_iter() { + let simplified = groups.par_chunk_map(AsyncComputeTaskPool::get(), 1, |_, groups| { + let mut group = groups[0].clone(); + // If the group only has a single meshlet we can't simplify it - if group_meshlets.len() == 1 { - continue; + if group.meshlets.len() == 1 { + return Err(group); } + let s = debug_span!("simplify group", meshlets = group.meshlets.len()); + let _e = s.enter(); + // Simplify the group to ~50% triangle count let Some((simplified_group_indices, mut group_error)) = simplify_meshlet_group( - &group_meshlets, + &group, &meshlets, &vertices, vertex_normals, @@ -157,51 +159,70 @@ impl MeshletMesh { &vertex_locks, ) else { // Couldn't simplify the group enough - continue; + return Err(group); }; - // Compute LOD data for the group - let group_bounding_sphere = compute_lod_group_data( - &group_meshlets, - &mut group_error, - &mut bounding_spheres, - &mut simplification_errors, - ); + // Force the group error to be atleast as large as all of its constituent meshlet's + // individual errors. + for &id in group.meshlets.iter() { + group_error = group_error.max(cull_data[id as usize].error); + } + group.parent_error = group_error; // Build new meshlets using the simplified group - let new_meshlets_count = split_simplified_group_into_new_meshlets( + let new_meshlets = compute_meshlets( &simplified_group_indices, &vertices, &position_only_vertex_remap, position_only_vertex_count, - &mut meshlets, + Some((group.lod_bounds, group.parent_error)), ); - // Calculate the culling bounding sphere for the new meshlets and set their LOD group data - let new_meshlet_ids = (meshlets.len() - new_meshlets_count)..meshlets.len(); - bounding_spheres.extend(new_meshlet_ids.clone().map(|meshlet_id| { - MeshletBoundingSpheres { - culling_sphere: compute_meshlet_bounds(meshlets.get(meshlet_id), &vertices), - lod_group_sphere: group_bounding_sphere, - lod_parent_group_sphere: MeshletBoundingSphere { - center: Vec3::ZERO, - radius: 0.0, - }, + Ok((group, new_meshlets)) + }); + + let first_group = all_groups.len() as u32; + let mut passed_tris = 0; + let mut stuck_tris = 0; + for group in simplified { + match group { + Ok((group, (new_meshlets, new_cull_data))) => { + let start = meshlets.len(); + merge_meshlets(&mut meshlets, new_meshlets); + cull_data.extend(new_cull_data); + let end = meshlets.len(); + let new_meshlet_ids = start as u32..end as u32; + + passed_tris += triangles_in_meshlets(&meshlets, new_meshlet_ids.clone()); + simplification_queue.extend(new_meshlet_ids); + all_groups.push(group); } - })); - simplification_errors.extend(iter::repeat_n( - MeshletSimplificationError { - group_error, - parent_group_error: f16::MAX, - }, - new_meshlet_ids.len(), - )); + Err(group) => { + stuck_tris += + triangles_in_meshlets(&meshlets, group.meshlets.iter().copied()); + stuck.push(group); + } + } } - // Set simplification queue to the list of newly created meshlets - simplification_queue = next_lod_start..meshlets.len(); + // If we have enough triangles that passed, we can retry simplifying the stuck + // meshlets. + if passed_tris > stuck_tris / 3 { + simplification_queue.extend(stuck.drain(..).flat_map(|group| group.meshlets)); + } + + bvh.add_lod(first_group, &all_groups); } + // If there's any stuck meshlets left, add another LOD level with only them + if !stuck.is_empty() { + let first_group = all_groups.len() as u32; + all_groups.extend(stuck); + bvh.add_lod(first_group, &all_groups); + } + + let (bvh, aabb, depth) = bvh.build(&mut meshlets, all_groups, &mut cull_data); + // Copy vertex attributes per meshlet and compress let mut vertex_positions = BitVec::::new(); let mut vertex_normals = Vec::new(); @@ -227,9 +248,17 @@ impl MeshletMesh { vertex_normals: vertex_normals.into(), vertex_uvs: vertex_uvs.into(), indices: meshlets.triangles.into(), + bvh: bvh.into(), meshlets: bevy_meshlets.into(), - meshlet_bounding_spheres: bounding_spheres.into(), - meshlet_simplification_errors: simplification_errors.into(), + meshlet_cull_data: cull_data + .into_iter() + .map(|cull_data| MeshletCullData { + aabb: aabb_to_meshlet(cull_data.aabb, cull_data.error, 0), + lod_group_sphere: sphere_to_meshlet(cull_data.lod_group_sphere), + }) + .collect(), + aabb, + bvh_depth: depth, }) } } @@ -244,7 +273,11 @@ fn validate_input_mesh(mesh: &Mesh) -> Result, MeshToMeshletMeshC Mesh::ATTRIBUTE_NORMAL.id, Mesh::ATTRIBUTE_UV_0.id, ]) { - return Err(MeshToMeshletMeshConversionError::WrongMeshVertexAttributes); + return Err(MeshToMeshletMeshConversionError::WrongMeshVertexAttributes( + mesh.attributes() + .map(|(attribute, _)| format!("{attribute:?}")) + .collect(), + )); } match mesh.indices() { @@ -254,12 +287,19 @@ fn validate_input_mesh(mesh: &Mesh) -> Result, MeshToMeshletMeshC } } +fn triangles_in_meshlets(meshlets: &Meshlets, ids: impl IntoIterator) -> u32 { + ids.into_iter() + .map(|id| meshlets.get(id as _).triangles.len() as u32 / 3) + .sum() +} + fn compute_meshlets( indices: &[u32], vertices: &VertexDataAdapter, position_only_vertex_remap: &[u32], position_only_vertex_count: usize, -) -> Meshlets { + prev_lod_data: Option<(BoundingSphere, f32)>, +) -> (Meshlets, Vec) { // For each vertex, build a list of all triangles that use it let mut vertices_to_triangles = vec![Vec::new(); position_only_vertex_count]; for (i, index) in indices.iter().enumerate() { @@ -293,6 +333,7 @@ fn compute_meshlets( } // The order of triangles depends on hash traversal order; to produce deterministic results, sort them + // TODO: Wouldn't it be faster to use a `BTreeMap` above instead of `HashMap` + sorting? for list in connected_triangles_per_triangle.iter_mut() { list.sort_unstable(); } @@ -336,40 +377,52 @@ fn compute_meshlets( vertices: Vec::new(), triangles: Vec::new(), }; + let mut cull_data = Vec::new(); + let get_vertex = |&v: &u32| { + *bytemuck::from_bytes::( + &vertices.reader.get_ref() + [vertices.position_offset + v as usize * vertices.vertex_stride..][..12], + ) + }; for meshlet_indices in &indices_per_meshlet { let meshlet = build_meshlets(meshlet_indices, vertices, 255, 128, 0.0); - let vertex_offset = meshlets.vertices.len() as u32; - let triangle_offset = meshlets.triangles.len() as u32; - meshlets.vertices.extend_from_slice(&meshlet.vertices); - meshlets.triangles.extend_from_slice(&meshlet.triangles); - meshlets - .meshlets - .extend(meshlet.meshlets.into_iter().map(|mut meshlet| { - meshlet.vertex_offset += vertex_offset; - meshlet.triangle_offset += triangle_offset; - meshlet - })); + for meshlet in meshlet.iter() { + let (lod_group_sphere, error) = prev_lod_data.unwrap_or_else(|| { + let bounds = meshopt::compute_meshlet_bounds(meshlet, vertices); + (BoundingSphere::new(bounds.center, bounds.radius), 0.0) + }); + + cull_data.push(TempMeshletCullData { + aabb: Aabb3d::from_point_cloud( + Isometry3d::IDENTITY, + meshlet.vertices.iter().map(get_vertex), + ), + lod_group_sphere, + error, + }); + } + merge_meshlets(&mut meshlets, meshlet); } - meshlets + (meshlets, cull_data) } fn find_connected_meshlets( - simplification_queue: Range, + simplification_queue: &[u32], meshlets: &Meshlets, position_only_vertex_remap: &[u32], position_only_vertex_count: usize, ) -> Vec> { // For each vertex, build a list of all meshlets that use it let mut vertices_to_meshlets = vec![Vec::new(); position_only_vertex_count]; - for meshlet_id in simplification_queue.clone() { - let meshlet = meshlets.get(meshlet_id); + for (id_index, &meshlet_id) in simplification_queue.iter().enumerate() { + let meshlet = meshlets.get(meshlet_id as _); for index in meshlet.triangles { let vertex_id = position_only_vertex_remap[meshlet.vertices[*index as usize] as usize]; let vertex_to_meshlets = &mut vertices_to_meshlets[vertex_id as usize]; // Meshlets are added in order, so we can just check the last element to deduplicate, // in the case of two triangles sharing the same vertex within a single meshlet - if vertex_to_meshlets.last() != Some(&meshlet_id) { - vertex_to_meshlets.push(meshlet_id); + if vertex_to_meshlets.last() != Some(&id_index) { + vertex_to_meshlets.push(id_index); } } } @@ -389,13 +442,12 @@ fn find_connected_meshlets( let mut connected_meshlets_per_meshlet = vec![Vec::new(); simplification_queue.len()]; for ((meshlet_id1, meshlet_id2), shared_vertex_count) in meshlet_pair_to_shared_vertex_count { // We record both id1->id2 and id2->id1 as adjacency is symmetrical - connected_meshlets_per_meshlet[meshlet_id1 - simplification_queue.start] - .push((meshlet_id2, shared_vertex_count)); - connected_meshlets_per_meshlet[meshlet_id2 - simplification_queue.start] - .push((meshlet_id1, shared_vertex_count)); + connected_meshlets_per_meshlet[meshlet_id1].push((meshlet_id2, shared_vertex_count)); + connected_meshlets_per_meshlet[meshlet_id2].push((meshlet_id1, shared_vertex_count)); } // The order of meshlets depends on hash traversal order; to produce deterministic results, sort them + // TODO: Wouldn't it be faster to use a `BTreeMap` above instead of `HashMap` + sorting? for list in connected_meshlets_per_meshlet.iter_mut() { list.sort_unstable(); } @@ -405,16 +457,17 @@ fn find_connected_meshlets( // METIS manual: https://github.com/KarypisLab/METIS/blob/e0f1b88b8efcb24ffa0ec55eabb78fbe61e58ae7/manual/manual.pdf fn group_meshlets( + simplification_queue: &[u32], + meshlet_cull_data: &[TempMeshletCullData], connected_meshlets_per_meshlet: &[Vec<(usize, usize)>], - simplification_queue: Range, -) -> Vec> { +) -> Vec { let mut xadj = Vec::with_capacity(simplification_queue.len() + 1); let mut adjncy = Vec::new(); let mut adjwgt = Vec::new(); for connected_meshlets in connected_meshlets_per_meshlet { xadj.push(adjncy.len() as i32); for (connected_meshlet_id, shared_vertex_count) in connected_meshlets { - adjncy.push((connected_meshlet_id - simplification_queue.start) as i32); + adjncy.push(*connected_meshlet_id as i32); adjwgt.push(*shared_vertex_count as i32); // TODO: Additional weight based on meshlet spatial proximity } @@ -436,16 +489,22 @@ fn group_meshlets( .part_recursive(&mut group_per_meshlet) .unwrap(); - let mut groups = vec![SmallVec::new(); partition_count]; + let mut groups = vec![TempMeshletGroup::default(); partition_count]; for (i, meshlet_group) in group_per_meshlet.into_iter().enumerate() { - groups[meshlet_group as usize].push(i + simplification_queue.start); + let group = &mut groups[meshlet_group as usize]; + let meshlet_id = simplification_queue[i]; + + group.meshlets.push(meshlet_id); + let data = &meshlet_cull_data[meshlet_id as usize]; + group.aabb = group.aabb.merge(&data.aabb); + group.lod_bounds = merge_spheres(group.lod_bounds, data.lod_group_sphere); } groups } fn lock_group_borders( vertex_locks: &mut [bool], - groups: &[SmallVec<[usize; TARGET_MESHLETS_PER_GROUP]>], + groups: &[TempMeshletGroup], meshlets: &Meshlets, position_only_vertex_remap: &[u32], position_only_vertex_count: usize, @@ -453,9 +512,9 @@ fn lock_group_borders( let mut position_only_locks = vec![-1; position_only_vertex_count]; // Iterate over position-only based vertices of all meshlets in all groups - for (group_id, group_meshlets) in groups.iter().enumerate() { - for meshlet_id in group_meshlets { - let meshlet = meshlets.get(*meshlet_id); + for (group_id, group) in groups.iter().enumerate() { + for &meshlet_id in group.meshlets.iter() { + let meshlet = meshlets.get(meshlet_id as usize); for index in meshlet.triangles { let vertex_id = position_only_vertex_remap[meshlet.vertices[*index as usize] as usize] as usize; @@ -480,21 +539,25 @@ fn lock_group_borders( } fn simplify_meshlet_group( - group_meshlets: &[usize], + group: &TempMeshletGroup, meshlets: &Meshlets, vertices: &VertexDataAdapter<'_>, vertex_normals: &[f32], vertex_stride: usize, vertex_locks: &[bool], -) -> Option<(Vec, f16)> { +) -> Option<(Vec, f32)> { // Build a new index buffer into the mesh vertex data by combining all meshlet data in the group - let mut group_indices = Vec::new(); - for meshlet_id in group_meshlets { - let meshlet = meshlets.get(*meshlet_id); - for meshlet_index in meshlet.triangles { - group_indices.push(meshlet.vertices[*meshlet_index as usize]); - } - } + let group_indices = group + .meshlets + .iter() + .flat_map(|&meshlet_id| { + let meshlet = meshlets.get(meshlet_id as _); + meshlet + .triangles + .iter() + .map(|&meshlet_index| meshlet.vertices[meshlet_index as usize]) + }) + .collect::>(); // Simplify the group to ~50% triangle count let mut error = 0.0; @@ -511,96 +574,28 @@ fn simplify_meshlet_group( Some(&mut error), ); - // Check if we were able to simplify at least a little + // Check if we were able to simplify if simplified_group_indices.len() as f32 / group_indices.len() as f32 > SIMPLIFICATION_FAILURE_PERCENTAGE { return None; } - Some((simplified_group_indices, f16::from_f32(error))) + Some((simplified_group_indices, error)) } -fn compute_lod_group_data( - group_meshlets: &[usize], - group_error: &mut f16, - bounding_spheres: &mut [MeshletBoundingSpheres], - simplification_errors: &mut [MeshletSimplificationError], -) -> MeshletBoundingSphere { - let mut group_bounding_sphere = MeshletBoundingSphere { - center: Vec3::ZERO, - radius: 0.0, - }; - - // Compute the lod group sphere center as a weighted average of the children spheres - let mut weight = 0.0; - for meshlet_id in group_meshlets { - let meshlet_lod_bounding_sphere = bounding_spheres[*meshlet_id].lod_group_sphere; - group_bounding_sphere.center += - meshlet_lod_bounding_sphere.center * meshlet_lod_bounding_sphere.radius; - weight += meshlet_lod_bounding_sphere.radius; - } - group_bounding_sphere.center /= weight; - - // Force parent group sphere to contain all child group spheres (we're currently building the parent from its children) - // TODO: This does not produce the absolute minimal bounding sphere. Doing so is non-trivial. - // "Smallest enclosing balls of balls" http://www.inf.ethz.ch/personal/emo/DoctThesisFiles/fischer05.pdf - for meshlet_id in group_meshlets { - let meshlet_lod_bounding_sphere = bounding_spheres[*meshlet_id].lod_group_sphere; - let d = meshlet_lod_bounding_sphere - .center - .distance(group_bounding_sphere.center); - group_bounding_sphere.radius = group_bounding_sphere - .radius - .max(meshlet_lod_bounding_sphere.radius + d); - } - - // Force parent error to be >= child error (we're currently building the parent from its children) - for meshlet_id in group_meshlets { - *group_error = group_error.max(simplification_errors[*meshlet_id].group_error); - } - - // Set the children's lod parent group data to the new lod group we just made - for meshlet_id in group_meshlets { - bounding_spheres[*meshlet_id].lod_parent_group_sphere = group_bounding_sphere; - simplification_errors[*meshlet_id].parent_group_error = *group_error; - } - - group_bounding_sphere -} - -fn split_simplified_group_into_new_meshlets( - simplified_group_indices: &[u32], - vertices: &VertexDataAdapter<'_>, - position_only_vertex_remap: &[u32], - position_only_vertex_count: usize, - meshlets: &mut Meshlets, -) -> usize { - let simplified_meshlets = compute_meshlets( - simplified_group_indices, - vertices, - position_only_vertex_remap, - position_only_vertex_count, - ); - let new_meshlets_count = simplified_meshlets.len(); - +fn merge_meshlets(meshlets: &mut Meshlets, merge: Meshlets) { let vertex_offset = meshlets.vertices.len() as u32; let triangle_offset = meshlets.triangles.len() as u32; - meshlets - .vertices - .extend_from_slice(&simplified_meshlets.vertices); - meshlets - .triangles - .extend_from_slice(&simplified_meshlets.triangles); + meshlets.vertices.extend_from_slice(&merge.vertices); + meshlets.triangles.extend_from_slice(&merge.triangles); meshlets .meshlets - .extend(simplified_meshlets.meshlets.into_iter().map(|mut meshlet| { + .extend(merge.meshlets.into_iter().map(|mut meshlet| { meshlet.vertex_offset += vertex_offset; meshlet.triangle_offset += triangle_offset; meshlet })); - - new_meshlets_count } fn build_and_compress_per_meshlet_vertex_data( @@ -688,14 +683,397 @@ fn build_and_compress_per_meshlet_vertex_data( }); } -fn compute_meshlet_bounds( - meshlet: meshopt::Meshlet<'_>, - vertices: &VertexDataAdapter<'_>, -) -> MeshletBoundingSphere { - let bounds = meshopt::compute_meshlet_bounds(meshlet, vertices); +fn merge_spheres(a: BoundingSphere, b: BoundingSphere) -> BoundingSphere { + let sr = a.radius().min(b.radius()); + let br = a.radius().max(b.radius()); + let len = a.center.distance(b.center); + if len + sr <= br || sr == 0.0 || len == 0.0 { + if a.radius() > b.radius() { + a + } else { + b + } + } else { + let radius = (sr + br + len) / 2.0; + let center = + (a.center + b.center + (a.radius() - b.radius()) * (a.center - b.center) / len) / 2.0; + BoundingSphere::new(center, radius) + } +} + +#[derive(Copy, Clone)] +struct TempMeshletCullData { + aabb: Aabb3d, + lod_group_sphere: BoundingSphere, + error: f32, +} + +#[derive(Clone)] +struct TempMeshletGroup { + aabb: Aabb3d, + lod_bounds: BoundingSphere, + parent_error: f32, + meshlets: SmallVec<[u32; TARGET_MESHLETS_PER_GROUP]>, +} + +impl Default for TempMeshletGroup { + fn default() -> Self { + Self { + aabb: aabb_default(), // Default AABB to merge into + lod_bounds: BoundingSphere::new(Vec3A::ZERO, 0.0), + parent_error: f32::MAX, + meshlets: SmallVec::new(), + } + } +} + +// All the BVH build code was stolen from https://github.com/SparkyPotato/radiance/blob/4aa17a3a5be7a0466dc69713e249bbcee9f46057/crates/rad-renderer/src/assets/mesh/virtual_mesh.rs because it works and I'm lazy and don't want to reimplement it +struct TempBvhNode { + group: u32, + aabb: Aabb3d, + children: SmallVec<[u32; 8]>, +} + +#[derive(Default)] +struct BvhBuilder { + nodes: Vec, + lods: Vec>, +} + +impl BvhBuilder { + fn add_lod(&mut self, offset: u32, all_groups: &[TempMeshletGroup]) { + let first = self.nodes.len() as u32; + self.nodes.extend( + all_groups + .iter() + .enumerate() + .skip(offset as _) + .map(|(i, group)| TempBvhNode { + group: i as u32, + aabb: group.aabb, + children: SmallVec::new(), + }), + ); + let end = self.nodes.len() as u32; + if first != end { + self.lods.push(first..end); + } + } + + fn surface_area(&self, nodes: &[u32]) -> f32 { + nodes + .iter() + .map(|&x| self.nodes[x as usize].aabb) + .reduce(|a, b| a.merge(&b)) + .expect("cannot find surface area of zero nodes") + .visible_area() + } + + fn sort_nodes_by_sah(&self, nodes: &mut [u32], splits: [usize; 8]) { + // We use a BVH8, so just recursively binary split 3 times for near-optimal SAH + for i in 0..3 { + let parts = 1 << i; // 2^i + let nodes_per_split = 8 >> i; // 8 / 2^i + let half_count = nodes_per_split / 2; + let mut offset = 0; + for p in 0..parts { + let first = p * nodes_per_split; + let mut s0 = 0; + let mut s1 = 0; + for i in 0..half_count { + s0 += splits[first + i]; + s1 += splits[first + half_count + i]; + } + let c = s0 + s1; + let nodes = &mut nodes[offset..(offset + c)]; + offset += c; + + let mut cost = f32::MAX; + let mut axis = 0; + let key = |x, ax| self.nodes[x as usize].aabb.center()[ax]; + for ax in 0..3 { + nodes.sort_unstable_by(|&x, &y| key(x, ax).partial_cmp(&key(y, ax)).unwrap()); + let (left, right) = nodes.split_at(s0); + let c = self.surface_area(left) + self.surface_area(right); + if c < cost { + axis = ax; + cost = c; + } + } + if axis != 2 { + nodes.sort_unstable_by(|&x, &y| { + key(x, axis).partial_cmp(&key(y, axis)).unwrap() + }); + } + } + } + } + + fn build_temp_inner(&mut self, nodes: &mut [u32], optimize: bool) -> u32 { + let count = nodes.len(); + if count == 1 { + nodes[0] + } else if count <= 8 { + let i = self.nodes.len(); + self.nodes.push(TempBvhNode { + group: u32::MAX, + aabb: aabb_default(), + children: nodes.iter().copied().collect(), + }); + i as _ + } else { + // We need to split the nodes into 8 groups, with the smallest possible tree depth. + // Additionally, no child should be more than one level deeper than the others. + // At `l` levels, we can fit upto 8^l nodes. + // The `max_child_size` is the largest power of 8 <= `count` (any larger and we'd have + // unfilled nodes). + // The `min_child_size` is thus 1 level (8 times) smaller. + // After distributing `min_child_size` to all children, we have distributed + // `min_child_size * 8` nodes (== `max_child_size`). + // The remaining nodes are then distributed left to right. + let max_child_size = 1 << ((count.ilog2() / 3) * 3); + let min_child_size = max_child_size >> 3; + let max_extra_per_node = max_child_size - min_child_size; + let mut extra = count - max_child_size; // 8 * min_child_size + let splits = core::array::from_fn(|_| { + let size = extra.min(max_extra_per_node); + extra -= size; + min_child_size + size + }); + + if optimize { + self.sort_nodes_by_sah(nodes, splits); + } + + let mut offset = 0; + let children = splits + .into_iter() + .map(|size| { + let i = self.build_temp_inner(&mut nodes[offset..(offset + size)], optimize); + offset += size; + i + }) + .collect(); + + let i = self.nodes.len(); + self.nodes.push(TempBvhNode { + group: u32::MAX, + aabb: aabb_default(), + children, + }); + i as _ + } + } + + fn build_temp(&mut self) -> u32 { + let mut lods = Vec::with_capacity(self.lods.len()); + for lod in core::mem::take(&mut self.lods) { + let mut lod: Vec<_> = lod.collect(); + let root = self.build_temp_inner(&mut lod, true); + let node = &self.nodes[root as usize]; + if node.group != u32::MAX || node.children.len() == 8 { + lods.push(root); + } else { + lods.extend(node.children.iter().copied()); + } + } + self.build_temp_inner(&mut lods, false) + } + + fn build_inner( + &self, + groups: &[TempMeshletGroup], + out: &mut Vec, + max_depth: &mut u32, + node: u32, + depth: u32, + ) -> u32 { + *max_depth = depth.max(*max_depth); + let node = &self.nodes[node as usize]; + let onode = out.len(); + out.push(BvhNode::default()); + + for (i, &child_id) in node.children.iter().enumerate() { + let child = &self.nodes[child_id as usize]; + if child.group != u32::MAX { + let group = &groups[child.group as usize]; + let out = &mut out[onode]; + out.aabbs[i] = aabb_to_meshlet(group.aabb, group.parent_error, group.meshlets[0]); + out.lod_bounds[i] = sphere_to_meshlet(group.lod_bounds); + out.child_counts[i] = group.meshlets[1] as _; + } else { + let child_id = self.build_inner(groups, out, max_depth, child_id, depth + 1); + let child = &out[child_id as usize]; + let mut aabb = aabb_default(); + let mut parent_error = 0.0f32; + let mut lod_bounds = BoundingSphere::new(Vec3A::ZERO, 0.0); + for i in 0..8 { + if child.child_counts[i] == 0 { + break; + } + + aabb = aabb.merge(&Aabb3d::new( + child.aabbs[i].center, + child.aabbs[i].half_extent, + )); + lod_bounds = merge_spheres( + lod_bounds, + BoundingSphere::new(child.lod_bounds[i].center, child.lod_bounds[i].radius), + ); + parent_error = parent_error.max(child.aabbs[i].error); + } + + let out = &mut out[onode]; + out.aabbs[i] = aabb_to_meshlet(aabb, parent_error, child_id); + out.lod_bounds[i] = sphere_to_meshlet(lod_bounds); + out.child_counts[i] = u8::MAX; + } + } + + onode as _ + } + + fn build( + mut self, + meshlets: &mut Meshlets, + mut groups: Vec, + cull_data: &mut Vec, + ) -> (Vec, MeshletAabb, u32) { + // The BVH requires group meshlets to be contiguous, so remap them first. + let mut remap = Vec::with_capacity(meshlets.meshlets.len()); + let mut remapped_cull_data = Vec::with_capacity(cull_data.len()); + for group in groups.iter_mut() { + let first = remap.len() as u32; + let count = group.meshlets.len() as u32; + remap.extend( + group + .meshlets + .iter() + .map(|&m| meshlets.meshlets[m as usize]), + ); + remapped_cull_data.extend(group.meshlets.iter().map(|&m| cull_data[m as usize])); + group.meshlets.resize(2, 0); + group.meshlets[0] = first; + group.meshlets[1] = count; + } + meshlets.meshlets = remap; + *cull_data = remapped_cull_data; + + let mut out = vec![]; + let mut aabb = aabb_default(); + let mut max_depth = 0; + + if self.nodes.len() == 1 { + let mut o = BvhNode::default(); + let group = &groups[0]; + o.aabbs[0] = aabb_to_meshlet(group.aabb, group.parent_error, group.meshlets[0]); + o.lod_bounds[0] = sphere_to_meshlet(group.lod_bounds); + o.child_counts[0] = group.meshlets[1] as _; + out.push(o); + aabb = group.aabb; + max_depth = 1; + } else { + let root = self.build_temp(); + let root = self.build_inner(&groups, &mut out, &mut max_depth, root, 1); + assert_eq!(root, 0, "root must be 0"); + + let root = &out[0]; + for i in 0..8 { + if root.child_counts[i] == 0 { + break; + } + + aabb = aabb.merge(&Aabb3d::new( + root.aabbs[i].center, + root.aabbs[i].half_extent, + )); + } + } + + let mut reachable = vec![false; meshlets.meshlets.len()]; + verify_bvh(&out, cull_data, &mut reachable, 0); + assert!( + reachable.iter().all(|&x| x), + "all meshlets must be reachable" + ); + + ( + out, + MeshletAabb { + center: aabb.center().into(), + half_extent: aabb.half_size().into(), + }, + max_depth, + ) + } +} + +fn verify_bvh( + out: &[BvhNode], + cull_data: &[TempMeshletCullData], + reachable: &mut [bool], + node: u32, +) { + let node = &out[node as usize]; + for i in 0..8 { + let sphere = node.lod_bounds[i]; + let error = node.aabbs[i].error; + if node.child_counts[i] == u8::MAX { + let child = &out[node.aabbs[i].child_offset as usize]; + for i in 0..8 { + if child.child_counts[i] == 0 { + break; + } + assert!( + child.aabbs[i].error <= error, + "BVH errors are not monotonic" + ); + let sphere_error = (sphere.center - child.lod_bounds[i].center).length() + - (sphere.radius - child.lod_bounds[i].radius); + assert!( + sphere_error <= 0.0001, + "BVH lod spheres are not monotonic ({sphere_error})" + ); + } + verify_bvh(out, cull_data, reachable, node.aabbs[i].child_offset); + } else { + for m in 0..node.child_counts[i] as u32 { + let mid = (m + node.aabbs[i].child_offset) as usize; + let meshlet = &cull_data[mid]; + assert!(meshlet.error <= error, "meshlet errors are not monotonic"); + let sphere_error = (Vec3A::from(sphere.center) - meshlet.lod_group_sphere.center) + .length() + - (sphere.radius - meshlet.lod_group_sphere.radius()); + assert!( + sphere_error <= 0.0001, + "meshlet lod spheres are not monotonic: ({sphere_error})" + ); + reachable[mid] = true; + } + } + } +} + +fn aabb_default() -> Aabb3d { + Aabb3d { + min: Vec3A::INFINITY, + max: Vec3A::NEG_INFINITY, + } +} + +fn aabb_to_meshlet(aabb: Aabb3d, error: f32, child_offset: u32) -> MeshletAabbErrorOffset { + MeshletAabbErrorOffset { + center: aabb.center().into(), + error, + half_extent: aabb.half_size().into(), + child_offset, + } +} + +fn sphere_to_meshlet(sphere: BoundingSphere) -> MeshletBoundingSphere { MeshletBoundingSphere { - center: bounds.center.into(), - radius: bounds.radius, + center: sphere.center.into(), + radius: sphere.radius(), } } @@ -726,8 +1104,8 @@ fn pack2x16snorm(v: Vec2) -> u32 { pub enum MeshToMeshletMeshConversionError { #[error("Mesh primitive topology is not TriangleList")] WrongMeshPrimitiveTopology, - #[error("Mesh vertex attributes are not {{POSITION, NORMAL, UV_0}}")] - WrongMeshVertexAttributes, + #[error("Mesh vertex attributes are not {{POSITION, NORMAL, UV_0}}: {0:?}")] + WrongMeshVertexAttributes(Vec), #[error("Mesh has no indices")] MeshMissingIndices, } diff --git a/crates/bevy_pbr/src/meshlet/instance_manager.rs b/crates/bevy_pbr/src/meshlet/instance_manager.rs index 661d4791ae..94d03a925a 100644 --- a/crates/bevy_pbr/src/meshlet/instance_manager.rs +++ b/crates/bevy_pbr/src/meshlet/instance_manager.rs @@ -1,8 +1,9 @@ use super::{meshlet_mesh_manager::MeshletMeshManager, MeshletMesh, MeshletMesh3d}; +use crate::DUMMY_MESH_MATERIAL; use crate::{ - material::DUMMY_MESH_MATERIAL, Material, MaterialBindingId, MeshFlags, MeshTransforms, - MeshUniform, NotShadowCaster, NotShadowReceiver, PreviousGlobalTransform, - RenderMaterialBindings, RenderMaterialInstances, + meshlet::asset::MeshletAabb, MaterialBindingId, MeshFlags, MeshTransforms, MeshUniform, + NotShadowCaster, NotShadowReceiver, PreviousGlobalTransform, RenderMaterialBindings, + RenderMaterialInstances, }; use bevy_asset::{AssetEvent, AssetServer, Assets, UntypedAssetId}; use bevy_ecs::{ @@ -17,32 +18,33 @@ use bevy_render::{ render_resource::StorageBuffer, sync_world::MainEntity, view::RenderLayers, MainWorld, }; use bevy_transform::components::GlobalTransform; -use core::ops::{DerefMut, Range}; +use core::ops::DerefMut; /// Manages data for each entity with a [`MeshletMesh`]. #[derive(Resource)] pub struct InstanceManager { /// Amount of instances in the scene. pub scene_instance_count: u32, - /// Amount of clusters in the scene. - pub scene_cluster_count: u32, + /// The max BVH depth of any instance in the scene. This is used to control the number of + /// dependent dispatches emitted for BVH traversal. + pub max_bvh_depth: u32, /// Per-instance [`MainEntity`], [`RenderLayers`], and [`NotShadowCaster`]. pub instances: Vec<(MainEntity, RenderLayers, bool)>, /// Per-instance [`MeshUniform`]. pub instance_uniforms: StorageBuffer>, + /// Per-instance model-space AABB. + pub instance_aabbs: StorageBuffer>, /// Per-instance material ID. pub instance_material_ids: StorageBuffer>, - /// Per-instance count of meshlets in the instance's [`MeshletMesh`]. - pub instance_meshlet_counts: StorageBuffer>, - /// Per-instance index to the start of the instance's slice of the meshlets buffer. - pub instance_meshlet_slice_starts: StorageBuffer>, + /// Per-instance index to the root node of the instance's BVH. + pub instance_bvh_root_nodes: StorageBuffer>, /// Per-view per-instance visibility bit. Used for [`RenderLayers`] and [`NotShadowCaster`] support. pub view_instance_visibility: EntityHashMap>>, - /// Next material ID available for a [`Material`]. + /// Next material ID available. next_material_id: u32, - /// Map of [`Material`] to material ID. + /// Map of material asset to material ID. material_id_lookup: HashMap, /// Set of material IDs used in the scene. material_ids_present_in_scene: HashSet, @@ -52,7 +54,7 @@ impl InstanceManager { pub fn new() -> Self { Self { scene_instance_count: 0, - scene_cluster_count: 0, + max_bvh_depth: 0, instances: Vec::new(), instance_uniforms: { @@ -60,19 +62,19 @@ impl InstanceManager { buffer.set_label(Some("meshlet_instance_uniforms")); buffer }, + instance_aabbs: { + let mut buffer = StorageBuffer::default(); + buffer.set_label(Some("meshlet_instance_aabbs")); + buffer + }, instance_material_ids: { let mut buffer = StorageBuffer::default(); buffer.set_label(Some("meshlet_instance_material_ids")); buffer }, - instance_meshlet_counts: { + instance_bvh_root_nodes: { let mut buffer = StorageBuffer::default(); - buffer.set_label(Some("meshlet_instance_meshlet_counts")); - buffer - }, - instance_meshlet_slice_starts: { - let mut buffer = StorageBuffer::default(); - buffer.set_label(Some("meshlet_instance_meshlet_slice_starts")); + buffer.set_label(Some("meshlet_instance_bvh_root_nodes")); buffer }, view_instance_visibility: EntityHashMap::default(), @@ -86,7 +88,9 @@ impl InstanceManager { pub fn add_instance( &mut self, instance: MainEntity, - meshlets_slice: Range, + root_bvh_node: u32, + aabb: MeshletAabb, + bvh_depth: u32, transform: &GlobalTransform, previous_transform: Option<&PreviousGlobalTransform>, render_layers: Option<&RenderLayers>, @@ -139,16 +143,12 @@ impl InstanceManager { not_shadow_caster, )); self.instance_uniforms.get_mut().push(mesh_uniform); + self.instance_aabbs.get_mut().push(aabb); self.instance_material_ids.get_mut().push(0); - self.instance_meshlet_counts - .get_mut() - .push(meshlets_slice.len() as u32); - self.instance_meshlet_slice_starts - .get_mut() - .push(meshlets_slice.start); + self.instance_bvh_root_nodes.get_mut().push(root_bvh_node); self.scene_instance_count += 1; - self.scene_cluster_count += meshlets_slice.len() as u32; + self.max_bvh_depth = self.max_bvh_depth.max(bvh_depth); } /// Get the material ID for a [`crate::Material`]. @@ -168,13 +168,13 @@ impl InstanceManager { pub fn reset(&mut self, entities: &Entities) { self.scene_instance_count = 0; - self.scene_cluster_count = 0; + self.max_bvh_depth = 0; self.instances.clear(); self.instance_uniforms.get_mut().clear(); + self.instance_aabbs.get_mut().clear(); self.instance_material_ids.get_mut().clear(); - self.instance_meshlet_counts.get_mut().clear(); - self.instance_meshlet_slice_starts.get_mut().clear(); + self.instance_bvh_root_nodes.get_mut().clear(); self.view_instance_visibility .retain(|view_entity, _| entities.contains(*view_entity)); self.view_instance_visibility @@ -233,6 +233,7 @@ pub fn extract_meshlet_mesh_entities( } // Iterate over every instance + // TODO: Switch to change events to not upload every instance every frame. for ( instance, meshlet_mesh, @@ -252,13 +253,15 @@ pub fn extract_meshlet_mesh_entities( } // Upload the instance's MeshletMesh asset data if not done already done - let meshlets_slice = + let (root_bvh_node, aabb, bvh_depth) = meshlet_mesh_manager.queue_upload_if_needed(meshlet_mesh.id(), &mut assets); // Add the instance's data to the instance manager instance_manager.add_instance( instance.into(), - meshlets_slice, + root_bvh_node, + aabb, + bvh_depth, transform, previous_transform, render_layers, @@ -272,7 +275,7 @@ pub fn extract_meshlet_mesh_entities( /// For each entity in the scene, record what material ID its material was assigned in the `prepare_material_meshlet_meshes` systems, /// and note that the material is used by at least one entity in the scene. -pub fn queue_material_meshlet_meshes( +pub fn queue_material_meshlet_meshes( mut instance_manager: ResMut, render_material_instances: Res, ) { @@ -280,16 +283,14 @@ pub fn queue_material_meshlet_meshes( for (i, (instance, _, _)) in instance_manager.instances.iter().enumerate() { if let Some(material_instance) = render_material_instances.instances.get(instance) { - if let Ok(material_asset_id) = material_instance.asset_id.try_typed::() { - if let Some(material_id) = instance_manager - .material_id_lookup - .get(&material_asset_id.untyped()) - { - instance_manager - .material_ids_present_in_scene - .insert(*material_id); - instance_manager.instance_material_ids.get_mut()[i] = *material_id; - } + if let Some(material_id) = instance_manager + .material_id_lookup + .get(&material_instance.asset_id) + { + instance_manager + .material_ids_present_in_scene + .insert(*material_id); + instance_manager.instance_material_ids.get_mut()[i] = *material_id; } } } diff --git a/crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs b/crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs index 90d35d0514..95c8859520 100644 --- a/crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs +++ b/crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs @@ -1,12 +1,8 @@ use super::{ - instance_manager::InstanceManager, resource_manager::ResourceManager, - MESHLET_MESH_MATERIAL_SHADER_HANDLE, + instance_manager::InstanceManager, pipelines::MeshletPipelines, + resource_manager::ResourceManager, }; -use crate::{ - environment_map::EnvironmentMapLight, irradiance_volume::IrradianceVolume, - material_bind_groups::MaterialBindGroupAllocator, *, -}; -use bevy_asset::AssetServer; +use crate::{environment_map::EnvironmentMapLight, irradiance_volume::IrradianceVolume, *}; use bevy_core_pipeline::{ core_3d::Camera3d, prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass}, @@ -14,14 +10,15 @@ use bevy_core_pipeline::{ }; use bevy_derive::{Deref, DerefMut}; use bevy_platform::collections::{HashMap, HashSet}; +use bevy_render::erased_render_asset::ErasedRenderAssets; use bevy_render::{ camera::TemporalJitter, mesh::{Mesh, MeshVertexBufferLayout, MeshVertexBufferLayoutRef, MeshVertexBufferLayouts}, - render_asset::RenderAssets, render_resource::*, view::ExtractedView, }; -use core::hash::Hash; +use bevy_utils::default; +use core::any::{Any, TypeId}; /// A list of `(Material ID, Pipeline, BindGroup)` for a view for use in [`super::MeshletMainOpaquePass3dNode`]. #[derive(Component, Deref, DerefMut, Default)] @@ -29,17 +26,17 @@ pub struct MeshletViewMaterialsMainOpaquePass(pub Vec<(u32, CachedRenderPipeline /// Prepare [`Material`] pipelines for [`super::MeshletMesh`] entities for use in [`super::MeshletMainOpaquePass3dNode`], /// and register the material with [`InstanceManager`]. -pub fn prepare_material_meshlet_meshes_main_opaque_pass( +pub fn prepare_material_meshlet_meshes_main_opaque_pass( resource_manager: ResMut, mut instance_manager: ResMut, - mut cache: Local>, + mut cache: Local>, pipeline_cache: Res, - material_pipeline: Res>, + material_pipeline: Res, mesh_pipeline: Res, - render_materials: Res>>, + render_materials: Res>, + meshlet_pipelines: Res, render_material_instances: Res, - material_bind_group_allocator: Res>, - asset_server: Res, + material_bind_group_allocators: Res, mut mesh_vertex_buffer_layouts: ResMut, mut views: Query< ( @@ -62,9 +59,7 @@ pub fn prepare_material_meshlet_meshes_main_opaque_pass( ), With, >, -) where - M::Data: PartialEq + Eq + Hash + Clone, -{ +) { let fake_vertex_buffer_layout = &fake_vertex_buffer_layout(&mut mesh_vertex_buffer_layouts); for ( @@ -151,17 +146,12 @@ pub fn prepare_material_meshlet_meshes_main_opaque_pass( for material_id in render_material_instances .instances .values() - .flat_map(|instance| instance.asset_id.try_typed::().ok()) + .map(|instance| instance.asset_id) .collect::>() { let Some(material) = render_materials.get(material_id) else { continue; }; - let Some(material_bind_group) = - material_bind_group_allocator.get(material.binding.group) - else { - continue; - }; if material.properties.render_method != OpaqueRendererMethod::Forward || material.properties.alpha_mode != AlphaMode::Opaque @@ -170,15 +160,18 @@ pub fn prepare_material_meshlet_meshes_main_opaque_pass( continue; } - let Ok(material_pipeline_descriptor) = material_pipeline.specialize( - MaterialPipelineKey { - mesh_key: view_key, - bind_group_data: material_bind_group - .get_extra_data(material.binding.slot) - .clone(), - }, - fake_vertex_buffer_layout, - ) else { + let erased_key = ErasedMaterialPipelineKey { + mesh_key: view_key, + material_key: material.properties.material_key.clone(), + type_id: material_id.type_id(), + }; + let material_pipeline_specializer = MaterialPipelineSpecializer { + pipeline: material_pipeline.clone(), + properties: material.properties.clone(), + }; + let Ok(material_pipeline_descriptor) = + material_pipeline_specializer.specialize(erased_key, fake_vertex_buffer_layout) + else { continue; }; let material_fragment = material_pipeline_descriptor.fragment.unwrap(); @@ -191,7 +184,12 @@ pub fn prepare_material_meshlet_meshes_main_opaque_pass( layout.main_layout.clone(), layout.binding_array_layout.clone(), resource_manager.material_shade_bind_group_layout.clone(), - material_pipeline.material_layout.clone(), + material + .properties + .material_layout + .as_ref() + .unwrap() + .clone(), ]; let pipeline_descriptor = RenderPipelineDescriptor { @@ -199,7 +197,7 @@ pub fn prepare_material_meshlet_meshes_main_opaque_pass( layout, push_constant_ranges: vec![], vertex: VertexState { - shader: MESHLET_MESH_MATERIAL_SHADER_HANDLE, + shader: meshlet_pipelines.meshlet_mesh_material.clone(), shader_defs: shader_defs.clone(), entry_point: material_pipeline_descriptor.vertex.entry_point, buffers: Vec::new(), @@ -214,10 +212,9 @@ pub fn prepare_material_meshlet_meshes_main_opaque_pass( }), multisample: MultisampleState::default(), fragment: Some(FragmentState { - shader: match M::meshlet_mesh_fragment_shader() { - ShaderRef::Default => MESHLET_MESH_MATERIAL_SHADER_HANDLE, - ShaderRef::Handle(handle) => handle, - ShaderRef::Path(path) => asset_server.load(path), + shader: match material.properties.get_shader(MeshletFragmentShader) { + Some(shader) => shader.clone(), + None => meshlet_pipelines.meshlet_mesh_material.clone(), }, shader_defs, entry_point: material_fragment.entry_point, @@ -225,10 +222,14 @@ pub fn prepare_material_meshlet_meshes_main_opaque_pass( }), zero_initialize_workgroup_memory: false, }; + let type_id = material_id.type_id(); + let Some(material_bind_group_allocator) = material_bind_group_allocators.get(&type_id) + else { + continue; + }; + let material_id = instance_manager.get_material_id(material_id); - let material_id = instance_manager.get_material_id(material_id.untyped()); - - let pipeline_id = *cache.entry(view_key).or_insert_with(|| { + let pipeline_id = *cache.entry((view_key, type_id)).or_insert_with(|| { pipeline_cache.queue_render_pipeline(pipeline_descriptor.clone()) }); @@ -258,17 +259,17 @@ pub struct MeshletViewMaterialsDeferredGBufferPrepass( /// Prepare [`Material`] pipelines for [`super::MeshletMesh`] entities for use in [`super::MeshletPrepassNode`], /// and [`super::MeshletDeferredGBufferPrepassNode`] and register the material with [`InstanceManager`]. -pub fn prepare_material_meshlet_meshes_prepass( +pub fn prepare_material_meshlet_meshes_prepass( resource_manager: ResMut, mut instance_manager: ResMut, - mut cache: Local>, + mut cache: Local>, pipeline_cache: Res, - prepass_pipeline: Res>, - render_materials: Res>>, + prepass_pipeline: Res, + material_bind_group_allocators: Res, + render_materials: Res>, + meshlet_pipelines: Res, render_material_instances: Res, mut mesh_vertex_buffer_layouts: ResMut, - material_bind_group_allocator: Res>, - asset_server: Res, mut views: Query< ( &mut MeshletViewMaterialsPrepass, @@ -278,9 +279,7 @@ pub fn prepare_material_meshlet_meshes_prepass( ), With, >, -) where - M::Data: PartialEq + Eq + Hash + Clone, -{ +) { let fake_vertex_buffer_layout = &fake_vertex_buffer_layout(&mut mesh_vertex_buffer_layouts); for ( @@ -305,14 +304,14 @@ pub fn prepare_material_meshlet_meshes_prepass( for material_id in render_material_instances .instances .values() - .flat_map(|instance| instance.asset_id.try_typed::().ok()) + .map(|instance| instance.asset_id) .collect::>() { let Some(material) = render_materials.get(material_id) else { continue; }; - let Some(material_bind_group) = - material_bind_group_allocator.get(material.binding.group) + let Some(material_bind_group_allocator) = + material_bind_group_allocators.get(&material_id.type_id()) else { continue; }; @@ -333,15 +332,18 @@ pub fn prepare_material_meshlet_meshes_prepass( continue; } - let Ok(material_pipeline_descriptor) = prepass_pipeline.specialize( - MaterialPipelineKey { - mesh_key: view_key, - bind_group_data: material_bind_group - .get_extra_data(material.binding.slot) - .clone(), - }, - fake_vertex_buffer_layout, - ) else { + let erased_key = ErasedMaterialPipelineKey { + mesh_key: view_key, + material_key: material.properties.material_key.clone(), + type_id: material_id.type_id(), + }; + let material_pipeline_specializer = PrepassPipelineSpecializer { + pipeline: prepass_pipeline.clone(), + properties: material.properties.clone(), + }; + let Ok(material_pipeline_descriptor) = + material_pipeline_specializer.specialize(erased_key, fake_vertex_buffer_layout) + else { continue; }; let material_fragment = material_pipeline_descriptor.fragment.unwrap(); @@ -350,38 +352,47 @@ pub fn prepare_material_meshlet_meshes_prepass( shader_defs.push("MESHLET_MESH_MATERIAL_PASS".into()); let view_layout = if view_key.contains(MeshPipelineKey::MOTION_VECTOR_PREPASS) { - prepass_pipeline.internal.view_layout_motion_vectors.clone() + prepass_pipeline.view_layout_motion_vectors.clone() } else { - prepass_pipeline - .internal - .view_layout_no_motion_vectors - .clone() + prepass_pipeline.view_layout_no_motion_vectors.clone() }; let fragment_shader = if view_key.contains(MeshPipelineKey::DEFERRED_PREPASS) { - M::meshlet_mesh_deferred_fragment_shader() + material + .properties + .get_shader(MeshletDeferredFragmentShader) + .unwrap_or(meshlet_pipelines.meshlet_mesh_material.clone()) } else { - M::meshlet_mesh_prepass_fragment_shader() + material + .properties + .get_shader(MeshletPrepassFragmentShader) + .unwrap_or(meshlet_pipelines.meshlet_mesh_material.clone()) }; - let entry_point = match fragment_shader { - ShaderRef::Default => "prepass_fragment".into(), - _ => material_fragment.entry_point, + let entry_point = if fragment_shader == meshlet_pipelines.meshlet_mesh_material { + material_fragment.entry_point.clone() + } else { + None }; let pipeline_descriptor = RenderPipelineDescriptor { label: material_pipeline_descriptor.label, layout: vec![ view_layout, + prepass_pipeline.empty_layout.clone(), resource_manager.material_shade_bind_group_layout.clone(), - prepass_pipeline.internal.material_layout.clone(), + material + .properties + .material_layout + .as_ref() + .unwrap() + .clone(), ], - push_constant_ranges: vec![], vertex: VertexState { - shader: MESHLET_MESH_MATERIAL_SHADER_HANDLE, + shader: meshlet_pipelines.meshlet_mesh_material.clone(), shader_defs: shader_defs.clone(), entry_point: material_pipeline_descriptor.vertex.entry_point, - buffers: Vec::new(), + ..default() }, primitive: PrimitiveState::default(), depth_stencil: Some(DepthStencilState { @@ -391,25 +402,22 @@ pub fn prepare_material_meshlet_meshes_prepass( stencil: StencilState::default(), bias: DepthBiasState::default(), }), - multisample: MultisampleState::default(), fragment: Some(FragmentState { - shader: match fragment_shader { - ShaderRef::Default => MESHLET_MESH_MATERIAL_SHADER_HANDLE, - ShaderRef::Handle(handle) => handle, - ShaderRef::Path(path) => asset_server.load(path), - }, + shader: fragment_shader, shader_defs, entry_point, targets: material_fragment.targets, }), - zero_initialize_workgroup_memory: false, + ..default() }; - let material_id = instance_manager.get_material_id(material_id.untyped()); + let material_id = instance_manager.get_material_id(material_id); - let pipeline_id = *cache.entry(view_key).or_insert_with(|| { - pipeline_cache.queue_render_pipeline(pipeline_descriptor.clone()) - }); + let pipeline_id = *cache + .entry((view_key, material_id.type_id())) + .or_insert_with(|| { + pipeline_cache.queue_render_pipeline(pipeline_descriptor.clone()) + }); let Some(material_bind_group) = material_bind_group_allocator.get(material.binding.group) diff --git a/crates/bevy_pbr/src/meshlet/material_shade_nodes.rs b/crates/bevy_pbr/src/meshlet/material_shade_nodes.rs index cb05de38fb..39dcb0c169 100644 --- a/crates/bevy_pbr/src/meshlet/material_shade_nodes.rs +++ b/crates/bevy_pbr/src/meshlet/material_shade_nodes.rs @@ -18,7 +18,7 @@ use bevy_ecs::{ world::World, }; use bevy_render::{ - camera::ExtractedCamera, + camera::{ExtractedCamera, MainPassResolutionOverride}, render_graph::{NodeRunError, RenderGraphContext, ViewNode}, render_resource::{ LoadOp, Operations, PipelineCache, RenderPassDepthStencilAttachment, RenderPassDescriptor, @@ -42,6 +42,7 @@ impl ViewNode for MeshletMainOpaquePass3dNode { &'static ViewLightProbesUniformOffset, &'static ViewScreenSpaceReflectionsUniformOffset, &'static ViewEnvironmentMapUniformOffset, + Option<&'static MainPassResolutionOverride>, &'static MeshletViewMaterialsMainOpaquePass, &'static MeshletViewBindGroups, &'static MeshletViewResources, @@ -61,6 +62,7 @@ impl ViewNode for MeshletMainOpaquePass3dNode { view_light_probes_offset, view_ssr_offset, view_environment_map_offset, + resolution_override, meshlet_view_materials, meshlet_view_bind_groups, meshlet_view_resources, @@ -101,7 +103,7 @@ impl ViewNode for MeshletMainOpaquePass3dNode { occlusion_query_set: None, }); if let Some(viewport) = camera.viewport.as_ref() { - render_pass.set_camera_viewport(viewport); + render_pass.set_camera_viewport(&viewport.with_override(resolution_override)); } render_pass.set_bind_group( @@ -116,7 +118,8 @@ impl ViewNode for MeshletMainOpaquePass3dNode { **view_environment_map_offset, ], ); - render_pass.set_bind_group(1, meshlet_material_shade_bind_group, &[]); + render_pass.set_bind_group(1, &mesh_view_bind_group.binding_array, &[]); + render_pass.set_bind_group(2, meshlet_material_shade_bind_group, &[]); // 1 fullscreen triangle draw per material for (material_id, material_pipeline_id, material_bind_group) in @@ -128,7 +131,7 @@ impl ViewNode for MeshletMainOpaquePass3dNode { { let x = *material_id * 3; render_pass.set_render_pipeline(material_pipeline); - render_pass.set_bind_group(2, material_bind_group, &[]); + render_pass.set_bind_group(3, material_bind_group, &[]); render_pass.draw(x..(x + 3), 0..1); } } @@ -147,6 +150,7 @@ impl ViewNode for MeshletPrepassNode { &'static ViewPrepassTextures, &'static ViewUniformOffset, &'static PreviousViewUniformOffset, + Option<&'static MainPassResolutionOverride>, Has, &'static MeshletViewMaterialsPrepass, &'static MeshletViewBindGroups, @@ -162,6 +166,7 @@ impl ViewNode for MeshletPrepassNode { view_prepass_textures, view_uniform_offset, previous_view_uniform_offset, + resolution_override, view_has_motion_vector_prepass, meshlet_view_materials, meshlet_view_bind_groups, @@ -219,7 +224,7 @@ impl ViewNode for MeshletPrepassNode { occlusion_query_set: None, }); if let Some(viewport) = camera.viewport.as_ref() { - render_pass.set_camera_viewport(viewport); + render_pass.set_camera_viewport(&viewport.with_override(resolution_override)); } if view_has_motion_vector_prepass { @@ -239,7 +244,8 @@ impl ViewNode for MeshletPrepassNode { ); } - render_pass.set_bind_group(1, meshlet_material_shade_bind_group, &[]); + render_pass.set_bind_group(1, &prepass_view_bind_group.empty_bind_group, &[]); + render_pass.set_bind_group(2, meshlet_material_shade_bind_group, &[]); // 1 fullscreen triangle draw per material for (material_id, material_pipeline_id, material_bind_group) in @@ -270,6 +276,7 @@ impl ViewNode for MeshletDeferredGBufferPrepassNode { &'static ViewPrepassTextures, &'static ViewUniformOffset, &'static PreviousViewUniformOffset, + Option<&'static MainPassResolutionOverride>, Has, &'static MeshletViewMaterialsDeferredGBufferPrepass, &'static MeshletViewBindGroups, @@ -285,6 +292,7 @@ impl ViewNode for MeshletDeferredGBufferPrepassNode { view_prepass_textures, view_uniform_offset, previous_view_uniform_offset, + resolution_override, view_has_motion_vector_prepass, meshlet_view_materials, meshlet_view_bind_groups, @@ -347,7 +355,7 @@ impl ViewNode for MeshletDeferredGBufferPrepassNode { occlusion_query_set: None, }); if let Some(viewport) = camera.viewport.as_ref() { - render_pass.set_camera_viewport(viewport); + render_pass.set_camera_viewport(&viewport.with_override(resolution_override)); } if view_has_motion_vector_prepass { @@ -367,7 +375,8 @@ impl ViewNode for MeshletDeferredGBufferPrepassNode { ); } - render_pass.set_bind_group(1, meshlet_material_shade_bind_group, &[]); + render_pass.set_bind_group(1, &prepass_view_bind_group.empty_bind_group, &[]); + render_pass.set_bind_group(2, meshlet_material_shade_bind_group, &[]); // 1 fullscreen triangle draw per material for (material_id, material_pipeline_id, material_bind_group) in diff --git a/crates/bevy_pbr/src/meshlet/meshlet_bindings.wgsl b/crates/bevy_pbr/src/meshlet/meshlet_bindings.wgsl index 63e92f15e6..4533b2bd7f 100644 --- a/crates/bevy_pbr/src/meshlet/meshlet_bindings.wgsl +++ b/crates/bevy_pbr/src/meshlet/meshlet_bindings.wgsl @@ -5,6 +5,13 @@ #import bevy_pbr::prepass_bindings::PreviousViewUniforms #import bevy_pbr::utils::octahedral_decode_signed +struct BvhNode { + aabbs: array, + lod_bounds: array, 8>, + child_counts: array, + _padding: vec2, +} + struct Meshlet { start_vertex_position_bit: u32, start_vertex_attribute_id: u32, @@ -24,15 +31,34 @@ fn get_meshlet_triangle_count(meshlet: ptr) -> u32 { return extractBits((*meshlet).packed_a, 8u, 8u); } -struct MeshletBoundingSpheres { - culling_sphere: MeshletBoundingSphere, - lod_group_sphere: MeshletBoundingSphere, - lod_parent_group_sphere: MeshletBoundingSphere, +struct MeshletCullData { + aabb: MeshletAabbErrorOffset, + lod_group_sphere: vec4, } -struct MeshletBoundingSphere { +struct MeshletAabb { center: vec3, - radius: f32, + half_extent: vec3, +} + +struct MeshletAabbErrorOffset { + center_and_error: vec4, + half_extent_and_child_offset: vec4, +} + +fn get_aabb(aabb: ptr) -> MeshletAabb { + return MeshletAabb( + (*aabb).center_and_error.xyz, + (*aabb).half_extent_and_child_offset.xyz, + ); +} + +fn get_aabb_error(aabb: ptr) -> f32 { + return (*aabb).center_and_error.w; +} + +fn get_aabb_child_offset(aabb: ptr) -> u32 { + return bitcast((*aabb).half_extent_and_child_offset.w); } struct DispatchIndirectArgs { @@ -48,63 +74,133 @@ struct DrawIndirectArgs { first_instance: u32, } +// Either a BVH node or a meshlet, along with the instance it is associated with. +// Refers to BVH nodes in `meshlet_bvh_cull_queue` and `meshlet_second_pass_bvh_queue`, where `offset` is the index into `meshlet_bvh_nodes`. +// Refers to meshlets in `meshlet_meshlet_cull_queue` and `meshlet_raster_clusters`. +// In `meshlet_meshlet_cull_queue`, `offset` is the index into `meshlet_cull_data`. +// In `meshlet_raster_clusters`, `offset` is the index into `meshlets`. +struct InstancedOffset { + instance_id: u32, + offset: u32, +} + const CENTIMETERS_PER_METER = 100.0; -#ifdef MESHLET_FILL_CLUSTER_BUFFERS_PASS -var scene_instance_count: u32; -@group(0) @binding(0) var meshlet_instance_meshlet_counts: array; // Per entity instance -@group(0) @binding(1) var meshlet_instance_meshlet_slice_starts: array; // Per entity instance -@group(0) @binding(2) var meshlet_cluster_instance_ids: array; // Per cluster -@group(0) @binding(3) var meshlet_cluster_meshlet_ids: array; // Per cluster -@group(0) @binding(4) var meshlet_global_cluster_count: atomic; // Single object shared between all workgroups +#ifdef MESHLET_INSTANCE_CULLING_PASS +struct Constants { scene_instance_count: u32 } +var constants: Constants; + +// Cull data +@group(0) @binding(0) var depth_pyramid: texture_2d; +@group(0) @binding(1) var view: View; +@group(0) @binding(2) var previous_view: PreviousViewUniforms; + +// Per entity instance data +@group(0) @binding(3) var meshlet_instance_uniforms: array; +@group(0) @binding(4) var meshlet_view_instance_visibility: array; // 1 bit per entity instance, packed as a bitmask +@group(0) @binding(5) var meshlet_instance_aabbs: array; +@group(0) @binding(6) var meshlet_instance_bvh_root_nodes: array; + +// BVH cull queue data +@group(0) @binding(7) var meshlet_bvh_cull_count_write: atomic; +@group(0) @binding(8) var meshlet_bvh_cull_dispatch: DispatchIndirectArgs; +@group(0) @binding(9) var meshlet_bvh_cull_queue: array; + +// Second pass queue data +#ifdef MESHLET_FIRST_CULLING_PASS +@group(0) @binding(10) var meshlet_second_pass_instance_count: atomic; +@group(0) @binding(11) var meshlet_second_pass_instance_dispatch: DispatchIndirectArgs; +@group(0) @binding(12) var meshlet_second_pass_instance_candidates: array; +#else +@group(0) @binding(10) var meshlet_second_pass_instance_count: u32; +@group(0) @binding(11) var meshlet_second_pass_instance_candidates: array; +#endif #endif -#ifdef MESHLET_CULLING_PASS -struct Constants { scene_cluster_count: u32, meshlet_raster_cluster_rightmost_slot: u32 } +#ifdef MESHLET_BVH_CULLING_PASS +struct Constants { read_from_front: u32, rightmost_slot: u32 } var constants: Constants; -@group(0) @binding(0) var meshlet_cluster_meshlet_ids: array; // Per cluster -@group(0) @binding(1) var meshlet_bounding_spheres: array; // Per meshlet -@group(0) @binding(2) var meshlet_simplification_errors: array; // Per meshlet -@group(0) @binding(3) var meshlet_cluster_instance_ids: array; // Per cluster -@group(0) @binding(4) var meshlet_instance_uniforms: array; // Per entity instance -@group(0) @binding(5) var meshlet_view_instance_visibility: array; // 1 bit per entity instance, packed as a bitmask -@group(0) @binding(6) var meshlet_second_pass_candidates: array>; // 1 bit per cluster , packed as a bitmask -@group(0) @binding(7) var meshlet_software_raster_indirect_args: DispatchIndirectArgs; // Single object shared between all workgroups -@group(0) @binding(8) var meshlet_hardware_raster_indirect_args: DrawIndirectArgs; // Single object shared between all workgroups -@group(0) @binding(9) var meshlet_raster_clusters: array; // Single object shared between all workgroups -@group(0) @binding(10) var depth_pyramid: texture_2d; // From the end of the last frame for the first culling pass, and from the first raster pass for the second culling pass -@group(0) @binding(11) var view: View; -@group(0) @binding(12) var previous_view: PreviousViewUniforms; -fn should_cull_instance(instance_id: u32) -> bool { - let bit_offset = instance_id % 32u; - let packed_visibility = meshlet_view_instance_visibility[instance_id / 32u]; - return bool(extractBits(packed_visibility, bit_offset, 1u)); -} +// Cull data +@group(0) @binding(0) var depth_pyramid: texture_2d; // From the end of the last frame for the first culling pass, and from the first raster pass for the second culling pass +@group(0) @binding(1) var view: View; +@group(0) @binding(2) var previous_view: PreviousViewUniforms; -// TODO: Load 4x per workgroup instead of once per thread? -fn cluster_is_second_pass_candidate(cluster_id: u32) -> bool { - let packed_candidates = meshlet_second_pass_candidates[cluster_id / 32u]; - let bit_offset = cluster_id % 32u; - return bool(extractBits(packed_candidates, bit_offset, 1u)); -} +// Global mesh data +@group(0) @binding(3) var meshlet_bvh_nodes: array; + +// Per entity instance data +@group(0) @binding(4) var meshlet_instance_uniforms: array; + +// BVH cull queue data +@group(0) @binding(5) var meshlet_bvh_cull_count_read: u32; +@group(0) @binding(6) var meshlet_bvh_cull_count_write: atomic; +@group(0) @binding(7) var meshlet_bvh_cull_dispatch: DispatchIndirectArgs; +@group(0) @binding(8) var meshlet_bvh_cull_queue: array; + +// Meshlet cull queue data +@group(0) @binding(9) var meshlet_meshlet_cull_count_early: atomic; +@group(0) @binding(10) var meshlet_meshlet_cull_count_late: atomic; +@group(0) @binding(11) var meshlet_meshlet_cull_dispatch_early: DispatchIndirectArgs; +@group(0) @binding(12) var meshlet_meshlet_cull_dispatch_late: DispatchIndirectArgs; +@group(0) @binding(13) var meshlet_meshlet_cull_queue: array; + +// Second pass queue data +#ifdef MESHLET_FIRST_CULLING_PASS +@group(0) @binding(14) var meshlet_second_pass_bvh_count: atomic; +@group(0) @binding(15) var meshlet_second_pass_bvh_dispatch: DispatchIndirectArgs; +@group(0) @binding(16) var meshlet_second_pass_bvh_queue: array; +#endif +#endif + +#ifdef MESHLET_CLUSTER_CULLING_PASS +struct Constants { rightmost_slot: u32 } +var constants: Constants; + +// Cull data +@group(0) @binding(0) var depth_pyramid: texture_2d; // From the end of the last frame for the first culling pass, and from the first raster pass for the second culling pass +@group(0) @binding(1) var view: View; +@group(0) @binding(2) var previous_view: PreviousViewUniforms; + +// Global mesh data +@group(0) @binding(3) var meshlet_cull_data: array; + +// Per entity instance data +@group(0) @binding(4) var meshlet_instance_uniforms: array; + +// Raster queue data +@group(0) @binding(5) var meshlet_software_raster_indirect_args: DispatchIndirectArgs; +@group(0) @binding(6) var meshlet_hardware_raster_indirect_args: DrawIndirectArgs; +@group(0) @binding(7) var meshlet_previous_raster_counts: array; +@group(0) @binding(8) var meshlet_raster_clusters: array; + +// Meshlet cull queue data +@group(0) @binding(9) var meshlet_meshlet_cull_count_read: u32; + +// Second pass queue data +#ifdef MESHLET_FIRST_CULLING_PASS +@group(0) @binding(10) var meshlet_meshlet_cull_count_write: atomic; +@group(0) @binding(11) var meshlet_meshlet_cull_dispatch: DispatchIndirectArgs; +@group(0) @binding(12) var meshlet_meshlet_cull_queue: array; +#else +@group(0) @binding(10) var meshlet_meshlet_cull_queue: array; +#endif #endif #ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS -@group(0) @binding(0) var meshlet_cluster_meshlet_ids: array; // Per cluster +@group(0) @binding(0) var meshlet_raster_clusters: array; // Per cluster @group(0) @binding(1) var meshlets: array; // Per meshlet @group(0) @binding(2) var meshlet_indices: array; // Many per meshlet @group(0) @binding(3) var meshlet_vertex_positions: array; // Many per meshlet -@group(0) @binding(4) var meshlet_cluster_instance_ids: array; // Per cluster -@group(0) @binding(5) var meshlet_instance_uniforms: array; // Per entity instance -@group(0) @binding(6) var meshlet_raster_clusters: array; // Single object shared between all workgroups -@group(0) @binding(7) var meshlet_software_raster_cluster_count: u32; +@group(0) @binding(4) var meshlet_instance_uniforms: array; // Per entity instance +@group(0) @binding(5) var meshlet_previous_raster_counts: array; +@group(0) @binding(6) var meshlet_software_raster_cluster_count: u32; #ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT -@group(0) @binding(8) var meshlet_visibility_buffer: texture_storage_2d; +@group(0) @binding(7) var meshlet_visibility_buffer: texture_storage_2d; #else -@group(0) @binding(8) var meshlet_visibility_buffer: texture_storage_2d; +@group(0) @binding(7) var meshlet_visibility_buffer: texture_storage_2d; #endif -@group(0) @binding(9) var view: View; +@group(0) @binding(8) var view: View; // TODO: Load only twice, instead of 3x in cases where you load 3 indices per thread? fn get_meshlet_vertex_id(index_id: u32) -> u32 { @@ -150,14 +246,13 @@ fn get_meshlet_vertex_position(meshlet: ptr, vertex_id: u32) #ifdef MESHLET_MESH_MATERIAL_PASS @group(2) @binding(0) var meshlet_visibility_buffer: texture_storage_2d; -@group(2) @binding(1) var meshlet_cluster_meshlet_ids: array; // Per cluster +@group(2) @binding(1) var meshlet_raster_clusters: array; // Per cluster @group(2) @binding(2) var meshlets: array; // Per meshlet @group(2) @binding(3) var meshlet_indices: array; // Many per meshlet @group(2) @binding(4) var meshlet_vertex_positions: array; // Many per meshlet @group(2) @binding(5) var meshlet_vertex_normals: array; // Many per meshlet @group(2) @binding(6) var meshlet_vertex_uvs: array>; // Many per meshlet -@group(2) @binding(7) var meshlet_cluster_instance_ids: array; // Per cluster -@group(2) @binding(8) var meshlet_instance_uniforms: array; // Per entity instance +@group(2) @binding(7) var meshlet_instance_uniforms: array; // Per entity instance // TODO: Load only twice, instead of 3x in cases where you load 3 indices per thread? fn get_meshlet_vertex_id(index_id: u32) -> u32 { diff --git a/crates/bevy_pbr/src/meshlet/meshlet_cull_shared.wgsl b/crates/bevy_pbr/src/meshlet/meshlet_cull_shared.wgsl new file mode 100644 index 0000000000..975dd74f1c --- /dev/null +++ b/crates/bevy_pbr/src/meshlet/meshlet_cull_shared.wgsl @@ -0,0 +1,207 @@ +#define_import_path bevy_pbr::meshlet_cull_shared + +#import bevy_pbr::meshlet_bindings::{ + MeshletAabb, + DispatchIndirectArgs, + InstancedOffset, + depth_pyramid, + view, + previous_view, + meshlet_instance_uniforms, +} +#import bevy_render::maths::affine3_to_square + +// https://github.com/zeux/meshoptimizer/blob/1e48e96c7e8059321de492865165e9ef071bffba/demo/nanite.cpp#L115 +fn lod_error_is_imperceptible(lod_sphere: vec4, simplification_error: f32, instance_id: u32) -> bool { + let world_from_local = affine3_to_square(meshlet_instance_uniforms[instance_id].world_from_local); + let world_scale = max(length(world_from_local[0]), max(length(world_from_local[1]), length(world_from_local[2]))); + let camera_pos = view.world_position; + + let projection = view.clip_from_view; + if projection[3][3] == 1.0 { + // Orthographic + let world_error = simplification_error * world_scale; + let proj = projection[1][1]; + let height = 2.0 / proj; + let norm_error = world_error / height; + return norm_error * view.viewport.w < 1.0; + } else { + // Perspective + var near = projection[3][2]; + let world_sphere_center = (world_from_local * vec4(lod_sphere.xyz, 1.0)).xyz; + let world_sphere_radius = lod_sphere.w * world_scale; + let d_pos = world_sphere_center - camera_pos; + let d = sqrt(dot(d_pos, d_pos)) - world_sphere_radius; + let norm_error = simplification_error / max(d, near) * projection[1][1] * 0.5; + return norm_error * view.viewport.w < 1.0; + } +} + +fn normalize_plane(p: vec4) -> vec4 { + return p / length(p.xyz); +} + +// https://fgiesen.wordpress.com/2012/08/31/frustum-planes-from-the-projection-matrix/ +// https://fgiesen.wordpress.com/2010/10/17/view-frustum-culling/ +fn aabb_in_frustum(aabb: MeshletAabb, instance_id: u32) -> bool { + let world_from_local = affine3_to_square(meshlet_instance_uniforms[instance_id].world_from_local); + let clip_from_local = view.clip_from_world * world_from_local; + let row_major = transpose(clip_from_local); + let planes = array( + row_major[3] + row_major[0], + row_major[3] - row_major[0], + row_major[3] + row_major[1], + row_major[3] - row_major[1], + row_major[2], + ); + + for (var i = 0; i < 5; i++) { + let plane = normalize_plane(planes[i]); + let flipped = aabb.half_extent * sign(plane.xyz); + if dot(aabb.center + flipped, plane.xyz) <= -plane.w { + return false; + } + } + return true; +} + +struct ScreenAabb { + min: vec3, + max: vec3, +} + +fn min8(a: vec3, b: vec3, c: vec3, d: vec3, e: vec3, f: vec3, g: vec3, h: vec3) -> vec3 { + return min(min(min(a, b), min(c, d)), min(min(e, f), min(g, h))); +} + +fn max8(a: vec3, b: vec3, c: vec3, d: vec3, e: vec3, f: vec3, g: vec3, h: vec3) -> vec3 { + return max(max(max(a, b), max(c, d)), max(max(e, f), max(g, h))); +} + +fn min8_4(a: vec4, b: vec4, c: vec4, d: vec4, e: vec4, f: vec4, g: vec4, h: vec4) -> vec4 { + return min(min(min(a, b), min(c, d)), min(min(e, f), min(g, h))); +} + +// https://zeux.io/2023/01/12/approximate-projected-bounds/ +fn project_aabb(clip_from_local: mat4x4, near: f32, aabb: MeshletAabb, out: ptr) -> bool { + let extent = aabb.half_extent * 2.0; + let sx = clip_from_local * vec4(extent.x, 0.0, 0.0, 0.0); + let sy = clip_from_local * vec4(0.0, extent.y, 0.0, 0.0); + let sz = clip_from_local * vec4(0.0, 0.0, extent.z, 0.0); + + let p0 = clip_from_local * vec4(aabb.center - aabb.half_extent, 1.0); + let p1 = p0 + sz; + let p2 = p0 + sy; + let p3 = p2 + sz; + let p4 = p0 + sx; + let p5 = p4 + sz; + let p6 = p4 + sy; + let p7 = p6 + sz; + + let depth = min8_4(p0, p1, p2, p3, p4, p5, p6, p7).w; + // do not occlusion cull if we are inside the aabb + if depth < near { + return false; + } + + let dp0 = p0.xyz / p0.w; + let dp1 = p1.xyz / p1.w; + let dp2 = p2.xyz / p2.w; + let dp3 = p3.xyz / p3.w; + let dp4 = p4.xyz / p4.w; + let dp5 = p5.xyz / p5.w; + let dp6 = p6.xyz / p6.w; + let dp7 = p7.xyz / p7.w; + let min = min8(dp0, dp1, dp2, dp3, dp4, dp5, dp6, dp7); + let max = max8(dp0, dp1, dp2, dp3, dp4, dp5, dp6, dp7); + var vaabb = vec4(min.xy, max.xy); + // convert ndc to texture coordinates by rescaling and flipping Y + vaabb = vaabb.xwzy * vec4(0.5, -0.5, 0.5, -0.5) + 0.5; + (*out).min = vec3(vaabb.xy, min.z); + (*out).max = vec3(vaabb.zw, max.z); + return true; +} + +fn sample_hzb(smin: vec2, smax: vec2, mip: i32) -> f32 { + let texel = vec4(0, 1, 2, 3); + let sx = min(smin.x + texel, smax.xxxx); + let sy = min(smin.y + texel, smax.yyyy); + // TODO: switch to min samplers when wgpu has them + // sampling 16 times a finer mip is worth the extra cost for better culling + let a = sample_hzb_row(sx, sy.x, mip); + let b = sample_hzb_row(sx, sy.y, mip); + let c = sample_hzb_row(sx, sy.z, mip); + let d = sample_hzb_row(sx, sy.w, mip); + return min(min(a, b), min(c, d)); +} + +fn sample_hzb_row(sx: vec4, sy: u32, mip: i32) -> f32 { + let a = textureLoad(depth_pyramid, vec2(sx.x, sy), mip).x; + let b = textureLoad(depth_pyramid, vec2(sx.y, sy), mip).x; + let c = textureLoad(depth_pyramid, vec2(sx.z, sy), mip).x; + let d = textureLoad(depth_pyramid, vec2(sx.w, sy), mip).x; + return min(min(a, b), min(c, d)); +} + +// TODO: We should probably be using a POT HZB texture? +fn occlusion_cull_screen_aabb(aabb: ScreenAabb, screen: vec2) -> bool { + let hzb_size = ceil(screen * 0.5); + let aabb_min = aabb.min.xy * hzb_size; + let aabb_max = aabb.max.xy * hzb_size; + + let min_texel = vec2(max(aabb_min, vec2(0.0))); + let max_texel = vec2(min(aabb_max, hzb_size - 1.0)); + let size = max_texel - min_texel; + let max_size = max(size.x, size.y); + + // note: add 1 before max because the unsigned overflow behavior is intentional + // it wraps around firstLeadingBit(0) = ~0 to 0 + // TODO: we actually sample a 4x4 block, so ideally this would be `max(..., 3u) - 3u`. + // However, since our HZB is not a power of two, we need to be extra-conservative to not over-cull, so we go up a mip. + var mip = max(firstLeadingBit(max_size) + 1u, 2u) - 2u; + + if any((max_texel >> vec2(mip)) > (min_texel >> vec2(mip)) + 3) { + mip += 1u; + } + + let smin = min_texel >> vec2(mip); + let smax = max_texel >> vec2(mip); + + let curr_depth = sample_hzb(smin, smax, i32(mip)); + return aabb.max.z <= curr_depth; +} + +fn occlusion_cull_projection() -> mat4x4 { +#ifdef FIRST_CULLING_PASS + return view.clip_from_world; +#else + return previous_view.clip_from_world; +#endif +} + +fn occlusion_cull_clip_from_local(instance_id: u32) -> mat4x4 { +#ifdef FIRST_CULLING_PASS + let prev_world_from_local = affine3_to_square(meshlet_instance_uniforms[instance_id].previous_world_from_local); + return previous_view.clip_from_world * prev_world_from_local; +#else + let world_from_local = affine3_to_square(meshlet_instance_uniforms[instance_id].world_from_local); + return view.clip_from_world * world_from_local; +#endif +} + +fn should_occlusion_cull_aabb(aabb: MeshletAabb, instance_id: u32) -> bool { + let projection = occlusion_cull_projection(); + var near: f32; + if projection[3][3] == 1.0 { + near = projection[3][2] / projection[2][2]; + } else { + near = projection[3][2]; + } + + let clip_from_local = occlusion_cull_clip_from_local(instance_id); + var screen_aabb = ScreenAabb(vec3(0.0), vec3(0.0)); + if project_aabb(clip_from_local, near, aabb, &screen_aabb) { + return occlusion_cull_screen_aabb(screen_aabb, view.viewport.zw); + } + return false; +} diff --git a/crates/bevy_pbr/src/meshlet/meshlet_mesh_manager.rs b/crates/bevy_pbr/src/meshlet/meshlet_mesh_manager.rs index 0f4aab7509..93eb5a1afe 100644 --- a/crates/bevy_pbr/src/meshlet/meshlet_mesh_manager.rs +++ b/crates/bevy_pbr/src/meshlet/meshlet_mesh_manager.rs @@ -1,8 +1,6 @@ -use super::{ - asset::{Meshlet, MeshletBoundingSpheres, MeshletSimplificationError}, - persistent_buffer::PersistentGpuBuffer, - MeshletMesh, -}; +use crate::meshlet::asset::{BvhNode, MeshletAabb, MeshletCullData}; + +use super::{asset::Meshlet, persistent_buffer::PersistentGpuBuffer, MeshletMesh}; use alloc::sync::Arc; use bevy_asset::{AssetId, Assets}; use bevy_ecs::{ @@ -25,10 +23,11 @@ pub struct MeshletMeshManager { pub vertex_normals: PersistentGpuBuffer>, pub vertex_uvs: PersistentGpuBuffer>, pub indices: PersistentGpuBuffer>, + pub bvh_nodes: PersistentGpuBuffer>, pub meshlets: PersistentGpuBuffer>, - pub meshlet_bounding_spheres: PersistentGpuBuffer>, - pub meshlet_simplification_errors: PersistentGpuBuffer>, - meshlet_mesh_slices: HashMap, [Range; 7]>, + pub meshlet_cull_data: PersistentGpuBuffer>, + meshlet_mesh_slices: + HashMap, ([Range; 7], MeshletAabb, u32)>, } impl FromWorld for MeshletMeshManager { @@ -39,26 +38,21 @@ impl FromWorld for MeshletMeshManager { vertex_normals: PersistentGpuBuffer::new("meshlet_vertex_normals", render_device), vertex_uvs: PersistentGpuBuffer::new("meshlet_vertex_uvs", render_device), indices: PersistentGpuBuffer::new("meshlet_indices", render_device), + bvh_nodes: PersistentGpuBuffer::new("meshlet_bvh_nodes", render_device), meshlets: PersistentGpuBuffer::new("meshlets", render_device), - meshlet_bounding_spheres: PersistentGpuBuffer::new( - "meshlet_bounding_spheres", - render_device, - ), - meshlet_simplification_errors: PersistentGpuBuffer::new( - "meshlet_simplification_errors", - render_device, - ), + meshlet_cull_data: PersistentGpuBuffer::new("meshlet_cull_data", render_device), meshlet_mesh_slices: HashMap::default(), } } } impl MeshletMeshManager { + // Returns the index of the root BVH node, as well as the depth of the BVH. pub fn queue_upload_if_needed( &mut self, asset_id: AssetId, assets: &mut Assets, - ) -> Range { + ) -> (u32, MeshletAabb, u32) { let queue_meshlet_mesh = |asset_id: &AssetId| { let meshlet_mesh = assets.remove_untracked(*asset_id).expect( "MeshletMesh asset was already unloaded but is not registered with MeshletMeshManager", @@ -84,51 +78,59 @@ impl MeshletMeshManager { indices_slice.start, ), ); - let meshlet_bounding_spheres_slice = self - .meshlet_bounding_spheres - .queue_write(Arc::clone(&meshlet_mesh.meshlet_bounding_spheres), ()); - let meshlet_simplification_errors_slice = self - .meshlet_simplification_errors - .queue_write(Arc::clone(&meshlet_mesh.meshlet_simplification_errors), ()); + let base_meshlet_index = (meshlets_slice.start / size_of::() as u64) as u32; + let bvh_node_slice = self + .bvh_nodes + .queue_write(Arc::clone(&meshlet_mesh.bvh), base_meshlet_index); + let meshlet_cull_data_slice = self + .meshlet_cull_data + .queue_write(Arc::clone(&meshlet_mesh.meshlet_cull_data), ()); - [ - vertex_positions_slice, - vertex_normals_slice, - vertex_uvs_slice, - indices_slice, - meshlets_slice, - meshlet_bounding_spheres_slice, - meshlet_simplification_errors_slice, - ] + ( + [ + vertex_positions_slice, + vertex_normals_slice, + vertex_uvs_slice, + indices_slice, + bvh_node_slice, + meshlets_slice, + meshlet_cull_data_slice, + ], + meshlet_mesh.aabb, + meshlet_mesh.bvh_depth, + ) }; // If the MeshletMesh asset has not been uploaded to the GPU yet, queue it for uploading - let [_, _, _, _, meshlets_slice, _, _] = self + let ([_, _, _, _, bvh_node_slice, _, _], aabb, bvh_depth) = self .meshlet_mesh_slices .entry(asset_id) .or_insert_with_key(queue_meshlet_mesh) .clone(); - let meshlets_slice_start = meshlets_slice.start as u32 / size_of::() as u32; - let meshlets_slice_end = meshlets_slice.end as u32 / size_of::() as u32; - meshlets_slice_start..meshlets_slice_end + ( + (bvh_node_slice.start / size_of::() as u64) as u32, + aabb, + bvh_depth, + ) } pub fn remove(&mut self, asset_id: &AssetId) { - if let Some( - [vertex_positions_slice, vertex_normals_slice, vertex_uvs_slice, indices_slice, meshlets_slice, meshlet_bounding_spheres_slice, meshlet_simplification_errors_slice], - ) = self.meshlet_mesh_slices.remove(asset_id) + if let Some(( + [vertex_positions_slice, vertex_normals_slice, vertex_uvs_slice, indices_slice, bvh_node_slice, meshlets_slice, meshlet_cull_data_slice], + _, + _, + )) = self.meshlet_mesh_slices.remove(asset_id) { self.vertex_positions .mark_slice_unused(vertex_positions_slice); self.vertex_normals.mark_slice_unused(vertex_normals_slice); self.vertex_uvs.mark_slice_unused(vertex_uvs_slice); self.indices.mark_slice_unused(indices_slice); + self.bvh_nodes.mark_slice_unused(bvh_node_slice); self.meshlets.mark_slice_unused(meshlets_slice); - self.meshlet_bounding_spheres - .mark_slice_unused(meshlet_bounding_spheres_slice); - self.meshlet_simplification_errors - .mark_slice_unused(meshlet_simplification_errors_slice); + self.meshlet_cull_data + .mark_slice_unused(meshlet_cull_data_slice); } } } @@ -151,13 +153,13 @@ pub fn perform_pending_meshlet_mesh_writes( meshlet_mesh_manager .indices .perform_writes(&render_queue, &render_device); + meshlet_mesh_manager + .bvh_nodes + .perform_writes(&render_queue, &render_device); meshlet_mesh_manager .meshlets .perform_writes(&render_queue, &render_device); meshlet_mesh_manager - .meshlet_bounding_spheres - .perform_writes(&render_queue, &render_device); - meshlet_mesh_manager - .meshlet_simplification_errors + .meshlet_cull_data .perform_writes(&render_queue, &render_device); } diff --git a/crates/bevy_pbr/src/meshlet/meshlet_mesh_material.wgsl b/crates/bevy_pbr/src/meshlet/meshlet_mesh_material.wgsl index 1309c7884c..4b114cbfc8 100644 --- a/crates/bevy_pbr/src/meshlet/meshlet_mesh_material.wgsl +++ b/crates/bevy_pbr/src/meshlet/meshlet_mesh_material.wgsl @@ -15,6 +15,7 @@ fn vertex(@builtin(vertex_index) vertex_input: u32) -> @builtin(position) vec4) -> @location(0) vec4 { let vertex_output = resolve_vertex_output(frag_coord); @@ -22,6 +23,7 @@ fn fragment(@builtin(position) frag_coord: vec4) -> @location(0) vec4 let color = vec3(rand_f(&rng), rand_f(&rng), rand_f(&rng)); return vec4(color, 1.0); } +#endif #ifdef PREPASS_FRAGMENT @fragment diff --git a/crates/bevy_pbr/src/meshlet/mod.rs b/crates/bevy_pbr/src/meshlet/mod.rs index 2375894613..94b623a280 100644 --- a/crates/bevy_pbr/src/meshlet/mod.rs +++ b/crates/bevy_pbr/src/meshlet/mod.rs @@ -58,7 +58,7 @@ use self::{ }; use crate::{graph::NodePbr, PreviousGlobalTransform}; use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, AssetApp, AssetId, Handle}; +use bevy_asset::{embedded_asset, AssetApp, AssetId, Handle}; use bevy_core_pipeline::{ core_3d::graph::{Core3d, Node3d}, prepass::{DeferredPrepass, MotionVectorPrepass, NormalPrepass}, @@ -74,8 +74,8 @@ use bevy_ecs::{ }; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ - render_graph::{RenderGraphApp, ViewNodeRunner}, - render_resource::Shader, + load_shader_library, + render_graph::{RenderGraphExt, ViewNodeRunner}, renderer::RenderDevice, settings::WgpuFeatures, view::{self, prepare_view_targets, Msaa, Visibility, VisibilityClass}, @@ -85,11 +85,6 @@ use bevy_transform::components::Transform; use derive_more::From; use tracing::error; -const MESHLET_BINDINGS_SHADER_HANDLE: Handle = - weak_handle!("d90ac78c-500f-48aa-b488-cc98eb3f6314"); -const MESHLET_MESH_MATERIAL_SHADER_HANDLE: Handle = - weak_handle!("db8d9001-6ca7-4d00-968a-d5f5b96b89c3"); - /// Provides a plugin for rendering large amounts of high-poly 3d meshes using an efficient GPU-driven method. See also [`MeshletMesh`]. /// /// Rendering dense scenes made of high-poly meshes with thousands or millions of triangles is extremely expensive in Bevy's standard renderer. @@ -152,66 +147,19 @@ impl Plugin for MeshletPlugin { std::process::exit(1); } - load_internal_asset!( - app, - MESHLET_CLEAR_VISIBILITY_BUFFER_SHADER_HANDLE, - "clear_visibility_buffer.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - MESHLET_BINDINGS_SHADER_HANDLE, - "meshlet_bindings.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - super::MESHLET_VISIBILITY_BUFFER_RESOLVE_SHADER_HANDLE, - "visibility_buffer_resolve.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - MESHLET_FILL_CLUSTER_BUFFERS_SHADER_HANDLE, - "fill_cluster_buffers.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - MESHLET_CULLING_SHADER_HANDLE, - "cull_clusters.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - MESHLET_VISIBILITY_BUFFER_SOFTWARE_RASTER_SHADER_HANDLE, - "visibility_buffer_software_raster.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - MESHLET_VISIBILITY_BUFFER_HARDWARE_RASTER_SHADER_HANDLE, - "visibility_buffer_hardware_raster.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - MESHLET_MESH_MATERIAL_SHADER_HANDLE, - "meshlet_mesh_material.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - MESHLET_RESOLVE_RENDER_TARGETS_SHADER_HANDLE, - "resolve_render_targets.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - MESHLET_REMAP_1D_TO_2D_DISPATCH_SHADER_HANDLE, - "remap_1d_to_2d_dispatch.wgsl", - Shader::from_wgsl - ); + load_shader_library!(app, "meshlet_bindings.wgsl"); + load_shader_library!(app, "visibility_buffer_resolve.wgsl"); + load_shader_library!(app, "meshlet_cull_shared.wgsl"); + embedded_asset!(app, "clear_visibility_buffer.wgsl"); + embedded_asset!(app, "cull_instances.wgsl"); + embedded_asset!(app, "cull_bvh.wgsl"); + embedded_asset!(app, "cull_clusters.wgsl"); + embedded_asset!(app, "visibility_buffer_software_raster.wgsl"); + embedded_asset!(app, "visibility_buffer_hardware_raster.wgsl"); + embedded_asset!(app, "meshlet_mesh_material.wgsl"); + embedded_asset!(app, "resolve_render_targets.wgsl"); + embedded_asset!(app, "remap_1d_to_2d_dispatch.wgsl"); + embedded_asset!(app, "fill_counts.wgsl"); app.init_asset::() .register_asset_loader(MeshletMeshLoader); @@ -283,6 +231,10 @@ impl Plugin for MeshletPlugin { .in_set(RenderSystems::ManageViews), prepare_meshlet_per_frame_resources.in_set(RenderSystems::PrepareResources), prepare_meshlet_view_bind_groups.in_set(RenderSystems::PrepareBindGroups), + queue_material_meshlet_meshes.in_set(RenderSystems::QueueMeshes), + prepare_material_meshlet_meshes_main_opaque_pass + .in_set(RenderSystems::QueueMeshes) + .before(queue_material_meshlet_meshes), ), ); } diff --git a/crates/bevy_pbr/src/meshlet/persistent_buffer.rs b/crates/bevy_pbr/src/meshlet/persistent_buffer.rs index 85dec457f9..e8f4669227 100644 --- a/crates/bevy_pbr/src/meshlet/persistent_buffer.rs +++ b/crates/bevy_pbr/src/meshlet/persistent_buffer.rs @@ -71,7 +71,7 @@ impl PersistentGpuBuffer { let mut buffer_view = render_queue .write_buffer_with(&self.buffer, buffer_slice.start, buffer_slice_size) .unwrap(); - data.write_bytes_le(metadata, &mut buffer_view); + data.write_bytes_le(metadata, &mut buffer_view, buffer_slice.start); } let queue_saturation = queue_count as f32 / self.write_queue.capacity() as f32; @@ -123,5 +123,10 @@ pub trait PersistentGpuBufferable { /// Convert `self` + `metadata` into bytes (little-endian), and write to the provided buffer slice. /// Any bytes not written to in the slice will be zeroed out when uploaded to the GPU. - fn write_bytes_le(&self, metadata: Self::Metadata, buffer_slice: &mut [u8]); + fn write_bytes_le( + &self, + metadata: Self::Metadata, + buffer_slice: &mut [u8], + buffer_offset: BufferAddress, + ); } diff --git a/crates/bevy_pbr/src/meshlet/persistent_buffer_impls.rs b/crates/bevy_pbr/src/meshlet/persistent_buffer_impls.rs index 9c2667d3f3..210a52becd 100644 --- a/crates/bevy_pbr/src/meshlet/persistent_buffer_impls.rs +++ b/crates/bevy_pbr/src/meshlet/persistent_buffer_impls.rs @@ -1,9 +1,50 @@ -use super::{ - asset::{Meshlet, MeshletBoundingSpheres, MeshletSimplificationError}, - persistent_buffer::PersistentGpuBufferable, -}; +use crate::meshlet::asset::{BvhNode, MeshletCullData}; + +use super::{asset::Meshlet, persistent_buffer::PersistentGpuBufferable}; use alloc::sync::Arc; use bevy_math::Vec2; +use bevy_render::render_resource::BufferAddress; + +impl PersistentGpuBufferable for Arc<[BvhNode]> { + type Metadata = u32; + + fn size_in_bytes(&self) -> usize { + self.len() * size_of::() + } + + fn write_bytes_le( + &self, + base_meshlet_index: Self::Metadata, + buffer_slice: &mut [u8], + buffer_offset: BufferAddress, + ) { + const SIZE: usize = size_of::(); + for (i, &node) in self.iter().enumerate() { + let bytes: [u8; SIZE] = + bytemuck::cast(node.offset_aabbs(base_meshlet_index, buffer_offset)); + buffer_slice[i * SIZE..(i + 1) * SIZE].copy_from_slice(&bytes); + } + } +} + +impl BvhNode { + fn offset_aabbs(mut self, base_meshlet_index: u32, buffer_offset: BufferAddress) -> Self { + let size = size_of::(); + let base_bvh_node_index = (buffer_offset / size as u64) as u32; + for i in 0..self.aabbs.len() { + self.aabbs[i].child_offset += if self.child_is_bvh_node(i) { + base_bvh_node_index + } else { + base_meshlet_index + }; + } + self + } + + fn child_is_bvh_node(&self, i: usize) -> bool { + self.child_counts[i] == u8::MAX + } +} impl PersistentGpuBufferable for Arc<[Meshlet]> { type Metadata = (u64, u64, u64); @@ -16,6 +57,7 @@ impl PersistentGpuBufferable for Arc<[Meshlet]> { &self, (vertex_position_offset, vertex_attribute_offset, index_offset): Self::Metadata, buffer_slice: &mut [u8], + _: BufferAddress, ) { let vertex_position_offset = (vertex_position_offset * 8) as u32; let vertex_attribute_offset = (vertex_attribute_offset as usize / size_of::()) as u32; @@ -37,6 +79,18 @@ impl PersistentGpuBufferable for Arc<[Meshlet]> { } } +impl PersistentGpuBufferable for Arc<[MeshletCullData]> { + type Metadata = (); + + fn size_in_bytes(&self) -> usize { + self.len() * size_of::() + } + + fn write_bytes_le(&self, _: Self::Metadata, buffer_slice: &mut [u8], _: BufferAddress) { + buffer_slice.clone_from_slice(bytemuck::cast_slice(self)); + } +} + impl PersistentGpuBufferable for Arc<[u8]> { type Metadata = (); @@ -44,7 +98,7 @@ impl PersistentGpuBufferable for Arc<[u8]> { self.len() } - fn write_bytes_le(&self, _: Self::Metadata, buffer_slice: &mut [u8]) { + fn write_bytes_le(&self, _: Self::Metadata, buffer_slice: &mut [u8], _: BufferAddress) { buffer_slice.clone_from_slice(self); } } @@ -56,7 +110,7 @@ impl PersistentGpuBufferable for Arc<[u32]> { self.len() * size_of::() } - fn write_bytes_le(&self, _: Self::Metadata, buffer_slice: &mut [u8]) { + fn write_bytes_le(&self, _: Self::Metadata, buffer_slice: &mut [u8], _: BufferAddress) { buffer_slice.clone_from_slice(bytemuck::cast_slice(self)); } } @@ -68,31 +122,7 @@ impl PersistentGpuBufferable for Arc<[Vec2]> { self.len() * size_of::() } - fn write_bytes_le(&self, _: Self::Metadata, buffer_slice: &mut [u8]) { - buffer_slice.clone_from_slice(bytemuck::cast_slice(self)); - } -} - -impl PersistentGpuBufferable for Arc<[MeshletBoundingSpheres]> { - type Metadata = (); - - fn size_in_bytes(&self) -> usize { - self.len() * size_of::() - } - - fn write_bytes_le(&self, _: Self::Metadata, buffer_slice: &mut [u8]) { - buffer_slice.clone_from_slice(bytemuck::cast_slice(self)); - } -} - -impl PersistentGpuBufferable for Arc<[MeshletSimplificationError]> { - type Metadata = (); - - fn size_in_bytes(&self) -> usize { - self.len() * size_of::() - } - - fn write_bytes_le(&self, _: Self::Metadata, buffer_slice: &mut [u8]) { + fn write_bytes_le(&self, _: Self::Metadata, buffer_slice: &mut [u8], _: BufferAddress) { buffer_slice.clone_from_slice(bytemuck::cast_slice(self)); } } diff --git a/crates/bevy_pbr/src/meshlet/pipelines.rs b/crates/bevy_pbr/src/meshlet/pipelines.rs index 243bbddf22..6ac22f0fba 100644 --- a/crates/bevy_pbr/src/meshlet/pipelines.rs +++ b/crates/bevy_pbr/src/meshlet/pipelines.rs @@ -1,5 +1,5 @@ use super::resource_manager::ResourceManager; -use bevy_asset::{weak_handle, Handle}; +use bevy_asset::{load_embedded_asset, Handle}; use bevy_core_pipeline::{ core_3d::CORE_3D_DEPTH_FORMAT, experimental::mip_generation::DOWNSAMPLE_DEPTH_SHADER_HANDLE, FullscreenShader, @@ -9,29 +9,18 @@ use bevy_ecs::{ world::{FromWorld, World}, }; use bevy_render::render_resource::*; - -pub const MESHLET_CLEAR_VISIBILITY_BUFFER_SHADER_HANDLE: Handle = - weak_handle!("a4bf48e4-5605-4d1c-987e-29c7b1ec95dc"); -pub const MESHLET_FILL_CLUSTER_BUFFERS_SHADER_HANDLE: Handle = - weak_handle!("80ccea4a-8234-4ee0-af74-77b3cad503cf"); -pub const MESHLET_CULLING_SHADER_HANDLE: Handle = - weak_handle!("d71c5879-97fa-49d1-943e-ed9162fe8adb"); -pub const MESHLET_VISIBILITY_BUFFER_SOFTWARE_RASTER_SHADER_HANDLE: Handle = - weak_handle!("68cc6826-8321-43d1-93d5-4f61f0456c13"); -pub const MESHLET_VISIBILITY_BUFFER_HARDWARE_RASTER_SHADER_HANDLE: Handle = - weak_handle!("4b4e3020-748f-4baf-b011-87d9d2a12796"); -pub const MESHLET_RESOLVE_RENDER_TARGETS_SHADER_HANDLE: Handle = - weak_handle!("c218ce17-cf59-4268-8898-13ecf384f133"); -pub const MESHLET_REMAP_1D_TO_2D_DISPATCH_SHADER_HANDLE: Handle = - weak_handle!("f5b7edfc-2eac-4407-8f5c-1265d4d795c2"); +use bevy_utils::default; #[derive(Resource)] pub struct MeshletPipelines { - fill_cluster_buffers: CachedComputePipelineId, clear_visibility_buffer: CachedComputePipelineId, clear_visibility_buffer_shadow_view: CachedComputePipelineId, - cull_first: CachedComputePipelineId, - cull_second: CachedComputePipelineId, + first_instance_cull: CachedComputePipelineId, + second_instance_cull: CachedComputePipelineId, + first_bvh_cull: CachedComputePipelineId, + second_bvh_cull: CachedComputePipelineId, + first_meshlet_cull: CachedComputePipelineId, + second_meshlet_cull: CachedComputePipelineId, downsample_depth_first: CachedComputePipelineId, downsample_depth_second: CachedComputePipelineId, downsample_depth_first_shadow_view: CachedComputePipelineId, @@ -45,21 +34,35 @@ pub struct MeshletPipelines { resolve_depth_shadow_view: CachedRenderPipelineId, resolve_material_depth: CachedRenderPipelineId, remap_1d_to_2d_dispatch: Option, + fill_counts: CachedComputePipelineId, + pub(crate) meshlet_mesh_material: Handle, } impl FromWorld for MeshletPipelines { fn from_world(world: &mut World) -> Self { let resource_manager = world.resource::(); - let fill_cluster_buffers_bind_group_layout = resource_manager - .fill_cluster_buffers_bind_group_layout - .clone(); let clear_visibility_buffer_bind_group_layout = resource_manager .clear_visibility_buffer_bind_group_layout .clone(); let clear_visibility_buffer_shadow_view_bind_group_layout = resource_manager .clear_visibility_buffer_shadow_view_bind_group_layout .clone(); - let cull_layout = resource_manager.culling_bind_group_layout.clone(); + let first_instance_cull_bind_group_layout = resource_manager + .first_instance_cull_bind_group_layout + .clone(); + let second_instance_cull_bind_group_layout = resource_manager + .second_instance_cull_bind_group_layout + .clone(); + let first_bvh_cull_bind_group_layout = + resource_manager.first_bvh_cull_bind_group_layout.clone(); + let second_bvh_cull_bind_group_layout = + resource_manager.second_bvh_cull_bind_group_layout.clone(); + let first_meshlet_cull_bind_group_layout = resource_manager + .first_meshlet_cull_bind_group_layout + .clone(); + let second_meshlet_cull_bind_group_layout = resource_manager + .second_meshlet_cull_bind_group_layout + .clone(); let downsample_depth_layout = resource_manager.downsample_depth_bind_group_layout.clone(); let downsample_depth_shadow_view_layout = resource_manager .downsample_depth_shadow_view_bind_group_layout @@ -82,24 +85,24 @@ impl FromWorld for MeshletPipelines { .clone(); let vertex_state = world.resource::().to_vertex_state(); + let fill_counts_layout = resource_manager.fill_counts_bind_group_layout.clone(); + + let clear_visibility_buffer = load_embedded_asset!(world, "clear_visibility_buffer.wgsl"); + let cull_instances = load_embedded_asset!(world, "cull_instances.wgsl"); + let cull_bvh = load_embedded_asset!(world, "cull_bvh.wgsl"); + let cull_clusters = load_embedded_asset!(world, "cull_clusters.wgsl"); + let visibility_buffer_software_raster = + load_embedded_asset!(world, "visibility_buffer_software_raster.wgsl"); + let visibility_buffer_hardware_raster = + load_embedded_asset!(world, "visibility_buffer_hardware_raster.wgsl"); + let resolve_render_targets = load_embedded_asset!(world, "resolve_render_targets.wgsl"); + let remap_1d_to_2d_dispatch = load_embedded_asset!(world, "remap_1d_to_2d_dispatch.wgsl"); + let fill_counts = load_embedded_asset!(world, "fill_counts.wgsl"); + let meshlet_mesh_material = load_embedded_asset!(world, "meshlet_mesh_material.wgsl"); + let pipeline_cache = world.resource_mut::(); Self { - fill_cluster_buffers: pipeline_cache.queue_compute_pipeline( - ComputePipelineDescriptor { - label: Some("meshlet_fill_cluster_buffers_pipeline".into()), - layout: vec![fill_cluster_buffers_bind_group_layout], - push_constant_ranges: vec![PushConstantRange { - stages: ShaderStages::COMPUTE, - range: 0..4, - }], - shader: MESHLET_FILL_CLUSTER_BUFFERS_SHADER_HANDLE, - shader_defs: vec!["MESHLET_FILL_CLUSTER_BUFFERS_PASS".into()], - entry_point: "fill_cluster_buffers".into(), - zero_initialize_workgroup_memory: false, - }, - ), - clear_visibility_buffer: pipeline_cache.queue_compute_pipeline( ComputePipelineDescriptor { label: Some("meshlet_clear_visibility_buffer_pipeline".into()), @@ -108,10 +111,9 @@ impl FromWorld for MeshletPipelines { stages: ShaderStages::COMPUTE, range: 0..8, }], - shader: MESHLET_CLEAR_VISIBILITY_BUFFER_SHADER_HANDLE, + shader: clear_visibility_buffer.clone(), shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into()], - entry_point: "clear_visibility_buffer".into(), - zero_initialize_workgroup_memory: false, + ..default() }, ), @@ -123,43 +125,101 @@ impl FromWorld for MeshletPipelines { stages: ShaderStages::COMPUTE, range: 0..8, }], - shader: MESHLET_CLEAR_VISIBILITY_BUFFER_SHADER_HANDLE, - shader_defs: vec![], - entry_point: "clear_visibility_buffer".into(), - zero_initialize_workgroup_memory: false, + shader: clear_visibility_buffer, + ..default() }, ), - cull_first: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { - label: Some("meshlet_culling_first_pipeline".into()), - layout: vec![cull_layout.clone()], + first_instance_cull: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("meshlet_first_instance_cull_pipeline".into()), + layout: vec![first_instance_cull_bind_group_layout.clone()], push_constant_ranges: vec![PushConstantRange { stages: ShaderStages::COMPUTE, - range: 0..8, + range: 0..4, }], - shader: MESHLET_CULLING_SHADER_HANDLE, + shader: cull_instances.clone(), shader_defs: vec![ - "MESHLET_CULLING_PASS".into(), + "MESHLET_INSTANCE_CULLING_PASS".into(), "MESHLET_FIRST_CULLING_PASS".into(), ], - entry_point: "cull_clusters".into(), - zero_initialize_workgroup_memory: false, + ..default() }), - cull_second: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { - label: Some("meshlet_culling_second_pipeline".into()), - layout: vec![cull_layout], + second_instance_cull: pipeline_cache.queue_compute_pipeline( + ComputePipelineDescriptor { + label: Some("meshlet_second_instance_cull_pipeline".into()), + layout: vec![second_instance_cull_bind_group_layout.clone()], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..4, + }], + shader: cull_instances, + shader_defs: vec![ + "MESHLET_INSTANCE_CULLING_PASS".into(), + "MESHLET_SECOND_CULLING_PASS".into(), + ], + ..default() + }, + ), + + first_bvh_cull: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("meshlet_first_bvh_cull_pipeline".into()), + layout: vec![first_bvh_cull_bind_group_layout.clone()], push_constant_ranges: vec![PushConstantRange { stages: ShaderStages::COMPUTE, range: 0..8, }], - shader: MESHLET_CULLING_SHADER_HANDLE, + shader: cull_bvh.clone(), shader_defs: vec![ - "MESHLET_CULLING_PASS".into(), + "MESHLET_BVH_CULLING_PASS".into(), + "MESHLET_FIRST_CULLING_PASS".into(), + ], + ..default() + }), + + second_bvh_cull: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("meshlet_second_bvh_cull_pipeline".into()), + layout: vec![second_bvh_cull_bind_group_layout.clone()], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..8, + }], + shader: cull_bvh, + shader_defs: vec![ + "MESHLET_BVH_CULLING_PASS".into(), "MESHLET_SECOND_CULLING_PASS".into(), ], - entry_point: "cull_clusters".into(), - zero_initialize_workgroup_memory: false, + ..default() + }), + + first_meshlet_cull: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("meshlet_first_meshlet_cull_pipeline".into()), + layout: vec![first_meshlet_cull_bind_group_layout.clone()], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..4, + }], + shader: cull_clusters.clone(), + shader_defs: vec![ + "MESHLET_CLUSTER_CULLING_PASS".into(), + "MESHLET_FIRST_CULLING_PASS".into(), + ], + ..default() + }), + + second_meshlet_cull: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("meshlet_second_meshlet_cull_pipeline".into()), + layout: vec![second_meshlet_cull_bind_group_layout.clone()], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..4, + }], + shader: cull_clusters, + shader_defs: vec![ + "MESHLET_CLUSTER_CULLING_PASS".into(), + "MESHLET_SECOND_CULLING_PASS".into(), + ], + ..default() }), downsample_depth_first: pipeline_cache.queue_compute_pipeline( @@ -175,8 +235,8 @@ impl FromWorld for MeshletPipelines { "MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into(), "MESHLET".into(), ], - entry_point: "downsample_depth_first".into(), - zero_initialize_workgroup_memory: false, + entry_point: Some("downsample_depth_first".into()), + ..default() }, ), @@ -193,8 +253,8 @@ impl FromWorld for MeshletPipelines { "MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into(), "MESHLET".into(), ], - entry_point: "downsample_depth_second".into(), - zero_initialize_workgroup_memory: false, + entry_point: Some("downsample_depth_second".into()), + ..default() }, ), @@ -208,8 +268,8 @@ impl FromWorld for MeshletPipelines { }], shader: DOWNSAMPLE_DEPTH_SHADER_HANDLE, shader_defs: vec!["MESHLET".into()], - entry_point: "downsample_depth_first".into(), - zero_initialize_workgroup_memory: false, + entry_point: Some("downsample_depth_first".into()), + ..default() }, ), @@ -223,7 +283,7 @@ impl FromWorld for MeshletPipelines { }], shader: DOWNSAMPLE_DEPTH_SHADER_HANDLE, shader_defs: vec!["MESHLET".into()], - entry_point: "downsample_depth_second".into(), + entry_point: Some("downsample_depth_second".into()), zero_initialize_workgroup_memory: false, }, ), @@ -233,7 +293,7 @@ impl FromWorld for MeshletPipelines { label: Some("meshlet_visibility_buffer_software_raster_pipeline".into()), layout: vec![visibility_buffer_raster_layout.clone()], push_constant_ranges: vec![], - shader: MESHLET_VISIBILITY_BUFFER_SOFTWARE_RASTER_SHADER_HANDLE, + shader: visibility_buffer_software_raster.clone(), shader_defs: vec![ "MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into(), "MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into(), @@ -244,8 +304,7 @@ impl FromWorld for MeshletPipelines { } .into(), ], - entry_point: "rasterize_cluster".into(), - zero_initialize_workgroup_memory: false, + ..default() }, ), @@ -256,7 +315,7 @@ impl FromWorld for MeshletPipelines { ), layout: vec![visibility_buffer_raster_shadow_view_layout.clone()], push_constant_ranges: vec![], - shader: MESHLET_VISIBILITY_BUFFER_SOFTWARE_RASTER_SHADER_HANDLE, + shader: visibility_buffer_software_raster, shader_defs: vec![ "MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into(), if remap_1d_to_2d_dispatch_layout.is_some() { @@ -266,8 +325,7 @@ impl FromWorld for MeshletPipelines { } .into(), ], - entry_point: "rasterize_cluster".into(), - zero_initialize_workgroup_memory: false, + ..default() }, ), @@ -280,39 +338,27 @@ impl FromWorld for MeshletPipelines { range: 0..4, }], vertex: VertexState { - shader: MESHLET_VISIBILITY_BUFFER_HARDWARE_RASTER_SHADER_HANDLE, + shader: visibility_buffer_hardware_raster.clone(), shader_defs: vec![ "MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into(), "MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into(), ], - entry_point: "vertex".into(), - buffers: vec![], + ..default() }, - primitive: PrimitiveState { - topology: PrimitiveTopology::TriangleList, - strip_index_format: None, - front_face: FrontFace::Ccw, - cull_mode: Some(Face::Back), - unclipped_depth: false, - polygon_mode: PolygonMode::Fill, - conservative: false, - }, - depth_stencil: None, - multisample: MultisampleState::default(), fragment: Some(FragmentState { - shader: MESHLET_VISIBILITY_BUFFER_HARDWARE_RASTER_SHADER_HANDLE, + shader: visibility_buffer_hardware_raster.clone(), shader_defs: vec![ "MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into(), "MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into(), ], - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format: TextureFormat::R8Uint, blend: None, write_mask: ColorWrites::empty(), })], + ..default() }), - zero_initialize_workgroup_memory: false, + ..default() }, ), @@ -327,33 +373,21 @@ impl FromWorld for MeshletPipelines { range: 0..4, }], vertex: VertexState { - shader: MESHLET_VISIBILITY_BUFFER_HARDWARE_RASTER_SHADER_HANDLE, + shader: visibility_buffer_hardware_raster.clone(), shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into()], - entry_point: "vertex".into(), - buffers: vec![], + ..default() }, - primitive: PrimitiveState { - topology: PrimitiveTopology::TriangleList, - strip_index_format: None, - front_face: FrontFace::Ccw, - cull_mode: Some(Face::Back), - unclipped_depth: false, - polygon_mode: PolygonMode::Fill, - conservative: false, - }, - depth_stencil: None, - multisample: MultisampleState::default(), fragment: Some(FragmentState { - shader: MESHLET_VISIBILITY_BUFFER_HARDWARE_RASTER_SHADER_HANDLE, + shader: visibility_buffer_hardware_raster.clone(), shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into()], - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format: TextureFormat::R8Uint, blend: None, write_mask: ColorWrites::empty(), })], + ..default() }), - zero_initialize_workgroup_memory: false, + ..default() }, ), @@ -369,41 +403,27 @@ impl FromWorld for MeshletPipelines { range: 0..4, }], vertex: VertexState { - shader: MESHLET_VISIBILITY_BUFFER_HARDWARE_RASTER_SHADER_HANDLE, + shader: visibility_buffer_hardware_raster.clone(), shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into()], - entry_point: "vertex".into(), - buffers: vec![], + ..default() }, - primitive: PrimitiveState { - topology: PrimitiveTopology::TriangleList, - strip_index_format: None, - front_face: FrontFace::Ccw, - cull_mode: Some(Face::Back), - unclipped_depth: true, - polygon_mode: PolygonMode::Fill, - conservative: false, - }, - depth_stencil: None, - multisample: MultisampleState::default(), fragment: Some(FragmentState { - shader: MESHLET_VISIBILITY_BUFFER_HARDWARE_RASTER_SHADER_HANDLE, + shader: visibility_buffer_hardware_raster, shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into()], - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format: TextureFormat::R8Uint, blend: None, write_mask: ColorWrites::empty(), })], + ..default() }), - zero_initialize_workgroup_memory: false, + ..default() }), resolve_depth: pipeline_cache.queue_render_pipeline(RenderPipelineDescriptor { label: Some("meshlet_resolve_depth_pipeline".into()), layout: vec![resolve_depth_layout], - push_constant_ranges: vec![], vertex: vertex_state.clone(), - primitive: PrimitiveState::default(), depth_stencil: Some(DepthStencilState { format: CORE_3D_DEPTH_FORMAT, depth_write_enabled: true, @@ -411,23 +431,20 @@ impl FromWorld for MeshletPipelines { stencil: StencilState::default(), bias: DepthBiasState::default(), }), - multisample: MultisampleState::default(), fragment: Some(FragmentState { - shader: MESHLET_RESOLVE_RENDER_TARGETS_SHADER_HANDLE, + shader: resolve_render_targets.clone(), shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into()], - entry_point: "resolve_depth".into(), - targets: vec![], + entry_point: Some("resolve_depth".into()), + ..default() }), - zero_initialize_workgroup_memory: false, + ..default() }), resolve_depth_shadow_view: pipeline_cache.queue_render_pipeline( RenderPipelineDescriptor { label: Some("meshlet_resolve_depth_pipeline".into()), layout: vec![resolve_depth_shadow_view_layout], - push_constant_ranges: vec![], vertex: vertex_state.clone(), - primitive: PrimitiveState::default(), depth_stencil: Some(DepthStencilState { format: CORE_3D_DEPTH_FORMAT, depth_write_enabled: true, @@ -435,14 +452,12 @@ impl FromWorld for MeshletPipelines { stencil: StencilState::default(), bias: DepthBiasState::default(), }), - multisample: MultisampleState::default(), fragment: Some(FragmentState { - shader: MESHLET_RESOLVE_RENDER_TARGETS_SHADER_HANDLE, - shader_defs: vec![], - entry_point: "resolve_depth".into(), - targets: vec![], + shader: resolve_render_targets.clone(), + entry_point: Some("resolve_depth".into()), + ..default() }), - zero_initialize_workgroup_memory: false, + ..default() }, ), @@ -450,7 +465,6 @@ impl FromWorld for MeshletPipelines { RenderPipelineDescriptor { label: Some("meshlet_resolve_material_depth_pipeline".into()), layout: vec![resolve_material_depth_layout], - push_constant_ranges: vec![], vertex: vertex_state, primitive: PrimitiveState::default(), depth_stencil: Some(DepthStencilState { @@ -460,17 +474,29 @@ impl FromWorld for MeshletPipelines { stencil: StencilState::default(), bias: DepthBiasState::default(), }), - multisample: MultisampleState::default(), fragment: Some(FragmentState { - shader: MESHLET_RESOLVE_RENDER_TARGETS_SHADER_HANDLE, + shader: resolve_render_targets, shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into()], - entry_point: "resolve_material_depth".into(), + entry_point: Some("resolve_material_depth".into()), targets: vec![], }), - zero_initialize_workgroup_memory: false, + ..default() }, ), + fill_counts: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("meshlet_fill_counts_pipeline".into()), + layout: vec![fill_counts_layout], + shader: fill_counts, + shader_defs: vec![if remap_1d_to_2d_dispatch_layout.is_some() { + "MESHLET_2D_DISPATCH" + } else { + "" + } + .into()], + ..default() + }), + remap_1d_to_2d_dispatch: remap_1d_to_2d_dispatch_layout.map(|layout| { pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { label: Some("meshlet_remap_1d_to_2d_dispatch_pipeline".into()), @@ -479,12 +505,12 @@ impl FromWorld for MeshletPipelines { stages: ShaderStages::COMPUTE, range: 0..4, }], - shader: MESHLET_REMAP_1D_TO_2D_DISPATCH_SHADER_HANDLE, - shader_defs: vec![], - entry_point: "remap_dispatch".into(), - zero_initialize_workgroup_memory: false, + shader: remap_1d_to_2d_dispatch, + ..default() }) }), + + meshlet_mesh_material, } } } @@ -504,6 +530,9 @@ impl MeshletPipelines { &ComputePipeline, &ComputePipeline, &ComputePipeline, + &ComputePipeline, + &ComputePipeline, + &ComputePipeline, &RenderPipeline, &RenderPipeline, &RenderPipeline, @@ -511,15 +540,19 @@ impl MeshletPipelines { &RenderPipeline, &RenderPipeline, Option<&ComputePipeline>, + &ComputePipeline, )> { let pipeline_cache = world.get_resource::()?; let pipeline = world.get_resource::()?; Some(( - pipeline_cache.get_compute_pipeline(pipeline.fill_cluster_buffers)?, pipeline_cache.get_compute_pipeline(pipeline.clear_visibility_buffer)?, pipeline_cache.get_compute_pipeline(pipeline.clear_visibility_buffer_shadow_view)?, - pipeline_cache.get_compute_pipeline(pipeline.cull_first)?, - pipeline_cache.get_compute_pipeline(pipeline.cull_second)?, + pipeline_cache.get_compute_pipeline(pipeline.first_instance_cull)?, + pipeline_cache.get_compute_pipeline(pipeline.second_instance_cull)?, + pipeline_cache.get_compute_pipeline(pipeline.first_bvh_cull)?, + pipeline_cache.get_compute_pipeline(pipeline.second_bvh_cull)?, + pipeline_cache.get_compute_pipeline(pipeline.first_meshlet_cull)?, + pipeline_cache.get_compute_pipeline(pipeline.second_meshlet_cull)?, pipeline_cache.get_compute_pipeline(pipeline.downsample_depth_first)?, pipeline_cache.get_compute_pipeline(pipeline.downsample_depth_second)?, pipeline_cache.get_compute_pipeline(pipeline.downsample_depth_first_shadow_view)?, @@ -540,6 +573,7 @@ impl MeshletPipelines { Some(id) => Some(pipeline_cache.get_compute_pipeline(id)?), None => None, }, + pipeline_cache.get_compute_pipeline(pipeline.fill_counts)?, )) } } diff --git a/crates/bevy_pbr/src/meshlet/remap_1d_to_2d_dispatch.wgsl b/crates/bevy_pbr/src/meshlet/remap_1d_to_2d_dispatch.wgsl index fc98443634..b9970c42b4 100644 --- a/crates/bevy_pbr/src/meshlet/remap_1d_to_2d_dispatch.wgsl +++ b/crates/bevy_pbr/src/meshlet/remap_1d_to_2d_dispatch.wgsl @@ -13,11 +13,12 @@ var max_compute_workgroups_per_dimension: u32; @compute @workgroup_size(1, 1, 1) fn remap_dispatch() { - meshlet_software_raster_cluster_count = meshlet_software_raster_indirect_args.x; + let cluster_count = meshlet_software_raster_indirect_args.x; - if meshlet_software_raster_cluster_count > max_compute_workgroups_per_dimension { - let n = u32(ceil(sqrt(f32(meshlet_software_raster_cluster_count)))); + if cluster_count > max_compute_workgroups_per_dimension { + let n = u32(ceil(sqrt(f32(cluster_count)))); meshlet_software_raster_indirect_args.x = n; meshlet_software_raster_indirect_args.y = n; + meshlet_software_raster_cluster_count = cluster_count; } } diff --git a/crates/bevy_pbr/src/meshlet/resolve_render_targets.wgsl b/crates/bevy_pbr/src/meshlet/resolve_render_targets.wgsl index eaa4eed6c4..6fef0cc227 100644 --- a/crates/bevy_pbr/src/meshlet/resolve_render_targets.wgsl +++ b/crates/bevy_pbr/src/meshlet/resolve_render_targets.wgsl @@ -1,11 +1,12 @@ #import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput +#import bevy_pbr::meshlet_bindings::InstancedOffset #ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT @group(0) @binding(0) var meshlet_visibility_buffer: texture_storage_2d; #else @group(0) @binding(0) var meshlet_visibility_buffer: texture_storage_2d; #endif -@group(0) @binding(1) var meshlet_cluster_instance_ids: array; // Per cluster +@group(0) @binding(1) var meshlet_raster_clusters: array; // Per cluster @group(0) @binding(2) var meshlet_instance_material_ids: array; // Per entity instance /// This pass writes out the depth texture. @@ -33,7 +34,7 @@ fn resolve_material_depth(in: FullscreenVertexOutput) -> @builtin(frag_depth) f3 if depth == 0lu { discard; } let cluster_id = u32(visibility) >> 7u; - let instance_id = meshlet_cluster_instance_ids[cluster_id]; + let instance_id = meshlet_raster_clusters[cluster_id].instance_id; let material_id = meshlet_instance_material_ids[instance_id]; return f32(material_id) / 65535.0; } diff --git a/crates/bevy_pbr/src/meshlet/resource_manager.rs b/crates/bevy_pbr/src/meshlet/resource_manager.rs index d33752d426..dacab4afc4 100644 --- a/crates/bevy_pbr/src/meshlet/resource_manager.rs +++ b/crates/bevy_pbr/src/meshlet/resource_manager.rs @@ -1,6 +1,5 @@ use super::{instance_manager::InstanceManager, meshlet_mesh_manager::MeshletMeshManager}; use crate::ShadowView; -use alloc::sync::Arc; use bevy_core_pipeline::{ core_3d::Camera3d, experimental::mip_generation::{self, ViewDepthPyramid}, @@ -13,6 +12,7 @@ use bevy_ecs::{ resource::Resource, system::{Commands, Query, Res, ResMut}, }; +use bevy_image::ToExtents; use bevy_math::{UVec2, Vec4Swizzles}; use bevy_render::{ render_resource::*, @@ -21,25 +21,26 @@ use bevy_render::{ view::{ExtractedView, RenderLayers, ViewUniform, ViewUniforms}, }; use binding_types::*; -use core::{iter, sync::atomic::AtomicBool}; -use encase::internal::WriteInto; +use core::iter; /// Manages per-view and per-cluster GPU resources for [`super::MeshletPlugin`]. #[derive(Resource)] pub struct ResourceManager { /// Intermediate buffer of cluster IDs for use with rasterizing the visibility buffer visibility_buffer_raster_clusters: Buffer, + /// Intermediate buffer of previous counts of clusters in rasterizer buckets + pub visibility_buffer_raster_cluster_prev_counts: Buffer, /// Intermediate buffer of count of clusters to software rasterize software_raster_cluster_count: Buffer, - /// Rightmost slot index of [`Self::visibility_buffer_raster_clusters`] - raster_cluster_rightmost_slot: u32, + /// BVH traversal queues + bvh_traversal_queues: [Buffer; 2], + /// Cluster cull candidate queue + cluster_cull_candidate_queue: Buffer, + /// Rightmost slot index of [`Self::visibility_buffer_raster_clusters`], [`Self::bvh_traversal_queues`], and [`Self::cluster_cull_candidate_queue`] + cull_queue_rightmost_slot: u32, - /// Per-cluster instance ID - cluster_instance_ids: Option, - /// Per-cluster meshlet ID - cluster_meshlet_ids: Option, - /// Per-cluster bitmask of whether or not it's a candidate for the second raster pass - second_pass_candidates_buffer: Option, + /// Second pass instance candidates + second_pass_candidates: Option, /// Sampler for a depth pyramid depth_pyramid_sampler: Sampler, /// Dummy texture view for binding depth pyramids with less than the maximum amount of mips @@ -49,10 +50,14 @@ pub struct ResourceManager { previous_depth_pyramids: EntityHashMap, // Bind group layouts - pub fill_cluster_buffers_bind_group_layout: BindGroupLayout, pub clear_visibility_buffer_bind_group_layout: BindGroupLayout, pub clear_visibility_buffer_shadow_view_bind_group_layout: BindGroupLayout, - pub culling_bind_group_layout: BindGroupLayout, + pub first_instance_cull_bind_group_layout: BindGroupLayout, + pub second_instance_cull_bind_group_layout: BindGroupLayout, + pub first_bvh_cull_bind_group_layout: BindGroupLayout, + pub second_bvh_cull_bind_group_layout: BindGroupLayout, + pub first_meshlet_cull_bind_group_layout: BindGroupLayout, + pub second_meshlet_cull_bind_group_layout: BindGroupLayout, pub visibility_buffer_raster_bind_group_layout: BindGroupLayout, pub visibility_buffer_raster_shadow_view_bind_group_layout: BindGroupLayout, pub downsample_depth_bind_group_layout: BindGroupLayout, @@ -61,6 +66,7 @@ pub struct ResourceManager { pub resolve_depth_shadow_view_bind_group_layout: BindGroupLayout, pub resolve_material_depth_bind_group_layout: BindGroupLayout, pub material_shade_bind_group_layout: BindGroupLayout, + pub fill_counts_bind_group_layout: BindGroupLayout, pub remap_1d_to_2d_dispatch_bind_group_layout: Option, } @@ -68,25 +74,53 @@ impl ResourceManager { pub fn new(cluster_buffer_slots: u32, render_device: &RenderDevice) -> Self { let needs_dispatch_remap = cluster_buffer_slots > render_device.limits().max_compute_workgroups_per_dimension; + // The IDs are a (u32, u32) of instance and index. + let cull_queue_size = 2 * cluster_buffer_slots as u64 * size_of::() as u64; Self { visibility_buffer_raster_clusters: render_device.create_buffer(&BufferDescriptor { label: Some("meshlet_visibility_buffer_raster_clusters"), - size: cluster_buffer_slots as u64 * size_of::() as u64, + size: cull_queue_size, usage: BufferUsages::STORAGE, mapped_at_creation: false, }), + visibility_buffer_raster_cluster_prev_counts: render_device.create_buffer( + &BufferDescriptor { + label: Some("meshlet_visibility_buffer_raster_cluster_prev_counts"), + size: size_of::() as u64 * 2, + usage: BufferUsages::STORAGE | BufferUsages::COPY_DST, + mapped_at_creation: false, + }, + ), software_raster_cluster_count: render_device.create_buffer(&BufferDescriptor { label: Some("meshlet_software_raster_cluster_count"), size: size_of::() as u64, usage: BufferUsages::STORAGE, mapped_at_creation: false, }), - raster_cluster_rightmost_slot: cluster_buffer_slots - 1, + bvh_traversal_queues: [ + render_device.create_buffer(&BufferDescriptor { + label: Some("meshlet_bvh_traversal_queue_0"), + size: cull_queue_size, + usage: BufferUsages::STORAGE, + mapped_at_creation: false, + }), + render_device.create_buffer(&BufferDescriptor { + label: Some("meshlet_bvh_traversal_queue_1"), + size: cull_queue_size, + usage: BufferUsages::STORAGE, + mapped_at_creation: false, + }), + ], + cluster_cull_candidate_queue: render_device.create_buffer(&BufferDescriptor { + label: Some("meshlet_cluster_cull_candidate_queue"), + size: cull_queue_size, + usage: BufferUsages::STORAGE, + mapped_at_creation: false, + }), + cull_queue_rightmost_slot: cluster_buffer_slots - 1, - cluster_instance_ids: None, - cluster_meshlet_ids: None, - second_pass_candidates_buffer: None, + second_pass_candidates: None, depth_pyramid_sampler: render_device.create_sampler(&SamplerDescriptor { label: Some("meshlet_depth_pyramid_sampler"), ..SamplerDescriptor::default() @@ -100,19 +134,6 @@ impl ResourceManager { previous_depth_pyramids: EntityHashMap::default(), // TODO: Buffer min sizes - fill_cluster_buffers_bind_group_layout: render_device.create_bind_group_layout( - "meshlet_fill_cluster_buffers_bind_group_layout", - &BindGroupLayoutEntries::sequential( - ShaderStages::COMPUTE, - ( - storage_buffer_read_only_sized(false, None), - storage_buffer_read_only_sized(false, None), - storage_buffer_sized(false, None), - storage_buffer_sized(false, None), - storage_buffer_sized(false, None), - ), - ), - ), clear_visibility_buffer_bind_group_layout: render_device.create_bind_group_layout( "meshlet_clear_visibility_buffer_bind_group_layout", &BindGroupLayoutEntries::single( @@ -128,24 +149,131 @@ impl ResourceManager { texture_storage_2d(TextureFormat::R32Uint, StorageTextureAccess::WriteOnly), ), ), - culling_bind_group_layout: render_device.create_bind_group_layout( - "meshlet_culling_bind_group_layout", + first_instance_cull_bind_group_layout: render_device.create_bind_group_layout( + "meshlet_first_instance_culling_bind_group_layout", &BindGroupLayoutEntries::sequential( ShaderStages::COMPUTE, ( - storage_buffer_read_only_sized(false, None), - storage_buffer_read_only_sized(false, None), - storage_buffer_read_only_sized(false, None), - storage_buffer_read_only_sized(false, None), - storage_buffer_read_only_sized(false, None), - storage_buffer_read_only_sized(false, None), - storage_buffer_sized(false, None), - storage_buffer_sized(false, None), - storage_buffer_sized(false, None), - storage_buffer_sized(false, None), texture_2d(TextureSampleType::Float { filterable: false }), uniform_buffer::(true), uniform_buffer::(true), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + ), + ), + ), + second_instance_cull_bind_group_layout: render_device.create_bind_group_layout( + "meshlet_second_instance_culling_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + texture_2d(TextureSampleType::Float { filterable: false }), + uniform_buffer::(true), + uniform_buffer::(true), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + ), + ), + ), + first_bvh_cull_bind_group_layout: render_device.create_bind_group_layout( + "meshlet_first_bvh_culling_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + texture_2d(TextureSampleType::Float { filterable: false }), + uniform_buffer::(true), + uniform_buffer::(true), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + ), + ), + ), + second_bvh_cull_bind_group_layout: render_device.create_bind_group_layout( + "meshlet_second_bvh_culling_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + texture_2d(TextureSampleType::Float { filterable: false }), + uniform_buffer::(true), + uniform_buffer::(true), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + ), + ), + ), + first_meshlet_cull_bind_group_layout: render_device.create_bind_group_layout( + "meshlet_first_meshlet_culling_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + texture_2d(TextureSampleType::Float { filterable: false }), + uniform_buffer::(true), + uniform_buffer::(true), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + ), + ), + ), + second_meshlet_cull_bind_group_layout: render_device.create_bind_group_layout( + "meshlet_second_meshlet_culling_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + texture_2d(TextureSampleType::Float { filterable: false }), + uniform_buffer::(true), + uniform_buffer::(true), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), ), ), ), @@ -215,7 +343,6 @@ impl ResourceManager { storage_buffer_read_only_sized(false, None), storage_buffer_read_only_sized(false, None), storage_buffer_read_only_sized(false, None), - storage_buffer_read_only_sized(false, None), texture_storage_2d(TextureFormat::R64Uint, StorageTextureAccess::Atomic), uniform_buffer::(true), ), @@ -234,7 +361,6 @@ impl ResourceManager { storage_buffer_read_only_sized(false, None), storage_buffer_read_only_sized(false, None), storage_buffer_read_only_sized(false, None), - storage_buffer_read_only_sized(false, None), texture_storage_2d( TextureFormat::R32Uint, StorageTextureAccess::Atomic, @@ -281,10 +407,35 @@ impl ResourceManager { storage_buffer_read_only_sized(false, None), storage_buffer_read_only_sized(false, None), storage_buffer_read_only_sized(false, None), - storage_buffer_read_only_sized(false, None), ), ), ), + fill_counts_bind_group_layout: if needs_dispatch_remap { + render_device.create_bind_group_layout( + "meshlet_fill_counts_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + ), + ), + ) + } else { + render_device.create_bind_group_layout( + "meshlet_fill_counts_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + ), + ), + ) + }, remap_1d_to_2d_dispatch_bind_group_layout: needs_dispatch_remap.then(|| { render_device.create_bind_group_layout( "meshlet_remap_1d_to_2d_dispatch_bind_group_layout", @@ -306,57 +457,56 @@ impl ResourceManager { #[derive(Component)] pub struct MeshletViewResources { pub scene_instance_count: u32, - pub scene_cluster_count: u32, - pub second_pass_candidates_buffer: Buffer, + pub rightmost_slot: u32, + pub max_bvh_depth: u32, instance_visibility: Buffer, pub dummy_render_target: CachedTexture, pub visibility_buffer: CachedTexture, - pub visibility_buffer_software_raster_indirect_args_first: Buffer, - pub visibility_buffer_software_raster_indirect_args_second: Buffer, - pub visibility_buffer_hardware_raster_indirect_args_first: Buffer, - pub visibility_buffer_hardware_raster_indirect_args_second: Buffer, + pub second_pass_count: Buffer, + pub second_pass_dispatch: Buffer, + pub second_pass_candidates: Buffer, + pub first_bvh_cull_count_front: Buffer, + pub first_bvh_cull_dispatch_front: Buffer, + pub first_bvh_cull_count_back: Buffer, + pub first_bvh_cull_dispatch_back: Buffer, + pub first_bvh_cull_queue: Buffer, + pub second_bvh_cull_count_front: Buffer, + pub second_bvh_cull_dispatch_front: Buffer, + pub second_bvh_cull_count_back: Buffer, + pub second_bvh_cull_dispatch_back: Buffer, + pub second_bvh_cull_queue: Buffer, + pub front_meshlet_cull_count: Buffer, + pub front_meshlet_cull_dispatch: Buffer, + pub back_meshlet_cull_count: Buffer, + pub back_meshlet_cull_dispatch: Buffer, + pub meshlet_cull_queue: Buffer, + pub visibility_buffer_software_raster_indirect_args: Buffer, + pub visibility_buffer_hardware_raster_indirect_args: Buffer, pub depth_pyramid: ViewDepthPyramid, previous_depth_pyramid: TextureView, pub material_depth: Option, pub view_size: UVec2, - pub raster_cluster_rightmost_slot: u32, not_shadow_view: bool, } #[derive(Component)] pub struct MeshletViewBindGroups { - pub first_node: Arc, - pub fill_cluster_buffers: BindGroup, pub clear_visibility_buffer: BindGroup, - pub culling_first: BindGroup, - pub culling_second: BindGroup, + pub first_instance_cull: BindGroup, + pub second_instance_cull: BindGroup, + pub first_bvh_cull_ping: BindGroup, + pub first_bvh_cull_pong: BindGroup, + pub second_bvh_cull_ping: BindGroup, + pub second_bvh_cull_pong: BindGroup, + pub first_meshlet_cull: BindGroup, + pub second_meshlet_cull: BindGroup, pub downsample_depth: BindGroup, pub visibility_buffer_raster: BindGroup, pub resolve_depth: BindGroup, pub resolve_material_depth: Option, pub material_shade: Option, - pub remap_1d_to_2d_dispatch: Option<(BindGroup, BindGroup)>, -} - -// TODO: Try using Queue::write_buffer_with() in queue_meshlet_mesh_upload() to reduce copies -fn upload_storage_buffer( - buffer: &mut StorageBuffer>, - render_device: &RenderDevice, - render_queue: &RenderQueue, -) where - Vec: WriteInto, -{ - let inner = buffer.buffer(); - let capacity = inner.map_or(0, |b| b.size()); - let size = buffer.get().size().get() as BufferAddress; - - if capacity >= size { - let inner = inner.unwrap(); - let bytes = bytemuck::must_cast_slice(buffer.get().as_slice()); - render_queue.write_buffer(inner, 0, bytes); - } else { - buffer.write_buffer(render_device, render_queue); - } + pub remap_1d_to_2d_dispatch: Option, + pub fill_counts: BindGroup, } // TODO: Cache things per-view and skip running this system / optimize this system @@ -374,7 +524,7 @@ pub fn prepare_meshlet_per_frame_resources( render_device: Res, mut commands: Commands, ) { - if instance_manager.scene_cluster_count == 0 { + if instance_manager.scene_instance_count == 0 { return; } @@ -384,41 +534,22 @@ pub fn prepare_meshlet_per_frame_resources( instance_manager .instance_uniforms .write_buffer(&render_device, &render_queue); - upload_storage_buffer( - &mut instance_manager.instance_material_ids, - &render_device, - &render_queue, - ); - upload_storage_buffer( - &mut instance_manager.instance_meshlet_counts, - &render_device, - &render_queue, - ); - upload_storage_buffer( - &mut instance_manager.instance_meshlet_slice_starts, - &render_device, - &render_queue, - ); + instance_manager + .instance_aabbs + .write_buffer(&render_device, &render_queue); + instance_manager + .instance_material_ids + .write_buffer(&render_device, &render_queue); + instance_manager + .instance_bvh_root_nodes + .write_buffer(&render_device, &render_queue); - let needed_buffer_size = 4 * instance_manager.scene_cluster_count as u64; - match &mut resource_manager.cluster_instance_ids { + let needed_buffer_size = 4 * instance_manager.scene_instance_count as u64; + let second_pass_candidates = match &mut resource_manager.second_pass_candidates { Some(buffer) if buffer.size() >= needed_buffer_size => buffer.clone(), slot => { let buffer = render_device.create_buffer(&BufferDescriptor { - label: Some("meshlet_cluster_instance_ids"), - size: needed_buffer_size, - usage: BufferUsages::STORAGE, - mapped_at_creation: false, - }); - *slot = Some(buffer.clone()); - buffer - } - }; - match &mut resource_manager.cluster_meshlet_ids { - Some(buffer) if buffer.size() >= needed_buffer_size => buffer.clone(), - slot => { - let buffer = render_device.create_buffer(&BufferDescriptor { - label: Some("meshlet_cluster_meshlet_ids"), + label: Some("meshlet_second_pass_candidates"), size: needed_buffer_size, usage: BufferUsages::STORAGE, mapped_at_creation: false, @@ -428,8 +559,6 @@ pub fn prepare_meshlet_per_frame_resources( } }; - let needed_buffer_size = - instance_manager.scene_cluster_count.div_ceil(u32::BITS) as u64 * size_of::() as u64; for (view_entity, view, render_layers, (_, shadow_view)) in &views { let not_shadow_view = shadow_view.is_none(); @@ -460,34 +589,15 @@ pub fn prepare_meshlet_per_frame_resources( vec[index] |= 1 << bit; } } - upload_storage_buffer(instance_visibility, &render_device, &render_queue); + instance_visibility.write_buffer(&render_device, &render_queue); let instance_visibility = instance_visibility.buffer().unwrap().clone(); - let second_pass_candidates_buffer = - match &mut resource_manager.second_pass_candidates_buffer { - Some(buffer) if buffer.size() >= needed_buffer_size => buffer.clone(), - slot => { - let buffer = render_device.create_buffer(&BufferDescriptor { - label: Some("meshlet_second_pass_candidates"), - size: needed_buffer_size, - usage: BufferUsages::STORAGE | BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - *slot = Some(buffer.clone()); - buffer - } - }; - // TODO: Remove this once wgpu allows render passes with no attachments let dummy_render_target = texture_cache.get( &render_device, TextureDescriptor { label: Some("meshlet_dummy_render_target"), - size: Extent3d { - width: view.viewport.z, - height: view.viewport.w, - depth_or_array_layers: 1, - }, + size: view.viewport.zw().to_extents(), mip_level_count: 1, sample_count: 1, dimension: TextureDimension::D2, @@ -501,11 +611,7 @@ pub fn prepare_meshlet_per_frame_resources( &render_device, TextureDescriptor { label: Some("meshlet_visibility_buffer"), - size: Extent3d { - width: view.viewport.z, - height: view.viewport.w, - depth_or_array_layers: 1, - }, + size: view.viewport.zw().to_extents(), mip_level_count: 1, sample_count: 1, dimension: TextureDimension::D2, @@ -519,34 +625,102 @@ pub fn prepare_meshlet_per_frame_resources( }, ); - let visibility_buffer_software_raster_indirect_args_first = render_device - .create_buffer_with_data(&BufferInitDescriptor { - label: Some("meshlet_visibility_buffer_software_raster_indirect_args_first"), + let second_pass_count = render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_second_pass_count"), + contents: bytemuck::bytes_of(&0u32), + usage: BufferUsages::STORAGE, + }); + let second_pass_dispatch = render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_second_pass_dispatch"), + contents: DispatchIndirectArgs { x: 0, y: 1, z: 1 }.as_bytes(), + usage: BufferUsages::STORAGE | BufferUsages::INDIRECT, + }); + + let first_bvh_cull_count_front = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_first_bvh_cull_count_front"), + contents: bytemuck::bytes_of(&0u32), + usage: BufferUsages::STORAGE | BufferUsages::COPY_DST, + }); + let first_bvh_cull_dispatch_front = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_first_bvh_cull_dispatch_front"), + contents: DispatchIndirectArgs { x: 0, y: 1, z: 1 }.as_bytes(), + usage: BufferUsages::STORAGE | BufferUsages::INDIRECT | BufferUsages::COPY_DST, + }); + let first_bvh_cull_count_back = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_first_bvh_cull_count_back"), + contents: bytemuck::bytes_of(&0u32), + usage: BufferUsages::STORAGE | BufferUsages::COPY_DST, + }); + let first_bvh_cull_dispatch_back = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_first_bvh_cull_dispatch_back"), + contents: DispatchIndirectArgs { x: 0, y: 1, z: 1 }.as_bytes(), + usage: BufferUsages::STORAGE | BufferUsages::INDIRECT | BufferUsages::COPY_DST, + }); + + let second_bvh_cull_count_front = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_second_bvh_cull_count_front"), + contents: bytemuck::bytes_of(&0u32), + usage: BufferUsages::STORAGE | BufferUsages::COPY_DST, + }); + let second_bvh_cull_dispatch_front = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_second_bvh_cull_dispatch_front"), + contents: DispatchIndirectArgs { x: 0, y: 1, z: 1 }.as_bytes(), + usage: BufferUsages::STORAGE | BufferUsages::INDIRECT | BufferUsages::COPY_DST, + }); + let second_bvh_cull_count_back = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_second_bvh_cull_count_back"), + contents: bytemuck::bytes_of(&0u32), + usage: BufferUsages::STORAGE | BufferUsages::COPY_DST, + }); + let second_bvh_cull_dispatch_back = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_second_bvh_cull_dispatch_back"), + contents: DispatchIndirectArgs { x: 0, y: 1, z: 1 }.as_bytes(), + usage: BufferUsages::STORAGE | BufferUsages::INDIRECT | BufferUsages::COPY_DST, + }); + + let front_meshlet_cull_count = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_front_meshlet_cull_count"), + contents: bytemuck::bytes_of(&0u32), + usage: BufferUsages::STORAGE, + }); + let front_meshlet_cull_dispatch = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_front_meshlet_cull_dispatch"), contents: DispatchIndirectArgs { x: 0, y: 1, z: 1 }.as_bytes(), usage: BufferUsages::STORAGE | BufferUsages::INDIRECT, }); - let visibility_buffer_software_raster_indirect_args_second = render_device - .create_buffer_with_data(&BufferInitDescriptor { - label: Some("visibility_buffer_software_raster_indirect_args_second"), + let back_meshlet_cull_count = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_back_meshlet_cull_count"), + contents: bytemuck::bytes_of(&0u32), + usage: BufferUsages::STORAGE, + }); + let back_meshlet_cull_dispatch = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_back_meshlet_cull_dispatch"), contents: DispatchIndirectArgs { x: 0, y: 1, z: 1 }.as_bytes(), usage: BufferUsages::STORAGE | BufferUsages::INDIRECT, }); - let visibility_buffer_hardware_raster_indirect_args_first = render_device + let visibility_buffer_software_raster_indirect_args = render_device .create_buffer_with_data(&BufferInitDescriptor { - label: Some("meshlet_visibility_buffer_hardware_raster_indirect_args_first"), - contents: DrawIndirectArgs { - vertex_count: 128 * 3, - instance_count: 0, - first_vertex: 0, - first_instance: 0, - } - .as_bytes(), + label: Some("meshlet_visibility_buffer_software_raster_indirect_args"), + contents: DispatchIndirectArgs { x: 0, y: 1, z: 1 }.as_bytes(), usage: BufferUsages::STORAGE | BufferUsages::INDIRECT, }); - let visibility_buffer_hardware_raster_indirect_args_second = render_device + + let visibility_buffer_hardware_raster_indirect_args = render_device .create_buffer_with_data(&BufferInitDescriptor { - label: Some("visibility_buffer_hardware_raster_indirect_args_second"), + label: Some("meshlet_visibility_buffer_hardware_raster_indirect_args"), contents: DrawIndirectArgs { vertex_count: 128 * 3, instance_count: 0, @@ -577,11 +751,7 @@ pub fn prepare_meshlet_per_frame_resources( let material_depth = TextureDescriptor { label: Some("meshlet_material_depth"), - size: Extent3d { - width: view.viewport.z, - height: view.viewport.w, - depth_or_array_layers: 1, - }, + size: view.viewport.zw().to_extents(), mip_level_count: 1, sample_count: 1, dimension: TextureDimension::D2, @@ -592,21 +762,36 @@ pub fn prepare_meshlet_per_frame_resources( commands.entity(view_entity).insert(MeshletViewResources { scene_instance_count: instance_manager.scene_instance_count, - scene_cluster_count: instance_manager.scene_cluster_count, - second_pass_candidates_buffer, + rightmost_slot: resource_manager.cull_queue_rightmost_slot, + max_bvh_depth: instance_manager.max_bvh_depth, instance_visibility, dummy_render_target, visibility_buffer, - visibility_buffer_software_raster_indirect_args_first, - visibility_buffer_software_raster_indirect_args_second, - visibility_buffer_hardware_raster_indirect_args_first, - visibility_buffer_hardware_raster_indirect_args_second, + second_pass_count, + second_pass_dispatch, + second_pass_candidates: second_pass_candidates.clone(), + first_bvh_cull_count_front, + first_bvh_cull_dispatch_front, + first_bvh_cull_count_back, + first_bvh_cull_dispatch_back, + first_bvh_cull_queue: resource_manager.bvh_traversal_queues[0].clone(), + second_bvh_cull_count_front, + second_bvh_cull_dispatch_front, + second_bvh_cull_count_back, + second_bvh_cull_dispatch_back, + second_bvh_cull_queue: resource_manager.bvh_traversal_queues[1].clone(), + front_meshlet_cull_count, + front_meshlet_cull_dispatch, + back_meshlet_cull_count, + back_meshlet_cull_dispatch, + meshlet_cull_queue: resource_manager.cluster_cull_candidate_queue.clone(), + visibility_buffer_software_raster_indirect_args, + visibility_buffer_hardware_raster_indirect_args, depth_pyramid, previous_depth_pyramid, material_depth: not_shadow_view .then(|| texture_cache.get(&render_device, material_depth)), view_size: view.viewport.zw(), - raster_cluster_rightmost_slot: resource_manager.raster_cluster_rightmost_slot, not_shadow_view, }); } @@ -622,49 +807,15 @@ pub fn prepare_meshlet_view_bind_groups( render_device: Res, mut commands: Commands, ) { - let ( - Some(cluster_instance_ids), - Some(cluster_meshlet_ids), - Some(view_uniforms), - Some(previous_view_uniforms), - ) = ( - resource_manager.cluster_instance_ids.as_ref(), - resource_manager.cluster_meshlet_ids.as_ref(), + let (Some(view_uniforms), Some(previous_view_uniforms)) = ( view_uniforms.uniforms.binding(), previous_view_uniforms.uniforms.binding(), - ) - else { + ) else { return; }; - let first_node = Arc::new(AtomicBool::new(true)); - - let fill_cluster_buffers_global_cluster_count = - render_device.create_buffer(&BufferDescriptor { - label: Some("meshlet_fill_cluster_buffers_global_cluster_count"), - size: 4, - usage: BufferUsages::STORAGE, - mapped_at_creation: false, - }); - // TODO: Some of these bind groups can be reused across multiple views for (view_entity, view_resources) in &views { - let entries = BindGroupEntries::sequential(( - instance_manager.instance_meshlet_counts.binding().unwrap(), - instance_manager - .instance_meshlet_slice_starts - .binding() - .unwrap(), - cluster_instance_ids.as_entire_binding(), - cluster_meshlet_ids.as_entire_binding(), - fill_cluster_buffers_global_cluster_count.as_entire_binding(), - )); - let fill_cluster_buffers = render_device.create_bind_group( - "meshlet_fill_cluster_buffers", - &resource_manager.fill_cluster_buffers_bind_group_layout, - &entries, - ); - let clear_visibility_buffer = render_device.create_bind_group( "meshlet_clear_visibility_buffer_bind_group", if view_resources.not_shadow_view { @@ -675,62 +826,241 @@ pub fn prepare_meshlet_view_bind_groups( &BindGroupEntries::single(&view_resources.visibility_buffer.default_view), ); - let entries = BindGroupEntries::sequential(( - cluster_meshlet_ids.as_entire_binding(), - meshlet_mesh_manager.meshlet_bounding_spheres.binding(), - meshlet_mesh_manager.meshlet_simplification_errors.binding(), - cluster_instance_ids.as_entire_binding(), - instance_manager.instance_uniforms.binding().unwrap(), - view_resources.instance_visibility.as_entire_binding(), - view_resources - .second_pass_candidates_buffer - .as_entire_binding(), - view_resources - .visibility_buffer_software_raster_indirect_args_first - .as_entire_binding(), - view_resources - .visibility_buffer_hardware_raster_indirect_args_first - .as_entire_binding(), - resource_manager - .visibility_buffer_raster_clusters - .as_entire_binding(), - &view_resources.previous_depth_pyramid, - view_uniforms.clone(), - previous_view_uniforms.clone(), - )); - let culling_first = render_device.create_bind_group( - "meshlet_culling_first_bind_group", - &resource_manager.culling_bind_group_layout, - &entries, + let first_instance_cull = render_device.create_bind_group( + "meshlet_first_instance_cull_bind_group", + &resource_manager.first_instance_cull_bind_group_layout, + &BindGroupEntries::sequential(( + &view_resources.previous_depth_pyramid, + view_uniforms.clone(), + previous_view_uniforms.clone(), + instance_manager.instance_uniforms.binding().unwrap(), + view_resources.instance_visibility.as_entire_binding(), + instance_manager.instance_aabbs.binding().unwrap(), + instance_manager.instance_bvh_root_nodes.binding().unwrap(), + view_resources + .first_bvh_cull_count_front + .as_entire_binding(), + view_resources + .first_bvh_cull_dispatch_front + .as_entire_binding(), + view_resources.first_bvh_cull_queue.as_entire_binding(), + view_resources.second_pass_count.as_entire_binding(), + view_resources.second_pass_dispatch.as_entire_binding(), + view_resources.second_pass_candidates.as_entire_binding(), + )), ); - let entries = BindGroupEntries::sequential(( - cluster_meshlet_ids.as_entire_binding(), - meshlet_mesh_manager.meshlet_bounding_spheres.binding(), - meshlet_mesh_manager.meshlet_simplification_errors.binding(), - cluster_instance_ids.as_entire_binding(), - instance_manager.instance_uniforms.binding().unwrap(), - view_resources.instance_visibility.as_entire_binding(), - view_resources - .second_pass_candidates_buffer - .as_entire_binding(), - view_resources - .visibility_buffer_software_raster_indirect_args_second - .as_entire_binding(), - view_resources - .visibility_buffer_hardware_raster_indirect_args_second - .as_entire_binding(), - resource_manager - .visibility_buffer_raster_clusters - .as_entire_binding(), - &view_resources.depth_pyramid.all_mips, - view_uniforms.clone(), - previous_view_uniforms.clone(), - )); - let culling_second = render_device.create_bind_group( - "meshlet_culling_second_bind_group", - &resource_manager.culling_bind_group_layout, - &entries, + let second_instance_cull = render_device.create_bind_group( + "meshlet_second_instance_cull_bind_group", + &resource_manager.second_instance_cull_bind_group_layout, + &BindGroupEntries::sequential(( + &view_resources.previous_depth_pyramid, + view_uniforms.clone(), + previous_view_uniforms.clone(), + instance_manager.instance_uniforms.binding().unwrap(), + view_resources.instance_visibility.as_entire_binding(), + instance_manager.instance_aabbs.binding().unwrap(), + instance_manager.instance_bvh_root_nodes.binding().unwrap(), + view_resources + .second_bvh_cull_count_front + .as_entire_binding(), + view_resources + .second_bvh_cull_dispatch_front + .as_entire_binding(), + view_resources.second_bvh_cull_queue.as_entire_binding(), + view_resources.second_pass_count.as_entire_binding(), + view_resources.second_pass_candidates.as_entire_binding(), + )), + ); + + let first_bvh_cull_ping = render_device.create_bind_group( + "meshlet_first_bvh_cull_ping_bind_group", + &resource_manager.first_bvh_cull_bind_group_layout, + &BindGroupEntries::sequential(( + &view_resources.previous_depth_pyramid, + view_uniforms.clone(), + previous_view_uniforms.clone(), + meshlet_mesh_manager.bvh_nodes.binding(), + instance_manager.instance_uniforms.binding().unwrap(), + view_resources + .first_bvh_cull_count_front + .as_entire_binding(), + view_resources.first_bvh_cull_count_back.as_entire_binding(), + view_resources + .first_bvh_cull_dispatch_back + .as_entire_binding(), + view_resources.first_bvh_cull_queue.as_entire_binding(), + view_resources.front_meshlet_cull_count.as_entire_binding(), + view_resources.back_meshlet_cull_count.as_entire_binding(), + view_resources + .front_meshlet_cull_dispatch + .as_entire_binding(), + view_resources + .back_meshlet_cull_dispatch + .as_entire_binding(), + view_resources.meshlet_cull_queue.as_entire_binding(), + view_resources + .second_bvh_cull_count_front + .as_entire_binding(), + view_resources + .second_bvh_cull_dispatch_front + .as_entire_binding(), + view_resources.second_bvh_cull_queue.as_entire_binding(), + )), + ); + + let first_bvh_cull_pong = render_device.create_bind_group( + "meshlet_first_bvh_cull_pong_bind_group", + &resource_manager.first_bvh_cull_bind_group_layout, + &BindGroupEntries::sequential(( + &view_resources.previous_depth_pyramid, + view_uniforms.clone(), + previous_view_uniforms.clone(), + meshlet_mesh_manager.bvh_nodes.binding(), + instance_manager.instance_uniforms.binding().unwrap(), + view_resources.first_bvh_cull_count_back.as_entire_binding(), + view_resources + .first_bvh_cull_count_front + .as_entire_binding(), + view_resources + .first_bvh_cull_dispatch_front + .as_entire_binding(), + view_resources.first_bvh_cull_queue.as_entire_binding(), + view_resources.front_meshlet_cull_count.as_entire_binding(), + view_resources.back_meshlet_cull_count.as_entire_binding(), + view_resources + .front_meshlet_cull_dispatch + .as_entire_binding(), + view_resources + .back_meshlet_cull_dispatch + .as_entire_binding(), + view_resources.meshlet_cull_queue.as_entire_binding(), + view_resources + .second_bvh_cull_count_front + .as_entire_binding(), + view_resources + .second_bvh_cull_dispatch_front + .as_entire_binding(), + view_resources.second_bvh_cull_queue.as_entire_binding(), + )), + ); + + let second_bvh_cull_ping = render_device.create_bind_group( + "meshlet_second_bvh_cull_ping_bind_group", + &resource_manager.second_bvh_cull_bind_group_layout, + &BindGroupEntries::sequential(( + &view_resources.previous_depth_pyramid, + view_uniforms.clone(), + previous_view_uniforms.clone(), + meshlet_mesh_manager.bvh_nodes.binding(), + instance_manager.instance_uniforms.binding().unwrap(), + view_resources + .second_bvh_cull_count_front + .as_entire_binding(), + view_resources + .second_bvh_cull_count_back + .as_entire_binding(), + view_resources + .second_bvh_cull_dispatch_back + .as_entire_binding(), + view_resources.second_bvh_cull_queue.as_entire_binding(), + view_resources.front_meshlet_cull_count.as_entire_binding(), + view_resources.back_meshlet_cull_count.as_entire_binding(), + view_resources + .front_meshlet_cull_dispatch + .as_entire_binding(), + view_resources + .back_meshlet_cull_dispatch + .as_entire_binding(), + view_resources.meshlet_cull_queue.as_entire_binding(), + )), + ); + + let second_bvh_cull_pong = render_device.create_bind_group( + "meshlet_second_bvh_cull_pong_bind_group", + &resource_manager.second_bvh_cull_bind_group_layout, + &BindGroupEntries::sequential(( + &view_resources.previous_depth_pyramid, + view_uniforms.clone(), + previous_view_uniforms.clone(), + meshlet_mesh_manager.bvh_nodes.binding(), + instance_manager.instance_uniforms.binding().unwrap(), + view_resources + .second_bvh_cull_count_back + .as_entire_binding(), + view_resources + .second_bvh_cull_count_front + .as_entire_binding(), + view_resources + .second_bvh_cull_dispatch_front + .as_entire_binding(), + view_resources.second_bvh_cull_queue.as_entire_binding(), + view_resources.front_meshlet_cull_count.as_entire_binding(), + view_resources.back_meshlet_cull_count.as_entire_binding(), + view_resources + .front_meshlet_cull_dispatch + .as_entire_binding(), + view_resources + .back_meshlet_cull_dispatch + .as_entire_binding(), + view_resources.meshlet_cull_queue.as_entire_binding(), + )), + ); + + let first_meshlet_cull = render_device.create_bind_group( + "meshlet_first_meshlet_cull_bind_group", + &resource_manager.first_meshlet_cull_bind_group_layout, + &BindGroupEntries::sequential(( + &view_resources.previous_depth_pyramid, + view_uniforms.clone(), + previous_view_uniforms.clone(), + meshlet_mesh_manager.meshlet_cull_data.binding(), + instance_manager.instance_uniforms.binding().unwrap(), + view_resources + .visibility_buffer_software_raster_indirect_args + .as_entire_binding(), + view_resources + .visibility_buffer_hardware_raster_indirect_args + .as_entire_binding(), + resource_manager + .visibility_buffer_raster_cluster_prev_counts + .as_entire_binding(), + resource_manager + .visibility_buffer_raster_clusters + .as_entire_binding(), + view_resources.front_meshlet_cull_count.as_entire_binding(), + view_resources.back_meshlet_cull_count.as_entire_binding(), + view_resources + .back_meshlet_cull_dispatch + .as_entire_binding(), + view_resources.meshlet_cull_queue.as_entire_binding(), + )), + ); + + let second_meshlet_cull = render_device.create_bind_group( + "meshlet_second_meshlet_cull_bind_group", + &resource_manager.second_meshlet_cull_bind_group_layout, + &BindGroupEntries::sequential(( + &view_resources.previous_depth_pyramid, + view_uniforms.clone(), + previous_view_uniforms.clone(), + meshlet_mesh_manager.meshlet_cull_data.binding(), + instance_manager.instance_uniforms.binding().unwrap(), + view_resources + .visibility_buffer_software_raster_indirect_args + .as_entire_binding(), + view_resources + .visibility_buffer_hardware_raster_indirect_args + .as_entire_binding(), + resource_manager + .visibility_buffer_raster_cluster_prev_counts + .as_entire_binding(), + resource_manager + .visibility_buffer_raster_clusters + .as_entire_binding(), + view_resources.back_meshlet_cull_count.as_entire_binding(), + view_resources.meshlet_cull_queue.as_entire_binding(), + )), ); let downsample_depth = view_resources.depth_pyramid.create_bind_group( @@ -745,22 +1075,6 @@ pub fn prepare_meshlet_view_bind_groups( &resource_manager.depth_pyramid_sampler, ); - let entries = BindGroupEntries::sequential(( - cluster_meshlet_ids.as_entire_binding(), - meshlet_mesh_manager.meshlets.binding(), - meshlet_mesh_manager.indices.binding(), - meshlet_mesh_manager.vertex_positions.binding(), - cluster_instance_ids.as_entire_binding(), - instance_manager.instance_uniforms.binding().unwrap(), - resource_manager - .visibility_buffer_raster_clusters - .as_entire_binding(), - resource_manager - .software_raster_cluster_count - .as_entire_binding(), - &view_resources.visibility_buffer.default_view, - view_uniforms.clone(), - )); let visibility_buffer_raster = render_device.create_bind_group( "meshlet_visibility_raster_buffer_bind_group", if view_resources.not_shadow_view { @@ -768,7 +1082,23 @@ pub fn prepare_meshlet_view_bind_groups( } else { &resource_manager.visibility_buffer_raster_shadow_view_bind_group_layout }, - &entries, + &BindGroupEntries::sequential(( + resource_manager + .visibility_buffer_raster_clusters + .as_entire_binding(), + meshlet_mesh_manager.meshlets.binding(), + meshlet_mesh_manager.indices.binding(), + meshlet_mesh_manager.vertex_positions.binding(), + instance_manager.instance_uniforms.binding().unwrap(), + resource_manager + .visibility_buffer_raster_cluster_prev_counts + .as_entire_binding(), + resource_manager + .software_raster_cluster_count + .as_entire_binding(), + &view_resources.visibility_buffer.default_view, + view_uniforms.clone(), + )), ); let resolve_depth = render_device.create_bind_group( @@ -782,34 +1112,35 @@ pub fn prepare_meshlet_view_bind_groups( ); let resolve_material_depth = view_resources.material_depth.as_ref().map(|_| { - let entries = BindGroupEntries::sequential(( - &view_resources.visibility_buffer.default_view, - cluster_instance_ids.as_entire_binding(), - instance_manager.instance_material_ids.binding().unwrap(), - )); render_device.create_bind_group( "meshlet_resolve_material_depth_bind_group", &resource_manager.resolve_material_depth_bind_group_layout, - &entries, + &BindGroupEntries::sequential(( + &view_resources.visibility_buffer.default_view, + resource_manager + .visibility_buffer_raster_clusters + .as_entire_binding(), + instance_manager.instance_material_ids.binding().unwrap(), + )), ) }); let material_shade = view_resources.material_depth.as_ref().map(|_| { - let entries = BindGroupEntries::sequential(( - &view_resources.visibility_buffer.default_view, - cluster_meshlet_ids.as_entire_binding(), - meshlet_mesh_manager.meshlets.binding(), - meshlet_mesh_manager.indices.binding(), - meshlet_mesh_manager.vertex_positions.binding(), - meshlet_mesh_manager.vertex_normals.binding(), - meshlet_mesh_manager.vertex_uvs.binding(), - cluster_instance_ids.as_entire_binding(), - instance_manager.instance_uniforms.binding().unwrap(), - )); render_device.create_bind_group( "meshlet_mesh_material_shade_bind_group", &resource_manager.material_shade_bind_group_layout, - &entries, + &BindGroupEntries::sequential(( + &view_resources.visibility_buffer.default_view, + resource_manager + .visibility_buffer_raster_clusters + .as_entire_binding(), + meshlet_mesh_manager.meshlets.binding(), + meshlet_mesh_manager.indices.binding(), + meshlet_mesh_manager.vertex_positions.binding(), + meshlet_mesh_manager.vertex_normals.binding(), + meshlet_mesh_manager.vertex_uvs.binding(), + instance_manager.instance_uniforms.binding().unwrap(), + )), ) }); @@ -817,46 +1148,77 @@ pub fn prepare_meshlet_view_bind_groups( .remap_1d_to_2d_dispatch_bind_group_layout .as_ref() .map(|layout| { - ( - render_device.create_bind_group( - "meshlet_remap_1d_to_2d_dispatch_first_bind_group", - layout, - &BindGroupEntries::sequential(( - view_resources - .visibility_buffer_software_raster_indirect_args_first - .as_entire_binding(), - resource_manager - .software_raster_cluster_count - .as_entire_binding(), - )), - ), - render_device.create_bind_group( - "meshlet_remap_1d_to_2d_dispatch_second_bind_group", - layout, - &BindGroupEntries::sequential(( - view_resources - .visibility_buffer_software_raster_indirect_args_second - .as_entire_binding(), - resource_manager - .software_raster_cluster_count - .as_entire_binding(), - )), - ), + render_device.create_bind_group( + "meshlet_remap_1d_to_2d_dispatch_bind_group", + layout, + &BindGroupEntries::sequential(( + view_resources + .visibility_buffer_software_raster_indirect_args + .as_entire_binding(), + resource_manager + .software_raster_cluster_count + .as_entire_binding(), + )), ) }); + let fill_counts = if resource_manager + .remap_1d_to_2d_dispatch_bind_group_layout + .is_some() + { + render_device.create_bind_group( + "meshlet_fill_counts_bind_group", + &resource_manager.fill_counts_bind_group_layout, + &BindGroupEntries::sequential(( + view_resources + .visibility_buffer_software_raster_indirect_args + .as_entire_binding(), + view_resources + .visibility_buffer_hardware_raster_indirect_args + .as_entire_binding(), + resource_manager + .visibility_buffer_raster_cluster_prev_counts + .as_entire_binding(), + resource_manager + .software_raster_cluster_count + .as_entire_binding(), + )), + ) + } else { + render_device.create_bind_group( + "meshlet_fill_counts_bind_group", + &resource_manager.fill_counts_bind_group_layout, + &BindGroupEntries::sequential(( + view_resources + .visibility_buffer_software_raster_indirect_args + .as_entire_binding(), + view_resources + .visibility_buffer_hardware_raster_indirect_args + .as_entire_binding(), + resource_manager + .visibility_buffer_raster_cluster_prev_counts + .as_entire_binding(), + )), + ) + }; + commands.entity(view_entity).insert(MeshletViewBindGroups { - first_node: Arc::clone(&first_node), - fill_cluster_buffers, clear_visibility_buffer, - culling_first, - culling_second, + first_instance_cull, + second_instance_cull, + first_bvh_cull_ping, + first_bvh_cull_pong, + second_bvh_cull_ping, + second_bvh_cull_pong, + first_meshlet_cull, + second_meshlet_cull, downsample_depth, visibility_buffer_raster, resolve_depth, resolve_material_depth, material_shade, remap_1d_to_2d_dispatch, + fill_counts, }); } } diff --git a/crates/bevy_pbr/src/meshlet/visibility_buffer_hardware_raster.wgsl b/crates/bevy_pbr/src/meshlet/visibility_buffer_hardware_raster.wgsl index 3525d38e6d..2a251443fb 100644 --- a/crates/bevy_pbr/src/meshlet/visibility_buffer_hardware_raster.wgsl +++ b/crates/bevy_pbr/src/meshlet/visibility_buffer_hardware_raster.wgsl @@ -5,6 +5,7 @@ meshlet_cluster_instance_ids, meshlet_instance_uniforms, meshlet_raster_clusters, + meshlet_previous_raster_counts, meshlet_visibility_buffer, view, get_meshlet_triangle_count, @@ -27,17 +28,17 @@ struct VertexOutput { @vertex fn vertex(@builtin(instance_index) instance_index: u32, @builtin(vertex_index) vertex_index: u32) -> VertexOutput { - let cluster_id = meshlet_raster_clusters[meshlet_raster_cluster_rightmost_slot - instance_index]; - let meshlet_id = meshlet_cluster_meshlet_ids[cluster_id]; - var meshlet = meshlets[meshlet_id]; + let cluster_in_draw = meshlet_previous_raster_counts[1] + instance_index; + let cluster_id = meshlet_raster_cluster_rightmost_slot - cluster_in_draw; + let instanced_offset = meshlet_raster_clusters[cluster_id]; + var meshlet = meshlets[instanced_offset.offset]; let triangle_id = vertex_index / 3u; if triangle_id >= get_meshlet_triangle_count(&meshlet) { return dummy_vertex(); } - let index_id = (triangle_id * 3u) + (vertex_index % 3u); + let index_id = vertex_index; let vertex_id = get_meshlet_vertex_id(meshlet.start_index_id + index_id); - let instance_id = meshlet_cluster_instance_ids[cluster_id]; - let instance_uniform = meshlet_instance_uniforms[instance_id]; + let instance_uniform = meshlet_instance_uniforms[instanced_offset.instance_id]; let vertex_position = get_meshlet_vertex_position(&meshlet, vertex_id); let world_from_local = affine3_to_square(instance_uniform.world_from_local); diff --git a/crates/bevy_pbr/src/meshlet/visibility_buffer_raster_node.rs b/crates/bevy_pbr/src/meshlet/visibility_buffer_raster_node.rs index 20054d2d2f..160097fc50 100644 --- a/crates/bevy_pbr/src/meshlet/visibility_buffer_raster_node.rs +++ b/crates/bevy_pbr/src/meshlet/visibility_buffer_raster_node.rs @@ -2,14 +2,16 @@ use super::{ pipelines::MeshletPipelines, resource_manager::{MeshletViewBindGroups, MeshletViewResources}, }; -use crate::{LightEntity, ShadowView, ViewLightEntities}; +use crate::{ + meshlet::resource_manager::ResourceManager, LightEntity, ShadowView, ViewLightEntities, +}; use bevy_color::LinearRgba; use bevy_core_pipeline::prepass::PreviousViewUniformOffset; use bevy_ecs::{ query::QueryState, world::{FromWorld, World}, }; -use bevy_math::{ops, UVec2}; +use bevy_math::UVec2; use bevy_render::{ camera::ExtractedCamera, render_graph::{Node, NodeRunError, RenderGraphContext}, @@ -17,7 +19,6 @@ use bevy_render::{ renderer::RenderContext, view::{ViewDepthTexture, ViewUniformOffset}, }; -use core::sync::atomic::Ordering; /// Rasterize meshlets into a depth buffer, and optional visibility buffer + material depth buffer for shading passes. pub struct MeshletVisibilityBufferRasterPassNode { @@ -76,11 +77,14 @@ impl Node for MeshletVisibilityBufferRasterPassNode { }; let Some(( - fill_cluster_buffers_pipeline, clear_visibility_buffer_pipeline, clear_visibility_buffer_shadow_view_pipeline, - culling_first_pipeline, - culling_second_pipeline, + first_instance_cull_pipeline, + second_instance_cull_pipeline, + first_bvh_cull_pipeline, + second_bvh_cull_pipeline, + first_meshlet_cull_pipeline, + second_meshlet_cull_pipeline, downsample_depth_first_pipeline, downsample_depth_second_pipeline, downsample_depth_first_shadow_view_pipeline, @@ -94,69 +98,60 @@ impl Node for MeshletVisibilityBufferRasterPassNode { resolve_depth_shadow_view_pipeline, resolve_material_depth_pipeline, remap_1d_to_2d_dispatch_pipeline, + fill_counts_pipeline, )) = MeshletPipelines::get(world) else { return Ok(()); }; - let first_node = meshlet_view_bind_groups - .first_node - .fetch_and(false, Ordering::SeqCst); - - let div_ceil = meshlet_view_resources.scene_cluster_count.div_ceil(128); - let thread_per_cluster_workgroups = ops::cbrt(div_ceil as f32).ceil() as u32; - render_context .command_encoder() .push_debug_group("meshlet_visibility_buffer_raster"); - if first_node { - fill_cluster_buffers_pass( - render_context, - &meshlet_view_bind_groups.fill_cluster_buffers, - fill_cluster_buffers_pipeline, - meshlet_view_resources.scene_instance_count, - ); - } + + let resource_manager = world.get_resource::().unwrap(); + render_context.command_encoder().clear_buffer( + &resource_manager.visibility_buffer_raster_cluster_prev_counts, + 0, + None, + ); + clear_visibility_buffer_pass( render_context, &meshlet_view_bind_groups.clear_visibility_buffer, clear_visibility_buffer_pipeline, meshlet_view_resources.view_size, ); - render_context.command_encoder().clear_buffer( - &meshlet_view_resources.second_pass_candidates_buffer, - 0, - None, - ); - cull_pass( - "culling_first", + + render_context + .command_encoder() + .push_debug_group("meshlet_first_pass"); + first_cull( render_context, - &meshlet_view_bind_groups.culling_first, + meshlet_view_bind_groups, + meshlet_view_resources, view_offset, previous_view_offset, - culling_first_pipeline, - thread_per_cluster_workgroups, - meshlet_view_resources.scene_cluster_count, - meshlet_view_resources.raster_cluster_rightmost_slot, - meshlet_view_bind_groups - .remap_1d_to_2d_dispatch - .as_ref() - .map(|(bg1, _)| bg1), + first_instance_cull_pipeline, + first_bvh_cull_pipeline, + first_meshlet_cull_pipeline, remap_1d_to_2d_dispatch_pipeline, ); raster_pass( true, render_context, - &meshlet_view_resources.visibility_buffer_software_raster_indirect_args_first, - &meshlet_view_resources.visibility_buffer_hardware_raster_indirect_args_first, + &meshlet_view_resources.visibility_buffer_software_raster_indirect_args, + &meshlet_view_resources.visibility_buffer_hardware_raster_indirect_args, &meshlet_view_resources.dummy_render_target.default_view, meshlet_view_bind_groups, view_offset, visibility_buffer_software_raster_pipeline, visibility_buffer_hardware_raster_pipeline, + fill_counts_pipeline, Some(camera), - meshlet_view_resources.raster_cluster_rightmost_slot, + meshlet_view_resources.rightmost_slot, ); + render_context.command_encoder().pop_debug_group(); + meshlet_view_resources.depth_pyramid.downsample_depth( "downsample_depth", render_context, @@ -165,35 +160,37 @@ impl Node for MeshletVisibilityBufferRasterPassNode { downsample_depth_first_pipeline, downsample_depth_second_pipeline, ); - cull_pass( - "culling_second", + + render_context + .command_encoder() + .push_debug_group("meshlet_second_pass"); + second_cull( render_context, - &meshlet_view_bind_groups.culling_second, + meshlet_view_bind_groups, + meshlet_view_resources, view_offset, previous_view_offset, - culling_second_pipeline, - thread_per_cluster_workgroups, - meshlet_view_resources.scene_cluster_count, - meshlet_view_resources.raster_cluster_rightmost_slot, - meshlet_view_bind_groups - .remap_1d_to_2d_dispatch - .as_ref() - .map(|(_, bg2)| bg2), + second_instance_cull_pipeline, + second_bvh_cull_pipeline, + second_meshlet_cull_pipeline, remap_1d_to_2d_dispatch_pipeline, ); raster_pass( false, render_context, - &meshlet_view_resources.visibility_buffer_software_raster_indirect_args_second, - &meshlet_view_resources.visibility_buffer_hardware_raster_indirect_args_second, + &meshlet_view_resources.visibility_buffer_software_raster_indirect_args, + &meshlet_view_resources.visibility_buffer_hardware_raster_indirect_args, &meshlet_view_resources.dummy_render_target.default_view, meshlet_view_bind_groups, view_offset, visibility_buffer_software_raster_pipeline, visibility_buffer_hardware_raster_pipeline, + fill_counts_pipeline, Some(camera), - meshlet_view_resources.raster_cluster_rightmost_slot, + meshlet_view_resources.rightmost_slot, ); + render_context.command_encoder().pop_debug_group(); + resolve_depth( render_context, view_depth.get_attachment(StoreOp::Store), @@ -248,40 +245,37 @@ impl Node for MeshletVisibilityBufferRasterPassNode { clear_visibility_buffer_shadow_view_pipeline, meshlet_view_resources.view_size, ); - render_context.command_encoder().clear_buffer( - &meshlet_view_resources.second_pass_candidates_buffer, - 0, - None, - ); - cull_pass( - "culling_first", + + render_context + .command_encoder() + .push_debug_group("meshlet_first_pass"); + first_cull( render_context, - &meshlet_view_bind_groups.culling_first, + meshlet_view_bind_groups, + meshlet_view_resources, view_offset, previous_view_offset, - culling_first_pipeline, - thread_per_cluster_workgroups, - meshlet_view_resources.scene_cluster_count, - meshlet_view_resources.raster_cluster_rightmost_slot, - meshlet_view_bind_groups - .remap_1d_to_2d_dispatch - .as_ref() - .map(|(bg1, _)| bg1), + first_instance_cull_pipeline, + first_bvh_cull_pipeline, + first_meshlet_cull_pipeline, remap_1d_to_2d_dispatch_pipeline, ); raster_pass( true, render_context, - &meshlet_view_resources.visibility_buffer_software_raster_indirect_args_first, - &meshlet_view_resources.visibility_buffer_hardware_raster_indirect_args_first, + &meshlet_view_resources.visibility_buffer_software_raster_indirect_args, + &meshlet_view_resources.visibility_buffer_hardware_raster_indirect_args, &meshlet_view_resources.dummy_render_target.default_view, meshlet_view_bind_groups, view_offset, visibility_buffer_software_raster_shadow_view_pipeline, shadow_visibility_buffer_hardware_raster_pipeline, + fill_counts_pipeline, None, - meshlet_view_resources.raster_cluster_rightmost_slot, + meshlet_view_resources.rightmost_slot, ); + render_context.command_encoder().pop_debug_group(); + meshlet_view_resources.depth_pyramid.downsample_depth( "downsample_depth", render_context, @@ -290,35 +284,37 @@ impl Node for MeshletVisibilityBufferRasterPassNode { downsample_depth_first_shadow_view_pipeline, downsample_depth_second_shadow_view_pipeline, ); - cull_pass( - "culling_second", + + render_context + .command_encoder() + .push_debug_group("meshlet_second_pass"); + second_cull( render_context, - &meshlet_view_bind_groups.culling_second, + meshlet_view_bind_groups, + meshlet_view_resources, view_offset, previous_view_offset, - culling_second_pipeline, - thread_per_cluster_workgroups, - meshlet_view_resources.scene_cluster_count, - meshlet_view_resources.raster_cluster_rightmost_slot, - meshlet_view_bind_groups - .remap_1d_to_2d_dispatch - .as_ref() - .map(|(_, bg2)| bg2), + second_instance_cull_pipeline, + second_bvh_cull_pipeline, + second_meshlet_cull_pipeline, remap_1d_to_2d_dispatch_pipeline, ); raster_pass( false, render_context, - &meshlet_view_resources.visibility_buffer_software_raster_indirect_args_second, - &meshlet_view_resources.visibility_buffer_hardware_raster_indirect_args_second, + &meshlet_view_resources.visibility_buffer_software_raster_indirect_args, + &meshlet_view_resources.visibility_buffer_hardware_raster_indirect_args, &meshlet_view_resources.dummy_render_target.default_view, meshlet_view_bind_groups, view_offset, visibility_buffer_software_raster_shadow_view_pipeline, shadow_visibility_buffer_hardware_raster_pipeline, + fill_counts_pipeline, None, - meshlet_view_resources.raster_cluster_rightmost_slot, + meshlet_view_resources.rightmost_slot, ); + render_context.command_encoder().pop_debug_group(); + resolve_depth( render_context, shadow_view.depth_attachment.get_attachment(StoreOp::Store), @@ -341,39 +337,6 @@ impl Node for MeshletVisibilityBufferRasterPassNode { } } -fn fill_cluster_buffers_pass( - render_context: &mut RenderContext, - fill_cluster_buffers_bind_group: &BindGroup, - fill_cluster_buffers_pass_pipeline: &ComputePipeline, - scene_instance_count: u32, -) { - let mut fill_cluster_buffers_pass_workgroups_x = scene_instance_count; - let mut fill_cluster_buffers_pass_workgroups_y = 1; - if scene_instance_count - > render_context - .render_device() - .limits() - .max_compute_workgroups_per_dimension - { - fill_cluster_buffers_pass_workgroups_x = (scene_instance_count as f32).sqrt().ceil() as u32; - fill_cluster_buffers_pass_workgroups_y = fill_cluster_buffers_pass_workgroups_x; - } - - let command_encoder = render_context.command_encoder(); - let mut fill_pass = command_encoder.begin_compute_pass(&ComputePassDescriptor { - label: Some("fill_cluster_buffers"), - timestamp_writes: None, - }); - fill_pass.set_pipeline(fill_cluster_buffers_pass_pipeline); - fill_pass.set_push_constants(0, &scene_instance_count.to_le_bytes()); - fill_pass.set_bind_group(0, fill_cluster_buffers_bind_group, &[]); - fill_pass.dispatch_workgroups( - fill_cluster_buffers_pass_workgroups_x, - fill_cluster_buffers_pass_workgroups_y, - 1, - ); -} - // TODO: Replace this with vkCmdClearColorImage once wgpu supports it fn clear_visibility_buffer_pass( render_context: &mut RenderContext, @@ -397,82 +360,231 @@ fn clear_visibility_buffer_pass( ); } -fn cull_pass( - label: &'static str, +fn first_cull( render_context: &mut RenderContext, - culling_bind_group: &BindGroup, + meshlet_view_bind_groups: &MeshletViewBindGroups, + meshlet_view_resources: &MeshletViewResources, view_offset: &ViewUniformOffset, previous_view_offset: &PreviousViewUniformOffset, - culling_pipeline: &ComputePipeline, - culling_workgroups: u32, - scene_cluster_count: u32, - raster_cluster_rightmost_slot: u32, - remap_1d_to_2d_dispatch_bind_group: Option<&BindGroup>, - remap_1d_to_2d_dispatch_pipeline: Option<&ComputePipeline>, + first_instance_cull_pipeline: &ComputePipeline, + first_bvh_cull_pipeline: &ComputePipeline, + first_meshlet_cull_pipeline: &ComputePipeline, + remap_1d_to_2d_pipeline: Option<&ComputePipeline>, ) { - let max_compute_workgroups_per_dimension = render_context - .render_device() - .limits() - .max_compute_workgroups_per_dimension; + let workgroups = meshlet_view_resources.scene_instance_count.div_ceil(128); + cull_pass( + "meshlet_first_instance_cull", + render_context, + &meshlet_view_bind_groups.first_instance_cull, + view_offset, + previous_view_offset, + first_instance_cull_pipeline, + &[meshlet_view_resources.scene_instance_count], + ) + .dispatch_workgroups(workgroups, 1, 1); + render_context + .command_encoder() + .push_debug_group("meshlet_first_bvh_cull"); + let mut ping = true; + for _ in 0..meshlet_view_resources.max_bvh_depth { + cull_pass( + "meshlet_first_bvh_cull_dispatch", + render_context, + if ping { + &meshlet_view_bind_groups.first_bvh_cull_ping + } else { + &meshlet_view_bind_groups.first_bvh_cull_pong + }, + view_offset, + previous_view_offset, + first_bvh_cull_pipeline, + &[ping as u32, meshlet_view_resources.rightmost_slot], + ) + .dispatch_workgroups_indirect( + if ping { + &meshlet_view_resources.first_bvh_cull_dispatch_front + } else { + &meshlet_view_resources.first_bvh_cull_dispatch_back + }, + 0, + ); + render_context.command_encoder().clear_buffer( + if ping { + &meshlet_view_resources.first_bvh_cull_count_front + } else { + &meshlet_view_resources.first_bvh_cull_count_back + }, + 0, + Some(4), + ); + render_context.command_encoder().clear_buffer( + if ping { + &meshlet_view_resources.first_bvh_cull_dispatch_front + } else { + &meshlet_view_resources.first_bvh_cull_dispatch_back + }, + 0, + Some(4), + ); + ping = !ping; + } + render_context.command_encoder().pop_debug_group(); + + let mut pass = cull_pass( + "meshlet_first_meshlet_cull", + render_context, + &meshlet_view_bind_groups.first_meshlet_cull, + view_offset, + previous_view_offset, + first_meshlet_cull_pipeline, + &[meshlet_view_resources.rightmost_slot], + ); + pass.dispatch_workgroups_indirect(&meshlet_view_resources.front_meshlet_cull_dispatch, 0); + remap_1d_to_2d( + pass, + remap_1d_to_2d_pipeline, + meshlet_view_bind_groups.remap_1d_to_2d_dispatch.as_ref(), + ); +} + +fn second_cull( + render_context: &mut RenderContext, + meshlet_view_bind_groups: &MeshletViewBindGroups, + meshlet_view_resources: &MeshletViewResources, + view_offset: &ViewUniformOffset, + previous_view_offset: &PreviousViewUniformOffset, + second_instance_cull_pipeline: &ComputePipeline, + second_bvh_cull_pipeline: &ComputePipeline, + second_meshlet_cull_pipeline: &ComputePipeline, + remap_1d_to_2d_pipeline: Option<&ComputePipeline>, +) { + cull_pass( + "meshlet_second_instance_cull", + render_context, + &meshlet_view_bind_groups.second_instance_cull, + view_offset, + previous_view_offset, + second_instance_cull_pipeline, + &[meshlet_view_resources.scene_instance_count], + ) + .dispatch_workgroups_indirect(&meshlet_view_resources.second_pass_dispatch, 0); + + render_context + .command_encoder() + .push_debug_group("meshlet_second_bvh_cull"); + let mut ping = true; + for _ in 0..meshlet_view_resources.max_bvh_depth { + cull_pass( + "meshlet_second_bvh_cull_dispatch", + render_context, + if ping { + &meshlet_view_bind_groups.second_bvh_cull_ping + } else { + &meshlet_view_bind_groups.second_bvh_cull_pong + }, + view_offset, + previous_view_offset, + second_bvh_cull_pipeline, + &[ping as u32, meshlet_view_resources.rightmost_slot], + ) + .dispatch_workgroups_indirect( + if ping { + &meshlet_view_resources.second_bvh_cull_dispatch_front + } else { + &meshlet_view_resources.second_bvh_cull_dispatch_back + }, + 0, + ); + ping = !ping; + } + render_context.command_encoder().pop_debug_group(); + + let mut pass = cull_pass( + "meshlet_second_meshlet_cull", + render_context, + &meshlet_view_bind_groups.second_meshlet_cull, + view_offset, + previous_view_offset, + second_meshlet_cull_pipeline, + &[meshlet_view_resources.rightmost_slot], + ); + pass.dispatch_workgroups_indirect(&meshlet_view_resources.back_meshlet_cull_dispatch, 0); + remap_1d_to_2d( + pass, + remap_1d_to_2d_pipeline, + meshlet_view_bind_groups.remap_1d_to_2d_dispatch.as_ref(), + ); +} + +fn cull_pass<'a>( + label: &'static str, + render_context: &'a mut RenderContext, + bind_group: &'a BindGroup, + view_offset: &'a ViewUniformOffset, + previous_view_offset: &'a PreviousViewUniformOffset, + pipeline: &'a ComputePipeline, + push_constants: &[u32], +) -> ComputePass<'a> { let command_encoder = render_context.command_encoder(); - let mut cull_pass = command_encoder.begin_compute_pass(&ComputePassDescriptor { + let mut pass = command_encoder.begin_compute_pass(&ComputePassDescriptor { label: Some(label), timestamp_writes: None, }); - cull_pass.set_pipeline(culling_pipeline); - cull_pass.set_push_constants( + pass.set_pipeline(pipeline); + pass.set_bind_group( 0, - bytemuck::cast_slice(&[scene_cluster_count, raster_cluster_rightmost_slot]), - ); - cull_pass.set_bind_group( - 0, - culling_bind_group, + bind_group, &[view_offset.offset, previous_view_offset.offset], ); - cull_pass.dispatch_workgroups(culling_workgroups, culling_workgroups, culling_workgroups); + pass.set_push_constants(0, bytemuck::cast_slice(push_constants)); + pass +} - if let (Some(remap_1d_to_2d_dispatch_pipeline), Some(remap_1d_to_2d_dispatch_bind_group)) = ( - remap_1d_to_2d_dispatch_pipeline, - remap_1d_to_2d_dispatch_bind_group, - ) { - cull_pass.set_pipeline(remap_1d_to_2d_dispatch_pipeline); - cull_pass.set_push_constants(0, &max_compute_workgroups_per_dimension.to_be_bytes()); - cull_pass.set_bind_group(0, remap_1d_to_2d_dispatch_bind_group, &[]); - cull_pass.dispatch_workgroups(1, 1, 1); +fn remap_1d_to_2d( + mut pass: ComputePass, + pipeline: Option<&ComputePipeline>, + bind_group: Option<&BindGroup>, +) { + if let (Some(pipeline), Some(bind_group)) = (pipeline, bind_group) { + pass.set_pipeline(pipeline); + pass.set_bind_group(0, bind_group, &[]); + pass.dispatch_workgroups(1, 1, 1); } } fn raster_pass( first_pass: bool, render_context: &mut RenderContext, - visibility_buffer_hardware_software_indirect_args: &Buffer, + visibility_buffer_software_raster_indirect_args: &Buffer, visibility_buffer_hardware_raster_indirect_args: &Buffer, dummy_render_target: &TextureView, meshlet_view_bind_groups: &MeshletViewBindGroups, view_offset: &ViewUniformOffset, - visibility_buffer_hardware_software_pipeline: &ComputePipeline, + visibility_buffer_software_raster_pipeline: &ComputePipeline, visibility_buffer_hardware_raster_pipeline: &RenderPipeline, + fill_counts_pipeline: &ComputePipeline, camera: Option<&ExtractedCamera>, raster_cluster_rightmost_slot: u32, ) { - let command_encoder = render_context.command_encoder(); - let mut software_pass = command_encoder.begin_compute_pass(&ComputePassDescriptor { - label: Some(if first_pass { - "raster_software_first" - } else { - "raster_software_second" - }), - timestamp_writes: None, - }); - software_pass.set_pipeline(visibility_buffer_hardware_software_pipeline); + let mut software_pass = + render_context + .command_encoder() + .begin_compute_pass(&ComputePassDescriptor { + label: Some(if first_pass { + "raster_software_first" + } else { + "raster_software_second" + }), + timestamp_writes: None, + }); + software_pass.set_pipeline(visibility_buffer_software_raster_pipeline); software_pass.set_bind_group( 0, &meshlet_view_bind_groups.visibility_buffer_raster, &[view_offset.offset], ); - software_pass - .dispatch_workgroups_indirect(visibility_buffer_hardware_software_indirect_args, 0); + software_pass.dispatch_workgroups_indirect(visibility_buffer_software_raster_indirect_args, 0); drop(software_pass); let mut hardware_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { @@ -508,6 +620,18 @@ fn raster_pass( &[view_offset.offset], ); hardware_pass.draw_indirect(visibility_buffer_hardware_raster_indirect_args, 0); + drop(hardware_pass); + + let mut fill_counts_pass = + render_context + .command_encoder() + .begin_compute_pass(&ComputePassDescriptor { + label: Some("fill_counts"), + timestamp_writes: None, + }); + fill_counts_pass.set_pipeline(fill_counts_pipeline); + fill_counts_pass.set_bind_group(0, &meshlet_view_bind_groups.fill_counts, &[]); + fill_counts_pass.dispatch_workgroups(1, 1, 1); } fn resolve_depth( diff --git a/crates/bevy_pbr/src/meshlet/visibility_buffer_resolve.wgsl b/crates/bevy_pbr/src/meshlet/visibility_buffer_resolve.wgsl index 4c56c5874a..8d8a22b943 100644 --- a/crates/bevy_pbr/src/meshlet/visibility_buffer_resolve.wgsl +++ b/crates/bevy_pbr/src/meshlet/visibility_buffer_resolve.wgsl @@ -4,9 +4,8 @@ meshlet_bindings::{ Meshlet, meshlet_visibility_buffer, - meshlet_cluster_meshlet_ids, + meshlet_raster_clusters, meshlets, - meshlet_cluster_instance_ids, meshlet_instance_uniforms, get_meshlet_vertex_id, get_meshlet_vertex_position, @@ -106,7 +105,8 @@ struct VertexOutput { fn resolve_vertex_output(frag_coord: vec4) -> VertexOutput { let packed_ids = u32(textureLoad(meshlet_visibility_buffer, vec2(frag_coord.xy)).r); let cluster_id = packed_ids >> 7u; - let meshlet_id = meshlet_cluster_meshlet_ids[cluster_id]; + let instanced_offset = meshlet_raster_clusters[cluster_id]; + let meshlet_id = instanced_offset.offset; var meshlet = meshlets[meshlet_id]; let triangle_id = extractBits(packed_ids, 0u, 7u); @@ -116,7 +116,7 @@ fn resolve_vertex_output(frag_coord: vec4) -> VertexOutput { let vertex_1 = load_vertex(&meshlet, vertex_ids[1]); let vertex_2 = load_vertex(&meshlet, vertex_ids[2]); - let instance_id = meshlet_cluster_instance_ids[cluster_id]; + let instance_id = instanced_offset.instance_id; var instance_uniform = meshlet_instance_uniforms[instance_id]; let world_from_local = affine3_to_square(instance_uniform.world_from_local); diff --git a/crates/bevy_pbr/src/meshlet/visibility_buffer_software_raster.wgsl b/crates/bevy_pbr/src/meshlet/visibility_buffer_software_raster.wgsl index 60f6f1b3ea..0ddfff8964 100644 --- a/crates/bevy_pbr/src/meshlet/visibility_buffer_software_raster.wgsl +++ b/crates/bevy_pbr/src/meshlet/visibility_buffer_software_raster.wgsl @@ -5,6 +5,7 @@ meshlet_cluster_instance_ids, meshlet_instance_uniforms, meshlet_raster_clusters, + meshlet_previous_raster_counts, meshlet_software_raster_cluster_count, meshlet_visibility_buffer, view, @@ -40,12 +41,11 @@ fn rasterize_cluster( if workgroup_id_1d >= meshlet_software_raster_cluster_count { return; } #endif - let cluster_id = meshlet_raster_clusters[workgroup_id_1d]; - let meshlet_id = meshlet_cluster_meshlet_ids[cluster_id]; - var meshlet = meshlets[meshlet_id]; + let cluster_id = workgroup_id_1d + meshlet_previous_raster_counts[0]; + let instanced_offset = meshlet_raster_clusters[cluster_id]; + var meshlet = meshlets[instanced_offset.offset]; - let instance_id = meshlet_cluster_instance_ids[cluster_id]; - let instance_uniform = meshlet_instance_uniforms[instance_id]; + let instance_uniform = meshlet_instance_uniforms[instanced_offset.instance_id]; let world_from_local = affine3_to_square(instance_uniform.world_from_local); // Load and project 1 vertex per thread, and then again if there are more than 128 vertices in the meshlet diff --git a/crates/bevy_pbr/src/pbr_material.rs b/crates/bevy_pbr/src/pbr_material.rs index cbd8445483..0207a81ed0 100644 --- a/crates/bevy_pbr/src/pbr_material.rs +++ b/crates/bevy_pbr/src/pbr_material.rs @@ -1185,7 +1185,8 @@ impl AsBindGroupShaderType for StandardMaterial { bitflags! { /// The pipeline key for `StandardMaterial`, packed into 64 bits. - #[derive(Clone, Copy, PartialEq, Eq, Hash)] + #[repr(C)] + #[derive(Clone, Copy, PartialEq, Eq, Hash, bytemuck::Pod, bytemuck::Zeroable)] pub struct StandardMaterialKey: u64 { const CULL_FRONT = 0x000001; const CULL_BACK = 0x000002; @@ -1404,7 +1405,7 @@ impl Material for StandardMaterial { } fn specialize( - _pipeline: &MaterialPipeline, + _pipeline: &MaterialPipeline, descriptor: &mut RenderPipelineDescriptor, _layout: &MeshVertexBufferLayoutRef, key: MaterialPipelineKey, diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index 8732f92e82..0dda6127f0 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -2,12 +2,13 @@ mod prepass_bindings; use crate::{ alpha_mode_pipeline_key, binding_arrays_are_usable, buffer_layout, - collect_meshes_for_gpu_building, material_bind_groups::MaterialBindGroupAllocator, - queue_material_meshes, set_mesh_motion_vector_flags, setup_morph_and_skinning_defs, skin, - DrawMesh, EntitySpecializationTicks, Material, MaterialPipeline, MaterialPipelineKey, - MeshLayouts, MeshPipeline, MeshPipelineKey, OpaqueRendererMethod, PreparedMaterial, + collect_meshes_for_gpu_building, set_mesh_motion_vector_flags, setup_morph_and_skinning_defs, + skin, DeferredDrawFunction, DeferredFragmentShader, DeferredVertexShader, DrawMesh, + EntitySpecializationTicks, ErasedMaterialPipelineKey, Material, MaterialPipeline, + MaterialProperties, MeshLayouts, MeshPipeline, MeshPipelineKey, OpaqueRendererMethod, + PreparedMaterial, PrepassDrawFunction, PrepassFragmentShader, PrepassVertexShader, RenderLightmaps, RenderMaterialInstances, RenderMeshInstanceFlags, RenderMeshInstances, - RenderPhaseType, SetMaterialBindGroup, SetMeshBindGroup, ShadowView, StandardMaterial, + RenderPhaseType, SetMaterialBindGroup, SetMeshBindGroup, ShadowView, }; use bevy_app::{App, Plugin, PreUpdate}; use bevy_render::{ @@ -24,7 +25,7 @@ use bevy_render::{ }; pub use prepass_bindings::*; -use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, Handle}; use bevy_core_pipeline::{ core_3d::CORE_3D_DEPTH_FORMAT, deferred::*, prelude::Camera3d, prepass::*, }; @@ -55,30 +56,25 @@ use crate::meshlet::{ MeshletMesh3d, }; +use alloc::sync::Arc; use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::component::Tick; -use bevy_ecs::system::SystemChangeTick; +use bevy_ecs::{component::Tick, system::SystemChangeTick}; use bevy_platform::collections::HashMap; -use bevy_render::sync_world::MainEntityHashMap; -use bevy_render::view::RenderVisibleEntities; -use bevy_render::RenderSystems::{PrepareAssets, PrepareResources}; -use core::{hash::Hash, marker::PhantomData}; +use bevy_render::{ + erased_render_asset::ErasedRenderAssets, + sync_world::MainEntityHashMap, + view::RenderVisibleEntities, + RenderSystems::{PrepareAssets, PrepareResources}, +}; +use bevy_utils::default; +use core::marker::PhantomData; /// Sets up everything required to use the prepass pipeline. /// /// This does not add the actual prepasses, see [`PrepassPlugin`] for that. -pub struct PrepassPipelinePlugin(PhantomData); +pub struct PrepassPipelinePlugin; -impl Default for PrepassPipelinePlugin { - fn default() -> Self { - Self(Default::default()) - } -} - -impl Plugin for PrepassPipelinePlugin -where - M::Data: PartialEq + Eq + Hash + Clone, -{ +impl Plugin for PrepassPipelinePlugin { fn build(&self, app: &mut App) { embedded_asset!(app, "prepass.wgsl"); @@ -93,10 +89,9 @@ where render_app .add_systems( Render, - prepare_prepass_view_bind_group::.in_set(RenderSystems::PrepareBindGroups), + prepare_prepass_view_bind_group.in_set(RenderSystems::PrepareBindGroups), ) - .init_resource::>>() - .allow_ambiguous_resource::>>(); + .init_resource::>(); } fn finish(&self, app: &mut App) { @@ -105,34 +100,27 @@ where }; render_app - .init_resource::>() + .init_resource::() .init_resource::(); } } -/// Sets up the prepasses for a [`Material`]. +/// Sets up the prepasses for a material. /// /// This depends on the [`PrepassPipelinePlugin`]. -pub struct PrepassPlugin { +pub struct PrepassPlugin { /// Debugging flags that can optionally be set when constructing the renderer. pub debug_flags: RenderDebugFlags, - pub phantom: PhantomData, } -impl PrepassPlugin { +impl PrepassPlugin { /// Creates a new [`PrepassPlugin`] with the given debug flags. pub fn new(debug_flags: RenderDebugFlags) -> Self { - PrepassPlugin { - debug_flags, - phantom: PhantomData, - } + PrepassPlugin { debug_flags } } } -impl Plugin for PrepassPlugin -where - M::Data: PartialEq + Eq + Hash + Clone, -{ +impl Plugin for PrepassPlugin { fn build(&self, app: &mut App) { let no_prepass_plugin_loaded = app .world() @@ -174,41 +162,45 @@ where render_app .init_resource::() .init_resource::() - .init_resource::>() - .add_render_command::>() - .add_render_command::>() - .add_render_command::>() - .add_render_command::>() + .init_resource::() + .add_render_command::() + .add_render_command::() + .add_render_command::() + .add_render_command::() .add_systems( Render, ( check_prepass_views_need_specialization.in_set(PrepareAssets), - specialize_prepass_material_meshes:: + specialize_prepass_material_meshes .in_set(RenderSystems::PrepareMeshes) - .after(prepare_assets::>) .after(prepare_assets::) .after(collect_meshes_for_gpu_building) .after(set_mesh_motion_vector_flags), - queue_prepass_material_meshes:: - .in_set(RenderSystems::QueueMeshes) - .after(prepare_assets::>) - // queue_material_meshes only writes to `material_bind_group_id`, which `queue_prepass_material_meshes` doesn't read - .ambiguous_with(queue_material_meshes::), + queue_prepass_material_meshes.in_set(RenderSystems::QueueMeshes), ), ); #[cfg(feature = "meshlet")] render_app.add_systems( Render, - prepare_material_meshlet_meshes_prepass:: + prepare_material_meshlet_meshes_prepass .in_set(RenderSystems::QueueMeshes) - .after(prepare_assets::>) - .before(queue_material_meshlet_meshes::) + .before(queue_material_meshlet_meshes) .run_if(resource_exists::), ); } } +/// Marker resource for whether prepass is enabled globally for this material type +#[derive(Resource, Debug)] +pub struct PrepassEnabled(PhantomData); + +impl Default for PrepassEnabled { + fn default() -> Self { + PrepassEnabled(PhantomData) + } +} + #[derive(Resource)] struct AnyPrepassPluginLoaded; @@ -261,24 +253,12 @@ pub fn update_mesh_previous_global_transforms( } } -#[derive(Resource)] -pub struct PrepassPipeline { - pub internal: PrepassPipelineInternal, - pub material_pipeline: MaterialPipeline, -} - -/// Internal fields of the `PrepassPipeline` that don't need the generic bound -/// This is done as an optimization to not recompile the same code multiple time -pub struct PrepassPipelineInternal { +#[derive(Resource, Clone)] +pub struct PrepassPipeline { pub view_layout_motion_vectors: BindGroupLayout, pub view_layout_no_motion_vectors: BindGroupLayout, pub mesh_layouts: MeshLayouts, pub empty_layout: BindGroupLayout, - pub material_layout: BindGroupLayout, - pub prepass_material_vertex_shader: Option>, - pub prepass_material_fragment_shader: Option>, - pub deferred_material_vertex_shader: Option>, - pub deferred_material_fragment_shader: Option>, pub default_prepass_shader: Handle, /// Whether skins will use uniform buffers on account of storage buffers @@ -290,14 +270,13 @@ pub struct PrepassPipelineInternal { /// Whether binding arrays (a.k.a. bindless textures) are usable on the /// current render device. pub binding_arrays_are_usable: bool, + pub material_pipeline: MaterialPipeline, } -impl FromWorld for PrepassPipeline { +impl FromWorld for PrepassPipeline { fn from_world(world: &mut World) -> Self { let render_device = world.resource::(); let render_adapter = world.resource::(); - let asset_server = world.resource::(); - let visibility_ranges_buffer_binding_type = render_device .get_supported_read_only_binding_type(VISIBILITY_RANGES_STORAGE_BUFFER_COUNT); @@ -354,49 +333,27 @@ impl FromWorld for PrepassPipeline { let depth_clip_control_supported = render_device .features() .contains(WgpuFeatures::DEPTH_CLIP_CONTROL); - let internal = PrepassPipelineInternal { + PrepassPipeline { view_layout_motion_vectors, view_layout_no_motion_vectors, mesh_layouts: mesh_pipeline.mesh_layouts.clone(), - prepass_material_vertex_shader: match M::prepass_vertex_shader() { - ShaderRef::Default => None, - ShaderRef::Handle(handle) => Some(handle), - ShaderRef::Path(path) => Some(asset_server.load(path)), - }, - prepass_material_fragment_shader: match M::prepass_fragment_shader() { - ShaderRef::Default => None, - ShaderRef::Handle(handle) => Some(handle), - ShaderRef::Path(path) => Some(asset_server.load(path)), - }, - deferred_material_vertex_shader: match M::deferred_vertex_shader() { - ShaderRef::Default => None, - ShaderRef::Handle(handle) => Some(handle), - ShaderRef::Path(path) => Some(asset_server.load(path)), - }, - deferred_material_fragment_shader: match M::deferred_fragment_shader() { - ShaderRef::Default => None, - ShaderRef::Handle(handle) => Some(handle), - ShaderRef::Path(path) => Some(asset_server.load(path)), - }, default_prepass_shader: load_embedded_asset!(world, "prepass.wgsl"), - material_layout: M::bind_group_layout(render_device), skins_use_uniform_buffers: skin::skins_use_uniform_buffers(render_device), depth_clip_control_supported, binding_arrays_are_usable: binding_arrays_are_usable(render_device, render_adapter), empty_layout: render_device.create_bind_group_layout("prepass_empty_layout", &[]), - }; - PrepassPipeline { - internal, - material_pipeline: world.resource::>().clone(), + material_pipeline: world.resource::().clone(), } } } -impl SpecializedMeshPipeline for PrepassPipeline -where - M::Data: PartialEq + Eq + Hash + Clone, -{ - type Key = MaterialPipelineKey; +pub struct PrepassPipelineSpecializer { + pub(crate) pipeline: PrepassPipeline, + pub(crate) properties: Arc, +} + +impl SpecializedMeshPipeline for PrepassPipelineSpecializer { + type Key = ErasedMaterialPipelineKey; fn specialize( &self, @@ -404,28 +361,36 @@ where layout: &MeshVertexBufferLayoutRef, ) -> Result { let mut shader_defs = Vec::new(); - if self.material_pipeline.bindless { + if self.properties.bindless { shader_defs.push("BINDLESS".into()); } - let mut descriptor = self - .internal - .specialize(key.mesh_key, shader_defs, layout)?; + let mut descriptor = + self.pipeline + .specialize(key.mesh_key, shader_defs, layout, &self.properties)?; // This is a bit risky because it's possible to change something that would // break the prepass but be fine in the main pass. // Since this api is pretty low-level it doesn't matter that much, but it is a potential issue. - M::specialize(&self.material_pipeline, &mut descriptor, layout, key)?; + if let Some(specialize) = self.properties.specialize { + specialize( + &self.pipeline.material_pipeline, + &mut descriptor, + layout, + key, + )?; + } Ok(descriptor) } } -impl PrepassPipelineInternal { +impl PrepassPipeline { fn specialize( &self, mesh_key: MeshPipelineKey, shader_defs: Vec, layout: &MeshVertexBufferLayoutRef, + material_properties: &MaterialProperties, ) -> Result { let mut shader_defs = shader_defs; let mut bind_group_layouts = vec![ @@ -445,7 +410,13 @@ impl PrepassPipelineInternal { // NOTE: Eventually, it would be nice to only add this when the shaders are overloaded by the Material. // The main limitation right now is that bind group order is hardcoded in shaders. - bind_group_layouts.push(self.material_layout.clone()); + bind_group_layouts.push( + material_properties + .material_layout + .as_ref() + .unwrap() + .clone(), + ); #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] shader_defs.push("WEBGL2".into()); shader_defs.push("VERTEX_OUTPUT_INSTANCE_INDEX".into()); @@ -581,17 +552,19 @@ impl PrepassPipelineInternal { let fragment_required = !targets.is_empty() || emulate_unclipped_depth || (mesh_key.contains(MeshPipelineKey::MAY_DISCARD) - && self.prepass_material_fragment_shader.is_some()); + && material_properties + .get_shader(PrepassFragmentShader) + .is_some()); let fragment = fragment_required.then(|| { // Use the fragment shader from the material let frag_shader_handle = if mesh_key.contains(MeshPipelineKey::DEFERRED_PREPASS) { - match self.deferred_material_fragment_shader.clone() { + match material_properties.get_shader(DeferredFragmentShader) { Some(frag_shader_handle) => frag_shader_handle, None => self.default_prepass_shader.clone(), } } else { - match self.prepass_material_fragment_shader.clone() { + match material_properties.get_shader(PrepassFragmentShader) { Some(frag_shader_handle) => frag_shader_handle, None => self.default_prepass_shader.clone(), } @@ -599,41 +572,37 @@ impl PrepassPipelineInternal { FragmentState { shader: frag_shader_handle, - entry_point: "fragment".into(), shader_defs: shader_defs.clone(), targets, + ..default() } }); // Use the vertex shader from the material if present let vert_shader_handle = if mesh_key.contains(MeshPipelineKey::DEFERRED_PREPASS) { - if let Some(handle) = &self.deferred_material_vertex_shader { - handle.clone() + if let Some(handle) = material_properties.get_shader(DeferredVertexShader) { + handle } else { self.default_prepass_shader.clone() } - } else if let Some(handle) = &self.prepass_material_vertex_shader { - handle.clone() + } else if let Some(handle) = material_properties.get_shader(PrepassVertexShader) { + handle } else { self.default_prepass_shader.clone() }; let descriptor = RenderPipelineDescriptor { vertex: VertexState { shader: vert_shader_handle, - entry_point: "vertex".into(), shader_defs, buffers: vec![vertex_buffer_layout], + ..default() }, fragment, layout: bind_group_layouts, primitive: PrimitiveState { topology: mesh_key.primitive_topology(), - strip_index_format: None, - front_face: FrontFace::Ccw, - cull_mode: None, unclipped_depth, - polygon_mode: PolygonMode::Fill, - conservative: false, + ..default() }, depth_stencil: Some(DepthStencilState { format: CORE_3D_DEPTH_FORMAT, @@ -656,9 +625,8 @@ impl PrepassPipelineInternal { mask: !0, alpha_to_coverage_enabled: false, }, - push_constant_ranges: vec![], label: Some("prepass_pipeline".into()), - zero_initialize_workgroup_memory: false, + ..default() }; Ok(descriptor) } @@ -736,12 +704,12 @@ pub struct PrepassViewBindGroup { impl FromWorld for PrepassViewBindGroup { fn from_world(world: &mut World) -> Self { - let pipeline = world.resource::>(); + let pipeline = world.resource::(); let render_device = world.resource::(); let empty_bind_group = render_device.create_bind_group( "prepass_view_empty_bind_group", - &pipeline.internal.empty_layout, + &pipeline.empty_layout, &[], ); PrepassViewBindGroup { @@ -752,9 +720,9 @@ impl FromWorld for PrepassViewBindGroup { } } -pub fn prepare_prepass_view_bind_group( +pub fn prepare_prepass_view_bind_group( render_device: Res, - prepass_pipeline: Res>, + prepass_pipeline: Res, view_uniforms: Res, globals_buffer: Res, previous_view_uniforms: Res, @@ -768,7 +736,7 @@ pub fn prepare_prepass_view_bind_group( ) { prepass_view_bind_group.no_motion_vectors = Some(render_device.create_bind_group( "prepass_view_no_motion_vectors_bind_group", - &prepass_pipeline.internal.view_layout_no_motion_vectors, + &prepass_pipeline.view_layout_no_motion_vectors, &BindGroupEntries::with_indices(( (0, view_binding.clone()), (1, globals_binding.clone()), @@ -779,7 +747,7 @@ pub fn prepare_prepass_view_bind_group( if let Some(previous_view_uniforms_binding) = previous_view_uniforms.uniforms.binding() { prepass_view_bind_group.motion_vectors = Some(render_device.create_bind_group( "prepass_view_motion_vectors_bind_group", - &prepass_pipeline.internal.view_layout_motion_vectors, + &prepass_pipeline.view_layout_motion_vectors, &BindGroupEntries::with_indices(( (0, view_binding), (1, globals_binding), @@ -792,40 +760,20 @@ pub fn prepare_prepass_view_bind_group( } /// Stores the [`SpecializedPrepassMaterialViewPipelineCache`] for each view. -#[derive(Resource, Deref, DerefMut)] -pub struct SpecializedPrepassMaterialPipelineCache { +#[derive(Resource, Deref, DerefMut, Default)] +pub struct SpecializedPrepassMaterialPipelineCache { // view_entity -> view pipeline cache #[deref] - map: HashMap>, - marker: PhantomData, + map: HashMap, } /// Stores the cached render pipeline ID for each entity in a single view, as /// well as the last time it was changed. -#[derive(Deref, DerefMut)] -pub struct SpecializedPrepassMaterialViewPipelineCache { +#[derive(Deref, DerefMut, Default)] +pub struct SpecializedPrepassMaterialViewPipelineCache { // material entity -> (tick, pipeline_id) #[deref] map: MainEntityHashMap<(Tick, CachedRenderPipelineId)>, - marker: PhantomData, -} - -impl Default for SpecializedPrepassMaterialPipelineCache { - fn default() -> Self { - Self { - map: HashMap::default(), - marker: PhantomData, - } - } -} - -impl Default for SpecializedPrepassMaterialViewPipelineCache { - fn default() -> Self { - Self { - map: HashMap::default(), - marker: PhantomData, - } - } } #[derive(Resource, Deref, DerefMut, Default, Clone)] @@ -870,14 +818,13 @@ pub fn check_prepass_views_need_specialization( } } -pub fn specialize_prepass_material_meshes( +pub fn specialize_prepass_material_meshes( render_meshes: Res>, - render_materials: Res>>, + render_materials: Res>, render_mesh_instances: Res, render_material_instances: Res, render_lightmaps: Res, render_visibility_ranges: Res, - material_bind_group_allocator: Res>, view_key_cache: Res, views: Query<( &ExtractedView, @@ -906,18 +853,15 @@ pub fn specialize_prepass_material_meshes( view_specialization_ticks, entity_specialization_ticks, ): ( - ResMut>, + ResMut, SystemChangeTick, - Res>, - ResMut>>, + Res, + ResMut>, Res, Res, - Res>, + Res, ), -) where - M: Material, - M::Data: PartialEq + Eq + Hash + Clone, -{ +) { for (extracted_view, visible_entities, msaa, motion_vector_prepass, deferred_prepass) in &views { if !opaque_deferred_render_phases.contains_key(&extracted_view.retained_view_entity) @@ -944,9 +888,6 @@ pub fn specialize_prepass_material_meshes( else { continue; }; - let Ok(material_asset_id) = material_instance.asset_id.try_typed::() else { - continue; - }; let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity) else { continue; @@ -962,15 +903,14 @@ pub fn specialize_prepass_material_meshes( if !needs_specialization { continue; } - let Some(material) = render_materials.get(material_asset_id) else { + let Some(material) = render_materials.get(material_instance.asset_id) else { continue; }; - let Some(material_bind_group) = - material_bind_group_allocator.get(material.binding.group) - else { - warn!("Couldn't get bind group for material"); + if !material.properties.prepass_enabled && !material.properties.shadows_enabled { + // If the material was previously specialized for prepass, remove it + view_specialized_material_pipeline_cache.remove(visible_entity); continue; - }; + } let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else { continue; }; @@ -1045,15 +985,19 @@ pub fn specialize_prepass_material_meshes( } } + let erased_key = ErasedMaterialPipelineKey { + mesh_key, + material_key: material.properties.material_key.clone(), + type_id: material_instance.asset_id.type_id(), + }; + let prepass_pipeline_specializer = PrepassPipelineSpecializer { + pipeline: prepass_pipeline.clone(), + properties: material.properties.clone(), + }; let pipeline_id = pipelines.specialize( &pipeline_cache, - &prepass_pipeline, - MaterialPipelineKey { - mesh_key, - bind_group_data: material_bind_group - .get_extra_data(material.binding.slot) - .clone(), - }, + &prepass_pipeline_specializer, + erased_key, &mesh.layout, ); let pipeline_id = match pipeline_id { @@ -1070,9 +1014,9 @@ pub fn specialize_prepass_material_meshes( } } -pub fn queue_prepass_material_meshes( +pub fn queue_prepass_material_meshes( render_mesh_instances: Res, - render_materials: Res>>, + render_materials: Res>, render_material_instances: Res, mesh_allocator: Res, gpu_preprocessing_support: Res, @@ -1081,10 +1025,8 @@ pub fn queue_prepass_material_meshes( mut opaque_deferred_render_phases: ResMut>, mut alpha_mask_deferred_render_phases: ResMut>, views: Query<(&ExtractedView, &RenderVisibleEntities)>, - specialized_material_pipeline_cache: Res>, -) where - M::Data: PartialEq + Eq + Hash + Clone, -{ + specialized_material_pipeline_cache: Res, +) { for (extracted_view, visible_entities) in &views { let ( mut opaque_phase, @@ -1137,14 +1079,11 @@ pub fn queue_prepass_material_meshes( else { continue; }; - let Ok(material_asset_id) = material_instance.asset_id.try_typed::() else { - continue; - }; let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity) else { continue; }; - let Some(material) = render_materials.get(material_asset_id) else { + let Some(material) = render_materials.get(material_instance.asset_id) else { continue; }; let (vertex_slab, index_slab) = mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); @@ -1162,7 +1101,7 @@ pub fn queue_prepass_material_meshes( OpaqueNoLightmap3dBatchSetKey { draw_function: material .properties - .deferred_draw_function_id + .get_draw_function(DeferredDrawFunction) .unwrap(), pipeline: *pipeline_id, material_bind_group_index: Some(material.binding.group.0), @@ -1187,7 +1126,7 @@ pub fn queue_prepass_material_meshes( OpaqueNoLightmap3dBatchSetKey { draw_function: material .properties - .prepass_draw_function_id + .get_draw_function(PrepassDrawFunction) .unwrap(), pipeline: *pipeline_id, material_bind_group_index: Some(material.binding.group.0), @@ -1212,7 +1151,10 @@ pub fn queue_prepass_material_meshes( let (vertex_slab, index_slab) = mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); let batch_set_key = OpaqueNoLightmap3dBatchSetKey { - draw_function: material.properties.deferred_draw_function_id.unwrap(), + draw_function: material + .properties + .get_draw_function(DeferredDrawFunction) + .unwrap(), pipeline: *pipeline_id, material_bind_group_index: Some(material.binding.group.0), vertex_slab: vertex_slab.unwrap_or_default(), @@ -1236,7 +1178,10 @@ pub fn queue_prepass_material_meshes( let (vertex_slab, index_slab) = mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); let batch_set_key = OpaqueNoLightmap3dBatchSetKey { - draw_function: material.properties.prepass_draw_function_id.unwrap(), + draw_function: material + .properties + .get_draw_function(PrepassDrawFunction) + .unwrap(), pipeline: *pipeline_id, material_bind_group_index: Some(material.binding.group.0), vertex_slab: vertex_slab.unwrap_or_default(), @@ -1331,11 +1276,11 @@ impl RenderCommand

for SetPrepassViewEmptyBindG } } -pub type DrawPrepass = ( +pub type DrawPrepass = ( SetItemPipeline, SetPrepassViewBindGroup<0>, SetPrepassViewEmptyBindGroup<1>, SetMeshBindGroup<2>, - SetMaterialBindGroup, + SetMaterialBindGroup<3>, DrawMesh, ); diff --git a/crates/bevy_pbr/src/render/gpu_preprocess.rs b/crates/bevy_pbr/src/render/gpu_preprocess.rs index eaa7e857b7..52df74cc26 100644 --- a/crates/bevy_pbr/src/render/gpu_preprocess.rs +++ b/crates/bevy_pbr/src/render/gpu_preprocess.rs @@ -38,7 +38,7 @@ use bevy_render::{ UntypedPhaseBatchedInstanceBuffers, }, experimental::occlusion_culling::OcclusionCulling, - render_graph::{Node, NodeRunError, RenderGraphApp, RenderGraphContext}, + render_graph::{Node, NodeRunError, RenderGraphContext, RenderGraphExt}, render_resource::{ binding_types::{storage_buffer, storage_buffer_read_only, texture_2d, uniform_buffer}, BindGroup, BindGroupEntries, BindGroupLayout, BindingResource, Buffer, BufferBinding, @@ -52,7 +52,7 @@ use bevy_render::{ view::{ExtractedView, NoIndirectDrawing, ViewUniform, ViewUniformOffset, ViewUniforms}, Render, RenderApp, RenderSystems, }; -use bevy_utils::TypeIdMap; +use bevy_utils::{default, TypeIdMap}; use bitflags::bitflags; use smallvec::{smallvec, SmallVec}; use tracing::warn; @@ -1275,8 +1275,7 @@ impl SpecializedComputePipeline for PreprocessPipeline { }, shader: self.shader.clone(), shader_defs, - entry_point: "main".into(), - zero_initialize_workgroup_memory: false, + ..default() } } } @@ -1637,11 +1636,8 @@ impl SpecializedComputePipeline for ResetIndirectBatchSetsPipeline { ComputePipelineDescriptor { label: Some("reset indirect batch sets".into()), layout: vec![self.bind_group_layout.clone()], - push_constant_ranges: vec![], shader: self.shader.clone(), - shader_defs: vec![], - entry_point: "main".into(), - zero_initialize_workgroup_memory: false, + ..default() } } } @@ -1691,11 +1687,9 @@ impl SpecializedComputePipeline for BuildIndirectParametersPipeline { ComputePipelineDescriptor { label: Some(label.into()), layout: vec![self.bind_group_layout.clone()], - push_constant_ranges: vec![], shader: self.shader.clone(), shader_defs, - entry_point: "main".into(), - zero_initialize_workgroup_memory: false, + ..default() } } } diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 83d28a7da7..1a55eb0926 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -1,5 +1,4 @@ use self::assign::ClusterableObjectType; -use crate::material_bind_groups::MaterialBindGroupAllocator; use crate::*; use bevy_asset::UntypedAssetId; use bevy_color::ColorToComponents; @@ -15,6 +14,7 @@ use bevy_ecs::{ use bevy_math::{ops, Mat4, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; use bevy_platform::collections::{HashMap, HashSet}; use bevy_platform::hash::FixedHasher; +use bevy_render::erased_render_asset::ErasedRenderAssets; use bevy_render::experimental::occlusion_culling::{ OcclusionCulling, OcclusionCullingSubview, OcclusionCullingSubviewEntities, }; @@ -44,7 +44,8 @@ use bevy_render::{ }; use bevy_transform::{components::GlobalTransform, prelude::Transform}; use bevy_utils::default; -use core::{hash::Hash, marker::PhantomData, ops::Range}; +use core::{hash::Hash, ops::Range}; +use decal::clustered::RenderClusteredDecals; #[cfg(feature = "trace")] use tracing::info_span; use tracing::{error, warn}; @@ -121,6 +122,7 @@ pub struct GpuDirectionalLight { num_cascades: u32, cascades_overlap_proportion: f32, depth_texture_base_index: u32, + decal_index: u32, } // NOTE: These must match the bit flags in bevy_pbr/src/render/mesh_view_types.wgsl! @@ -777,7 +779,10 @@ pub fn prepare_lights( directional_lights: Query<(Entity, &MainEntity, &ExtractedDirectionalLight)>, mut light_view_entities: Query<&mut LightViewEntities>, sorted_cameras: Res, - gpu_preprocessing_support: Res, + (gpu_preprocessing_support, decals): ( + Res, + Option>, + ), ) { let views_iter = views.iter(); let views_count = views_iter.len(); @@ -997,8 +1002,12 @@ pub fn prepare_lights( shadow_normal_bias: light.shadow_normal_bias, shadow_map_near_z: light.shadow_map_near_z, spot_light_tan_angle, - pad_a: 0.0, - pad_b: 0.0, + decal_index: decals + .as_ref() + .and_then(|decals| decals.get(entity)) + .and_then(|index| index.try_into().ok()) + .unwrap_or(u32::MAX), + pad: 0.0, soft_shadow_size: if light.soft_shadows_enabled { light.radius } else { @@ -1187,7 +1196,7 @@ pub fn prepare_lights( let mut gpu_directional_lights = [GpuDirectionalLight::default(); MAX_DIRECTIONAL_LIGHTS]; let mut num_directional_cascades_enabled_for_this_view = 0usize; let mut num_directional_lights_for_this_view = 0usize; - for (index, (_light_entity, _, light)) in directional_lights + for (index, (light_entity, _, light)) in directional_lights .iter() .filter(|(_light_entity, _, light)| light.render_layers.intersects(view_layers)) .enumerate() @@ -1241,6 +1250,11 @@ pub fn prepare_lights( num_cascades: num_cascades as u32, cascades_overlap_proportion: light.cascade_shadow_config.overlap_proportion, depth_texture_base_index: num_directional_cascades_enabled_for_this_view as u32, + decal_index: decals + .as_ref() + .and_then(|decals| decals.get(*light_entity)) + .and_then(|index| index.try_into().ok()) + .unwrap_or(u32::MAX), }; num_directional_cascades_enabled_for_this_view += num_cascades; } @@ -1717,37 +1731,17 @@ pub struct LightKeyCache(HashMap); #[derive(Resource, Deref, DerefMut, Default, Debug, Clone)] pub struct LightSpecializationTicks(HashMap); -#[derive(Resource, Deref, DerefMut)] -pub struct SpecializedShadowMaterialPipelineCache { +#[derive(Resource, Deref, DerefMut, Default)] +pub struct SpecializedShadowMaterialPipelineCache { // view light entity -> view pipeline cache #[deref] - map: HashMap>, - marker: PhantomData, + map: HashMap, } -#[derive(Deref, DerefMut)] -pub struct SpecializedShadowMaterialViewPipelineCache { +#[derive(Deref, DerefMut, Default)] +pub struct SpecializedShadowMaterialViewPipelineCache { #[deref] map: MainEntityHashMap<(Tick, CachedRenderPipelineId)>, - marker: PhantomData, -} - -impl Default for SpecializedShadowMaterialPipelineCache { - fn default() -> Self { - Self { - map: HashMap::default(), - marker: PhantomData, - } - } -} - -impl Default for SpecializedShadowMaterialViewPipelineCache { - fn default() -> Self { - Self { - map: MainEntityHashMap::default(), - marker: PhantomData, - } - } } pub fn check_views_lights_need_specialization( @@ -1789,23 +1783,16 @@ pub fn check_views_lights_need_specialization( } } -pub fn specialize_shadows( - prepass_pipeline: Res>, - ( - render_meshes, - render_mesh_instances, - render_materials, - render_material_instances, - material_bind_group_allocator, - ): ( +pub fn specialize_shadows( + prepass_pipeline: Res, + (render_meshes, render_mesh_instances, render_materials, render_material_instances): ( Res>, Res, - Res>>, + Res>, Res, - Res>, ), shadow_render_phases: Res>, - mut pipelines: ResMut>>, + mut pipelines: ResMut>, pipeline_cache: Res, render_lightmaps: Res, view_lights: Query<(Entity, &ViewLightEntities), With>, @@ -1817,13 +1804,11 @@ pub fn specialize_shadows( >, spot_light_entities: Query<&RenderVisibleMeshEntities, With>, light_key_cache: Res, - mut specialized_material_pipeline_cache: ResMut>, + mut specialized_material_pipeline_cache: ResMut, light_specialization_ticks: Res, - entity_specialization_ticks: Res>, + entity_specialization_ticks: Res, ticks: SystemChangeTick, -) where - M::Data: PartialEq + Eq + Hash + Clone, -{ +) { // Record the retained IDs of all shadow views so that we can expire old // pipeline IDs. let mut all_shadow_views: HashSet = HashSet::default(); @@ -1881,14 +1866,12 @@ pub fn specialize_shadows( .or_default(); for (_, visible_entity) in visible_entities.iter().copied() { - let Some(material_instances) = + let Some(material_instance) = render_material_instances.instances.get(&visible_entity) else { continue; }; - let Ok(material_asset_id) = material_instances.asset_id.try_typed::() else { - continue; - }; + let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(visible_entity) else { @@ -1905,20 +1888,19 @@ pub fn specialize_shadows( if !needs_specialization { continue; } - let Some(material) = render_materials.get(material_asset_id) else { + let Some(material) = render_materials.get(material_instance.asset_id) else { continue; }; + if !material.properties.shadows_enabled { + // If the material is not a shadow caster, we don't need to specialize it. + continue; + } if !mesh_instance .flags .contains(RenderMeshInstanceFlags::SHADOW_CASTER) { continue; } - let Some(material_bind_group) = - material_bind_group_allocator.get(material.binding.group) - else { - continue; - }; let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else { continue; }; @@ -1946,18 +1928,21 @@ pub fn specialize_shadows( | AlphaMode::AlphaToCoverage => MeshPipelineKey::MAY_DISCARD, _ => MeshPipelineKey::NONE, }; + let erased_key = ErasedMaterialPipelineKey { + mesh_key, + material_key: material.properties.material_key.clone(), + type_id: material_instance.asset_id.type_id(), + }; + let material_pipeline_specializer = PrepassPipelineSpecializer { + pipeline: prepass_pipeline.clone(), + properties: material.properties.clone(), + }; let pipeline_id = pipelines.specialize( &pipeline_cache, - &prepass_pipeline, - MaterialPipelineKey { - mesh_key, - bind_group_data: material_bind_group - .get_extra_data(material.binding.slot) - .clone(), - }, + &material_pipeline_specializer, + erased_key, &mesh.layout, ); - let pipeline_id = match pipeline_id { Ok(id) => id, Err(err) => { @@ -1979,10 +1964,9 @@ pub fn specialize_shadows( /// For each shadow cascade, iterates over all the meshes "visible" from it and /// adds them to [`BinnedRenderPhase`]s or [`SortedRenderPhase`]s as /// appropriate. -pub fn queue_shadows( - shadow_draw_functions: Res>, +pub fn queue_shadows( render_mesh_instances: Res, - render_materials: Res>>, + render_materials: Res>, render_material_instances: Res, mut shadow_render_phases: ResMut>, gpu_preprocessing_support: Res, @@ -1995,11 +1979,8 @@ pub fn queue_shadows( With, >, spot_light_entities: Query<&RenderVisibleMeshEntities, With>, - specialized_material_pipeline_cache: Res>, -) where - M::Data: PartialEq + Eq + Hash + Clone, -{ - let draw_shadow_mesh = shadow_draw_functions.read().id::>(); + specialized_material_pipeline_cache: Res, +) { for (entity, view_lights) in &view_lights { for view_light_entity in view_lights.lights.iter().copied() { let Ok((light_entity, extracted_view_light)) = @@ -2070,10 +2051,12 @@ pub fn queue_shadows( else { continue; }; - let Ok(material_asset_id) = material_instance.asset_id.try_typed::() else { + let Some(material) = render_materials.get(material_instance.asset_id) else { continue; }; - let Some(material) = render_materials.get(material_asset_id) else { + let Some(draw_function) = + material.properties.get_draw_function(ShadowsDrawFunction) + else { continue; }; @@ -2082,7 +2065,7 @@ pub fn queue_shadows( let batch_set_key = ShadowBatchSetKey { pipeline: *pipeline_id, - draw_function: draw_shadow_mesh, + draw_function, material_bind_group_index: Some(material.binding.group.0), vertex_slab: vertex_slab.unwrap_or_default(), index_slab, diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 50d7e98a48..dff8edb32b 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -2039,7 +2039,7 @@ impl GetFullBatchData for MeshPipeline { } bitflags::bitflags! { - #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + #[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Hash)] #[repr(transparent)] // NOTE: Apparently quadro drivers support up to 64x MSAA. /// MSAA uses the highest 3 bits for the MSAA log2(sample count) to support up to 128x MSAA. @@ -2557,6 +2557,9 @@ impl SpecializedMeshPipeline for MeshPipeline { if self.clustered_decals_are_usable { shader_defs.push("CLUSTERED_DECALS_ARE_USABLE".into()); + if cfg!(feature = "pbr_light_textures") { + shader_defs.push("LIGHT_TEXTURES".into()); + } } let format = if key.contains(MeshPipelineKey::HDR) { @@ -2578,30 +2581,26 @@ impl SpecializedMeshPipeline for MeshPipeline { Ok(RenderPipelineDescriptor { vertex: VertexState { shader: self.shader.clone(), - entry_point: "vertex".into(), shader_defs: shader_defs.clone(), buffers: vec![vertex_buffer_layout], + ..default() }, fragment: Some(FragmentState { shader: self.shader.clone(), shader_defs, - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format, blend, write_mask: ColorWrites::ALL, })], + ..default() }), layout: bind_group_layout, - push_constant_ranges: vec![], primitive: PrimitiveState { - front_face: FrontFace::Ccw, cull_mode: Some(Face::Back), unclipped_depth: false, - polygon_mode: PolygonMode::Fill, - conservative: false, topology: key.primitive_topology(), - strip_index_format: None, + ..default() }, depth_stencil: Some(DepthStencilState { format: CORE_3D_DEPTH_FORMAT, @@ -2625,7 +2624,7 @@ impl SpecializedMeshPipeline for MeshPipeline { alpha_to_coverage_enabled, }, label: Some(label), - zero_initialize_workgroup_memory: false, + ..default() }) } } diff --git a/crates/bevy_pbr/src/render/mesh_view_types.wgsl b/crates/bevy_pbr/src/render/mesh_view_types.wgsl index c8b2b53fbc..3ba62f1414 100644 --- a/crates/bevy_pbr/src/render/mesh_view_types.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_types.wgsl @@ -13,7 +13,7 @@ struct ClusterableObject { spot_light_tan_angle: f32, soft_shadow_size: f32, shadow_map_near_z: f32, - texture_index: u32, + decal_index: u32, pad: f32, }; @@ -40,6 +40,7 @@ struct DirectionalLight { num_cascades: u32, cascades_overlap_proportion: f32, depth_texture_base_index: u32, + decal_index: u32, }; const DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u << 0u; diff --git a/crates/bevy_pbr/src/render/pbr_functions.wgsl b/crates/bevy_pbr/src/render/pbr_functions.wgsl index 11e6d4d874..84f7b95661 100644 --- a/crates/bevy_pbr/src/render/pbr_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_functions.wgsl @@ -422,7 +422,7 @@ fn apply_pbr_lighting( shadow = shadows::fetch_point_shadow(light_id, in.world_position, in.world_normal); } - let light_contrib = lighting::point_light(light_id, &lighting_input, enable_diffuse); + let light_contrib = lighting::point_light(light_id, &lighting_input, enable_diffuse, true); direct_light += light_contrib * shadow; #ifdef STANDARD_MATERIAL_DIFFUSE_TRANSMISSION @@ -442,7 +442,7 @@ fn apply_pbr_lighting( } let transmitted_light_contrib = - lighting::point_light(light_id, &transmissive_lighting_input, enable_diffuse); + lighting::point_light(light_id, &transmissive_lighting_input, enable_diffuse, true); transmitted_light += transmitted_light_contrib * transmitted_shadow; #endif } diff --git a/crates/bevy_pbr/src/render/pbr_lighting.wgsl b/crates/bevy_pbr/src/render/pbr_lighting.wgsl index 01e09fe3b4..17cae13b92 100644 --- a/crates/bevy_pbr/src/render/pbr_lighting.wgsl +++ b/crates/bevy_pbr/src/render/pbr_lighting.wgsl @@ -456,10 +456,77 @@ fn perceptualRoughnessToRoughness(perceptualRoughness: f32) -> f32 { return clampedPerceptualRoughness * clampedPerceptualRoughness; } +// this must align with CubemapLayout in decal/clustered.rs +const CUBEMAP_TYPE_CROSS_VERTICAL: u32 = 0; +const CUBEMAP_TYPE_CROSS_HORIZONTAL: u32 = 1; +const CUBEMAP_TYPE_SEQUENCE_VERTICAL: u32 = 2; +const CUBEMAP_TYPE_SEQUENCE_HORIZONTAL: u32 = 3; + +const X_PLUS: u32 = 0; +const X_MINUS: u32 = 1; +const Y_PLUS: u32 = 2; +const Y_MINUS: u32 = 3; +const Z_MINUS: u32 = 4; +const Z_PLUS: u32 = 5; + +fn cubemap_uv(direction: vec3, cubemap_type: u32) -> vec2 { + let abs_direction = abs(direction); + let max_axis = max(abs_direction.x, max(abs_direction.y, abs_direction.z)); + + let face_index = select( + select(X_PLUS, X_MINUS, direction.x < 0.0), + select( + select(Y_PLUS, Y_MINUS, direction.y < 0.0), + select(Z_PLUS, Z_MINUS, direction.z < 0.0), + max_axis != abs_direction.y + ), + max_axis != abs_direction.x + ); + + var face_uv: vec2; + var divisor: f32; + var corner_uv: vec2 = vec2(0, 0); + var face_size: vec2; + + switch face_index { + case X_PLUS: { face_uv = vec2(direction.z, -direction.y); divisor = direction.x; } + case X_MINUS: { face_uv = vec2(-direction.z, -direction.y); divisor = -direction.x; } + case Y_PLUS: { face_uv = vec2(direction.x, -direction.z); divisor = direction.y; } + case Y_MINUS: { face_uv = vec2(direction.x, direction.z); divisor = -direction.y; } + case Z_PLUS: { face_uv = vec2(direction.x, direction.y); divisor = direction.z; } + case Z_MINUS: { face_uv = vec2(direction.x, -direction.y); divisor = -direction.z; } + default: {} + } + face_uv = (face_uv / divisor) * 0.5 + 0.5; + + switch cubemap_type { + case CUBEMAP_TYPE_CROSS_VERTICAL: { + face_size = vec2(1.0/3.0, 1.0/4.0); + corner_uv = vec2((0x111102u >> (4 * face_index)) & 0xFu, (0x132011u >> (4 * face_index)) & 0xFu); + } + case CUBEMAP_TYPE_CROSS_HORIZONTAL: { + face_size = vec2(1.0/4.0, 1.0/3.0); + corner_uv = vec2((0x131102u >> (4 * face_index)) & 0xFu, (0x112011u >> (4 * face_index)) & 0xFu); + } + case CUBEMAP_TYPE_SEQUENCE_HORIZONTAL: { + face_size = vec2(1.0/6.0, 1.0); + corner_uv.x = face_index; + } + case CUBEMAP_TYPE_SEQUENCE_VERTICAL: { + face_size = vec2(1.0, 1.0/6.0); + corner_uv.y = face_index; + } + default: {} + } + + return (vec2(corner_uv) + face_uv) * face_size; +} + fn point_light( light_id: u32, input: ptr, - enable_diffuse: bool + enable_diffuse: bool, + enable_texture: bool, ) -> vec3 { // Unpack. let diffuse_color = (*input).diffuse_color; @@ -555,8 +622,26 @@ fn point_light( color = diffuse + specular_light; #endif // STANDARD_MATERIAL_CLEARCOAT + var texture_sample = 1f; + +#ifdef LIGHT_TEXTURES + if enable_texture && (*light).decal_index != 0xFFFFFFFFu { + let relative_position = (view_bindings::clustered_decals.decals[(*light).decal_index].local_from_world * vec4(P, 1.0)).xyz; + let cubemap_type = view_bindings::clustered_decals.decals[(*light).decal_index].tag; + let decal_uv = cubemap_uv(relative_position, cubemap_type); + let image_index = view_bindings::clustered_decals.decals[(*light).decal_index].image_index; + + texture_sample = textureSampleLevel( + view_bindings::clustered_decal_textures[image_index], + view_bindings::clustered_decal_sampler, + decal_uv, + 0.0 + ).r; + } +#endif + return color * (*light).color_inverse_square_range.rgb * - (rangeAttenuation * derived_input.NdotL); + (rangeAttenuation * derived_input.NdotL) * texture_sample; } fn spot_light( @@ -565,7 +650,7 @@ fn spot_light( enable_diffuse: bool ) -> vec3 { // reuse the point light calculations - let point_light = point_light(light_id, input, enable_diffuse); + let point_light = point_light(light_id, input, enable_diffuse, false); let light = &view_bindings::clusterable_objects.data[light_id]; @@ -584,7 +669,27 @@ fn spot_light( let attenuation = saturate(cd * (*light).light_custom_data.z + (*light).light_custom_data.w); let spot_attenuation = attenuation * attenuation; - return point_light * spot_attenuation; + var texture_sample = 1f; + +#ifdef LIGHT_TEXTURES + if (*light).decal_index != 0xFFFFFFFFu { + let local_position = (view_bindings::clustered_decals.decals[(*light).decal_index].local_from_world * + vec4((*input).P, 1.0)).xyz; + if local_position.z < 0.0 { + let decal_uv = (local_position.xy / (local_position.z * (*light).spot_light_tan_angle)) * vec2(-0.5, 0.5) + 0.5; + let image_index = view_bindings::clustered_decals.decals[(*light).decal_index].image_index; + + texture_sample = textureSampleLevel( + view_bindings::clustered_decal_textures[image_index], + view_bindings::clustered_decal_sampler, + decal_uv, + 0.0 + ).r; + } + } +#endif + + return point_light * spot_attenuation * texture_sample; } fn directional_light( @@ -641,5 +746,31 @@ fn directional_light( color = (diffuse + specular_light) * derived_input.NdotL; #endif // STANDARD_MATERIAL_CLEARCOAT - return color * (*light).color.rgb; + var texture_sample = 1f; + +#ifdef LIGHT_TEXTURES + if (*light).decal_index != 0xFFFFFFFFu { + let local_position = (view_bindings::clustered_decals.decals[(*light).decal_index].local_from_world * + vec4((*input).P, 1.0)).xyz; + let decal_uv = local_position.xy * vec2(-0.5, 0.5) + 0.5; + + // if tiled or within tile + if (view_bindings::clustered_decals.decals[(*light).decal_index].tag != 0u) + || all(clamp(decal_uv, vec2(0.0), vec2(1.0)) == decal_uv) + { + let image_index = view_bindings::clustered_decals.decals[(*light).decal_index].image_index; + + texture_sample = textureSampleLevel( + view_bindings::clustered_decal_textures[image_index], + view_bindings::clustered_decal_sampler, + decal_uv - floor(decal_uv), + 0.0 + ).r; + } else { + texture_sample = 0f; + } + } +#endif + + return color * (*light).color.rgb * texture_sample; } diff --git a/crates/bevy_pbr/src/render/skin.rs b/crates/bevy_pbr/src/render/skin.rs index 476e06c1e7..f9ec672a66 100644 --- a/crates/bevy_pbr/src/render/skin.rs +++ b/crates/bevy_pbr/src/render/skin.rs @@ -220,7 +220,7 @@ pub fn prepare_skins( let mut new_size = uniform.current_buffer.size(); while new_size < needed_size { // 1.5× growth factor. - new_size += new_size / 2; + new_size = (new_size + new_size / 2).next_multiple_of(4); } // Create the new buffers. diff --git a/crates/bevy_pbr/src/ssao/mod.rs b/crates/bevy_pbr/src/ssao/mod.rs index 7d56495605..001aa67c12 100644 --- a/crates/bevy_pbr/src/ssao/mod.rs +++ b/crates/bevy_pbr/src/ssao/mod.rs @@ -23,7 +23,7 @@ use bevy_render::{ globals::{GlobalsBuffer, GlobalsUniform}, load_shader_library, prelude::Camera, - render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner}, + render_graph::{NodeRunError, RenderGraphContext, RenderGraphExt, ViewNode, ViewNodeRunner}, render_resource::{ binding_types::{ sampler, texture_2d, texture_depth_2d, texture_storage_2d, uniform_buffer, @@ -412,11 +412,8 @@ impl FromWorld for SsaoPipelines { preprocess_depth_bind_group_layout.clone(), common_bind_group_layout.clone(), ], - push_constant_ranges: vec![], shader: load_embedded_asset!(world, "preprocess_depth.wgsl"), - shader_defs: Vec::new(), - entry_point: "preprocess_depth".into(), - zero_initialize_workgroup_memory: false, + ..default() }); let spatial_denoise_pipeline = @@ -426,11 +423,8 @@ impl FromWorld for SsaoPipelines { spatial_denoise_bind_group_layout.clone(), common_bind_group_layout.clone(), ], - push_constant_ranges: vec![], shader: load_embedded_asset!(world, "spatial_denoise.wgsl"), - shader_defs: Vec::new(), - entry_point: "spatial_denoise".into(), - zero_initialize_workgroup_memory: false, + ..default() }); Self { @@ -481,11 +475,9 @@ impl SpecializedComputePipeline for SsaoPipelines { self.ssao_bind_group_layout.clone(), self.common_bind_group_layout.clone(), ], - push_constant_ranges: vec![], shader: self.shader.clone(), shader_defs, - entry_point: "ssao".into(), - zero_initialize_workgroup_memory: false, + ..default() } } } diff --git a/crates/bevy_pbr/src/ssr/mod.rs b/crates/bevy_pbr/src/ssr/mod.rs index f3d876ffde..6efc3531dd 100644 --- a/crates/bevy_pbr/src/ssr/mod.rs +++ b/crates/bevy_pbr/src/ssr/mod.rs @@ -25,7 +25,7 @@ use bevy_image::BevyDefault as _; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ extract_component::{ExtractComponent, ExtractComponentPlugin}, - render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner}, + render_graph::{NodeRunError, RenderGraphContext, RenderGraphExt, ViewNode, ViewNodeRunner}, render_resource::{ binding_types, AddressMode, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, CachedRenderPipelineId, ColorTargetState, ColorWrites, DynamicUniformBuffer, FilterMode, @@ -548,7 +548,6 @@ impl SpecializedRenderPipeline for ScreenSpaceReflectionsPipeline { fragment: Some(FragmentState { shader: self.fragment_shader.clone(), shader_defs, - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format: if key.is_hdr { ViewTarget::TEXTURE_FORMAT_HDR @@ -558,12 +557,9 @@ impl SpecializedRenderPipeline for ScreenSpaceReflectionsPipeline { blend: None, write_mask: ColorWrites::ALL, })], + ..default() }), - push_constant_ranges: vec![], - primitive: default(), - depth_stencil: None, - multisample: default(), - zero_initialize_workgroup_memory: false, + ..default() } } } diff --git a/crates/bevy_pbr/src/volumetric_fog/mod.rs b/crates/bevy_pbr/src/volumetric_fog/mod.rs index efc3c5370a..c9f1f230c6 100644 --- a/crates/bevy_pbr/src/volumetric_fog/mod.rs +++ b/crates/bevy_pbr/src/volumetric_fog/mod.rs @@ -47,7 +47,7 @@ use bevy_math::{ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ mesh::{Mesh, Meshable}, - render_graph::{RenderGraphApp, ViewNodeRunner}, + render_graph::{RenderGraphExt, ViewNodeRunner}, render_resource::SpecializedRenderPipelines, sync_component::SyncComponentPlugin, view::Visibility, diff --git a/crates/bevy_pbr/src/volumetric_fog/render.rs b/crates/bevy_pbr/src/volumetric_fog/render.rs index a5cd8e56f3..feb04a2983 100644 --- a/crates/bevy_pbr/src/volumetric_fog/render.rs +++ b/crates/bevy_pbr/src/volumetric_fog/render.rs @@ -2,7 +2,7 @@ use core::array; -use bevy_asset::{load_embedded_asset, weak_handle, AssetId, Handle}; +use bevy_asset::{load_embedded_asset, uuid_handle, AssetId, Handle}; use bevy_color::ColorToComponents as _; use bevy_core_pipeline::{ core_3d::Camera3d, @@ -31,10 +31,10 @@ use bevy_render::{ }, BindGroupLayout, BindGroupLayoutEntries, BindingResource, BlendComponent, BlendFactor, BlendOperation, BlendState, CachedRenderPipelineId, ColorTargetState, ColorWrites, - DynamicBindGroupEntries, DynamicUniformBuffer, Face, FragmentState, LoadOp, - MultisampleState, Operations, PipelineCache, PrimitiveState, RenderPassColorAttachment, - RenderPassDescriptor, RenderPipelineDescriptor, SamplerBindingType, Shader, ShaderStages, - ShaderType, SpecializedRenderPipeline, SpecializedRenderPipelines, StoreOp, TextureFormat, + DynamicBindGroupEntries, DynamicUniformBuffer, Face, FragmentState, LoadOp, Operations, + PipelineCache, PrimitiveState, RenderPassColorAttachment, RenderPassDescriptor, + RenderPipelineDescriptor, SamplerBindingType, Shader, ShaderStages, ShaderType, + SpecializedRenderPipeline, SpecializedRenderPipelines, StoreOp, TextureFormat, TextureSampleType, TextureUsages, VertexState, }, renderer::{RenderContext, RenderDevice, RenderQueue}, @@ -82,14 +82,14 @@ bitflags! { /// /// This mesh is simply stretched to the size of the framebuffer, as when the /// camera is inside a fog volume it's essentially a full-screen effect. -pub const PLANE_MESH: Handle = weak_handle!("92523617-c708-4fd0-b42f-ceb4300c930b"); +pub const PLANE_MESH: Handle = uuid_handle!("92523617-c708-4fd0-b42f-ceb4300c930b"); /// The cube mesh, which is used to render a fog volume that the camera is /// outside. /// /// Note that only the front faces of this cuboid will be rasterized in /// hardware. The back faces will be calculated in the shader via raytracing. -pub const CUBE_MESH: Handle = weak_handle!("4a1dd661-2d91-4377-a17a-a914e21e277e"); +pub const CUBE_MESH: Handle = uuid_handle!("4a1dd661-2d91-4377-a17a-a914e21e277e"); /// The total number of bind group layouts. /// @@ -566,23 +566,19 @@ impl SpecializedRenderPipeline for VolumetricFogPipeline { RenderPipelineDescriptor { label: Some("volumetric lighting pipeline".into()), layout, - push_constant_ranges: vec![], vertex: VertexState { shader: self.shader.clone(), shader_defs: shader_defs.clone(), - entry_point: "vertex".into(), buffers: vec![vertex_format], + ..default() }, primitive: PrimitiveState { cull_mode: Some(Face::Back), ..default() }, - depth_stencil: None, - multisample: MultisampleState::default(), fragment: Some(FragmentState { shader: self.shader.clone(), shader_defs, - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format: if key.flags.contains(VolumetricFogPipelineKeyFlags::HDR) { ViewTarget::TEXTURE_FORMAT_HDR @@ -606,8 +602,9 @@ impl SpecializedRenderPipeline for VolumetricFogPipeline { }), write_mask: ColorWrites::ALL, })], + ..default() }), - zero_initialize_workgroup_memory: false, + ..default() } } } diff --git a/crates/bevy_pbr/src/wireframe.rs b/crates/bevy_pbr/src/wireframe.rs index f7c193161d..9cf3bc08dd 100644 --- a/crates/bevy_pbr/src/wireframe.rs +++ b/crates/bevy_pbr/src/wireframe.rs @@ -38,7 +38,7 @@ use bevy_render::{ render_asset::{ prepare_assets, PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets, }, - render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner}, + render_graph::{NodeRunError, RenderGraphContext, RenderGraphExt, ViewNode, ViewNodeRunner}, render_phase::{ AddRenderCommand, BinnedPhaseItem, BinnedRenderPhasePlugin, BinnedRenderPhaseType, CachedRenderPipelinePhaseItem, DrawFunctionId, DrawFunctions, PhaseItem, diff --git a/crates/bevy_platform/src/thread.rs b/crates/bevy_platform/src/thread.rs index 6e4650382e..7fc7413bc6 100644 --- a/crates/bevy_platform/src/thread.rs +++ b/crates/bevy_platform/src/thread.rs @@ -21,7 +21,7 @@ crate::cfg::switch! { let start = Instant::now(); while start.elapsed() < dur { - spin_loop() + spin_loop(); } } } diff --git a/crates/bevy_platform/src/time/fallback.rs b/crates/bevy_platform/src/time/fallback.rs index c649f6a49d..2964c9d980 100644 --- a/crates/bevy_platform/src/time/fallback.rs +++ b/crates/bevy_platform/src/time/fallback.rs @@ -155,14 +155,14 @@ fn unset_getter() -> Duration { let nanos = unsafe { core::arch::x86::_rdtsc() }; - return Duration::from_nanos(nanos); + Duration::from_nanos(nanos) } #[cfg(target_arch = "x86_64")] => { // SAFETY: standard technique for getting a nanosecond counter on x86_64 let nanos = unsafe { core::arch::x86_64::_rdtsc() }; - return Duration::from_nanos(nanos); + Duration::from_nanos(nanos) } #[cfg(target_arch = "aarch64")] => { // SAFETY: standard technique for getting a nanosecond counter of aarch64 @@ -171,7 +171,7 @@ fn unset_getter() -> Duration { core::arch::asm!("mrs {}, cntvct_el0", out(reg) ticks); ticks }; - return Duration::from_nanos(nanos); + Duration::from_nanos(nanos) } _ => { panic!("An elapsed time getter has not been provided to `Instant`. Please use `Instant::set_elapsed(...)` before calling `Instant::now()`") diff --git a/crates/bevy_reflect/Cargo.toml b/crates/bevy_reflect/Cargo.toml index ae3a3a856e..aba26258b6 100644 --- a/crates/bevy_reflect/Cargo.toml +++ b/crates/bevy_reflect/Cargo.toml @@ -100,7 +100,7 @@ smallvec = { version = "1.11", default-features = false, optional = true } glam = { version = "0.29.3", default-features = false, features = [ "serde", ], optional = true } -petgraph = { version = "0.7", features = ["serde-1"], optional = true } +petgraph = { version = "0.8", features = ["serde-1"], optional = true } smol_str = { version = "0.2.0", default-features = false, features = [ "serde", ], optional = true } diff --git a/crates/bevy_reflect/derive/Cargo.toml b/crates/bevy_reflect/derive/Cargo.toml index 032046ae2f..b9eaa369cd 100644 --- a/crates/bevy_reflect/derive/Cargo.toml +++ b/crates/bevy_reflect/derive/Cargo.toml @@ -20,9 +20,10 @@ functions = [] [dependencies] bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.17.0-dev" } +indexmap = "2.0" proc-macro2 = "1.0" quote = "1.0" -syn = { version = "2.0", features = ["full"] } +syn = { version = "2.0", features = ["full", "extra-traits"] } uuid = { version = "1.13.1", features = ["v4"] } [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/crates/bevy_reflect/derive/src/custom_attributes.rs b/crates/bevy_reflect/derive/src/custom_attributes.rs index f12b6d7c12..1f141f79dc 100644 --- a/crates/bevy_reflect/derive/src/custom_attributes.rs +++ b/crates/bevy_reflect/derive/src/custom_attributes.rs @@ -28,6 +28,11 @@ impl CustomAttributes { Ok(()) } + /// Is the collection empty? + pub fn is_empty(&self) -> bool { + self.attributes.is_empty() + } + /// Parse `@` (custom attribute) attribute. /// /// Examples: diff --git a/crates/bevy_reflect/derive/src/derive_data.rs b/crates/bevy_reflect/derive/src/derive_data.rs index f825cb2905..9af7acda76 100644 --- a/crates/bevy_reflect/derive/src/derive_data.rs +++ b/crates/bevy_reflect/derive/src/derive_data.rs @@ -1,4 +1,5 @@ use core::fmt; +use indexmap::IndexSet; use proc_macro2::Span; use crate::{ @@ -481,7 +482,6 @@ impl<'a> ReflectMeta<'a> { where_clause_options: &WhereClauseOptions, ) -> proc_macro2::TokenStream { crate::registration::impl_get_type_registration( - self, where_clause_options, None, Option::>::None, @@ -514,25 +514,27 @@ impl<'a> StructField<'a> { }; let ty = self.reflected_type(); - let custom_attributes = self.attrs.custom_attributes.to_tokens(bevy_reflect_path); - #[cfg_attr( - not(feature = "documentation"), - expect( - unused_mut, - reason = "Needs to be mutable if `documentation` feature is enabled.", - ) - )] let mut info = quote! { - #field_info::new::<#ty>(#name).with_custom_attributes(#custom_attributes) + #field_info::new::<#ty>(#name) }; + let custom_attributes = &self.attrs.custom_attributes; + if !custom_attributes.is_empty() { + let custom_attributes = custom_attributes.to_tokens(bevy_reflect_path); + info.extend(quote! { + .with_custom_attributes(#custom_attributes) + }); + } + #[cfg(feature = "documentation")] { let docs = &self.doc; - info.extend(quote! { - .with_docs(#docs) - }); + if !docs.is_empty() { + info.extend(quote! { + .with_docs(#docs) + }); + } } info @@ -596,7 +598,6 @@ impl<'a> ReflectStruct<'a> { where_clause_options: &WhereClauseOptions, ) -> proc_macro2::TokenStream { crate::registration::impl_get_type_registration( - self.meta(), where_clause_options, self.serialization_data(), Some(self.active_types().iter()), @@ -605,9 +606,12 @@ impl<'a> ReflectStruct<'a> { /// Get a collection of types which are exposed to the reflection API pub fn active_types(&self) -> Vec { + // Collect via `IndexSet` to eliminate duplicate types. self.active_fields() .map(|field| field.reflected_type().clone()) - .collect() + .collect::>() + .into_iter() + .collect::>() } /// Get an iterator of fields which are exposed to the reflection API. @@ -653,19 +657,20 @@ impl<'a> ReflectStruct<'a> { .active_fields() .map(|field| field.to_info_tokens(bevy_reflect_path)); - let custom_attributes = self - .meta - .attrs - .custom_attributes() - .to_tokens(bevy_reflect_path); - let mut info = quote! { #bevy_reflect_path::#info_struct::new::(&[ #(#field_infos),* ]) - .with_custom_attributes(#custom_attributes) }; + let custom_attributes = self.meta.attrs.custom_attributes(); + if !custom_attributes.is_empty() { + let custom_attributes = custom_attributes.to_tokens(bevy_reflect_path); + info.extend(quote! { + .with_custom_attributes(#custom_attributes) + }); + } + if let Some(generics) = generate_generics(self.meta()) { info.extend(quote! { .with_generics(#generics) @@ -675,9 +680,11 @@ impl<'a> ReflectStruct<'a> { #[cfg(feature = "documentation")] { let docs = self.meta().doc(); - info.extend(quote! { - .with_docs(#docs) - }); + if !docs.is_empty() { + info.extend(quote! { + .with_docs(#docs) + }); + } } quote! { @@ -846,9 +853,12 @@ impl<'a> ReflectEnum<'a> { /// Get a collection of types which are exposed to the reflection API pub fn active_types(&self) -> Vec { + // Collect via `IndexSet` to eliminate duplicate types. self.active_fields() .map(|field| field.reflected_type().clone()) - .collect() + .collect::>() + .into_iter() + .collect::>() } /// Get an iterator of fields which are exposed to the reflection API @@ -868,7 +878,6 @@ impl<'a> ReflectEnum<'a> { where_clause_options: &WhereClauseOptions, ) -> proc_macro2::TokenStream { crate::registration::impl_get_type_registration( - self.meta(), where_clause_options, None, Some(self.active_fields().map(StructField::reflected_type)), @@ -884,19 +893,20 @@ impl<'a> ReflectEnum<'a> { .iter() .map(|variant| variant.to_info_tokens(bevy_reflect_path)); - let custom_attributes = self - .meta - .attrs - .custom_attributes() - .to_tokens(bevy_reflect_path); - let mut info = quote! { #bevy_reflect_path::EnumInfo::new::(&[ #(#variants),* ]) - .with_custom_attributes(#custom_attributes) }; + let custom_attributes = self.meta.attrs.custom_attributes(); + if !custom_attributes.is_empty() { + let custom_attributes = custom_attributes.to_tokens(bevy_reflect_path); + info.extend(quote! { + .with_custom_attributes(#custom_attributes) + }); + } + if let Some(generics) = generate_generics(self.meta()) { info.extend(quote! { .with_generics(#generics) @@ -906,9 +916,11 @@ impl<'a> ReflectEnum<'a> { #[cfg(feature = "documentation")] { let docs = self.meta().doc(); - info.extend(quote! { - .with_docs(#docs) - }); + if !docs.is_empty() { + info.extend(quote! { + .with_docs(#docs) + }); + } } quote! { @@ -1008,26 +1020,26 @@ impl<'a> EnumVariant<'a> { } }; - let custom_attributes = self.attrs.custom_attributes.to_tokens(bevy_reflect_path); - - #[cfg_attr( - not(feature = "documentation"), - expect( - unused_mut, - reason = "Needs to be mutable if `documentation` feature is enabled.", - ) - )] let mut info = quote! { #bevy_reflect_path::#info_struct::new(#args) - .with_custom_attributes(#custom_attributes) }; + let custom_attributes = &self.attrs.custom_attributes; + if !custom_attributes.is_empty() { + let custom_attributes = custom_attributes.to_tokens(bevy_reflect_path); + info.extend(quote! { + .with_custom_attributes(#custom_attributes) + }); + } + #[cfg(feature = "documentation")] { let docs = &self.doc; - info.extend(quote! { - .with_docs(#docs) - }); + if !docs.is_empty() { + info.extend(quote! { + .with_docs(#docs) + }); + } } quote! { diff --git a/crates/bevy_reflect/derive/src/documentation.rs b/crates/bevy_reflect/derive/src/documentation.rs index 33aec4c4f3..4fbcf775f4 100644 --- a/crates/bevy_reflect/derive/src/documentation.rs +++ b/crates/bevy_reflect/derive/src/documentation.rs @@ -61,6 +61,11 @@ impl Documentation { ) } + /// Is the collection empty? + pub fn is_empty(&self) -> bool { + self.docs.is_empty() + } + /// Push a new docstring to the collection pub fn push(&mut self, doc: String) { self.docs.push(doc); diff --git a/crates/bevy_reflect/derive/src/from_reflect.rs b/crates/bevy_reflect/derive/src/from_reflect.rs index d994cbd2f7..a0e6e444d3 100644 --- a/crates/bevy_reflect/derive/src/from_reflect.rs +++ b/crates/bevy_reflect/derive/src/from_reflect.rs @@ -146,7 +146,8 @@ fn impl_struct_internal( quote! { let mut #__this = <#reflect_ty as #FQDefault>::default(); #( - if let #fqoption::Some(__field) = #active_values() { + // The closure catches any failing `?` within `active_values`. + if let #fqoption::Some(__field) = (|| #active_values)() { // Iff field exists -> use its value #__this.#active_members = __field; } @@ -158,7 +159,7 @@ fn impl_struct_internal( quote! { let #__this = #constructor { - #(#active_members: #active_values()?,)* + #(#active_members: #active_values?,)* #(#ignored_members: #ignored_values,)* }; #FQOption::Some(#retval) @@ -274,13 +275,11 @@ fn get_active_fields( <#ty as #bevy_reflect_path::FromReflect>::from_reflect(field) }); quote! { - (|| - if let #FQOption::Some(field) = #get_field { - #value - } else { - #FQOption::Some(#path()) - } - ) + if let #FQOption::Some(field) = #get_field { + #value + } else { + #FQOption::Some(#path()) + } } } DefaultBehavior::Default => { @@ -288,13 +287,11 @@ fn get_active_fields( <#ty as #bevy_reflect_path::FromReflect>::from_reflect(field) }); quote! { - (|| - if let #FQOption::Some(field) = #get_field { - #value - } else { - #FQOption::Some(#FQDefault::default()) - } - ) + if let #FQOption::Some(field) = #get_field { + #value + } else { + #FQOption::Some(#FQDefault::default()) + } } } DefaultBehavior::Required => { @@ -302,7 +299,7 @@ fn get_active_fields( <#ty as #bevy_reflect_path::FromReflect>::from_reflect(#get_field?) }); quote! { - (|| #value) + #value } } }; diff --git a/crates/bevy_reflect/derive/src/impls/common.rs b/crates/bevy_reflect/derive/src/impls/common.rs index e8fdadb03e..87836e383d 100644 --- a/crates/bevy_reflect/derive/src/impls/common.rs +++ b/crates/bevy_reflect/derive/src/impls/common.rs @@ -4,10 +4,8 @@ use quote::quote; use crate::{derive_data::ReflectMeta, where_clause_options::WhereClauseOptions}; -pub fn impl_full_reflect( - meta: &ReflectMeta, - where_clause_options: &WhereClauseOptions, -) -> proc_macro2::TokenStream { +pub fn impl_full_reflect(where_clause_options: &WhereClauseOptions) -> proc_macro2::TokenStream { + let meta = where_clause_options.meta(); let bevy_reflect_path = meta.bevy_reflect_path(); let type_path = meta.type_path(); diff --git a/crates/bevy_reflect/derive/src/impls/enums.rs b/crates/bevy_reflect/derive/src/impls/enums.rs index 3cbd8cce95..f2272c7c81 100644 --- a/crates/bevy_reflect/derive/src/impls/enums.rs +++ b/crates/bevy_reflect/derive/src/impls/enums.rs @@ -36,8 +36,6 @@ pub(crate) fn impl_enum(reflect_enum: &ReflectEnum) -> proc_macro2::TokenStream let ref_index = Ident::new("__index_param", Span::call_site()); let ref_value = Ident::new("__value_param", Span::call_site()); - let where_clause_options = reflect_enum.where_clause_options(); - let EnumImpls { enum_field, enum_field_mut, @@ -57,14 +55,11 @@ pub(crate) fn impl_enum(reflect_enum: &ReflectEnum) -> proc_macro2::TokenStream .. } = TryApplyVariantBuilder::new(reflect_enum).build(&ref_value); - let typed_impl = impl_typed( - reflect_enum.meta(), - &where_clause_options, - reflect_enum.to_info_tokens(), - ); + let where_clause_options = reflect_enum.where_clause_options(); + let typed_impl = impl_typed(&where_clause_options, reflect_enum.to_info_tokens()); let type_path_impl = impl_type_path(reflect_enum.meta()); - let full_reflect_impl = impl_full_reflect(reflect_enum.meta(), &where_clause_options); + let full_reflect_impl = impl_full_reflect(&where_clause_options); let common_methods = common_partial_reflect_methods( reflect_enum.meta(), || Some(quote!(#bevy_reflect_path::enum_partial_eq)), @@ -75,8 +70,7 @@ pub(crate) fn impl_enum(reflect_enum: &ReflectEnum) -> proc_macro2::TokenStream #[cfg(not(feature = "functions"))] let function_impls = None::; #[cfg(feature = "functions")] - let function_impls = - crate::impls::impl_function_traits(reflect_enum.meta(), &where_clause_options); + let function_impls = crate::impls::impl_function_traits(&where_clause_options); let get_type_registration_impl = reflect_enum.get_type_registration(&where_clause_options); diff --git a/crates/bevy_reflect/derive/src/impls/func/from_arg.rs b/crates/bevy_reflect/derive/src/impls/func/from_arg.rs index 77b984e04a..2220a704ea 100644 --- a/crates/bevy_reflect/derive/src/impls/func/from_arg.rs +++ b/crates/bevy_reflect/derive/src/impls/func/from_arg.rs @@ -1,11 +1,9 @@ -use crate::{derive_data::ReflectMeta, where_clause_options::WhereClauseOptions}; +use crate::where_clause_options::WhereClauseOptions; use bevy_macro_utils::fq_std::FQResult; use quote::quote; -pub(crate) fn impl_from_arg( - meta: &ReflectMeta, - where_clause_options: &WhereClauseOptions, -) -> proc_macro2::TokenStream { +pub(crate) fn impl_from_arg(where_clause_options: &WhereClauseOptions) -> proc_macro2::TokenStream { + let meta = where_clause_options.meta(); let bevy_reflect = meta.bevy_reflect_path(); let type_path = meta.type_path(); diff --git a/crates/bevy_reflect/derive/src/impls/func/function_impls.rs b/crates/bevy_reflect/derive/src/impls/func/function_impls.rs index 64ca7ca7b7..acbda3459b 100644 --- a/crates/bevy_reflect/derive/src/impls/func/function_impls.rs +++ b/crates/bevy_reflect/derive/src/impls/func/function_impls.rs @@ -1,5 +1,4 @@ use crate::{ - derive_data::ReflectMeta, impls::func::{ from_arg::impl_from_arg, get_ownership::impl_get_ownership, into_return::impl_into_return, }, @@ -8,12 +7,11 @@ use crate::{ use quote::quote; pub(crate) fn impl_function_traits( - meta: &ReflectMeta, where_clause_options: &WhereClauseOptions, ) -> proc_macro2::TokenStream { - let get_ownership = impl_get_ownership(meta, where_clause_options); - let from_arg = impl_from_arg(meta, where_clause_options); - let into_return = impl_into_return(meta, where_clause_options); + let get_ownership = impl_get_ownership(where_clause_options); + let from_arg = impl_from_arg(where_clause_options); + let into_return = impl_into_return(where_clause_options); quote! { #get_ownership diff --git a/crates/bevy_reflect/derive/src/impls/func/get_ownership.rs b/crates/bevy_reflect/derive/src/impls/func/get_ownership.rs index 01d33eb7bb..abdfb803ed 100644 --- a/crates/bevy_reflect/derive/src/impls/func/get_ownership.rs +++ b/crates/bevy_reflect/derive/src/impls/func/get_ownership.rs @@ -1,10 +1,10 @@ -use crate::{derive_data::ReflectMeta, where_clause_options::WhereClauseOptions}; +use crate::where_clause_options::WhereClauseOptions; use quote::quote; pub(crate) fn impl_get_ownership( - meta: &ReflectMeta, where_clause_options: &WhereClauseOptions, ) -> proc_macro2::TokenStream { + let meta = where_clause_options.meta(); let bevy_reflect = meta.bevy_reflect_path(); let type_path = meta.type_path(); diff --git a/crates/bevy_reflect/derive/src/impls/func/into_return.rs b/crates/bevy_reflect/derive/src/impls/func/into_return.rs index f7d1e0b889..221028a99e 100644 --- a/crates/bevy_reflect/derive/src/impls/func/into_return.rs +++ b/crates/bevy_reflect/derive/src/impls/func/into_return.rs @@ -1,10 +1,10 @@ -use crate::{derive_data::ReflectMeta, where_clause_options::WhereClauseOptions}; +use crate::where_clause_options::WhereClauseOptions; use quote::quote; pub(crate) fn impl_into_return( - meta: &ReflectMeta, where_clause_options: &WhereClauseOptions, ) -> proc_macro2::TokenStream { + let meta = where_clause_options.meta(); let bevy_reflect = meta.bevy_reflect_path(); let type_path = meta.type_path(); diff --git a/crates/bevy_reflect/derive/src/impls/opaque.rs b/crates/bevy_reflect/derive/src/impls/opaque.rs index 2a08cadc28..a39b0b4849 100644 --- a/crates/bevy_reflect/derive/src/impls/opaque.rs +++ b/crates/bevy_reflect/derive/src/impls/opaque.rs @@ -21,7 +21,6 @@ pub(crate) fn impl_opaque(meta: &ReflectMeta) -> proc_macro2::TokenStream { let where_clause_options = WhereClauseOptions::new(meta); let typed_impl = impl_typed( - meta, &where_clause_options, quote! { let info = #bevy_reflect_path::OpaqueInfo::new::() #with_docs; @@ -30,7 +29,7 @@ pub(crate) fn impl_opaque(meta: &ReflectMeta) -> proc_macro2::TokenStream { ); let type_path_impl = impl_type_path(meta); - let full_reflect_impl = impl_full_reflect(meta, &where_clause_options); + let full_reflect_impl = impl_full_reflect(&where_clause_options); let common_methods = common_partial_reflect_methods(meta, || None, || None); let clone_fn = meta.attrs().get_clone_impl(bevy_reflect_path); @@ -54,7 +53,7 @@ pub(crate) fn impl_opaque(meta: &ReflectMeta) -> proc_macro2::TokenStream { #[cfg(not(feature = "functions"))] let function_impls = None::; #[cfg(feature = "functions")] - let function_impls = crate::impls::impl_function_traits(meta, &where_clause_options); + let function_impls = crate::impls::impl_function_traits(&where_clause_options); let (impl_generics, ty_generics, where_clause) = type_path.generics().split_for_impl(); let where_reflect_clause = where_clause_options.extend_where_clause(where_clause); diff --git a/crates/bevy_reflect/derive/src/impls/structs.rs b/crates/bevy_reflect/derive/src/impls/structs.rs index 7e10de3f2b..b78ce40a08 100644 --- a/crates/bevy_reflect/derive/src/impls/structs.rs +++ b/crates/bevy_reflect/derive/src/impls/structs.rs @@ -34,14 +34,10 @@ pub(crate) fn impl_struct(reflect_struct: &ReflectStruct) -> proc_macro2::TokenS } = FieldAccessors::new(reflect_struct); let where_clause_options = reflect_struct.where_clause_options(); - let typed_impl = impl_typed( - reflect_struct.meta(), - &where_clause_options, - reflect_struct.to_info_tokens(false), - ); + let typed_impl = impl_typed(&where_clause_options, reflect_struct.to_info_tokens(false)); let type_path_impl = impl_type_path(reflect_struct.meta()); - let full_reflect_impl = impl_full_reflect(reflect_struct.meta(), &where_clause_options); + let full_reflect_impl = impl_full_reflect(&where_clause_options); let common_methods = common_partial_reflect_methods( reflect_struct.meta(), || Some(quote!(#bevy_reflect_path::struct_partial_eq)), @@ -52,8 +48,7 @@ pub(crate) fn impl_struct(reflect_struct: &ReflectStruct) -> proc_macro2::TokenS #[cfg(not(feature = "functions"))] let function_impls = None::; #[cfg(feature = "functions")] - let function_impls = - crate::impls::impl_function_traits(reflect_struct.meta(), &where_clause_options); + let function_impls = crate::impls::impl_function_traits(&where_clause_options); let get_type_registration_impl = reflect_struct.get_type_registration(&where_clause_options); diff --git a/crates/bevy_reflect/derive/src/impls/tuple_structs.rs b/crates/bevy_reflect/derive/src/impls/tuple_structs.rs index 90c3555230..01b6a46b7b 100644 --- a/crates/bevy_reflect/derive/src/impls/tuple_structs.rs +++ b/crates/bevy_reflect/derive/src/impls/tuple_structs.rs @@ -24,14 +24,10 @@ pub(crate) fn impl_tuple_struct(reflect_struct: &ReflectStruct) -> proc_macro2:: let where_clause_options = reflect_struct.where_clause_options(); let get_type_registration_impl = reflect_struct.get_type_registration(&where_clause_options); - let typed_impl = impl_typed( - reflect_struct.meta(), - &where_clause_options, - reflect_struct.to_info_tokens(true), - ); + let typed_impl = impl_typed(&where_clause_options, reflect_struct.to_info_tokens(true)); let type_path_impl = impl_type_path(reflect_struct.meta()); - let full_reflect_impl = impl_full_reflect(reflect_struct.meta(), &where_clause_options); + let full_reflect_impl = impl_full_reflect(&where_clause_options); let common_methods = common_partial_reflect_methods( reflect_struct.meta(), || Some(quote!(#bevy_reflect_path::tuple_struct_partial_eq)), @@ -42,8 +38,7 @@ pub(crate) fn impl_tuple_struct(reflect_struct: &ReflectStruct) -> proc_macro2:: #[cfg(not(feature = "functions"))] let function_impls = None::; #[cfg(feature = "functions")] - let function_impls = - crate::impls::impl_function_traits(reflect_struct.meta(), &where_clause_options); + let function_impls = crate::impls::impl_function_traits(&where_clause_options); let (impl_generics, ty_generics, where_clause) = reflect_struct .meta() diff --git a/crates/bevy_reflect/derive/src/impls/typed.rs b/crates/bevy_reflect/derive/src/impls/typed.rs index da8254d149..d4b0644976 100644 --- a/crates/bevy_reflect/derive/src/impls/typed.rs +++ b/crates/bevy_reflect/derive/src/impls/typed.rs @@ -138,10 +138,10 @@ pub(crate) fn impl_type_path(meta: &ReflectMeta) -> TokenStream { } pub(crate) fn impl_typed( - meta: &ReflectMeta, where_clause_options: &WhereClauseOptions, type_info_generator: TokenStream, ) -> TokenStream { + let meta = where_clause_options.meta(); let type_path = meta.type_path(); let bevy_reflect_path = meta.bevy_reflect_path(); diff --git a/crates/bevy_reflect/derive/src/registration.rs b/crates/bevy_reflect/derive/src/registration.rs index f60791215c..ff8257cc1c 100644 --- a/crates/bevy_reflect/derive/src/registration.rs +++ b/crates/bevy_reflect/derive/src/registration.rs @@ -1,19 +1,16 @@ //! Contains code related specifically to Bevy's type registration. -use crate::{ - derive_data::ReflectMeta, serialization::SerializationDataDef, - where_clause_options::WhereClauseOptions, -}; +use crate::{serialization::SerializationDataDef, where_clause_options::WhereClauseOptions}; use quote::quote; use syn::Type; /// Creates the `GetTypeRegistration` impl for the given type data. pub(crate) fn impl_get_type_registration<'a>( - meta: &ReflectMeta, where_clause_options: &WhereClauseOptions, serialization_data: Option<&SerializationDataDef>, type_dependencies: Option>, ) -> proc_macro2::TokenStream { + let meta = where_clause_options.meta(); let type_path = meta.type_path(); let bevy_reflect_path = meta.bevy_reflect_path(); let registration_data = meta.attrs().idents(); diff --git a/crates/bevy_reflect/derive/src/where_clause_options.rs b/crates/bevy_reflect/derive/src/where_clause_options.rs index 1551e008d0..e63dbe599a 100644 --- a/crates/bevy_reflect/derive/src/where_clause_options.rs +++ b/crates/bevy_reflect/derive/src/where_clause_options.rs @@ -25,6 +25,10 @@ impl<'a, 'b> WhereClauseOptions<'a, 'b> { } } + pub fn meta(&self) -> &'a ReflectMeta<'b> { + self.meta + } + /// Extends the `where` clause for a type with additional bounds needed for the reflection impls. /// /// The default bounds added are as follows: diff --git a/crates/bevy_render/macros/src/as_bind_group.rs b/crates/bevy_render/macros/src/as_bind_group.rs index 7bac6796ac..b426088e22 100644 --- a/crates/bevy_render/macros/src/as_bind_group.rs +++ b/crates/bevy_render/macros/src/as_bind_group.rs @@ -1061,17 +1061,21 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { render_device: &#render_path::renderer::RenderDevice, (images, fallback_image, storage_buffers): &mut #ecs_path::system::SystemParamItem<'_, '_, Self::Param>, force_no_bindless: bool, - ) -> Result<#render_path::render_resource::UnpreparedBindGroup, #render_path::render_resource::AsBindGroupError> { + ) -> Result<#render_path::render_resource::UnpreparedBindGroup, #render_path::render_resource::AsBindGroupError> { #uniform_binding_type_declarations let bindings = #render_path::render_resource::BindingResources(vec![#(#binding_impls,)*]); Ok(#render_path::render_resource::UnpreparedBindGroup { bindings, - data: #get_prepared_data, }) } + #[allow(clippy::unused_unit)] + fn bind_group_data(&self) -> Self::Data { + #get_prepared_data + } + fn bind_group_layout_entries( render_device: &#render_path::renderer::RenderDevice, force_no_bindless: bool diff --git a/crates/bevy_render/macros/src/lib.rs b/crates/bevy_render/macros/src/lib.rs index 75cbdfa959..35990f465f 100644 --- a/crates/bevy_render/macros/src/lib.rs +++ b/crates/bevy_render/macros/src/lib.rs @@ -4,6 +4,7 @@ mod as_bind_group; mod extract_component; mod extract_resource; +mod specialize; use bevy_macro_utils::{derive_label, BevyManifest}; use proc_macro::TokenStream; @@ -14,6 +15,10 @@ pub(crate) fn bevy_render_path() -> syn::Path { BevyManifest::shared().get_path("bevy_render") } +pub(crate) fn bevy_ecs_path() -> syn::Path { + BevyManifest::shared().get_path("bevy_ecs") +} + #[proc_macro_derive(ExtractResource)] pub fn derive_extract_resource(input: TokenStream) -> TokenStream { extract_resource::derive_extract_resource(input) @@ -101,3 +106,43 @@ pub fn derive_render_sub_graph(input: TokenStream) -> TokenStream { .push(format_ident!("RenderSubGraph").into()); derive_label(input, "RenderSubGraph", &trait_path) } + +/// Derive macro generating an impl of the trait `Specialize` +/// +/// This only works for structs whose members all implement `Specialize` +#[proc_macro_derive(Specialize, attributes(specialize, key, base_descriptor))] +pub fn derive_specialize(input: TokenStream) -> TokenStream { + specialize::impl_specialize(input) +} + +/// Derive macro generating the most common impl of the trait `SpecializerKey` +#[proc_macro_derive(SpecializerKey)] +pub fn derive_specializer_key(input: TokenStream) -> TokenStream { + specialize::impl_specializer_key(input) +} + +#[proc_macro_derive(ShaderLabel)] +pub fn derive_shader_label(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let mut trait_path = bevy_render_path(); + trait_path + .segments + .push(format_ident!("render_phase").into()); + trait_path + .segments + .push(format_ident!("ShaderLabel").into()); + derive_label(input, "ShaderLabel", &trait_path) +} + +#[proc_macro_derive(DrawFunctionLabel)] +pub fn derive_draw_function_label(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let mut trait_path = bevy_render_path(); + trait_path + .segments + .push(format_ident!("render_phase").into()); + trait_path + .segments + .push(format_ident!("DrawFunctionLabel").into()); + derive_label(input, "DrawFunctionLabel", &trait_path) +} diff --git a/crates/bevy_render/macros/src/specialize.rs b/crates/bevy_render/macros/src/specialize.rs new file mode 100644 index 0000000000..092de6e8d7 --- /dev/null +++ b/crates/bevy_render/macros/src/specialize.rs @@ -0,0 +1,483 @@ +use bevy_macro_utils::fq_std::{FQDefault, FQResult}; +use proc_macro::TokenStream; +use proc_macro2::Span; +use quote::{format_ident, quote}; +use syn::{ + parse, + parse::{Parse, ParseStream}, + parse_macro_input, parse_quote, + spanned::Spanned, + Data, DataStruct, DeriveInput, Expr, Fields, Ident, Index, Member, Meta, MetaList, Pat, Path, + Token, Type, WherePredicate, +}; + +const SPECIALIZE_ATTR_IDENT: &str = "specialize"; +const SPECIALIZE_ALL_IDENT: &str = "all"; + +const KEY_ATTR_IDENT: &str = "key"; +const KEY_DEFAULT_IDENT: &str = "default"; + +const BASE_DESCRIPTOR_ATTR_IDENT: &str = "base_descriptor"; + +enum SpecializeImplTargets { + All, + Specific(Vec), +} + +impl Parse for SpecializeImplTargets { + fn parse(input: ParseStream) -> syn::Result { + let paths = input.parse_terminated(Path::parse, Token![,])?; + if paths + .first() + .is_some_and(|p| p.is_ident(SPECIALIZE_ALL_IDENT)) + { + Ok(SpecializeImplTargets::All) + } else { + Ok(SpecializeImplTargets::Specific(paths.into_iter().collect())) + } + } +} + +#[derive(Clone)] +enum Key { + Whole, + Default, + Index(Index), + Custom(Expr), +} + +impl Key { + fn expr(&self) -> Expr { + match self { + Key::Whole => parse_quote!(key), + Key::Default => parse_quote!(#FQDefault::default()), + Key::Index(index) => { + let member = Member::Unnamed(index.clone()); + parse_quote!(key.#member) + } + Key::Custom(expr) => expr.clone(), + } + } +} + +const KEY_ERROR_MSG: &str = "Invalid key override. Must be either `default` or a valid Rust expression of the correct key type"; + +impl Parse for Key { + fn parse(input: ParseStream) -> syn::Result { + if let Ok(ident) = input.parse::() { + if ident == KEY_DEFAULT_IDENT { + Ok(Key::Default) + } else { + Err(syn::Error::new_spanned(ident, KEY_ERROR_MSG)) + } + } else { + input.parse::().map(Key::Custom).map_err(|mut err| { + err.extend(syn::Error::new(err.span(), KEY_ERROR_MSG)); + err + }) + } + } +} + +#[derive(Clone)] +struct FieldInfo { + ty: Type, + member: Member, + key: Key, + use_base_descriptor: bool, +} + +impl FieldInfo { + fn key_ty(&self, specialize_path: &Path, target_path: &Path) -> Option { + let ty = &self.ty; + matches!(self.key, Key::Whole | Key::Index(_)) + .then_some(parse_quote!(<#ty as #specialize_path::Specialize<#target_path>>::Key)) + } + + fn key_ident(&self, ident: Ident) -> Option { + matches!(self.key, Key::Whole | Key::Index(_)).then_some(ident) + } + + fn specialize_expr(&self, specialize_path: &Path, target_path: &Path) -> Expr { + let FieldInfo { + ty, member, key, .. + } = &self; + let key_expr = key.expr(); + parse_quote!(<#ty as #specialize_path::Specialize<#target_path>>::specialize(&self.#member, #key_expr, descriptor)) + } + + fn specialize_predicate(&self, specialize_path: &Path, target_path: &Path) -> WherePredicate { + let ty = &self.ty; + if matches!(&self.key, Key::Default) { + parse_quote!(#ty: #specialize_path::Specialize<#target_path, Key: #FQDefault>) + } else { + parse_quote!(#ty: #specialize_path::Specialize<#target_path>) + } + } + + fn get_base_descriptor_predicate( + &self, + specialize_path: &Path, + target_path: &Path, + ) -> WherePredicate { + let ty = &self.ty; + parse_quote!(#ty: #specialize_path::GetBaseDescriptor<#target_path>) + } +} + +fn get_field_info(fields: &Fields, targets: &SpecializeImplTargets) -> syn::Result> { + let mut field_info: Vec = Vec::new(); + let mut used_count = 0; + let mut single_index = 0; + for (index, field) in fields.iter().enumerate() { + let field_ty = field.ty.clone(); + let field_member = field.ident.clone().map_or( + Member::Unnamed(Index { + index: index as u32, + span: field.span(), + }), + Member::Named, + ); + let key_index = Index { + index: used_count, + span: field.span(), + }; + + let mut use_key_field = true; + let mut key = Key::Index(key_index); + let mut use_base_descriptor = false; + for attr in &field.attrs { + match &attr.meta { + Meta::Path(path) if path.is_ident(&BASE_DESCRIPTOR_ATTR_IDENT) => { + use_base_descriptor = true; + } + Meta::List(MetaList { path, tokens, .. }) if path.is_ident(&KEY_ATTR_IDENT) => { + let owned_tokens = tokens.clone().into(); + let Ok(parsed_key) = parse::(owned_tokens) else { + return Err(syn::Error::new( + attr.span(), + "Invalid key override attribute", + )); + }; + key = parsed_key; + if matches!( + (&key, &targets), + (Key::Custom(_), SpecializeImplTargets::All) + ) { + return Err(syn::Error::new( + attr.span(), + "#[key(default)] is the only key override type allowed with #[specialize(all)]", + )); + } + use_key_field = false; + } + _ => {} + } + } + + if use_key_field { + used_count += 1; + single_index = index; + } + + field_info.push(FieldInfo { + ty: field_ty, + member: field_member, + key, + use_base_descriptor, + }); + } + + if used_count == 1 { + field_info[single_index].key = Key::Whole; + } + + Ok(field_info) +} + +fn get_struct_fields<'a>(ast: &'a DeriveInput, derive_name: &str) -> syn::Result<&'a Fields> { + match &ast.data { + Data::Struct(DataStruct { fields, .. }) => Ok(fields), + Data::Enum(data_enum) => Err(syn::Error::new( + data_enum.enum_token.span(), + format!("#[derive({derive_name})] only supports structs."), + )), + Data::Union(data_union) => Err(syn::Error::new( + data_union.union_token.span(), + format!("#[derive({derive_name})] only supports structs."), + )), + } +} + +fn get_specialize_targets( + ast: &DeriveInput, + derive_name: &str, +) -> syn::Result { + let specialize_attr = ast.attrs.iter().find_map(|attr| { + if attr.path().is_ident(SPECIALIZE_ATTR_IDENT) { + if let Meta::List(meta_list) = &attr.meta { + return Some(meta_list); + } + } + None + }); + let Some(specialize_meta_list) = specialize_attr else { + return Err(syn::Error::new( + Span::call_site(), + format!("#[derive({derive_name})] must be accompanied by #[specialize(..targets)].\n Example usages: #[specialize(RenderPipeline)], #[specialize(all)]") + )); + }; + parse::(specialize_meta_list.tokens.clone().into()) +} + +macro_rules! guard { + ($expr: expr) => { + match $expr { + Ok(__val) => __val, + Err(err) => return err.to_compile_error().into(), + } + }; +} + +pub fn impl_specialize(input: TokenStream) -> TokenStream { + let bevy_render_path: Path = crate::bevy_render_path(); + let specialize_path = { + let mut path = bevy_render_path.clone(); + path.segments.push(format_ident!("render_resource").into()); + path + }; + + let ecs_path = crate::bevy_ecs_path(); + + let ast = parse_macro_input!(input as DeriveInput); + let targets = guard!(get_specialize_targets(&ast, "Specialize")); + let fields = guard!(get_struct_fields(&ast, "Specialize")); + let field_info = guard!(get_field_info(fields, &targets)); + + let key_idents: Vec> = field_info + .iter() + .enumerate() + .map(|(i, field_info)| field_info.key_ident(format_ident!("key{i}"))) + .collect(); + let key_tuple_idents: Vec = key_idents.iter().flatten().cloned().collect(); + let ignore_pat: Pat = parse_quote!(_); + let key_patterns: Vec = key_idents + .iter() + .map(|key_ident| match key_ident { + Some(key_ident) => parse_quote!(#key_ident), + None => ignore_pat.clone(), + }) + .collect(); + + let base_descriptor_fields = field_info + .iter() + .filter(|field| field.use_base_descriptor) + .collect::>(); + + if base_descriptor_fields.len() > 1 { + return syn::Error::new( + Span::call_site(), + "Too many #[base_descriptor] attributes found. It must be present on exactly one field", + ) + .into_compile_error() + .into(); + } + + let base_descriptor_field = base_descriptor_fields.first().copied(); + + match targets { + SpecializeImplTargets::All => { + let specialize_impl = impl_specialize_all( + &specialize_path, + &ecs_path, + &ast, + &field_info, + &key_patterns, + &key_tuple_idents, + ); + let get_base_descriptor_impl = base_descriptor_field + .map(|field_info| impl_get_base_descriptor_all(&specialize_path, &ast, field_info)) + .unwrap_or_default(); + [specialize_impl, get_base_descriptor_impl] + .into_iter() + .collect() + } + SpecializeImplTargets::Specific(targets) => { + let specialize_impls = targets.iter().map(|target| { + impl_specialize_specific( + &specialize_path, + &ecs_path, + &ast, + &field_info, + target, + &key_patterns, + &key_tuple_idents, + ) + }); + let get_base_descriptor_impls = targets.iter().filter_map(|target| { + base_descriptor_field.map(|field_info| { + impl_get_base_descriptor_specific(&specialize_path, &ast, field_info, target) + }) + }); + specialize_impls.chain(get_base_descriptor_impls).collect() + } + } +} + +fn impl_specialize_all( + specialize_path: &Path, + ecs_path: &Path, + ast: &DeriveInput, + field_info: &[FieldInfo], + key_patterns: &[Pat], + key_tuple_idents: &[Ident], +) -> TokenStream { + let target_path = Path::from(format_ident!("T")); + let key_elems: Vec = field_info + .iter() + .filter_map(|field_info| field_info.key_ty(specialize_path, &target_path)) + .collect(); + let specialize_exprs: Vec = field_info + .iter() + .map(|field_info| field_info.specialize_expr(specialize_path, &target_path)) + .collect(); + + let struct_name = &ast.ident; + let mut generics = ast.generics.clone(); + generics.params.insert( + 0, + parse_quote!(#target_path: #specialize_path::Specializable), + ); + + if !field_info.is_empty() { + let where_clause = generics.make_where_clause(); + for field in field_info { + where_clause + .predicates + .push(field.specialize_predicate(specialize_path, &target_path)); + } + } + + let (_, type_generics, _) = ast.generics.split_for_impl(); + let (impl_generics, _, where_clause) = &generics.split_for_impl(); + + TokenStream::from(quote! { + impl #impl_generics #specialize_path::Specialize<#target_path> for #struct_name #type_generics #where_clause { + type Key = (#(#key_elems),*); + + fn specialize( + &self, + key: Self::Key, + descriptor: &mut <#target_path as #specialize_path::Specializable>::Descriptor + ) -> #FQResult<#specialize_path::Canonical, #ecs_path::error::BevyError> { + #(let #key_patterns = #specialize_exprs?;)* + #FQResult::Ok((#(#key_tuple_idents),*)) + } + } + }) +} + +fn impl_specialize_specific( + specialize_path: &Path, + ecs_path: &Path, + ast: &DeriveInput, + field_info: &[FieldInfo], + target_path: &Path, + key_patterns: &[Pat], + key_tuple_idents: &[Ident], +) -> TokenStream { + let key_elems: Vec = field_info + .iter() + .filter_map(|field_info| field_info.key_ty(specialize_path, target_path)) + .collect(); + let specialize_exprs: Vec = field_info + .iter() + .map(|field_info| field_info.specialize_expr(specialize_path, target_path)) + .collect(); + + let struct_name = &ast.ident; + let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl(); + + TokenStream::from(quote! { + impl #impl_generics #specialize_path::Specialize<#target_path> for #struct_name #type_generics #where_clause { + type Key = (#(#key_elems),*); + + fn specialize( + &self, + key: Self::Key, + descriptor: &mut <#target_path as #specialize_path::Specializable>::Descriptor + ) -> #FQResult<#specialize_path::Canonical, #ecs_path::error::BevyError> { + #(let #key_patterns = #specialize_exprs?;)* + #FQResult::Ok((#(#key_tuple_idents),*)) + } + } + }) +} + +fn impl_get_base_descriptor_specific( + specialize_path: &Path, + ast: &DeriveInput, + base_descriptor_field_info: &FieldInfo, + target_path: &Path, +) -> TokenStream { + let struct_name = &ast.ident; + let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl(); + let field_ty = &base_descriptor_field_info.ty; + let field_member = &base_descriptor_field_info.member; + TokenStream::from(quote!( + impl #impl_generics #specialize_path::GetBaseDescriptor<#target_path> for #struct_name #type_generics #where_clause { + fn get_base_descriptor(&self) -> <#target_path as #specialize_path::Specializable>::Descriptor { + <#field_ty as #specialize_path::GetBaseDescriptor<#target_path>>::base_descriptor(&self.#field_member) + } + } + )) +} + +fn impl_get_base_descriptor_all( + specialize_path: &Path, + ast: &DeriveInput, + base_descriptor_field_info: &FieldInfo, +) -> TokenStream { + let target_path = Path::from(format_ident!("T")); + let struct_name = &ast.ident; + let mut generics = ast.generics.clone(); + generics.params.insert( + 0, + parse_quote!(#target_path: #specialize_path::Specializable), + ); + + let where_clause = generics.make_where_clause(); + where_clause.predicates.push( + base_descriptor_field_info.get_base_descriptor_predicate(specialize_path, &target_path), + ); + + let (_, type_generics, _) = ast.generics.split_for_impl(); + let (impl_generics, _, where_clause) = &generics.split_for_impl(); + let field_ty = &base_descriptor_field_info.ty; + let field_member = &base_descriptor_field_info.member; + TokenStream::from(quote! { + impl #impl_generics #specialize_path::GetBaseDescriptor<#target_path> for #struct_name #type_generics #where_clause { + fn get_base_descriptor(&self) -> <#target_path as #specialize_path::Specializable>::Descriptor { + <#field_ty as #specialize_path::GetBaseDescriptor<#target_path>>::base_descriptor(&self.#field_member) + } + } + }) +} + +pub fn impl_specializer_key(input: TokenStream) -> TokenStream { + let bevy_render_path: Path = crate::bevy_render_path(); + let specialize_path = { + let mut path = bevy_render_path.clone(); + path.segments.push(format_ident!("render_resource").into()); + path + }; + + let ast = parse_macro_input!(input as DeriveInput); + let ident = ast.ident; + TokenStream::from(quote!( + impl #specialize_path::SpecializerKey for #ident { + const IS_CANONICAL: bool = true; + type Canonical = Self; + } + )) +} diff --git a/crates/bevy_render/src/batching/gpu_preprocessing.rs b/crates/bevy_render/src/batching/gpu_preprocessing.rs index ea5970431a..2fb0172b21 100644 --- a/crates/bevy_render/src/batching/gpu_preprocessing.rs +++ b/crates/bevy_render/src/batching/gpu_preprocessing.rs @@ -392,9 +392,12 @@ where } /// The buffer of GPU preprocessing work items for a single view. -#[expect( - clippy::large_enum_variant, - reason = "See https://github.com/bevyengine/bevy/issues/19220" +#[cfg_attr( + not(target_arch = "wasm32"), + expect( + clippy::large_enum_variant, + reason = "See https://github.com/bevyengine/bevy/issues/19220" + ) )] pub enum PreprocessWorkItemBuffers { /// The work items we use if we aren't using indirect drawing. diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index 2732a44316..20ec3f9c9f 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -111,6 +111,17 @@ impl Viewport { } } } + + pub fn with_override( + &self, + main_pass_resolution_override: Option<&MainPassResolutionOverride>, + ) -> Self { + let mut viewport = self.clone(); + if let Some(override_size) = main_pass_resolution_override { + viewport.physical_size = **override_size; + } + viewport + } } /// Settings to define a camera sub view. @@ -1366,6 +1377,19 @@ impl TemporalJitter { #[reflect(Default, Component)] pub struct MipBias(pub f32); +/// Override the resolution a 3d camera's main pass is rendered at. +/// +/// Does not affect post processing. +/// +/// ## Usage +/// +/// * Insert this component on a 3d camera entity in the render world. +/// * The resolution override must be smaller than the camera's viewport size. +/// * The resolution override is specified in physical pixels. +#[derive(Component, Reflect, Deref)] +#[reflect(Component)] +pub struct MainPassResolutionOverride(pub UVec2); + impl Default for MipBias { fn default() -> Self { Self(-1.0) diff --git a/crates/bevy_render/src/camera/mod.rs b/crates/bevy_render/src/camera/mod.rs index a2470a7660..1b2a3bdfd3 100644 --- a/crates/bevy_render/src/camera/mod.rs +++ b/crates/bevy_render/src/camera/mod.rs @@ -29,6 +29,7 @@ impl Plugin for CameraPlugin { .register_type::() .register_type::() .register_type::() + .register_type::() .init_resource::() .init_resource::() .add_plugins(( diff --git a/crates/bevy_render/src/erased_render_asset.rs b/crates/bevy_render/src/erased_render_asset.rs new file mode 100644 index 0000000000..ac2423990b --- /dev/null +++ b/crates/bevy_render/src/erased_render_asset.rs @@ -0,0 +1,431 @@ +use crate::{ + render_resource::AsBindGroupError, ExtractSchedule, MainWorld, Render, RenderApp, + RenderSystems, Res, +}; +use bevy_app::{App, Plugin, SubApp}; +pub use bevy_asset::RenderAssetUsages; +use bevy_asset::{Asset, AssetEvent, AssetId, Assets, UntypedAssetId}; +use bevy_ecs::{ + prelude::{Commands, EventReader, IntoScheduleConfigs, ResMut, Resource}, + schedule::{ScheduleConfigs, SystemSet}, + system::{ScheduleSystem, StaticSystemParam, SystemParam, SystemParamItem, SystemState}, + world::{FromWorld, Mut}, +}; +use bevy_platform::collections::{HashMap, HashSet}; +use bevy_render::render_asset::RenderAssetBytesPerFrameLimiter; +use core::marker::PhantomData; +use thiserror::Error; +use tracing::{debug, error}; + +#[derive(Debug, Error)] +pub enum PrepareAssetError { + #[error("Failed to prepare asset")] + RetryNextUpdate(E), + #[error("Failed to build bind group: {0}")] + AsBindGroupError(AsBindGroupError), +} + +/// The system set during which we extract modified assets to the render world. +#[derive(SystemSet, Clone, PartialEq, Eq, Debug, Hash)] +pub struct AssetExtractionSystems; + +/// Deprecated alias for [`AssetExtractionSystems`]. +#[deprecated(since = "0.17.0", note = "Renamed to `AssetExtractionSystems`.")] +pub type ExtractAssetsSet = AssetExtractionSystems; + +/// Describes how an asset gets extracted and prepared for rendering. +/// +/// In the [`ExtractSchedule`] step the [`ErasedRenderAsset::SourceAsset`] is transferred +/// from the "main world" into the "render world". +/// +/// After that in the [`RenderSystems::PrepareAssets`] step the extracted asset +/// is transformed into its GPU-representation of type [`ErasedRenderAsset`]. +pub trait ErasedRenderAsset: Send + Sync + 'static { + /// The representation of the asset in the "main world". + type SourceAsset: Asset + Clone; + /// The target representation of the asset in the "render world". + type ErasedAsset: Send + Sync + 'static + Sized; + + /// Specifies all ECS data required by [`ErasedRenderAsset::prepare_asset`]. + /// + /// For convenience use the [`lifetimeless`](bevy_ecs::system::lifetimeless) [`SystemParam`]. + type Param: SystemParam; + + /// Whether or not to unload the asset after extracting it to the render world. + #[inline] + fn asset_usage(_source_asset: &Self::SourceAsset) -> RenderAssetUsages { + RenderAssetUsages::default() + } + + /// Size of the data the asset will upload to the gpu. Specifying a return value + /// will allow the asset to be throttled via [`RenderAssetBytesPerFrameLimiter`]. + #[inline] + #[expect( + unused_variables, + reason = "The parameters here are intentionally unused by the default implementation; however, putting underscores here will result in the underscores being copied by rust-analyzer's tab completion." + )] + fn byte_len(erased_asset: &Self::SourceAsset) -> Option { + None + } + + /// Prepares the [`ErasedRenderAsset::SourceAsset`] for the GPU by transforming it into a [`ErasedRenderAsset`]. + /// + /// ECS data may be accessed via `param`. + fn prepare_asset( + source_asset: Self::SourceAsset, + asset_id: AssetId, + param: &mut SystemParamItem, + ) -> Result>; + + /// Called whenever the [`ErasedRenderAsset::SourceAsset`] has been removed. + /// + /// You can implement this method if you need to access ECS data (via + /// `_param`) in order to perform cleanup tasks when the asset is removed. + /// + /// The default implementation does nothing. + fn unload_asset( + _source_asset: AssetId, + _param: &mut SystemParamItem, + ) { + } +} + +/// This plugin extracts the changed assets from the "app world" into the "render world" +/// and prepares them for the GPU. They can then be accessed from the [`ErasedRenderAssets`] resource. +/// +/// Therefore it sets up the [`ExtractSchedule`] and +/// [`RenderSystems::PrepareAssets`] steps for the specified [`ErasedRenderAsset`]. +/// +/// The `AFTER` generic parameter can be used to specify that `A::prepare_asset` should not be run until +/// `prepare_assets::` has completed. This allows the `prepare_asset` function to depend on another +/// prepared [`ErasedRenderAsset`], for example `Mesh::prepare_asset` relies on `ErasedRenderAssets::` for morph +/// targets, so the plugin is created as `ErasedRenderAssetPlugin::::default()`. +pub struct ErasedRenderAssetPlugin< + A: ErasedRenderAsset, + AFTER: ErasedRenderAssetDependency + 'static = (), +> { + phantom: PhantomData (A, AFTER)>, +} + +impl Default + for ErasedRenderAssetPlugin +{ + fn default() -> Self { + Self { + phantom: Default::default(), + } + } +} + +impl Plugin + for ErasedRenderAssetPlugin +{ + fn build(&self, app: &mut App) { + app.init_resource::>(); + } + + fn finish(&self, app: &mut App) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app + .init_resource::>() + .init_resource::>() + .init_resource::>() + .add_systems( + ExtractSchedule, + extract_erased_render_asset::.in_set(AssetExtractionSystems), + ); + AFTER::register_system( + render_app, + prepare_erased_assets::.in_set(RenderSystems::PrepareAssets), + ); + } + } +} + +// helper to allow specifying dependencies between render assets +pub trait ErasedRenderAssetDependency { + fn register_system(render_app: &mut SubApp, system: ScheduleConfigs); +} + +impl ErasedRenderAssetDependency for () { + fn register_system(render_app: &mut SubApp, system: ScheduleConfigs) { + render_app.add_systems(Render, system); + } +} + +impl ErasedRenderAssetDependency for A { + fn register_system(render_app: &mut SubApp, system: ScheduleConfigs) { + render_app.add_systems(Render, system.after(prepare_erased_assets::)); + } +} + +/// Temporarily stores the extracted and removed assets of the current frame. +#[derive(Resource)] +pub struct ExtractedAssets { + /// The assets extracted this frame. + /// + /// These are assets that were either added or modified this frame. + pub extracted: Vec<(AssetId, A::SourceAsset)>, + + /// IDs of the assets that were removed this frame. + /// + /// These assets will not be present in [`ExtractedAssets::extracted`]. + pub removed: HashSet>, + + /// IDs of the assets that were modified this frame. + pub modified: HashSet>, + + /// IDs of the assets that were added this frame. + pub added: HashSet>, +} + +impl Default for ExtractedAssets { + fn default() -> Self { + Self { + extracted: Default::default(), + removed: Default::default(), + modified: Default::default(), + added: Default::default(), + } + } +} + +/// Stores all GPU representations ([`ErasedRenderAsset`]) +/// of [`ErasedRenderAsset::SourceAsset`] as long as they exist. +#[derive(Resource)] +pub struct ErasedRenderAssets(HashMap); + +impl Default for ErasedRenderAssets { + fn default() -> Self { + Self(Default::default()) + } +} + +impl ErasedRenderAssets { + pub fn get(&self, id: impl Into) -> Option<&ERA> { + self.0.get(&id.into()) + } + + pub fn get_mut(&mut self, id: impl Into) -> Option<&mut ERA> { + self.0.get_mut(&id.into()) + } + + pub fn insert(&mut self, id: impl Into, value: ERA) -> Option { + self.0.insert(id.into(), value) + } + + pub fn remove(&mut self, id: impl Into) -> Option { + self.0.remove(&id.into()) + } + + pub fn iter(&self) -> impl Iterator { + self.0.iter().map(|(k, v)| (*k, v)) + } + + pub fn iter_mut(&mut self) -> impl Iterator { + self.0.iter_mut().map(|(k, v)| (*k, v)) + } +} + +#[derive(Resource)] +struct CachedExtractErasedRenderAssetSystemState { + state: SystemState<( + EventReader<'static, 'static, AssetEvent>, + ResMut<'static, Assets>, + )>, +} + +impl FromWorld for CachedExtractErasedRenderAssetSystemState { + fn from_world(world: &mut bevy_ecs::world::World) -> Self { + Self { + state: SystemState::new(world), + } + } +} + +/// This system extracts all created or modified assets of the corresponding [`ErasedRenderAsset::SourceAsset`] type +/// into the "render world". +pub(crate) fn extract_erased_render_asset( + mut commands: Commands, + mut main_world: ResMut, +) { + main_world.resource_scope( + |world, mut cached_state: Mut>| { + let (mut events, mut assets) = cached_state.state.get_mut(world); + + let mut needs_extracting = >::default(); + let mut removed = >::default(); + let mut modified = >::default(); + + for event in events.read() { + #[expect( + clippy::match_same_arms, + reason = "LoadedWithDependencies is marked as a TODO, so it's likely this will no longer lint soon." + )] + match event { + AssetEvent::Added { id } => { + needs_extracting.insert(*id); + } + AssetEvent::Modified { id } => { + needs_extracting.insert(*id); + modified.insert(*id); + } + AssetEvent::Removed { .. } => { + // We don't care that the asset was removed from Assets in the main world. + // An asset is only removed from ErasedRenderAssets when its last handle is dropped (AssetEvent::Unused). + } + AssetEvent::Unused { id } => { + needs_extracting.remove(id); + modified.remove(id); + removed.insert(*id); + } + AssetEvent::LoadedWithDependencies { .. } => { + // TODO: handle this + } + } + } + + let mut extracted_assets = Vec::new(); + let mut added = >::default(); + for id in needs_extracting.drain() { + if let Some(asset) = assets.get(id) { + let asset_usage = A::asset_usage(asset); + if asset_usage.contains(RenderAssetUsages::RENDER_WORLD) { + if asset_usage == RenderAssetUsages::RENDER_WORLD { + if let Some(asset) = assets.remove(id) { + extracted_assets.push((id, asset)); + added.insert(id); + } + } else { + extracted_assets.push((id, asset.clone())); + added.insert(id); + } + } + } + } + + commands.insert_resource(ExtractedAssets:: { + extracted: extracted_assets, + removed, + modified, + added, + }); + cached_state.state.apply(world); + }, + ); +} + +// TODO: consider storing inside system? +/// All assets that should be prepared next frame. +#[derive(Resource)] +pub struct PrepareNextFrameAssets { + assets: Vec<(AssetId, A::SourceAsset)>, +} + +impl Default for PrepareNextFrameAssets { + fn default() -> Self { + Self { + assets: Default::default(), + } + } +} + +/// This system prepares all assets of the corresponding [`ErasedRenderAsset::SourceAsset`] type +/// which where extracted this frame for the GPU. +pub fn prepare_erased_assets( + mut extracted_assets: ResMut>, + mut render_assets: ResMut>, + mut prepare_next_frame: ResMut>, + param: StaticSystemParam<::Param>, + bpf: Res, +) { + let mut wrote_asset_count = 0; + + let mut param = param.into_inner(); + let queued_assets = core::mem::take(&mut prepare_next_frame.assets); + for (id, extracted_asset) in queued_assets { + if extracted_assets.removed.contains(&id) || extracted_assets.added.contains(&id) { + // skip previous frame's assets that have been removed or updated + continue; + } + + let write_bytes = if let Some(size) = A::byte_len(&extracted_asset) { + // we could check if available bytes > byte_len here, but we want to make some + // forward progress even if the asset is larger than the max bytes per frame. + // this way we always write at least one (sized) asset per frame. + // in future we could also consider partial asset uploads. + if bpf.exhausted() { + prepare_next_frame.assets.push((id, extracted_asset)); + continue; + } + size + } else { + 0 + }; + + match A::prepare_asset(extracted_asset, id, &mut param) { + Ok(prepared_asset) => { + render_assets.insert(id, prepared_asset); + bpf.write_bytes(write_bytes); + wrote_asset_count += 1; + } + Err(PrepareAssetError::RetryNextUpdate(extracted_asset)) => { + prepare_next_frame.assets.push((id, extracted_asset)); + } + Err(PrepareAssetError::AsBindGroupError(e)) => { + error!( + "{} Bind group construction failed: {e}", + core::any::type_name::() + ); + } + } + } + + for removed in extracted_assets.removed.drain() { + render_assets.remove(removed); + A::unload_asset(removed, &mut param); + } + + for (id, extracted_asset) in extracted_assets.extracted.drain(..) { + // we remove previous here to ensure that if we are updating the asset then + // any users will not see the old asset after a new asset is extracted, + // even if the new asset is not yet ready or we are out of bytes to write. + render_assets.remove(id); + + let write_bytes = if let Some(size) = A::byte_len(&extracted_asset) { + if bpf.exhausted() { + prepare_next_frame.assets.push((id, extracted_asset)); + continue; + } + size + } else { + 0 + }; + + match A::prepare_asset(extracted_asset, id, &mut param) { + Ok(prepared_asset) => { + render_assets.insert(id, prepared_asset); + bpf.write_bytes(write_bytes); + wrote_asset_count += 1; + } + Err(PrepareAssetError::RetryNextUpdate(extracted_asset)) => { + prepare_next_frame.assets.push((id, extracted_asset)); + } + Err(PrepareAssetError::AsBindGroupError(e)) => { + error!( + "{} Bind group construction failed: {e}", + core::any::type_name::() + ); + } + } + } + + if bpf.exhausted() && !prepare_next_frame.assets.is_empty() { + debug!( + "{} write budget exhausted with {} assets remaining (wrote {})", + core::any::type_name::(), + prepare_next_frame.assets.len(), + wrote_asset_count + ); + } +} diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index aa1e2da676..526d75dccd 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -26,6 +26,7 @@ pub mod alpha; pub mod batching; pub mod camera; pub mod diagnostic; +pub mod erased_render_asset; pub mod experimental; pub mod extract_component; pub mod extract_instances; @@ -216,10 +217,30 @@ pub enum RenderSystems { PostCleanup, } +/// The schedule that contains the app logic that is evaluated each tick +/// +/// This is highly inspired by [`bevy_app::Main`] +#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash, Default)] +pub struct MainRender; +impl MainRender { + pub fn run(world: &mut World, mut run_at_least_once: Local) { + if !*run_at_least_once { + let _ = world.try_run_schedule(RenderStartup); + *run_at_least_once = true; + } + + let _ = world.try_run_schedule(Render); + } +} + /// Deprecated alias for [`RenderSystems`]. #[deprecated(since = "0.17.0", note = "Renamed to `RenderSystems`.")] pub type RenderSet = RenderSystems; +/// The startup schedule of the [`RenderApp`] +#[derive(ScheduleLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct RenderStartup; + /// The main render schedule. #[derive(ScheduleLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] pub struct Render; @@ -530,7 +551,7 @@ unsafe fn initialize_render_app(app: &mut App) { app.init_resource::(); let mut render_app = SubApp::new(); - render_app.update_schedule = Some(Render.intern()); + render_app.update_schedule = Some(MainRender.intern()); let mut extract_schedule = Schedule::new(ExtractSchedule); // We skip applying any commands during the ExtractSchedule @@ -545,6 +566,7 @@ unsafe fn initialize_render_app(app: &mut App) { .add_schedule(extract_schedule) .add_schedule(Render::base_schedule()) .init_resource::() + .add_systems(MainRender, MainRender::run) .insert_resource(app.world().resource::().clone()) .add_systems(ExtractSchedule, PipelineCache::extract_shaders) .add_systems( diff --git a/crates/bevy_render/src/render_asset.rs b/crates/bevy_render/src/render_asset.rs index 0a5ad3e4ec..c35062eb85 100644 --- a/crates/bevy_render/src/render_asset.rs +++ b/crates/bevy_render/src/render_asset.rs @@ -499,14 +499,14 @@ impl RenderAssetBytesPerFrameLimiter { } /// Decreases the available bytes for the current frame. - fn write_bytes(&self, bytes: usize) { + pub(crate) fn write_bytes(&self, bytes: usize) { if self.max_bytes.is_some() && bytes > 0 { self.bytes_written.fetch_add(bytes, Ordering::Relaxed); } } /// Returns `true` if there are no remaining bytes available for writing this frame. - fn exhausted(&self) -> bool { + pub(crate) fn exhausted(&self) -> bool { if let Some(max_bytes) = self.max_bytes { let bytes_written = self.bytes_written.load(Ordering::Relaxed); bytes_written >= max_bytes diff --git a/crates/bevy_render/src/render_graph/app.rs b/crates/bevy_render/src/render_graph/app.rs index 338ae75d7a..fce6a13ad3 100644 --- a/crates/bevy_render/src/render_graph/app.rs +++ b/crates/bevy_render/src/render_graph/app.rs @@ -1,11 +1,11 @@ use bevy_app::{App, SubApp}; -use bevy_ecs::world::FromWorld; +use bevy_ecs::world::{FromWorld, World}; use tracing::warn; use super::{IntoRenderNodeArray, Node, RenderGraph, RenderLabel, RenderSubGraph}; /// Adds common [`RenderGraph`] operations to [`SubApp`] (and [`App`]). -pub trait RenderGraphApp { +pub trait RenderGraphExt { // Add a sub graph to the [`RenderGraph`] fn add_render_sub_graph(&mut self, sub_graph: impl RenderSubGraph) -> &mut Self; /// Add a [`Node`] to the [`RenderGraph`]: @@ -32,15 +32,15 @@ pub trait RenderGraphApp { ) -> &mut Self; } -impl RenderGraphApp for SubApp { +impl RenderGraphExt for World { fn add_render_graph_node( &mut self, sub_graph: impl RenderSubGraph, node_label: impl RenderLabel, ) -> &mut Self { let sub_graph = sub_graph.intern(); - let node = T::from_world(self.world_mut()); - let mut render_graph = self.world_mut().get_resource_mut::().expect( + let node = T::from_world(self); + let mut render_graph = self.get_resource_mut::().expect( "RenderGraph not found. Make sure you are using add_render_graph_node on the RenderApp", ); if let Some(graph) = render_graph.get_sub_graph_mut(sub_graph) { @@ -59,7 +59,7 @@ impl RenderGraphApp for SubApp { edges: impl IntoRenderNodeArray, ) -> &mut Self { let sub_graph = sub_graph.intern(); - let mut render_graph = self.world_mut().get_resource_mut::().expect( + let mut render_graph = self.get_resource_mut::().expect( "RenderGraph not found. Make sure you are using add_render_graph_edges on the RenderApp", ); if let Some(graph) = render_graph.get_sub_graph_mut(sub_graph) { @@ -79,7 +79,7 @@ impl RenderGraphApp for SubApp { input_node: impl RenderLabel, ) -> &mut Self { let sub_graph = sub_graph.intern(); - let mut render_graph = self.world_mut().get_resource_mut::().expect( + let mut render_graph = self.get_resource_mut::().expect( "RenderGraph not found. Make sure you are using add_render_graph_edge on the RenderApp", ); if let Some(graph) = render_graph.get_sub_graph_mut(sub_graph) { @@ -93,7 +93,7 @@ impl RenderGraphApp for SubApp { } fn add_render_sub_graph(&mut self, sub_graph: impl RenderSubGraph) -> &mut Self { - let mut render_graph = self.world_mut().get_resource_mut::().expect( + let mut render_graph = self.get_resource_mut::().expect( "RenderGraph not found. Make sure you are using add_render_sub_graph on the RenderApp", ); render_graph.add_sub_graph(sub_graph, RenderGraph::default()); @@ -101,13 +101,13 @@ impl RenderGraphApp for SubApp { } } -impl RenderGraphApp for App { +impl RenderGraphExt for SubApp { fn add_render_graph_node( &mut self, sub_graph: impl RenderSubGraph, node_label: impl RenderLabel, ) -> &mut Self { - SubApp::add_render_graph_node::(self.main_mut(), sub_graph, node_label); + World::add_render_graph_node::(self.world_mut(), sub_graph, node_label); self } @@ -117,7 +117,7 @@ impl RenderGraphApp for App { output_node: impl RenderLabel, input_node: impl RenderLabel, ) -> &mut Self { - SubApp::add_render_graph_edge(self.main_mut(), sub_graph, output_node, input_node); + World::add_render_graph_edge(self.world_mut(), sub_graph, output_node, input_node); self } @@ -126,12 +126,47 @@ impl RenderGraphApp for App { sub_graph: impl RenderSubGraph, edges: impl IntoRenderNodeArray, ) -> &mut Self { - SubApp::add_render_graph_edges(self.main_mut(), sub_graph, edges); + World::add_render_graph_edges(self.world_mut(), sub_graph, edges); self } fn add_render_sub_graph(&mut self, sub_graph: impl RenderSubGraph) -> &mut Self { - SubApp::add_render_sub_graph(self.main_mut(), sub_graph); + World::add_render_sub_graph(self.world_mut(), sub_graph); + self + } +} + +impl RenderGraphExt for App { + fn add_render_graph_node( + &mut self, + sub_graph: impl RenderSubGraph, + node_label: impl RenderLabel, + ) -> &mut Self { + World::add_render_graph_node::(self.world_mut(), sub_graph, node_label); + self + } + + fn add_render_graph_edge( + &mut self, + sub_graph: impl RenderSubGraph, + output_node: impl RenderLabel, + input_node: impl RenderLabel, + ) -> &mut Self { + World::add_render_graph_edge(self.world_mut(), sub_graph, output_node, input_node); + self + } + + fn add_render_graph_edges( + &mut self, + sub_graph: impl RenderSubGraph, + edges: impl IntoRenderNodeArray, + ) -> &mut Self { + World::add_render_graph_edges(self.world_mut(), sub_graph, edges); + self + } + + fn add_render_sub_graph(&mut self, sub_graph: impl RenderSubGraph) -> &mut Self { + World::add_render_sub_graph(self.world_mut(), sub_graph); self } } diff --git a/crates/bevy_render/src/render_phase/mod.rs b/crates/bevy_render/src/render_phase/mod.rs index 272418f67f..c58318f654 100644 --- a/crates/bevy_render/src/render_phase/mod.rs +++ b/crates/bevy_render/src/render_phase/mod.rs @@ -61,7 +61,9 @@ use crate::{ render_resource::{CachedRenderPipelineId, GpuArrayBufferIndex, PipelineCache}, Render, RenderApp, RenderSystems, }; +use bevy_ecs::intern::Interned; use bevy_ecs::{ + define_label, prelude::*, system::{lifetimeless::SRes, SystemParamItem}, }; @@ -69,6 +71,33 @@ use core::{fmt::Debug, hash::Hash, iter, marker::PhantomData, ops::Range, slice: use smallvec::SmallVec; use tracing::warn; +pub use bevy_render_macros::ShaderLabel; + +define_label!( + #[diagnostic::on_unimplemented( + note = "consider annotating `{Self}` with `#[derive(ShaderLabel)]`" + )] + /// Labels used to uniquely identify types of material shaders + ShaderLabel, + SHADER_LABEL_INTERNER +); + +/// A shorthand for `Interned`. +pub type InternedShaderLabel = Interned; + +pub use bevy_render_macros::DrawFunctionLabel; + +define_label!( + #[diagnostic::on_unimplemented( + note = "consider annotating `{Self}` with `#[derive(DrawFunctionLabel)]`" + )] + /// Labels used to uniquely identify types of material shaders + DrawFunctionLabel, + DRAW_FUNCTION_LABEL_INTERNER +); + +pub type InternedDrawFunctionLabel = Interned; + /// Stores the rendering instructions for a single phase that uses bins in all /// views. /// diff --git a/crates/bevy_render/src/render_resource/bind_group.rs b/crates/bevy_render/src/render_resource/bind_group.rs index 17de8455da..04b7747179 100644 --- a/crates/bevy_render/src/render_resource/bind_group.rs +++ b/crates/bevy_render/src/render_resource/bind_group.rs @@ -481,22 +481,27 @@ impl Deref for BindGroup { /// is_shaded: bool, /// } /// -/// #[derive(Copy, Clone, Hash, Eq, PartialEq)] +/// // Materials keys are intended to be small, cheap to hash, and +/// // uniquely identify a specific material permutation, which +/// // is why they are required to be `bytemuck::Pod` and `bytemuck::Zeroable` +/// // when using the `AsBindGroup` derive macro. +/// #[repr(C)] +/// #[derive(Copy, Clone, Hash, Eq, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] /// struct CoolMaterialKey { -/// is_shaded: bool, +/// is_shaded: u32, /// } /// /// impl From<&CoolMaterial> for CoolMaterialKey { /// fn from(material: &CoolMaterial) -> CoolMaterialKey { /// CoolMaterialKey { -/// is_shaded: material.is_shaded, +/// is_shaded: material.is_shaded as u32, /// } /// } /// } /// ``` pub trait AsBindGroup { /// Data that will be stored alongside the "prepared" bind group. - type Data: Send + Sync; + type Data: bytemuck::Pod + bytemuck::Zeroable + Send + Sync; type Param: SystemParam + 'static; @@ -531,8 +536,8 @@ pub trait AsBindGroup { layout: &BindGroupLayout, render_device: &RenderDevice, param: &mut SystemParamItem<'_, '_, Self::Param>, - ) -> Result, AsBindGroupError> { - let UnpreparedBindGroup { bindings, data } = + ) -> Result { + let UnpreparedBindGroup { bindings } = Self::unprepared_bind_group(self, layout, render_device, param, false)?; let entries = bindings @@ -548,10 +553,11 @@ pub trait AsBindGroup { Ok(PreparedBindGroup { bindings, bind_group, - data, }) } + fn bind_group_data(&self) -> Self::Data; + /// Returns a vec of (binding index, `OwnedBindingResource`). /// /// In cases where `OwnedBindingResource` is not available (as for bindless @@ -569,7 +575,7 @@ pub trait AsBindGroup { render_device: &RenderDevice, param: &mut SystemParamItem<'_, '_, Self::Param>, force_no_bindless: bool, - ) -> Result, AsBindGroupError>; + ) -> Result; /// Creates the bind group layout matching all bind groups returned by /// [`AsBindGroup::as_bind_group`] @@ -613,16 +619,14 @@ pub enum AsBindGroupError { } /// A prepared bind group returned as a result of [`AsBindGroup::as_bind_group`]. -pub struct PreparedBindGroup { +pub struct PreparedBindGroup { pub bindings: BindingResources, pub bind_group: BindGroup, - pub data: T, } /// a map containing `OwnedBindingResource`s, keyed by the target binding index -pub struct UnpreparedBindGroup { +pub struct UnpreparedBindGroup { pub bindings: BindingResources, - pub data: T, } /// A pair of binding index and binding resource, used as part of diff --git a/crates/bevy_render/src/render_resource/mod.rs b/crates/bevy_render/src/render_resource/mod.rs index 09be66e840..9233d9e4c4 100644 --- a/crates/bevy_render/src/render_resource/mod.rs +++ b/crates/bevy_render/src/render_resource/mod.rs @@ -12,6 +12,7 @@ mod pipeline_cache; mod pipeline_specializer; pub mod resource_macros; mod shader; +mod specialize; mod storage_buffer; mod texture; mod uniform_buffer; @@ -28,6 +29,7 @@ pub use pipeline::*; pub use pipeline_cache::*; pub use pipeline_specializer::*; pub use shader::*; +pub use specialize::*; pub use storage_buffer::*; pub use texture::*; pub use uniform_buffer::*; diff --git a/crates/bevy_render/src/render_resource/pipeline.rs b/crates/bevy_render/src/render_resource/pipeline.rs index b76174cac3..e94cf27cd3 100644 --- a/crates/bevy_render/src/render_resource/pipeline.rs +++ b/crates/bevy_render/src/render_resource/pipeline.rs @@ -88,7 +88,7 @@ impl Deref for ComputePipeline { } /// Describes a render (graphics) pipeline. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Default)] pub struct RenderPipelineDescriptor { /// Debug label of the pipeline. This will show up in graphics debuggers for easy identification. pub label: Option>, @@ -112,33 +112,33 @@ pub struct RenderPipelineDescriptor { pub zero_initialize_workgroup_memory: bool, } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, Default)] 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 with this name in the shader. - pub entry_point: Cow<'static, str>, + /// The name of the entry point in the compiled shader, or `None` if the default entry point + /// is used. + pub entry_point: Option>, /// The format of any vertex buffers used with this pipeline. pub buffers: Vec, } /// Describes the fragment process in a render pipeline. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Default)] 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 with this name in the shader. - pub entry_point: Cow<'static, str>, + /// The name of the entry point in the compiled shader, or `None` if the default entry point + /// is used. + pub entry_point: Option>, /// The color state of the render targets. pub targets: Vec>, } /// Describes a compute pipeline. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq, Default)] pub struct ComputePipelineDescriptor { pub label: Option>, pub layout: Vec, @@ -146,9 +146,9 @@ pub struct ComputePipelineDescriptor { /// 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 with this name in the shader. - pub entry_point: Cow<'static, str>, + /// The name of the entry point in the compiled shader, or `None` if the default entry point + /// is used. + pub entry_point: Option>, /// Whether to zero-initialize workgroup memory by default. If you're not sure, set this to true. /// If this is false, reading from workgroup variables before writing to them will result in garbage values. pub zero_initialize_workgroup_memory: bool, diff --git a/crates/bevy_render/src/render_resource/pipeline_cache.rs b/crates/bevy_render/src/render_resource/pipeline_cache.rs index ebd3229636..328c5e5600 100644 --- a/crates/bevy_render/src/render_resource/pipeline_cache.rs +++ b/crates/bevy_render/src/render_resource/pipeline_cache.rs @@ -14,7 +14,7 @@ use bevy_platform::collections::{hash_map::EntryRef, HashMap, HashSet}; use bevy_tasks::Task; use bevy_utils::default; use bevy_utils::WgpuWrapper; -use core::{future::Future, hash::Hash, mem, ops::Deref}; +use core::{future::Future, hash::Hash, mem}; use naga::valid::Capabilities; use std::sync::{Mutex, PoisonError}; use thiserror::Error; @@ -80,9 +80,12 @@ pub struct CachedPipeline { } /// State of a cached pipeline inserted into a [`PipelineCache`]. -#[expect( - clippy::large_enum_variant, - reason = "See https://github.com/bevyengine/bevy/issues/19220" +#[cfg_attr( + not(target_arch = "wasm32"), + expect( + clippy::large_enum_variant, + reason = "See https://github.com/bevyengine/bevy/issues/19220" + ) )] #[derive(Debug)] pub enum CachedPipelineState { @@ -866,7 +869,7 @@ impl PipelineCache { let fragment_data = descriptor.fragment.as_ref().map(|fragment| { ( fragment_module.unwrap(), - fragment.entry_point.deref(), + fragment.entry_point.as_deref(), fragment.targets.as_slice(), ) }); @@ -886,7 +889,7 @@ impl PipelineCache { primitive: descriptor.primitive, vertex: RawVertexState { buffers: &vertex_buffer_layouts, - entry_point: Some(descriptor.vertex.entry_point.deref()), + entry_point: descriptor.vertex.entry_point.as_deref(), module: &vertex_module, // TODO: Should this be the same as the fragment compilation options? compilation_options: compilation_options.clone(), @@ -894,7 +897,7 @@ impl PipelineCache { fragment: fragment_data .as_ref() .map(|(module, entry_point, targets)| RawFragmentState { - entry_point: Some(entry_point), + entry_point: entry_point.as_deref(), module, targets, // TODO: Should this be the same as the vertex compilation options? @@ -952,7 +955,7 @@ impl PipelineCache { label: descriptor.label.as_deref(), layout: layout.as_ref().map(|layout| -> &PipelineLayout { layout }), module: &compute_module, - entry_point: Some(&descriptor.entry_point), + entry_point: descriptor.entry_point.as_deref(), // TODO: Expose the rest of this somehow compilation_options: PipelineCompilationOptions { constants: &[], @@ -1114,9 +1117,12 @@ fn create_pipeline_task( } /// Type of error returned by a [`PipelineCache`] when the creation of a GPU pipeline object failed. -#[expect( - clippy::large_enum_variant, - reason = "See https://github.com/bevyengine/bevy/issues/19220" +#[cfg_attr( + not(target_arch = "wasm32"), + expect( + clippy::large_enum_variant, + reason = "See https://github.com/bevyengine/bevy/issues/19220" + ) )] #[derive(Error, Debug)] pub enum PipelineCacheError { diff --git a/crates/bevy_render/src/render_resource/specialize.rs b/crates/bevy_render/src/render_resource/specialize.rs new file mode 100644 index 0000000000..c2269b1a78 --- /dev/null +++ b/crates/bevy_render/src/render_resource/specialize.rs @@ -0,0 +1,400 @@ +use super::{ + CachedComputePipelineId, CachedRenderPipelineId, ComputePipeline, ComputePipelineDescriptor, + PipelineCache, RenderPipeline, RenderPipelineDescriptor, +}; +use bevy_ecs::{ + error::BevyError, + resource::Resource, + world::{FromWorld, World}, +}; +use bevy_platform::{ + collections::{ + hash_map::{Entry, VacantEntry}, + HashMap, + }, + hash::FixedHasher, +}; +use core::{hash::Hash, marker::PhantomData}; +use tracing::error; +use variadics_please::all_tuples; + +pub use bevy_render_macros::{Specialize, SpecializerKey}; + +/// Defines a type that is able to be "specialized" and cached by creating and transforming +/// its descriptor type. This is implemented for [`RenderPipeline`] and [`ComputePipeline`], and +/// likely will not have much utility for other types. +pub trait Specializable { + type Descriptor: PartialEq + Clone + Send + Sync; + type CachedId: Clone + Send + Sync; + fn queue(pipeline_cache: &PipelineCache, descriptor: Self::Descriptor) -> Self::CachedId; + fn get_descriptor(pipeline_cache: &PipelineCache, id: Self::CachedId) -> &Self::Descriptor; +} + +impl Specializable for RenderPipeline { + type Descriptor = RenderPipelineDescriptor; + type CachedId = CachedRenderPipelineId; + + fn queue(pipeline_cache: &PipelineCache, descriptor: Self::Descriptor) -> Self::CachedId { + pipeline_cache.queue_render_pipeline(descriptor) + } + + fn get_descriptor( + pipeline_cache: &PipelineCache, + id: CachedRenderPipelineId, + ) -> &Self::Descriptor { + pipeline_cache.get_render_pipeline_descriptor(id) + } +} + +impl Specializable for ComputePipeline { + type Descriptor = ComputePipelineDescriptor; + + type CachedId = CachedComputePipelineId; + + fn queue(pipeline_cache: &PipelineCache, descriptor: Self::Descriptor) -> Self::CachedId { + pipeline_cache.queue_compute_pipeline(descriptor) + } + + fn get_descriptor( + pipeline_cache: &PipelineCache, + id: CachedComputePipelineId, + ) -> &Self::Descriptor { + pipeline_cache.get_compute_pipeline_descriptor(id) + } +} + +/// Defines a type that is able to transform descriptors for a specializable +/// type T, based on a hashable key type. +/// +/// This is mainly used when "specializing" render +/// pipelines, i.e. specifying shader defs and binding layout based on the key, +/// the result of which can then be cached and accessed quickly later. +/// +/// This trait can be derived with `#[derive(Specializer)]` for structs whose +/// fields all implement [`Specializer`]. The key type will be tuple of the keys +/// of each field, and their specialization logic will be applied in field +/// order. Since derive macros can't have generic parameters, the derive macro +/// requires an additional `#[specialize(..targets)]` attribute to specify a +/// list of types to target for the implementation. `#[specialize(all)]` is +/// also allowed, and will generate a fully generic implementation at the cost +/// of slightly worse error messages. +/// +/// Additionally, each field can optionally take a `#[key]` attribute to +/// specify a "key override". This will "hide" that field's key from being +/// exposed by the wrapper, and always use the value given by the attribute. +/// Values for this attribute may either be `default` which will use the key's +/// [`Default`] implementation, or a valid rust +/// expression of the key type. +/// +/// Example: +/// ```rs +/// # use super::RenderPipeline; +/// # use super::RenderPipelineDescriptor; +/// # use bevy_ecs::error::BevyError; +/// +/// struct A; +/// struct B; +/// #[derive(Copy, Clone, PartialEq, Eq, Hash, SpecializerKey)] +/// struct BKey; +/// +/// impl Specializer for A { +/// type Key = (); +/// +/// fn specializer(&self, key: (), descriptor: &mut RenderPipelineDescriptor) -> Result<(), BevyError> { +/// # let _ = (key, descriptor); +/// //... +/// Ok(()) +/// } +/// } +/// +/// impl Specializer for B { +/// type Key = BKey; +/// +/// fn specialize(&self, _key: Bkey, _descriptor: &mut RenderPipelineDescriptor) -> Result { +/// # let _ = (key, descriptor); +/// //... +/// Ok(BKey) +/// } +/// } +/// +/// #[derive(Specializer)] +/// #[specialize(RenderPipeline)] +/// struct C { +/// #[key(default)] +/// a: A, +/// b: B, +/// } +/// +/// /* +/// The generated implementation: +/// impl Specializer for C { +/// type Key = BKey; +/// fn specialize( +/// &self, +/// key: Self::Key, +/// descriptor: &mut RenderPipelineDescriptor +/// ) -> Result, BevyError> { +/// let _ = self.a.specialize((), descriptor); +/// let key = self.b.specialize(key, descriptor); +/// Ok(key) +/// } +/// } +/// */ +/// ``` +pub trait Specializer: Send + Sync + 'static { + type Key: SpecializerKey; + fn specialize( + &self, + key: Self::Key, + descriptor: &mut T::Descriptor, + ) -> Result, BevyError>; +} + +/// Defines a type that is able to be used as a key for types that `impl Specialize` +/// +/// **Most types should implement this trait with `IS_CANONICAL = true` and `Canonical = Self`**. +/// This is the implementation generated by `#[derive(SpecializerKey)]` +/// +/// In this case, "canonical" means that each unique value of this type will produce +/// a unique specialized result, which isn't true in general. `MeshVertexBufferLayout` +/// is a good example of a type that's `Eq + Hash`, but that isn't canonical: vertex +/// attributes could be specified in any order, or there could be more attributes +/// provided than the specialized pipeline requires. Its `Canonical` key type would +/// be `VertexBufferLayout`, the final layout required by the pipeline. +/// +/// Processing keys into canonical keys this way allows the `SpecializedCache` to reuse +/// resources more eagerly where possible. +pub trait SpecializerKey: Clone + Hash + Eq { + /// Denotes whether this key is canonical or not. This should only be `true` + /// if and only if `Canonical = Self`. + const IS_CANONICAL: bool; + + /// The canonical key type to convert this into during specialization. + type Canonical: Hash + Eq; +} + +pub type Canonical = ::Canonical; + +impl Specializer for () { + type Key = (); + + fn specialize( + &self, + _key: Self::Key, + _descriptor: &mut T::Descriptor, + ) -> Result<(), BevyError> { + Ok(()) + } +} + +impl Specializer for PhantomData { + type Key = (); + + fn specialize( + &self, + _key: Self::Key, + _descriptor: &mut T::Descriptor, + ) -> Result<(), BevyError> { + Ok(()) + } +} + +macro_rules! impl_specialization_key_tuple { + ($($T:ident),*) => { + impl <$($T: SpecializerKey),*> SpecializerKey for ($($T,)*) { + const IS_CANONICAL: bool = true $(&& <$T as SpecializerKey>::IS_CANONICAL)*; + type Canonical = ($(Canonical<$T>,)*); + } + }; +} + +all_tuples!(impl_specialization_key_tuple, 0, 12, T); + +/// Defines a specializer that can also provide a "base descriptor". +/// +/// In order to be composable, [`Specializer`] implementers don't create full +/// descriptors, only transform them. However, [`SpecializedCache`]s need a +/// "base descriptor" at creation time in order to have something for the +/// [`Specializer`] implementation to work off of. This trait allows +/// [`SpecializedCache`] to impl [`FromWorld`] for [`Specializer`] +/// implementations that also satisfy [`FromWorld`] and [`GetBaseDescriptor`]. +/// +/// This trait can be also derived with `#[derive(Specializer)]`, by marking +/// a field with `#[base_descriptor]` to use its [`GetBaseDescriptor`] implementation. +/// +/// Example: +/// ```rs +/// struct A; +/// struct B; +/// +/// impl Specializer for A { +/// type Key = (); +/// +/// fn specialize(&self, _key: (), _descriptor: &mut RenderPipelineDescriptor) { +/// //... +/// } +/// } +/// +/// impl Specializer for B { +/// type Key = u32; +/// +/// fn specialize(&self, _key: u32, _descriptor: &mut RenderPipelineDescriptor) { +/// //... +/// } +/// } +/// +/// impl GetBaseDescriptor for B { +/// fn get_base_descriptor(&self) -> RenderPipelineDescriptor { +/// # todo!() +/// //... +/// } +/// } +/// +/// +/// #[derive(Specializer)] +/// #[specialize(RenderPipeline)] +/// struct C { +/// #[key(default)] +/// a: A, +/// #[base_descriptor] +/// b: B, +/// } +/// +/// /* +/// The generated implementation: +/// impl GetBaseDescriptor for C { +/// fn get_base_descriptor(&self) -> RenderPipelineDescriptor { +/// self.b.base_descriptor() +/// } +/// } +/// */ +/// ``` +pub trait GetBaseDescriptor: Specializer { + fn get_base_descriptor(&self) -> T::Descriptor; +} + +pub type SpecializerFn = + fn(>::Key, &mut ::Descriptor) -> Result<(), BevyError>; + +/// A cache for specializable resources. For a given key type the resulting +/// resource will only be created if it is missing, retrieving it from the +/// cache otherwise. +#[derive(Resource)] +pub struct SpecializedCache> { + specializer: S, + user_specializer: Option>, + base_descriptor: T::Descriptor, + primary_cache: HashMap, + secondary_cache: HashMap, T::CachedId>, +} + +impl> SpecializedCache { + /// Creates a new [`SpecializedCache`] from a [`Specializer`], + /// an optional "user specializer", and a base descriptor. The + /// user specializer is applied after the [`Specializer`], with + /// the same key. + #[inline] + pub fn new( + specializer: S, + user_specializer: Option>, + base_descriptor: T::Descriptor, + ) -> Self { + Self { + specializer, + user_specializer, + base_descriptor, + primary_cache: Default::default(), + secondary_cache: Default::default(), + } + } + + /// Specializes a resource given the [`Specializer`]'s key type. + #[inline] + pub fn specialize( + &mut self, + pipeline_cache: &PipelineCache, + key: S::Key, + ) -> Result { + let entry = self.primary_cache.entry(key.clone()); + match entry { + Entry::Occupied(entry) => Ok(entry.get().clone()), + Entry::Vacant(entry) => Self::specialize_slow( + &self.specializer, + self.user_specializer, + self.base_descriptor.clone(), + pipeline_cache, + key, + entry, + &mut self.secondary_cache, + ), + } + } + + #[cold] + fn specialize_slow( + specializer: &S, + user_specializer: Option>, + base_descriptor: T::Descriptor, + pipeline_cache: &PipelineCache, + key: S::Key, + primary_entry: VacantEntry, + secondary_cache: &mut HashMap, T::CachedId>, + ) -> Result { + let mut descriptor = base_descriptor.clone(); + let canonical_key = specializer.specialize(key.clone(), &mut descriptor)?; + + if let Some(user_specializer) = user_specializer { + (user_specializer)(key, &mut descriptor)?; + } + + // if the whole key is canonical, the secondary cache isn't needed. + if ::IS_CANONICAL { + return Ok(primary_entry + .insert(::queue(pipeline_cache, descriptor)) + .clone()); + } + + let id = match secondary_cache.entry(canonical_key) { + Entry::Occupied(entry) => { + if cfg!(debug_assertions) { + let stored_descriptor = + ::get_descriptor(pipeline_cache, entry.get().clone()); + if &descriptor != stored_descriptor { + error!( + "Invalid Specializer<{}> impl for {}: the cached descriptor \ + is not equal to the generated descriptor for the given key. \ + This means the Specializer implementation uses unused information \ + from the key to specialize the pipeline. This is not allowed \ + because it would invalidate the cache.", + core::any::type_name::(), + core::any::type_name::() + ); + } + } + entry.into_mut().clone() + } + Entry::Vacant(entry) => entry + .insert(::queue(pipeline_cache, descriptor)) + .clone(), + }; + + primary_entry.insert(id.clone()); + Ok(id) + } +} + +/// [`SpecializedCache`] implements [`FromWorld`] for [`Specializer`]s +/// that also satisfy [`FromWorld`] and [`GetBaseDescriptor`]. This will +/// create a [`SpecializedCache`] with no user specializer, and the base +/// descriptor take from the specializer's [`GetBaseDescriptor`] implementation. +impl FromWorld for SpecializedCache +where + T: Specializable, + S: FromWorld + Specializer + GetBaseDescriptor, +{ + fn from_world(world: &mut World) -> Self { + let specializer = S::from_world(world); + let base_descriptor = specializer.get_base_descriptor(); + Self::new(specializer, None, base_descriptor) + } +} diff --git a/crates/bevy_render/src/texture/mod.rs b/crates/bevy_render/src/texture/mod.rs index fe37dd4310..006ac2e5e8 100644 --- a/crates/bevy_render/src/texture/mod.rs +++ b/crates/bevy_render/src/texture/mod.rs @@ -21,7 +21,7 @@ use crate::{ render_asset::RenderAssetPlugin, renderer::RenderDevice, Render, RenderApp, RenderSystems, }; use bevy_app::{App, Plugin}; -use bevy_asset::{weak_handle, AssetApp, Assets, Handle}; +use bevy_asset::{uuid_handle, AssetApp, Assets, Handle}; use bevy_ecs::prelude::*; use tracing::warn; @@ -31,7 +31,7 @@ use tracing::warn; /// While that handle points to an opaque white 1 x 1 image, this handle points to a transparent 1 x 1 white image. // Number randomly selected by fair WolframAlpha query. Totally arbitrary. pub const TRANSPARENT_IMAGE_HANDLE: Handle = - weak_handle!("d18ad97e-a322-4981-9505-44c59a4b5e46"); + uuid_handle!("d18ad97e-a322-4981-9505-44c59a4b5e46"); // 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. diff --git a/crates/bevy_render/src/view/window/screenshot.rs b/crates/bevy_render/src/view/window/screenshot.rs index 9a64709f4e..13dd2670ab 100644 --- a/crates/bevy_render/src/view/window/screenshot.rs +++ b/crates/bevy_render/src/view/window/screenshot.rs @@ -461,29 +461,24 @@ impl SpecializedRenderPipeline for ScreenshotToScreenPipeline { label: Some(Cow::Borrowed("screenshot-to-screen")), layout: vec![self.bind_group_layout.clone()], vertex: VertexState { - buffers: vec![], - shader_defs: vec![], - entry_point: Cow::Borrowed("vs_main"), shader: self.shader.clone(), + ..default() }, primitive: wgpu::PrimitiveState { cull_mode: Some(wgpu::Face::Back), ..Default::default() }, - depth_stencil: None, multisample: Default::default(), fragment: Some(FragmentState { shader: self.shader.clone(), - entry_point: Cow::Borrowed("fs_main"), - shader_defs: vec![], targets: vec![Some(wgpu::ColorTargetState { format: key, blend: None, write_mask: wgpu::ColorWrites::ALL, })], + ..default() }), - push_constant_ranges: Vec::new(), - zero_initialize_workgroup_memory: false, + ..default() } } } diff --git a/crates/bevy_solari/Cargo.toml b/crates/bevy_solari/Cargo.toml index 1929ab42d1..40eaab2aab 100644 --- a/crates/bevy_solari/Cargo.toml +++ b/crates/bevy_solari/Cargo.toml @@ -3,7 +3,7 @@ name = "bevy_solari" version = "0.17.0-dev" edition = "2024" description = "Provides raytraced lighting for Bevy Engine" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -26,6 +26,7 @@ bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-fea bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } # other diff --git a/crates/bevy_solari/src/pathtracer/mod.rs b/crates/bevy_solari/src/pathtracer/mod.rs index 1e2cd95ed8..30cc15ba10 100644 --- a/crates/bevy_solari/src/pathtracer/mod.rs +++ b/crates/bevy_solari/src/pathtracer/mod.rs @@ -9,7 +9,7 @@ use bevy_core_pipeline::core_3d::graph::{Core3d, Node3d}; use bevy_ecs::{component::Component, reflect::ReflectComponent, schedule::IntoScheduleConfigs}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ - render_graph::{RenderGraphApp, ViewNodeRunner}, + render_graph::{RenderGraphExt, ViewNodeRunner}, renderer::RenderDevice, view::Hdr, ExtractSchedule, Render, RenderApp, RenderSystems, diff --git a/crates/bevy_solari/src/pathtracer/node.rs b/crates/bevy_solari/src/pathtracer/node.rs index 30031c1d51..325ea42dac 100644 --- a/crates/bevy_solari/src/pathtracer/node.rs +++ b/crates/bevy_solari/src/pathtracer/node.rs @@ -17,6 +17,7 @@ use bevy_render::{ renderer::{RenderContext, RenderDevice}, view::{ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms}, }; +use bevy_utils::default; pub mod graph { use bevy_render::render_graph::RenderLabel; @@ -119,11 +120,8 @@ impl FromWorld for PathtracerNode { scene_bindings.bind_group_layout.clone(), bind_group_layout.clone(), ], - push_constant_ranges: vec![], shader: load_embedded_asset!(world, "pathtracer.wgsl"), - shader_defs: vec![], - entry_point: "pathtrace".into(), - zero_initialize_workgroup_memory: false, + ..default() }); Self { diff --git a/crates/bevy_solari/src/realtime/mod.rs b/crates/bevy_solari/src/realtime/mod.rs index 9308ab5cf8..a8d6235f30 100644 --- a/crates/bevy_solari/src/realtime/mod.rs +++ b/crates/bevy_solari/src/realtime/mod.rs @@ -13,8 +13,7 @@ use bevy_ecs::{component::Component, reflect::ReflectComponent, schedule::IntoSc use bevy_pbr::DefaultOpaqueRendererMethod; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ - load_shader_library, - render_graph::{RenderGraphApp, ViewNodeRunner}, + render_graph::{RenderGraphExt, ViewNodeRunner}, renderer::RenderDevice, view::Hdr, ExtractSchedule, Render, RenderApp, RenderSystems, @@ -29,7 +28,6 @@ pub struct SolariLightingPlugin; impl Plugin for SolariLightingPlugin { fn build(&self, app: &mut App) { embedded_asset!(app, "restir_di.wgsl"); - load_shader_library!(app, "reservoir.wgsl"); app.register_type::() .insert_resource(DefaultOpaqueRendererMethod::deferred()); diff --git a/crates/bevy_solari/src/realtime/node.rs b/crates/bevy_solari/src/realtime/node.rs index 6060bb3c15..2fcc29b415 100644 --- a/crates/bevy_solari/src/realtime/node.rs +++ b/crates/bevy_solari/src/realtime/node.rs @@ -1,12 +1,15 @@ use super::{prepare::SolariLightingResources, SolariLighting}; use crate::scene::RaytracingSceneBindings; use bevy_asset::load_embedded_asset; -use bevy_core_pipeline::prepass::ViewPrepassTextures; +use bevy_core_pipeline::prepass::{ + PreviousViewData, PreviousViewUniformOffset, PreviousViewUniforms, ViewPrepassTextures, +}; use bevy_diagnostic::FrameCount; use bevy_ecs::{ query::QueryItem, world::{FromWorld, World}, }; +use bevy_image::ToExtents; use bevy_render::{ camera::ExtractedCamera, diagnostic::RecordDiagnostics, @@ -22,6 +25,7 @@ use bevy_render::{ renderer::{RenderContext, RenderDevice}, view::{ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms}, }; +use bevy_utils::default; pub mod graph { use bevy_render::render_graph::RenderLabel; @@ -44,6 +48,7 @@ impl ViewNode for SolariLightingNode { &'static ViewTarget, &'static ViewPrepassTextures, &'static ViewUniformOffset, + &'static PreviousViewUniformOffset, ); fn run( @@ -57,12 +62,14 @@ impl ViewNode for SolariLightingNode { view_target, view_prepass_textures, view_uniform_offset, + previous_view_uniform_offset, ): QueryItem, world: &World, ) -> Result<(), NodeRunError> { let pipeline_cache = world.resource::(); let scene_bindings = world.resource::(); let view_uniforms = world.resource::(); + let previous_view_uniforms = world.resource::(); let frame_count = world.resource::(); let ( Some(initial_and_temporal_pipeline), @@ -73,6 +80,7 @@ impl ViewNode for SolariLightingNode { Some(depth_buffer), Some(motion_vectors), Some(view_uniforms), + Some(previous_view_uniforms), ) = ( pipeline_cache.get_compute_pipeline(self.initial_and_temporal_pipeline), pipeline_cache.get_compute_pipeline(self.spatial_and_shade_pipeline), @@ -82,6 +90,7 @@ impl ViewNode for SolariLightingNode { view_prepass_textures.depth_view(), view_prepass_textures.motion_vectors_view(), view_uniforms.uniforms.binding(), + previous_view_uniforms.uniforms.binding(), ) else { return Ok(()); @@ -97,7 +106,10 @@ impl ViewNode for SolariLightingNode { gbuffer, depth_buffer, motion_vectors, + &solari_lighting_resources.previous_gbuffer.1, + &solari_lighting_resources.previous_depth.1, view_uniforms, + previous_view_uniforms, )), ); @@ -114,7 +126,14 @@ impl ViewNode for SolariLightingNode { let pass_span = diagnostics.pass_span(&mut pass, "solari_lighting"); pass.set_bind_group(0, scene_bindings, &[]); - pass.set_bind_group(1, &bind_group, &[view_uniform_offset.offset]); + pass.set_bind_group( + 1, + &bind_group, + &[ + view_uniform_offset.offset, + previous_view_uniform_offset.offset, + ], + ); pass.set_pipeline(initial_and_temporal_pipeline); pass.set_push_constants( @@ -127,6 +146,31 @@ impl ViewNode for SolariLightingNode { pass.dispatch_workgroups(viewport.x.div_ceil(8), viewport.y.div_ceil(8), 1); pass_span.end(&mut pass); + drop(pass); + + // TODO: Remove these copies, and double buffer instead + command_encoder.copy_texture_to_texture( + view_prepass_textures + .deferred + .clone() + .unwrap() + .texture + .texture + .as_image_copy(), + solari_lighting_resources.previous_gbuffer.0.as_image_copy(), + viewport.to_extents(), + ); + command_encoder.copy_texture_to_texture( + view_prepass_textures + .depth + .clone() + .unwrap() + .texture + .texture + .as_image_copy(), + solari_lighting_resources.previous_depth.0.as_image_copy(), + viewport.to_extents(), + ); Ok(()) } @@ -152,7 +196,10 @@ impl FromWorld for SolariLightingNode { texture_2d(TextureSampleType::Uint), texture_depth_2d(), texture_2d(TextureSampleType::Float { filterable: true }), + texture_2d(TextureSampleType::Uint), + texture_depth_2d(), uniform_buffer::(true), + uniform_buffer::(true), ), ), ); @@ -169,9 +216,8 @@ impl FromWorld for SolariLightingNode { range: 0..8, }], shader: load_embedded_asset!(world, "restir_di.wgsl"), - shader_defs: vec![], - entry_point: "initial_and_temporal".into(), - zero_initialize_workgroup_memory: false, + entry_point: Some("initial_and_temporal".into()), + ..default() }); let spatial_and_shade_pipeline = @@ -186,9 +232,8 @@ impl FromWorld for SolariLightingNode { range: 0..8, }], shader: load_embedded_asset!(world, "restir_di.wgsl"), - shader_defs: vec![], - entry_point: "spatial_and_shade".into(), - zero_initialize_workgroup_memory: false, + entry_point: Some("spatial_and_shade".into()), + ..default() }); Self { diff --git a/crates/bevy_solari/src/realtime/prepare.rs b/crates/bevy_solari/src/realtime/prepare.rs index 4f153bf0dc..992a75c451 100644 --- a/crates/bevy_solari/src/realtime/prepare.rs +++ b/crates/bevy_solari/src/realtime/prepare.rs @@ -1,14 +1,19 @@ use super::SolariLighting; +use bevy_core_pipeline::{core_3d::CORE_3D_DEPTH_FORMAT, deferred::DEFERRED_PREPASS_FORMAT}; use bevy_ecs::{ component::Component, entity::Entity, query::With, system::{Commands, Query, Res}, }; +use bevy_image::ToExtents; use bevy_math::UVec2; use bevy_render::{ camera::ExtractedCamera, - render_resource::{Buffer, BufferDescriptor, BufferUsages}, + render_resource::{ + Buffer, BufferDescriptor, BufferUsages, Texture, TextureDescriptor, TextureDimension, + TextureUsages, TextureView, TextureViewDescriptor, + }, renderer::RenderDevice, }; @@ -20,6 +25,8 @@ const RESERVOIR_STRUCT_SIZE: u64 = 32; pub struct SolariLightingResources { pub reservoirs_a: Buffer, pub reservoirs_b: Buffer, + pub previous_gbuffer: (Texture, TextureView), + pub previous_depth: (Texture, TextureView), pub view_size: UVec2, } @@ -56,9 +63,35 @@ pub fn prepare_solari_lighting_resources( mapped_at_creation: false, }); + let previous_gbuffer = render_device.create_texture(&TextureDescriptor { + label: Some("solari_lighting_previous_gbuffer"), + size: view_size.to_extents(), + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: DEFERRED_PREPASS_FORMAT, + usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, + view_formats: &[], + }); + let previous_gbuffer_view = previous_gbuffer.create_view(&TextureViewDescriptor::default()); + + let previous_depth = render_device.create_texture(&TextureDescriptor { + label: Some("solari_lighting_previous_depth"), + size: view_size.to_extents(), + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: CORE_3D_DEPTH_FORMAT, + usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, + view_formats: &[], + }); + let previous_depth_view = previous_depth.create_view(&TextureViewDescriptor::default()); + commands.entity(entity).insert(SolariLightingResources { reservoirs_a, reservoirs_b, + previous_gbuffer: (previous_gbuffer, previous_gbuffer_view), + previous_depth: (previous_depth, previous_depth_view), view_size, }); } diff --git a/crates/bevy_solari/src/realtime/reservoir.wgsl b/crates/bevy_solari/src/realtime/reservoir.wgsl deleted file mode 100644 index 08a7e26f7c..0000000000 --- a/crates/bevy_solari/src/realtime/reservoir.wgsl +++ /dev/null @@ -1,30 +0,0 @@ -// https://intro-to-restir.cwyman.org/presentations/2023ReSTIR_Course_Notes.pdf - -#define_import_path bevy_solari::reservoir - -#import bevy_solari::sampling::LightSample - -const NULL_RESERVOIR_SAMPLE = 0xFFFFFFFFu; - -// Don't adjust the size of this struct without also adjusting RESERVOIR_STRUCT_SIZE. -struct Reservoir { - sample: LightSample, - weight_sum: f32, - confidence_weight: f32, - unbiased_contribution_weight: f32, - _padding: f32, -} - -fn empty_reservoir() -> Reservoir { - return Reservoir( - LightSample(vec2(NULL_RESERVOIR_SAMPLE, 0u), vec2(0.0)), - 0.0, - 0.0, - 0.0, - 0.0 - ); -} - -fn reservoir_valid(reservoir: Reservoir) -> bool { - return reservoir.sample.light_id.x != NULL_RESERVOIR_SAMPLE; -} diff --git a/crates/bevy_solari/src/realtime/restir_di.wgsl b/crates/bevy_solari/src/realtime/restir_di.wgsl index 511fd63d12..70de4564cc 100644 --- a/crates/bevy_solari/src/realtime/restir_di.wgsl +++ b/crates/bevy_solari/src/realtime/restir_di.wgsl @@ -1,11 +1,14 @@ +// https://intro-to-restir.cwyman.org/presentations/2023ReSTIR_Course_Notes.pdf + #import bevy_core_pipeline::tonemapping::tonemapping_luminance as luminance #import bevy_pbr::pbr_deferred_types::unpack_24bit_normal +#import bevy_pbr::prepass_bindings::PreviousViewUniforms #import bevy_pbr::rgb9e5::rgb9e5_to_vec3_ #import bevy_pbr::utils::{rand_f, octahedral_decode} #import bevy_render::maths::PI #import bevy_render::view::View -#import bevy_solari::reservoir::{Reservoir, empty_reservoir, reservoir_valid} -#import bevy_solari::sampling::{generate_random_light_sample, calculate_light_contribution, trace_light_visibility} +#import bevy_solari::sampling::{LightSample, generate_random_light_sample, calculate_light_contribution, trace_light_visibility, sample_disk} +#import bevy_solari::scene_bindings::{previous_frame_light_id_translations, LIGHT_NOT_PRESENT_THIS_FRAME} @group(1) @binding(0) var view_output: texture_storage_2d; @group(1) @binding(1) var reservoirs_a: array; @@ -13,13 +16,18 @@ @group(1) @binding(3) var gbuffer: texture_2d; @group(1) @binding(4) var depth_buffer: texture_depth_2d; @group(1) @binding(5) var motion_vectors: texture_2d; -@group(1) @binding(6) var view: View; +@group(1) @binding(6) var previous_gbuffer: texture_2d; +@group(1) @binding(7) var previous_depth_buffer: texture_depth_2d; +@group(1) @binding(8) var view: View; +@group(1) @binding(9) var previous_view: PreviousViewUniforms; struct PushConstants { frame_index: u32, reset: u32 } var constants: PushConstants; const INITIAL_SAMPLES = 32u; const SPATIAL_REUSE_RADIUS_PIXELS = 30.0; -const CONFIDENCE_WEIGHT_CAP = 20.0 * f32(INITIAL_SAMPLES); +const CONFIDENCE_WEIGHT_CAP = 20.0; + +const NULL_RESERVOIR_SAMPLE = 0xFFFFFFFFu; @compute @workgroup_size(8, 8, 1) fn initial_and_temporal(@builtin(global_invocation_id) global_id: vec3) { @@ -40,8 +48,10 @@ fn initial_and_temporal(@builtin(global_invocation_id) global_id: vec3) { let diffuse_brdf = base_color / PI; let initial_reservoir = generate_initial_reservoir(world_position, world_normal, diffuse_brdf, &rng); + let temporal_reservoir = load_temporal_reservoir(global_id.xy, depth, world_position, world_normal); + let combined_reservoir = merge_reservoirs(initial_reservoir, temporal_reservoir, world_position, world_normal, diffuse_brdf, &rng); - reservoirs_b[pixel_index] = initial_reservoir; + reservoirs_b[pixel_index] = combined_reservoir.merged_reservoir; } @compute @workgroup_size(8, 8, 1) @@ -65,15 +75,13 @@ fn spatial_and_shade(@builtin(global_invocation_id) global_id: vec3) { let emissive = rgb9e5_to_vec3_(gpixel.g); let input_reservoir = reservoirs_b[pixel_index]; + let spatial_reservoir = load_spatial_reservoir(global_id.xy, depth, world_position, world_normal, &rng); + let merge_result = merge_reservoirs(input_reservoir, spatial_reservoir, world_position, world_normal, diffuse_brdf, &rng); + let combined_reservoir = merge_result.merged_reservoir; - var radiance = vec3(0.0); - if reservoir_valid(input_reservoir) { - radiance = calculate_light_contribution(input_reservoir.sample, world_position, world_normal).radiance; - } + reservoirs_a[pixel_index] = combined_reservoir; - reservoirs_a[pixel_index] = input_reservoir; - - var pixel_color = radiance * input_reservoir.unbiased_contribution_weight; + var pixel_color = merge_result.selected_sample_radiance * combined_reservoir.unbiased_contribution_weight * combined_reservoir.visibility; pixel_color *= view.exposure; pixel_color *= diffuse_brdf; pixel_color += emissive; @@ -102,16 +110,180 @@ fn generate_initial_reservoir(world_position: vec3, world_normal: vec3 if reservoir_valid(reservoir) { let inverse_target_function = select(0.0, 1.0 / reservoir_target_function, reservoir_target_function > 0.0); reservoir.unbiased_contribution_weight = reservoir.weight_sum * inverse_target_function; - reservoir.unbiased_contribution_weight *= trace_light_visibility(reservoir.sample, world_position); + + reservoir.visibility = trace_light_visibility(reservoir.sample, world_position); + reservoir.unbiased_contribution_weight *= reservoir.visibility; } - reservoir.confidence_weight = f32(INITIAL_SAMPLES); + reservoir.confidence_weight = 1.0; return reservoir; } +fn load_temporal_reservoir(pixel_id: vec2, depth: f32, world_position: vec3, world_normal: vec3) -> Reservoir { + let motion_vector = textureLoad(motion_vectors, pixel_id, 0).xy; + let temporal_pixel_id_float = round(vec2(pixel_id) - (motion_vector * view.viewport.zw)); + let temporal_pixel_id = vec2(temporal_pixel_id_float); + if any(temporal_pixel_id_float < vec2(0.0)) || any(temporal_pixel_id_float >= view.viewport.zw) || bool(constants.reset) { + return empty_reservoir(); + } + + let temporal_depth = textureLoad(previous_depth_buffer, temporal_pixel_id, 0); + let temporal_gpixel = textureLoad(previous_gbuffer, temporal_pixel_id, 0); + let temporal_world_position = reconstruct_previous_world_position(temporal_pixel_id, temporal_depth); + let temporal_world_normal = octahedral_decode(unpack_24bit_normal(temporal_gpixel.a)); + if pixel_dissimilar(depth, world_position, temporal_world_position, world_normal, temporal_world_normal) { + return empty_reservoir(); + } + + let temporal_pixel_index = temporal_pixel_id.x + temporal_pixel_id.y * u32(view.viewport.z); + var temporal_reservoir = reservoirs_a[temporal_pixel_index]; + + temporal_reservoir.sample.light_id.x = previous_frame_light_id_translations[temporal_reservoir.sample.light_id.x]; + if temporal_reservoir.sample.light_id.x == LIGHT_NOT_PRESENT_THIS_FRAME { + return empty_reservoir(); + } + + temporal_reservoir.confidence_weight = min(temporal_reservoir.confidence_weight, CONFIDENCE_WEIGHT_CAP); + + return temporal_reservoir; +} + +fn load_spatial_reservoir(pixel_id: vec2, depth: f32, world_position: vec3, world_normal: vec3, rng: ptr) -> Reservoir { + let spatial_pixel_id = get_neighbor_pixel_id(pixel_id, rng); + + let spatial_depth = textureLoad(depth_buffer, spatial_pixel_id, 0); + let spatial_gpixel = textureLoad(gbuffer, spatial_pixel_id, 0); + let spatial_world_position = reconstruct_world_position(spatial_pixel_id, spatial_depth); + let spatial_world_normal = octahedral_decode(unpack_24bit_normal(spatial_gpixel.a)); + if pixel_dissimilar(depth, world_position, spatial_world_position, world_normal, spatial_world_normal) { + return empty_reservoir(); + } + + let spatial_pixel_index = spatial_pixel_id.x + spatial_pixel_id.y * u32(view.viewport.z); + var spatial_reservoir = reservoirs_b[spatial_pixel_index]; + + if reservoir_valid(spatial_reservoir) { + spatial_reservoir.visibility = trace_light_visibility(spatial_reservoir.sample, world_position); + } + + return spatial_reservoir; +} + +fn get_neighbor_pixel_id(center_pixel_id: vec2, rng: ptr) -> vec2 { + var spatial_id = vec2(center_pixel_id) + vec2(sample_disk(SPATIAL_REUSE_RADIUS_PIXELS, rng)); + spatial_id = clamp(spatial_id, vec2(0i), vec2(view.viewport.zw) - 1i); + return vec2(spatial_id); +} + fn reconstruct_world_position(pixel_id: vec2, depth: f32) -> vec3 { let uv = (vec2(pixel_id) + 0.5) / view.viewport.zw; let xy_ndc = (uv - vec2(0.5)) * vec2(2.0, -2.0); let world_pos = view.world_from_clip * vec4(xy_ndc, depth, 1.0); return world_pos.xyz / world_pos.w; } + +fn reconstruct_previous_world_position(pixel_id: vec2, depth: f32) -> vec3 { + let uv = (vec2(pixel_id) + 0.5) / view.viewport.zw; + let xy_ndc = (uv - vec2(0.5)) * vec2(2.0, -2.0); + let world_pos = previous_view.world_from_clip * vec4(xy_ndc, depth, 1.0); + return world_pos.xyz / world_pos.w; +} + +// Reject if tangent plane difference difference more than 0.3% or angle between normals more than 25 degrees +fn pixel_dissimilar(depth: f32, world_position: vec3, other_world_position: vec3, normal: vec3, other_normal: vec3) -> bool { + // https://developer.download.nvidia.com/video/gputechconf/gtc/2020/presentations/s22699-fast-denoising-with-self-stabilizing-recurrent-blurs.pdf#page=45 + let tangent_plane_distance = abs(dot(normal, other_world_position - world_position)); + let view_z = -depth_ndc_to_view_z(depth); + + return tangent_plane_distance / view_z > 0.003 || dot(normal, other_normal) < 0.906; +} + +fn depth_ndc_to_view_z(ndc_depth: f32) -> f32 { +#ifdef VIEW_PROJECTION_PERSPECTIVE + return -view.clip_from_view[3][2]() / ndc_depth; +#else ifdef VIEW_PROJECTION_ORTHOGRAPHIC + return -(view.clip_from_view[3][2] - ndc_depth) / view.clip_from_view[2][2]; +#else + let view_pos = view.view_from_clip * vec4(0.0, 0.0, ndc_depth, 1.0); + return view_pos.z / view_pos.w; +#endif +} + +// Don't adjust the size of this struct without also adjusting RESERVOIR_STRUCT_SIZE. +struct Reservoir { + sample: LightSample, + weight_sum: f32, + confidence_weight: f32, + unbiased_contribution_weight: f32, + visibility: f32, +} + +fn empty_reservoir() -> Reservoir { + return Reservoir( + LightSample(vec2(NULL_RESERVOIR_SAMPLE, 0u), vec2(0.0)), + 0.0, + 0.0, + 0.0, + 0.0 + ); +} + +fn reservoir_valid(reservoir: Reservoir) -> bool { + return reservoir.sample.light_id.x != NULL_RESERVOIR_SAMPLE; +} + +struct ReservoirMergeResult { + merged_reservoir: Reservoir, + selected_sample_radiance: vec3, +} + +fn merge_reservoirs( + canonical_reservoir: Reservoir, + other_reservoir: Reservoir, + world_position: vec3, + world_normal: vec3, + diffuse_brdf: vec3, + rng: ptr, +) -> ReservoirMergeResult { + // TODO: Balance heuristic MIS weights + let mis_weight_denominator = 1.0 / (canonical_reservoir.confidence_weight + other_reservoir.confidence_weight); + + let canonical_mis_weight = canonical_reservoir.confidence_weight * mis_weight_denominator; + let canonical_target_function = reservoir_target_function(canonical_reservoir, world_position, world_normal, diffuse_brdf); + let canonical_resampling_weight = canonical_mis_weight * (canonical_target_function.a * canonical_reservoir.unbiased_contribution_weight); + + let other_mis_weight = other_reservoir.confidence_weight * mis_weight_denominator; + let other_target_function = reservoir_target_function(other_reservoir, world_position, world_normal, diffuse_brdf); + let other_resampling_weight = other_mis_weight * (other_target_function.a * other_reservoir.unbiased_contribution_weight); + + var combined_reservoir = empty_reservoir(); + combined_reservoir.weight_sum = canonical_resampling_weight + other_resampling_weight; + combined_reservoir.confidence_weight = canonical_reservoir.confidence_weight + other_reservoir.confidence_weight; + + // https://yusuketokuyoshi.com/papers/2024/Efficient_Visibility_Reuse_for_Real-time_ReSTIR_(Supplementary_Document).pdf + combined_reservoir.visibility = max(0.0, (canonical_reservoir.visibility * canonical_resampling_weight + + other_reservoir.visibility * other_resampling_weight) / combined_reservoir.weight_sum); + + if rand_f(rng) < other_resampling_weight / combined_reservoir.weight_sum { + combined_reservoir.sample = other_reservoir.sample; + + let inverse_target_function = select(0.0, 1.0 / other_target_function.a, other_target_function.a > 0.0); + combined_reservoir.unbiased_contribution_weight = combined_reservoir.weight_sum * inverse_target_function; + + return ReservoirMergeResult(combined_reservoir, other_target_function.rgb); + } else { + combined_reservoir.sample = canonical_reservoir.sample; + + let inverse_target_function = select(0.0, 1.0 / canonical_target_function.a, canonical_target_function.a > 0.0); + combined_reservoir.unbiased_contribution_weight = combined_reservoir.weight_sum * inverse_target_function; + + return ReservoirMergeResult(combined_reservoir, canonical_target_function.rgb); + } +} + +fn reservoir_target_function(reservoir: Reservoir, world_position: vec3, world_normal: vec3, diffuse_brdf: vec3) -> vec4 { + if !reservoir_valid(reservoir) { return vec4(0.0); } + let light_contribution = calculate_light_contribution(reservoir.sample, world_position, world_normal).radiance; + let target_function = luminance(light_contribution * diffuse_brdf); + return vec4(light_contribution, target_function); +} diff --git a/crates/bevy_solari/src/scene/binder.rs b/crates/bevy_solari/src/scene/binder.rs index 889efb538e..f14b5dbe23 100644 --- a/crates/bevy_solari/src/scene/binder.rs +++ b/crates/bevy_solari/src/scene/binder.rs @@ -2,6 +2,7 @@ use super::{blas::BlasManager, extract::StandardMaterialAssets, RaytracingMesh3d use bevy_asset::{AssetId, Handle}; use bevy_color::{ColorToComponents, LinearRgba}; use bevy_ecs::{ + entity::{Entity, EntityHashMap}, resource::Resource, system::{Query, Res, ResMut}, world::{FromWorld, World}, @@ -26,19 +27,24 @@ const MAX_TEXTURE_COUNT: NonZeroU32 = NonZeroU32::new(5_000).unwrap(); /// const SUN_ANGULAR_DIAMETER_RADIANS: f32 = 0.00930842; +const TEXTURE_MAP_NONE: u32 = u32::MAX; +const LIGHT_NOT_PRESENT_THIS_FRAME: u32 = u32::MAX; + #[derive(Resource)] pub struct RaytracingSceneBindings { pub bind_group: Option, pub bind_group_layout: BindGroupLayout, + previous_frame_light_entities: Vec, } pub fn prepare_raytracing_scene_bindings( instances_query: Query<( + Entity, &RaytracingMesh3d, &MeshMaterial3d, &GlobalTransform, )>, - directional_lights_query: Query<&ExtractedDirectionalLight>, + directional_lights_query: Query<(Entity, &ExtractedDirectionalLight)>, mesh_allocator: Res, blas_manager: Res, material_assets: Res, @@ -50,6 +56,12 @@ pub fn prepare_raytracing_scene_bindings( ) { raytracing_scene_bindings.bind_group = None; + let mut this_frame_entity_to_light_id = EntityHashMap::::default(); + let previous_frame_light_entities: Vec<_> = raytracing_scene_bindings + .previous_frame_light_entities + .drain(..) + .collect(); + if instances_query.iter().len() == 0 { return; } @@ -72,6 +84,7 @@ pub fn prepare_raytracing_scene_bindings( let mut material_ids = StorageBufferList::::default(); let mut light_sources = StorageBufferList::::default(); let mut directional_lights = StorageBufferList::::default(); + let mut previous_frame_light_id_translations = StorageBufferList::::default(); let mut material_id_map: HashMap, u32, FixedHasher> = HashMap::default(); @@ -89,7 +102,7 @@ pub fn prepare_raytracing_scene_bindings( } None => None, }, - None => Some(u32::MAX), + None => Some(TEXTURE_MAP_NONE), } }; for (asset_id, material) in material_assets.iter() { @@ -126,7 +139,7 @@ pub fn prepare_raytracing_scene_bindings( } let mut instance_id = 0; - for (mesh, material, transform) in &instances_query { + for (entity, mesh, material, transform) in &instances_query { let Some(blas) = blas_manager.get(&mesh.id()) else { continue; }; @@ -178,6 +191,11 @@ pub fn prepare_raytracing_scene_bindings( instance_id as u32, (index_slice.range.len() / 3) as u32, )); + + this_frame_entity_to_light_id.insert(entity, light_sources.get().len() as u32 - 1); + raytracing_scene_bindings + .previous_frame_light_entities + .push(entity); } instance_id += 1; @@ -187,7 +205,7 @@ pub fn prepare_raytracing_scene_bindings( return; } - for directional_light in &directional_lights_query { + for (entity, directional_light) in &directional_lights_query { let directional_lights = directional_lights.get_mut(); let directional_light_id = directional_lights.len() as u32; @@ -196,6 +214,21 @@ pub fn prepare_raytracing_scene_bindings( light_sources .get_mut() .push(GpuLightSource::new_directional_light(directional_light_id)); + + this_frame_entity_to_light_id.insert(entity, light_sources.get().len() as u32 - 1); + raytracing_scene_bindings + .previous_frame_light_entities + .push(entity); + } + + for previous_frame_light_entity in previous_frame_light_entities { + let current_frame_index = this_frame_entity_to_light_id + .get(&previous_frame_light_entity) + .copied() + .unwrap_or(LIGHT_NOT_PRESENT_THIS_FRAME); + previous_frame_light_id_translations + .get_mut() + .push(current_frame_index); } materials.write_buffer(&render_device, &render_queue); @@ -204,6 +237,7 @@ pub fn prepare_raytracing_scene_bindings( material_ids.write_buffer(&render_device, &render_queue); light_sources.write_buffer(&render_device, &render_queue); directional_lights.write_buffer(&render_device, &render_queue); + previous_frame_light_id_translations.write_buffer(&render_device, &render_queue); let mut command_encoder = render_device.create_command_encoder(&CommandEncoderDescriptor { label: Some("build_tlas_command_encoder"), @@ -226,6 +260,7 @@ pub fn prepare_raytracing_scene_bindings( material_ids.binding().unwrap(), light_sources.binding().unwrap(), directional_lights.binding().unwrap(), + previous_frame_light_id_translations.binding().unwrap(), )), )); } @@ -253,9 +288,11 @@ impl FromWorld for RaytracingSceneBindings { storage_buffer_read_only_sized(false, None), storage_buffer_read_only_sized(false, None), storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), ), ), ), + previous_frame_light_entities: Vec::new(), } } } diff --git a/crates/bevy_solari/src/scene/raytracing_scene_bindings.wgsl b/crates/bevy_solari/src/scene/raytracing_scene_bindings.wgsl index 974ee50d7d..eeed96ad8e 100644 --- a/crates/bevy_solari/src/scene/raytracing_scene_bindings.wgsl +++ b/crates/bevy_solari/src/scene/raytracing_scene_bindings.wgsl @@ -59,6 +59,8 @@ struct DirectionalLight { inverse_pdf: f32, } +const LIGHT_NOT_PRESENT_THIS_FRAME = 0xFFFFFFFFu; + @group(0) @binding(0) var vertex_buffers: binding_array; @group(0) @binding(1) var index_buffers: binding_array; @group(0) @binding(2) var textures: binding_array>; @@ -70,6 +72,7 @@ struct DirectionalLight { @group(0) @binding(8) var material_ids: array; // TODO: Store material_id in instance_custom_index instead? @group(0) @binding(9) var light_sources: array; @group(0) @binding(10) var directional_lights: array; +@group(0) @binding(11) var previous_frame_light_id_translations: array; const RAY_T_MIN = 0.01f; const RAY_T_MAX = 100000.0f; diff --git a/crates/bevy_solari/src/scene/sampling.wgsl b/crates/bevy_solari/src/scene/sampling.wgsl index 06142192b6..be709f0bc8 100644 --- a/crates/bevy_solari/src/scene/sampling.wgsl +++ b/crates/bevy_solari/src/scene/sampling.wgsl @@ -1,7 +1,7 @@ #define_import_path bevy_solari::sampling #import bevy_pbr::utils::{rand_f, rand_vec2f, rand_range_u} -#import bevy_render::maths::PI_2 +#import bevy_render::maths::{PI, PI_2} #import bevy_solari::scene_bindings::{trace_ray, RAY_T_MIN, RAY_T_MAX, light_sources, directional_lights, LIGHT_SOURCE_KIND_DIRECTIONAL, resolve_triangle_data_full} // https://www.realtimerendering.com/raytracinggems/unofficial_RayTracingGems_v1.9.pdf#0004286901.INDD%3ASec28%3A303 @@ -15,6 +15,28 @@ fn sample_cosine_hemisphere(normal: vec3, rng: ptr) -> vec3< return vec3(x, y, z); } +// https://www.realtimerendering.com/raytracinggems/unofficial_RayTracingGems_v1.9.pdf#0004286901.INDD%3ASec19%3A294 +fn sample_disk(disk_radius: f32, rng: ptr) -> vec2 { + let ab = 2.0 * rand_vec2f(rng) - 1.0; + let a = ab.x; + var b = ab.y; + if (b == 0.0) { b = 1.0; } + + var phi: f32; + var r: f32; + if (a * a > b * b) { + r = disk_radius * a; + phi = (PI / 4.0) * (b / a); + } else { + r = disk_radius * b; + phi = (PI / 2.0) - (PI / 4.0) * (a / b); + } + + let x = r * cos(phi); + let y = r * sin(phi); + return vec2(x, y); +} + fn sample_random_light(ray_origin: vec3, origin_world_normal: vec3, rng: ptr) -> vec3 { let light_sample = generate_random_light_sample(rng); let light_contribution = calculate_light_contribution(light_sample, ray_origin, origin_world_normal); diff --git a/crates/bevy_sprite/src/mesh2d/material.rs b/crates/bevy_sprite/src/mesh2d/material.rs index fa784bd9af..06914690ca 100644 --- a/crates/bevy_sprite/src/mesh2d/material.rs +++ b/crates/bevy_sprite/src/mesh2d/material.rs @@ -410,7 +410,7 @@ where fn clone(&self) -> Self { Self { mesh_key: self.mesh_key, - bind_group_data: self.bind_group_data.clone(), + bind_group_data: self.bind_group_data, } } } @@ -753,7 +753,7 @@ pub fn specialize_material2d_meshes( &material2d_pipeline, Material2dKey { mesh_key, - bind_group_data: material_2d.key.clone(), + bind_group_data: material_2d.key, }, &mesh.layout, ); @@ -969,6 +969,7 @@ impl RenderAsset for PreparedMaterial2d { ): &mut SystemParamItem, _: Option<&Self>, ) -> Result> { + let bind_group_data = material.bind_group_data(); match material.as_bind_group(&pipeline.material2d_layout, render_device, material_param) { Ok(prepared) => { let mut mesh_pipeline_key_bits = Mesh2dPipelineKey::empty(); @@ -987,7 +988,7 @@ impl RenderAsset for PreparedMaterial2d { Ok(PreparedMaterial2d { bindings: prepared.bindings, bind_group: prepared.bind_group, - key: prepared.data, + key: bind_group_data, properties: Material2dProperties { depth_bias: material.depth_bias(), alpha_mode: material.alpha_mode(), diff --git a/crates/bevy_sprite/src/mesh2d/mesh.rs b/crates/bevy_sprite/src/mesh2d/mesh.rs index 26125c698c..08620c5a4d 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh.rs +++ b/crates/bevy_sprite/src/mesh2d/mesh.rs @@ -52,6 +52,7 @@ use bevy_render::{ Extract, ExtractSchedule, Render, RenderApp, RenderSystems, }; use bevy_transform::components::GlobalTransform; +use bevy_utils::default; use nonmax::NonMaxU32; use tracing::error; @@ -646,22 +647,21 @@ impl SpecializedMeshPipeline for Mesh2dPipeline { Ok(RenderPipelineDescriptor { vertex: VertexState { shader: self.shader.clone(), - entry_point: "vertex".into(), shader_defs: shader_defs.clone(), buffers: vec![vertex_buffer_layout], + ..default() }, fragment: Some(FragmentState { shader: self.shader.clone(), shader_defs, - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format, blend, write_mask: ColorWrites::ALL, })], + ..default() }), layout: vec![self.view_layout.clone(), self.mesh_layout.clone()], - push_constant_ranges: vec![], primitive: PrimitiveState { front_face: FrontFace::Ccw, cull_mode: None, @@ -693,7 +693,7 @@ impl SpecializedMeshPipeline for Mesh2dPipeline { alpha_to_coverage_enabled: false, }, label: Some(label.into()), - zero_initialize_workgroup_memory: false, + ..default() }) } } diff --git a/crates/bevy_sprite/src/mesh2d/wireframe2d.rs b/crates/bevy_sprite/src/mesh2d/wireframe2d.rs index e30c5b1f6c..f71d8c63f7 100644 --- a/crates/bevy_sprite/src/mesh2d/wireframe2d.rs +++ b/crates/bevy_sprite/src/mesh2d/wireframe2d.rs @@ -36,7 +36,7 @@ use bevy_render::{ render_asset::{ prepare_assets, PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets, }, - render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner}, + render_graph::{NodeRunError, RenderGraphContext, RenderGraphExt, ViewNode, ViewNodeRunner}, render_phase::{ AddRenderCommand, BinnedPhaseItem, BinnedRenderPhasePlugin, BinnedRenderPhaseType, CachedRenderPipelinePhaseItem, DrawFunctionId, DrawFunctions, InputUniformIndex, PhaseItem, diff --git a/crates/bevy_sprite/src/picking_backend.rs b/crates/bevy_sprite/src/picking_backend.rs index bde1a34b63..cda9955b95 100644 --- a/crates/bevy_sprite/src/picking_backend.rs +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -145,13 +145,15 @@ fn sprite_picking( continue; }; - let viewport_pos = camera - .logical_viewport_rect() - .map(|v| v.min) - .unwrap_or_default(); - let pos_in_viewport = location.position - viewport_pos; + let viewport_pos = location.position; + if let Some(viewport) = camera.logical_viewport_rect() { + if !viewport.contains(viewport_pos) { + // The pointer is outside the viewport, skip it + continue; + } + } - let Ok(cursor_ray_world) = camera.viewport_to_world(cam_transform, pos_in_viewport) else { + let Ok(cursor_ray_world) = camera.viewport_to_world(cam_transform, viewport_pos) else { continue; }; let cursor_ray_len = cam_ortho.far - cam_ortho.near; diff --git a/crates/bevy_sprite/src/render/mod.rs b/crates/bevy_sprite/src/render/mod.rs index 6c3554e696..cabab135c2 100644 --- a/crates/bevy_sprite/src/render/mod.rs +++ b/crates/bevy_sprite/src/render/mod.rs @@ -40,6 +40,7 @@ use bevy_render::{ Extract, }; use bevy_transform::components::GlobalTransform; +use bevy_utils::default; use bytemuck::{Pod, Zeroable}; use fixedbitset::FixedBitSet; @@ -264,30 +265,21 @@ impl SpecializedRenderPipeline for SpritePipeline { RenderPipelineDescriptor { vertex: VertexState { shader: self.shader.clone(), - entry_point: "vertex".into(), shader_defs: shader_defs.clone(), buffers: vec![instance_rate_vertex_buffer_layout], + ..default() }, fragment: Some(FragmentState { shader: self.shader.clone(), shader_defs, - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format, blend: Some(BlendState::ALPHA_BLENDING), write_mask: ColorWrites::ALL, })], + ..default() }), layout: vec![self.view_layout.clone(), self.material_layout.clone()], - primitive: PrimitiveState { - front_face: FrontFace::Ccw, - cull_mode: None, - unclipped_depth: false, - polygon_mode: PolygonMode::Fill, - conservative: false, - topology: PrimitiveTopology::TriangleList, - strip_index_format: None, - }, // Sprites are always alpha blended so they never need to write to depth. // They just need to read it in case an opaque mesh2d // that wrote to depth is present. @@ -313,8 +305,7 @@ impl SpecializedRenderPipeline for SpritePipeline { alpha_to_coverage_enabled: false, }, label: Some("sprite_pipeline".into()), - push_constant_ranges: Vec::new(), - zero_initialize_workgroup_memory: false, + ..default() } } } diff --git a/crates/bevy_text/src/font_atlas_set.rs b/crates/bevy_text/src/font_atlas_set.rs index 8d32127c38..8f0dd91168 100644 --- a/crates/bevy_text/src/font_atlas_set.rs +++ b/crates/bevy_text/src/font_atlas_set.rs @@ -172,8 +172,8 @@ impl FontAtlasSet { .get_glyph_index(cache_key) .map(|location| GlyphAtlasInfo { location, - texture_atlas: atlas.texture_atlas.clone_weak(), - texture: atlas.texture.clone_weak(), + texture_atlas: atlas.texture_atlas.id(), + texture: atlas.texture.id(), }) }) }) diff --git a/crates/bevy_text/src/glyph.rs b/crates/bevy_text/src/glyph.rs index ebae713af4..5bc2111dbd 100644 --- a/crates/bevy_text/src/glyph.rs +++ b/crates/bevy_text/src/glyph.rs @@ -1,6 +1,6 @@ //! This module exports types related to rendering glyphs. -use bevy_asset::Handle; +use bevy_asset::AssetId; use bevy_image::prelude::*; use bevy_math::{IVec2, Vec2}; use bevy_reflect::Reflect; @@ -38,14 +38,15 @@ pub struct PositionedGlyph { #[derive(Debug, Clone, Reflect)] #[reflect(Clone)] pub struct GlyphAtlasInfo { - /// A handle to the [`Image`] data for the texture atlas this glyph was placed in. + /// An asset ID to the [`Image`] data for the texture atlas this glyph was placed in. /// - /// A (weak) clone of the handle held by the [`FontAtlas`](crate::FontAtlas). - pub texture: Handle, - /// A handle to the [`TextureAtlasLayout`] map for the texture atlas this glyph was placed in. + /// An asset ID of the handle held by the [`FontAtlas`](crate::FontAtlas). + pub texture: AssetId, + /// An asset ID to the [`TextureAtlasLayout`] map for the texture atlas this glyph was placed + /// in. /// - /// A (weak) clone of the handle held by the [`FontAtlas`](crate::FontAtlas). - pub texture_atlas: Handle, + /// An asset ID of the handle held by the [`FontAtlas`](crate::FontAtlas). + pub texture_atlas: AssetId, /// Location and offset of a glyph within the texture atlas. pub location: GlyphAtlasLocation, } diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index dd6ca77246..8c1136c063 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -340,7 +340,7 @@ impl TextPipeline { ) })?; - let texture_atlas = texture_atlases.get(&atlas_info.texture_atlas).unwrap(); + let texture_atlas = texture_atlases.get(atlas_info.texture_atlas).unwrap(); let location = atlas_info.location; let glyph_rect = texture_atlas.textures[location.glyph_index]; let left = location.offset.x as f32; diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index ccfdb2a372..330f0d977a 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -276,7 +276,7 @@ impl From for cosmic_text::Align { /// `TextFont` determines the style of a text span within a [`ComputedTextBlock`], specifically /// the font face, the font size, and the color. -#[derive(Component, Clone, Debug, Reflect)] +#[derive(Component, Clone, Debug, Reflect, PartialEq)] #[reflect(Component, Default, Debug, Clone)] pub struct TextFont { /// The specific font face to use, as a `Handle` to a [`Font`] asset. diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index 8d4a926e1b..7fa3202d18 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -213,7 +213,7 @@ pub fn extract_text2d_sprite( current_span = *span_index; } let rect = texture_atlases - .get(&atlas_info.texture_atlas) + .get(atlas_info.texture_atlas) .unwrap() .textures[atlas_info.location.glyph_index] .as_rect(); @@ -232,7 +232,7 @@ pub fn extract_text2d_sprite( render_entity, transform, color, - image_handle_id: atlas_info.texture.id(), + image_handle_id: atlas_info.texture, flip_x: false, flip_y: false, kind: bevy_sprite::ExtractedSpriteKind::Slices { diff --git a/crates/bevy_ui/src/focus.rs b/crates/bevy_ui/src/focus.rs index f55cbb92b8..549d26a262 100644 --- a/crates/bevy_ui/src/focus.rs +++ b/crates/bevy_ui/src/focus.rs @@ -1,7 +1,4 @@ -use crate::{ - picking_backend::clip_check_recursive, ui_transform::UiGlobalTransform, ComputedNode, - ComputedNodeTarget, Node, UiStack, -}; +use crate::{ui_transform::UiGlobalTransform, ComputedNode, ComputedNodeTarget, Node, UiStack}; use bevy_ecs::{ change_detection::DetectChangesMut, entity::{ContainsEntity, Entity}, @@ -322,3 +319,27 @@ pub fn ui_focus_system( } } } + +/// Walk up the tree child-to-parent checking that `point` is not clipped by any ancestor node. +pub fn clip_check_recursive( + point: Vec2, + entity: Entity, + clipping_query: &Query<'_, '_, (&ComputedNode, &UiGlobalTransform, &Node)>, + child_of_query: &Query<&ChildOf>, +) -> bool { + if let Ok(child_of) = child_of_query.get(entity) { + let parent = child_of.0; + if let Ok((computed_node, transform, node)) = clipping_query.get(parent) { + if !computed_node + .resolve_clip_rect(node.overflow, node.overflow_clip_margin) + .contains(transform.inverse().transform_point2(point)) + { + // The point is clipped and should be ignored by picking + return false; + } + } + return clip_check_recursive(point, parent, clipping_query, child_of_query); + } + // Reached root, point unclipped by all ancestors + true +} diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index 484acbd4af..4d5bec8f07 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -311,12 +311,12 @@ with UI components as a child of an entity without UI components, your UI layout .map(|scroll_pos| { Vec2::new( if style.overflow.x == OverflowAxis::Scroll { - scroll_pos.offset_x + scroll_pos.x } else { 0.0 }, if style.overflow.y == OverflowAxis::Scroll { - scroll_pos.offset_y + scroll_pos.y } else { 0.0 }, @@ -333,7 +333,7 @@ with UI components as a child of an entity without UI components, your UI layout if clamped_scroll_position != scroll_position { commands .entity(entity) - .insert(ScrollPosition::from(clamped_scroll_position)); + .insert(ScrollPosition(clamped_scroll_position)); } let physical_scroll_position = diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 47d396b201..03c7cc5239 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -60,7 +60,7 @@ pub mod prelude { #[cfg(feature = "bevy_ui_debug")] pub use crate::render::UiDebugOptions; #[doc(hidden)] - pub use crate::widget::{Text, TextUiReader, TextUiWriter}; + pub use crate::widget::{Text, TextShadow, TextUiReader, TextUiWriter}; #[doc(hidden)] pub use { crate::{ @@ -184,7 +184,6 @@ impl Plugin for UiPlugin { .register_type::() .register_type::() .register_type::() - .register_type::() .register_type::() .register_type::() .register_type::() @@ -284,11 +283,12 @@ impl Plugin for UiPlugin { fn build_text_interop(app: &mut App) { use crate::widget::TextNodeFlags; use bevy_text::TextLayoutInfo; - use widget::Text; + use widget::{Text, TextShadow}; app.register_type::() .register_type::() - .register_type::(); + .register_type::() + .register_type::(); app.add_systems( PostUpdate, diff --git a/crates/bevy_ui/src/picking_backend.rs b/crates/bevy_ui/src/picking_backend.rs index 5647baee12..ccd61a3807 100644 --- a/crates/bevy_ui/src/picking_backend.rs +++ b/crates/bevy_ui/src/picking_backend.rs @@ -24,7 +24,7 @@ #![deny(missing_docs)] -use crate::{prelude::*, ui_transform::UiGlobalTransform, UiStack}; +use crate::{clip_check_recursive, prelude::*, ui_transform::UiGlobalTransform, UiStack}; use bevy_app::prelude::*; use bevy_ecs::{prelude::*, query::QueryData}; use bevy_math::Vec2; @@ -140,6 +140,10 @@ pub fn ui_picking( let mut pointer_pos = pointer_location.position * camera_data.target_scaling_factor().unwrap_or(1.); if let Some(viewport) = camera_data.physical_viewport_rect() { + if !viewport.as_rect().contains(pointer_pos) { + // The pointer is outside the viewport, skip it + continue; + } pointer_pos -= viewport.min.as_vec2(); } pointer_pos_by_camera @@ -252,27 +256,3 @@ pub fn ui_picking( output.write(PointerHits::new(*pointer, picks, order)); } } - -/// Walk up the tree child-to-parent checking that `point` is not clipped by any ancestor node. -pub fn clip_check_recursive( - point: Vec2, - entity: Entity, - clipping_query: &Query<'_, '_, (&ComputedNode, &UiGlobalTransform, &Node)>, - child_of_query: &Query<&ChildOf>, -) -> bool { - if let Ok(child_of) = child_of_query.get(entity) { - let parent = child_of.0; - if let Ok((computed_node, transform, node)) = clipping_query.get(parent) { - if !computed_node - .resolve_clip_rect(node.overflow, node.overflow_clip_margin) - .contains(transform.inverse().transform_point2(point)) - { - // The point is clipped and should be ignored by picking - return false; - } - } - return clip_check_recursive(point, parent, clipping_query, child_of_query); - } - // Reached root, point unclipped by all ancestors - true -} diff --git a/crates/bevy_ui/src/render/box_shadow.rs b/crates/bevy_ui/src/render/box_shadow.rs index b6f3f3501e..d3167adb56 100644 --- a/crates/bevy_ui/src/render/box_shadow.rs +++ b/crates/bevy_ui/src/render/box_shadow.rs @@ -2,6 +2,7 @@ use core::{hash::Hash, ops::Range}; +use super::{stack_z_offsets, UiCameraMap, UiCameraView, QUAD_INDICES, QUAD_VERTEX_POSITIONS}; use crate::prelude::UiGlobalTransform; use crate::{ BoxShadow, BoxShadowSamples, CalculatedClip, ComputedNode, ComputedNodeTarget, RenderUiSystems, @@ -30,10 +31,9 @@ use bevy_render::{ view::*, Extract, ExtractSchedule, Render, RenderSystems, }; +use bevy_utils::default; use bytemuck::{Pod, Zeroable}; -use super::{stack_z_offsets, UiCameraMap, UiCameraView, QUAD_INDICES, QUAD_VERTEX_POSITIONS}; - /// A plugin that enables the rendering of box shadows. pub struct BoxShadowPlugin; @@ -167,14 +167,13 @@ impl SpecializedRenderPipeline for BoxShadowPipeline { RenderPipelineDescriptor { vertex: VertexState { shader: self.shader.clone(), - entry_point: "vertex".into(), shader_defs: shader_defs.clone(), buffers: vec![vertex_layout], + ..default() }, fragment: Some(FragmentState { shader: self.shader.clone(), shader_defs, - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format: if key.hdr { ViewTarget::TEXTURE_FORMAT_HDR @@ -184,26 +183,11 @@ impl SpecializedRenderPipeline for BoxShadowPipeline { blend: Some(BlendState::ALPHA_BLENDING), write_mask: ColorWrites::ALL, })], + ..default() }), layout: vec![self.view_layout.clone()], - push_constant_ranges: Vec::new(), - primitive: PrimitiveState { - front_face: FrontFace::Ccw, - cull_mode: None, - unclipped_depth: false, - polygon_mode: PolygonMode::Fill, - 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("box_shadow_pipeline".into()), - zero_initialize_workgroup_memory: false, + ..default() } } } diff --git a/crates/bevy_ui/src/render/debug_overlay.rs b/crates/bevy_ui/src/render/debug_overlay.rs index c3ada22c2e..4bf9e2dd93 100644 --- a/crates/bevy_ui/src/render/debug_overlay.rs +++ b/crates/bevy_ui/src/render/debug_overlay.rs @@ -1,8 +1,14 @@ +use super::ExtractedUiItem; +use super::ExtractedUiNode; +use super::ExtractedUiNodes; +use super::NodeType; +use super::UiCameraMap; use crate::shader_flags; use crate::ui_node::ComputedNodeTarget; use crate::ui_transform::UiGlobalTransform; use crate::CalculatedClip; use crate::ComputedNode; +use crate::UiStack; use bevy_asset::AssetId; use bevy_color::Hsla; use bevy_ecs::entity::Entity; @@ -18,12 +24,6 @@ use bevy_render::view::InheritedVisibility; use bevy_render::Extract; use bevy_sprite::BorderRect; -use super::ExtractedUiItem; -use super::ExtractedUiNode; -use super::ExtractedUiNodes; -use super::NodeType; -use super::UiCameraMap; - /// Configuration for the UI debug overlay #[derive(Resource)] pub struct UiDebugOptions { @@ -68,6 +68,7 @@ pub fn extract_debug_overlay( &ComputedNodeTarget, )>, >, + ui_stack: Extract>, camera_map: Extract, ) { if !debug_options.enabled { @@ -89,7 +90,7 @@ pub fn extract_debug_overlay( extracted_uinodes.uinodes.push(ExtractedUiNode { render_entity: commands.spawn(TemporaryRenderEntity).id(), // Add a large number to the UI node's stack index so that the overlay is always drawn on top - stack_index: uinode.stack_index + u32::MAX / 2, + z_order: (ui_stack.uinodes.len() as u32 + uinode.stack_index()) as f32, color: Hsla::sequential_dispersed(entity.index()).into(), rect: Rect { min: Vec2::ZERO, diff --git a/crates/bevy_ui/src/render/gradient.rs b/crates/bevy_ui/src/render/gradient.rs index e1c845d481..9bf7e1ede3 100644 --- a/crates/bevy_ui/src/render/gradient.rs +++ b/crates/bevy_ui/src/render/gradient.rs @@ -4,6 +4,7 @@ use core::{ ops::Range, }; +use super::shader_flags::BORDER_ALL; use crate::*; use bevy_asset::*; use bevy_color::{ColorToComponents, LinearRgba}; @@ -30,10 +31,9 @@ use bevy_render::{ Extract, ExtractSchedule, Render, RenderSystems, }; use bevy_sprite::BorderRect; +use bevy_utils::default; use bytemuck::{Pod, Zeroable}; -use super::shader_flags::BORDER_ALL; - pub struct GradientPlugin; impl Plugin for GradientPlugin { @@ -198,14 +198,13 @@ impl SpecializedRenderPipeline for GradientPipeline { RenderPipelineDescriptor { vertex: VertexState { shader: self.shader.clone(), - entry_point: "vertex".into(), shader_defs: shader_defs.clone(), buffers: vec![vertex_layout], + ..default() }, fragment: Some(FragmentState { shader: self.shader.clone(), shader_defs, - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format: if key.hdr { ViewTarget::TEXTURE_FORMAT_HDR @@ -215,26 +214,11 @@ impl SpecializedRenderPipeline for GradientPipeline { blend: Some(BlendState::ALPHA_BLENDING), write_mask: ColorWrites::ALL, })], + ..default() }), layout: vec![self.view_layout.clone()], - push_constant_ranges: Vec::new(), - primitive: PrimitiveState { - front_face: FrontFace::Ccw, - cull_mode: None, - unclipped_depth: false, - polygon_mode: PolygonMode::Fill, - 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("ui_gradient_pipeline".into()), - zero_initialize_workgroup_memory: false, + ..default() } } } @@ -408,7 +392,11 @@ pub fn extract_gradients( if let Some(color) = gradient.get_single() { // With a single color stop there's no gradient, fill the node with the color extracted_uinodes.uinodes.push(ExtractedUiNode { - stack_index: uinode.stack_index, + z_order: uinode.stack_index as f32 + + match node_type { + NodeType::Rect => stack_z_offsets::GRADIENT, + NodeType::Border(_) => stack_z_offsets::BORDER_GRADIENT, + }, color: color.into(), rect: Rect { min: Vec2::ZERO, @@ -629,7 +617,13 @@ pub fn queue_gradient( draw_function, pipeline, entity: (gradient.render_entity, gradient.main_entity), - sort_key: FloatOrd(gradient.stack_index as f32 + stack_z_offsets::GRADIENT), + sort_key: FloatOrd( + gradient.stack_index as f32 + + match gradient.node_type { + NodeType::Rect => stack_z_offsets::GRADIENT, + NodeType::Border(_) => stack_z_offsets::BORDER_GRADIENT, + }, + ), batch_range: 0..0, extra_index: PhaseItemExtraIndex::None, index, diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index 61319eda9b..58de0c766a 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -9,11 +9,11 @@ mod debug_overlay; mod gradient; use crate::prelude::UiGlobalTransform; -use crate::widget::{ImageNode, ViewportNode}; +use crate::widget::{ImageNode, TextShadow, ViewportNode}; use crate::{ BackgroundColor, BorderColor, BoxShadowSamples, CalculatedClip, ComputedNode, - ComputedNodeTarget, Outline, ResolvedBorderRadius, TextShadow, UiAntiAlias, + ComputedNodeTarget, Outline, ResolvedBorderRadius, UiAntiAlias, }; use bevy_app::prelude::*; use bevy_asset::{AssetEvent, AssetId, Assets}; @@ -81,7 +81,7 @@ pub mod graph { } } -/// Z offsets of "extracted nodes" for a given entity. These exist to allow rendering multiple "extracted nodes" +/// Local Z offsets of "extracted nodes" for a given entity. These exist to allow rendering multiple "extracted nodes" /// for a given source entity (ex: render both a background color _and_ a custom material for a given node). /// /// When possible these offsets should be defined in _this_ module to ensure z-index coordination across contexts. @@ -97,10 +97,13 @@ pub mod graph { /// a positive offset on a node below. pub mod stack_z_offsets { pub const BOX_SHADOW: f32 = -0.1; - pub const TEXTURE_SLICE: f32 = 0.0; - pub const NODE: f32 = 0.0; - pub const GRADIENT: f32 = 0.1; - pub const MATERIAL: f32 = 0.18267; + pub const BACKGROUND_COLOR: f32 = 0.0; + pub const BORDER: f32 = 0.01; + pub const GRADIENT: f32 = 0.02; + pub const BORDER_GRADIENT: f32 = 0.03; + pub const IMAGE: f32 = 0.04; + pub const MATERIAL: f32 = 0.05; + pub const TEXT: f32 = 0.06; } #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] @@ -213,7 +216,7 @@ fn get_ui_graph(render_app: &mut SubApp) -> RenderGraph { } pub struct ExtractedUiNode { - pub stack_index: u32, + pub z_order: f32, pub color: LinearRgba, pub rect: Rect, pub image: AssetId, @@ -374,7 +377,7 @@ pub fn extract_uinode_background_colors( extracted_uinodes.uinodes.push(ExtractedUiNode { render_entity: commands.spawn(TemporaryRenderEntity).id(), - stack_index: uinode.stack_index, + z_order: uinode.stack_index as f32 + stack_z_offsets::BACKGROUND_COLOR, color: background_color.0.into(), rect: Rect { min: Vec2::ZERO, @@ -460,8 +463,8 @@ pub fn extract_uinode_images( }; extracted_uinodes.uinodes.push(ExtractedUiNode { + z_order: uinode.stack_index as f32 + stack_z_offsets::IMAGE, render_entity: commands.spawn(TemporaryRenderEntity).id(), - stack_index: uinode.stack_index, color: image.color.into(), rect, clip: clip.map(|clip| clip.clip), @@ -558,7 +561,7 @@ pub fn extract_uinode_borders( completed_flags |= border_flags; extracted_uinodes.uinodes.push(ExtractedUiNode { - stack_index: computed_node.stack_index, + z_order: computed_node.stack_index as f32 + stack_z_offsets::BORDER, color, rect: Rect { max: computed_node.size(), @@ -591,8 +594,8 @@ pub fn extract_uinode_borders( { let outline_size = computed_node.outlined_node_size(); extracted_uinodes.uinodes.push(ExtractedUiNode { + z_order: computed_node.stack_index as f32 + stack_z_offsets::BORDER, render_entity: commands.spawn(TemporaryRenderEntity).id(), - stack_index: computed_node.stack_index, color: outline.color.into(), rect: Rect { max: outline_size, @@ -782,8 +785,8 @@ pub fn extract_viewport_nodes( }; extracted_uinodes.uinodes.push(ExtractedUiNode { + z_order: uinode.stack_index as f32 + stack_z_offsets::IMAGE, render_entity: commands.spawn(TemporaryRenderEntity).id(), - stack_index: uinode.stack_index, color: LinearRgba::WHITE, rect: Rect { min: Vec2::ZERO, @@ -862,7 +865,7 @@ pub fn extract_text_sections( ) in text_layout_info.glyphs.iter().enumerate() { let rect = texture_atlases - .get(&atlas_info.texture_atlas) + .get(atlas_info.texture_atlas) .unwrap() .textures[atlas_info.location.glyph_index] .as_rect(); @@ -885,10 +888,10 @@ pub fn extract_text_sections( .map(|text_color| LinearRgba::from(text_color.0)) .unwrap_or_default(); extracted_uinodes.uinodes.push(ExtractedUiNode { + z_order: uinode.stack_index as f32 + stack_z_offsets::TEXT, render_entity: commands.spawn(TemporaryRenderEntity).id(), - stack_index: uinode.stack_index, color, - image: atlas_info.texture.id(), + image: atlas_info.texture, clip: clip.map(|clip| clip.clip), extracted_camera_entity, rect, @@ -953,7 +956,7 @@ pub fn extract_text_shadows( ) in text_layout_info.glyphs.iter().enumerate() { let rect = texture_atlases - .get(&atlas_info.texture_atlas) + .get(atlas_info.texture_atlas) .unwrap() .textures[atlas_info.location.glyph_index] .as_rect(); @@ -966,10 +969,10 @@ pub fn extract_text_shadows( info.span_index != *span_index || info.atlas_info.texture != atlas_info.texture }) { extracted_uinodes.uinodes.push(ExtractedUiNode { + z_order: uinode.stack_index as f32 + stack_z_offsets::TEXT, render_entity: commands.spawn(TemporaryRenderEntity).id(), - stack_index: uinode.stack_index, color: shadow.color.into(), - image: atlas_info.texture.id(), + image: atlas_info.texture, clip: clip.map(|clip| clip.clip), extracted_camera_entity, rect, @@ -1023,8 +1026,8 @@ pub fn extract_text_background_colors( }; extracted_uinodes.uinodes.push(ExtractedUiNode { + z_order: uinode.stack_index as f32 + stack_z_offsets::TEXT, render_entity: commands.spawn(TemporaryRenderEntity).id(), - stack_index: uinode.stack_index, color: text_background_color.0.to_linear(), rect: Rect { min: Vec2::ZERO, @@ -1167,7 +1170,7 @@ pub fn queue_uinodes( draw_function, pipeline, entity: (extracted_uinode.render_entity, extracted_uinode.main_entity), - sort_key: FloatOrd(extracted_uinode.stack_index as f32 + stack_z_offsets::NODE), + sort_key: FloatOrd(extracted_uinode.z_order), index, // batch_range will be calculated in prepare_uinodes batch_range: 0..0, diff --git a/crates/bevy_ui/src/render/pipeline.rs b/crates/bevy_ui/src/render/pipeline.rs index c020e038fb..7440c5abad 100644 --- a/crates/bevy_ui/src/render/pipeline.rs +++ b/crates/bevy_ui/src/render/pipeline.rs @@ -9,6 +9,7 @@ use bevy_render::{ renderer::RenderDevice, view::{ViewTarget, ViewUniform}, }; +use bevy_utils::default; #[derive(Resource)] pub struct UiPipeline { @@ -88,14 +89,13 @@ impl SpecializedRenderPipeline for UiPipeline { RenderPipelineDescriptor { vertex: VertexState { shader: self.shader.clone(), - entry_point: "vertex".into(), shader_defs: shader_defs.clone(), buffers: vec![vertex_layout], + ..default() }, fragment: Some(FragmentState { shader: self.shader.clone(), shader_defs, - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format: if key.hdr { ViewTarget::TEXTURE_FORMAT_HDR @@ -105,26 +105,11 @@ impl SpecializedRenderPipeline for UiPipeline { blend: Some(BlendState::ALPHA_BLENDING), write_mask: ColorWrites::ALL, })], + ..default() }), layout: vec![self.view_layout.clone(), self.image_layout.clone()], - push_constant_ranges: Vec::new(), - primitive: PrimitiveState { - front_face: FrontFace::Ccw, - cull_mode: None, - unclipped_depth: false, - polygon_mode: PolygonMode::Fill, - 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("ui_pipeline".into()), - zero_initialize_workgroup_memory: false, + ..default() } } } diff --git a/crates/bevy_ui/src/render/ui_material_pipeline.rs b/crates/bevy_ui/src/render/ui_material_pipeline.rs index 5d2201e609..84f3067b17 100644 --- a/crates/bevy_ui/src/render/ui_material_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_material_pipeline.rs @@ -23,6 +23,7 @@ use bevy_render::{ Extract, ExtractSchedule, Render, RenderSystems, }; use bevy_sprite::BorderRect; +use bevy_utils::default; use bytemuck::{Pod, Zeroable}; use core::{hash::Hash, marker::PhantomData, ops::Range}; @@ -152,14 +153,13 @@ where let mut descriptor = RenderPipelineDescriptor { vertex: VertexState { shader: self.vertex_shader.clone(), - entry_point: "vertex".into(), shader_defs: shader_defs.clone(), buffers: vec![vertex_layout], + ..default() }, fragment: Some(FragmentState { shader: self.fragment_shader.clone(), shader_defs, - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format: if key.hdr { ViewTarget::TEXTURE_FORMAT_HDR @@ -169,26 +169,10 @@ where blend: Some(BlendState::ALPHA_BLENDING), write_mask: ColorWrites::ALL, })], + ..default() }), - layout: vec![], - push_constant_ranges: Vec::new(), - primitive: PrimitiveState { - front_face: FrontFace::Ccw, - cull_mode: None, - unclipped_depth: false, - polygon_mode: PolygonMode::Fill, - 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("ui_material_pipeline".into()), - zero_initialize_workgroup_memory: false, + ..default() }; descriptor.layout = vec![self.view_layout.clone(), self.ui_layout.clone()]; @@ -583,11 +567,12 @@ impl RenderAsset for PreparedUiMaterial { (render_device, pipeline, material_param): &mut SystemParamItem, _: Option<&Self>, ) -> Result> { + let bind_group_data = material.bind_group_data(); match material.as_bind_group(&pipeline.ui_layout, render_device, material_param) { Ok(prepared) => Ok(PreparedUiMaterial { bindings: prepared.bindings, bind_group: prepared.bind_group, - key: prepared.data, + key: bind_group_data, }), Err(AsBindGroupError::RetryNextUpdate) => { Err(PrepareAssetError::RetryNextUpdate(material)) @@ -637,7 +622,7 @@ pub fn queue_ui_material_nodes( &ui_material_pipeline, UiMaterialKey { hdr: view.hdr, - bind_group_data: material.key.clone(), + bind_group_data: material.key, }, ); if transparent_phase.items.capacity() < extracted_uinodes.uinodes.len() { diff --git a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs index 0e232ab1cc..b217b052ec 100644 --- a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs @@ -26,6 +26,7 @@ use bevy_render::{ Extract, ExtractSchedule, Render, RenderSystems, }; use bevy_sprite::{SliceScaleMode, SpriteAssetEvents, SpriteImageMode, TextureSlicer}; +use bevy_utils::default; use binding_types::{sampler, texture_2d}; use bytemuck::{Pod, Zeroable}; use widget::ImageNode; @@ -175,14 +176,13 @@ impl SpecializedRenderPipeline for UiTextureSlicePipeline { RenderPipelineDescriptor { vertex: VertexState { shader: self.shader.clone(), - entry_point: "vertex".into(), shader_defs: shader_defs.clone(), buffers: vec![vertex_layout], + ..default() }, fragment: Some(FragmentState { shader: self.shader.clone(), shader_defs, - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format: if key.hdr { ViewTarget::TEXTURE_FORMAT_HDR @@ -192,26 +192,11 @@ impl SpecializedRenderPipeline for UiTextureSlicePipeline { blend: Some(BlendState::ALPHA_BLENDING), write_mask: ColorWrites::ALL, })], + ..default() }), layout: vec![self.view_layout.clone(), self.image_layout.clone()], - push_constant_ranges: Vec::new(), - primitive: PrimitiveState { - front_face: FrontFace::Ccw, - cull_mode: None, - unclipped_depth: false, - polygon_mode: PolygonMode::Fill, - 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("ui_texture_slice_pipeline".into()), - zero_initialize_workgroup_memory: false, + ..default() } } } @@ -366,9 +351,7 @@ pub fn queue_ui_slices( draw_function, pipeline, entity: (extracted_slicer.render_entity, extracted_slicer.main_entity), - sort_key: FloatOrd( - extracted_slicer.stack_index as f32 + stack_z_offsets::TEXTURE_SLICE, - ), + sort_key: FloatOrd(extracted_slicer.stack_index as f32 + stack_z_offsets::IMAGE), batch_range: 0..0, extra_index: PhaseItemExtraIndex::None, index, diff --git a/crates/bevy_ui/src/ui_material.rs b/crates/bevy_ui/src/ui_material.rs index 9f56e834a4..89a8df948c 100644 --- a/crates/bevy_ui/src/ui_material.rs +++ b/crates/bevy_ui/src/ui_material.rs @@ -141,7 +141,7 @@ where fn clone(&self) -> Self { Self { hdr: self.hdr, - bind_group_data: self.bind_group_data.clone(), + bind_group_data: self.bind_group_data, } } } diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 6418f69ff8..f6b8ee27b3 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -321,45 +321,27 @@ impl Default for ComputedNode { } } -/// The scroll position of the node. +/// The scroll position of the node. Values are in logical pixels, increasing from top-left to bottom-right. /// -/// Updating the values of `ScrollPosition` will reposition the children of the node by the offset amount. +/// Increasing the x-coordinate causes the scrolled content to visibly move left on the screen, while increasing the y-coordinate causes the scrolled content to move up. +/// This might seem backwards, however what's really happening is that +/// the scroll position is moving the visible "window" in the local coordinate system of the scrolled content - +/// moving the window down causes the content to move up. +/// +/// Updating the values of `ScrollPosition` will reposition the children of the node by the offset amount in logical pixels. /// `ScrollPosition` may be updated by the layout system when a layout change makes a previously valid `ScrollPosition` invalid. /// Changing this does nothing on a `Node` without setting at least one `OverflowAxis` to `OverflowAxis::Scroll`. -#[derive(Component, Debug, Clone, Reflect)] +#[derive(Component, Debug, Clone, Default, Deref, DerefMut, Reflect)] #[reflect(Component, Default, Clone)] -pub struct ScrollPosition { - /// How far across the node is scrolled, in logical pixels. (0 = not scrolled / scrolled to right) - pub offset_x: f32, - /// How far down the node is scrolled, in logical pixels. (0 = not scrolled / scrolled to top) - pub offset_y: f32, -} +pub struct ScrollPosition(pub Vec2); impl ScrollPosition { - pub const DEFAULT: Self = Self { - offset_x: 0.0, - offset_y: 0.0, - }; -} - -impl Default for ScrollPosition { - fn default() -> Self { - Self::DEFAULT - } -} - -impl From<&ScrollPosition> for Vec2 { - fn from(scroll_pos: &ScrollPosition) -> Self { - Vec2::new(scroll_pos.offset_x, scroll_pos.offset_y) - } + pub const DEFAULT: Self = Self(Vec2::ZERO); } impl From for ScrollPosition { - fn from(vec: Vec2) -> Self { - ScrollPosition { - offset_x: vec.x, - offset_y: vec.y, - } + fn from(value: Vec2) -> Self { + Self(value) } } @@ -2246,6 +2228,11 @@ pub struct CalculatedClip { pub clip: Rect, } +/// UI node entities with this component will ignore any clipping rect they inherit, +/// the node will not be clipped regardless of its ancestors' `Overflow` setting. +#[derive(Component)] +pub struct OverrideClip; + /// Indicates that this [`Node`] entity's front-to-back ordering is not controlled solely /// by its location in the UI hierarchy. A node with a higher z-index will appear on top /// of sibling nodes with a lower z-index. @@ -2876,28 +2863,6 @@ impl ComputedNodeTarget { } } -/// Adds a shadow behind text -/// -/// Not supported by `Text2d` -#[derive(Component, Copy, Clone, Debug, PartialEq, Reflect)] -#[reflect(Component, Default, Debug, Clone, PartialEq)] -pub struct TextShadow { - /// Shadow displacement in logical pixels - /// With a value of zero the shadow will be hidden directly behind the text - pub offset: Vec2, - /// Color of the shadow - pub color: Color, -} - -impl Default for TextShadow { - fn default() -> Self { - Self { - offset: Vec2::splat(4.), - color: Color::linear_rgba(0., 0., 0., 0.75), - } - } -} - #[cfg(test)] mod tests { use crate::GridPlacement; diff --git a/crates/bevy_ui/src/update.rs b/crates/bevy_ui/src/update.rs index c0e9d09d7b..0053e5a406 100644 --- a/crates/bevy_ui/src/update.rs +++ b/crates/bevy_ui/src/update.rs @@ -3,8 +3,8 @@ use crate::{ experimental::{UiChildren, UiRootNodes}, ui_transform::UiGlobalTransform, - CalculatedClip, ComputedNodeTarget, DefaultUiCamera, Display, Node, OverflowAxis, UiScale, - UiTargetCamera, + CalculatedClip, ComputedNodeTarget, DefaultUiCamera, Display, Node, OverflowAxis, OverrideClip, + UiScale, UiTargetCamera, }; use super::ComputedNode; @@ -12,7 +12,7 @@ use bevy_ecs::{ change_detection::DetectChangesMut, entity::Entity, hierarchy::ChildOf, - query::{Changed, With}, + query::{Changed, Has, With}, system::{Commands, Query, Res}, }; use bevy_math::{Rect, UVec2}; @@ -28,6 +28,7 @@ pub fn update_clipping_system( &ComputedNode, &UiGlobalTransform, Option<&mut CalculatedClip>, + Has, )>, ui_children: UiChildren, ) { @@ -50,15 +51,22 @@ fn update_clipping( &ComputedNode, &UiGlobalTransform, Option<&mut CalculatedClip>, + Has, )>, entity: Entity, mut maybe_inherited_clip: Option, ) { - let Ok((node, computed_node, transform, maybe_calculated_clip)) = node_query.get_mut(entity) + let Ok((node, computed_node, transform, maybe_calculated_clip, has_override_clip)) = + node_query.get_mut(entity) else { return; }; + // If the UI node entity has an `OverrideClip` component, discard any inherited clip rect + if has_override_clip { + maybe_inherited_clip = None; + } + // If `display` is None, clip the entire node and all its descendants by replacing the inherited clip with a default rect (which is empty) if node.display == Display::None { maybe_inherited_clip = Some(Rect::default()); diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index d7f8e243a4..1544a5ff7a 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -128,6 +128,28 @@ impl From for Text { } } +/// Adds a shadow behind text +/// +/// Not supported by `Text2d` +#[derive(Component, Copy, Clone, Debug, PartialEq, Reflect)] +#[reflect(Component, Default, Debug, Clone, PartialEq)] +pub struct TextShadow { + /// Shadow displacement in logical pixels + /// With a value of zero the shadow will be hidden directly behind the text + pub offset: Vec2, + /// Color of the shadow + pub color: Color, +} + +impl Default for TextShadow { + fn default() -> Self { + Self { + offset: Vec2::splat(4.), + color: Color::linear_rgba(0., 0., 0., 0.75), + } + } +} + /// UI alias for [`TextReader`]. pub type TextUiReader<'w, 's> = TextReader<'w, 's, Text>; diff --git a/crates/bevy_ui/src/widget/viewport.rs b/crates/bevy_ui/src/widget/viewport.rs index fe5b6eeb6d..120c3335c2 100644 --- a/crates/bevy_ui/src/widget/viewport.rs +++ b/crates/bevy_ui/src/widget/viewport.rs @@ -2,22 +2,32 @@ use bevy_asset::Assets; use bevy_ecs::{ component::Component, entity::Entity, - event::EventReader, query::{Changed, Or}, reflect::ReflectComponent, - system::{Commands, Query, Res, ResMut}, + system::{Query, ResMut}, +}; +#[cfg(feature = "bevy_ui_picking_backend")] +use bevy_ecs::{ + event::EventReader, + system::{Commands, Res}, }; use bevy_image::{Image, ToExtents}; -use bevy_math::{Rect, UVec2}; +#[cfg(feature = "bevy_ui_picking_backend")] +use bevy_math::Rect; +use bevy_math::UVec2; #[cfg(feature = "bevy_ui_picking_backend")] use bevy_picking::{ events::PointerState, hover::HoverMap, pointer::{Location, PointerId, PointerInput, PointerLocation}, }; +#[cfg(feature = "bevy_ui_picking_backend")] use bevy_platform::collections::HashMap; use bevy_reflect::Reflect; -use bevy_render::camera::{Camera, NormalizedRenderTarget}; +use bevy_render::camera::Camera; +#[cfg(feature = "bevy_ui_picking_backend")] +use bevy_render::camera::NormalizedRenderTarget; +#[cfg(feature = "bevy_ui_picking_backend")] use bevy_transform::components::GlobalTransform; #[cfg(feature = "bevy_ui_picking_backend")] use uuid::Uuid; diff --git a/crates/bevy_utils/Cargo.toml b/crates/bevy_utils/Cargo.toml index 4e74e6ea94..447c9966f4 100644 --- a/crates/bevy_utils/Cargo.toml +++ b/crates/bevy_utils/Cargo.toml @@ -18,7 +18,7 @@ parallel = ["bevy_platform/std", "dep:thread_local"] std = ["disqualified/alloc"] -debug = [] +debug = ["bevy_platform/alloc"] [dependencies] bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false } diff --git a/crates/bevy_utils/src/debug_info.rs b/crates/bevy_utils/src/debug_info.rs index c79c5ebe60..c50917d279 100644 --- a/crates/bevy_utils/src/debug_info.rs +++ b/crates/bevy_utils/src/debug_info.rs @@ -1,10 +1,13 @@ -use alloc::{borrow::Cow, fmt, string::String}; +use crate::cfg; +cfg::alloc! { + use alloc::{borrow::Cow, fmt, string::String}; +} #[cfg(feature = "debug")] use core::any::type_name; use disqualified::ShortName; #[cfg(not(feature = "debug"))] -const FEATURE_DISABLED: &'static str = "Enable the debug feature to see the name"; +const FEATURE_DISABLED: &str = "Enable the debug feature to see the name"; /// Wrapper to help debugging ECS issues. This is used to display the names of systems, components, ... /// @@ -16,14 +19,16 @@ pub struct DebugName { name: Cow<'static, str>, } -impl fmt::Display for DebugName { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - #[cfg(feature = "debug")] - f.write_str(self.name.as_ref())?; - #[cfg(not(feature = "debug"))] - f.write_str(FEATURE_DISABLED)?; +cfg::alloc! { + impl fmt::Display for DebugName { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + #[cfg(feature = "debug")] + f.write_str(self.name.as_ref())?; + #[cfg(not(feature = "debug"))] + f.write_str(FEATURE_DISABLED)?; - Ok(()) + Ok(()) + } } } @@ -31,7 +36,13 @@ impl DebugName { /// Create a new `DebugName` from a `&str` /// /// The value will be ignored if the `debug` feature is not enabled - #[cfg_attr(not(feature = "debug"), expect(unused_variables))] + #[cfg_attr( + not(feature = "debug"), + expect( + unused_variables, + reason = "The value will be ignored if the `debug` feature is not enabled" + ) + )] pub fn borrowed(value: &'static str) -> Self { DebugName { #[cfg(feature = "debug")] @@ -39,14 +50,22 @@ impl DebugName { } } - /// Create a new `DebugName` from a `String` - /// - /// The value will be ignored if the `debug` feature is not enabled - #[cfg_attr(not(feature = "debug"), expect(unused_variables))] - pub fn owned(value: String) -> Self { - DebugName { - #[cfg(feature = "debug")] - name: Cow::Owned(value), + cfg::alloc! { + /// Create a new `DebugName` from a `String` + /// + /// The value will be ignored if the `debug` feature is not enabled + #[cfg_attr( + not(feature = "debug"), + expect( + unused_variables, + reason = "The value will be ignored if the `debug` feature is not enabled" + ) + )] + pub fn owned(value: String) -> Self { + DebugName { + #[cfg(feature = "debug")] + name: Cow::Owned(value), + } } } @@ -79,19 +98,27 @@ impl DebugName { } } -impl From> for DebugName { - #[cfg_attr(not(feature = "debug"), expect(unused_variables))] - fn from(value: Cow<'static, str>) -> Self { - Self { - #[cfg(feature = "debug")] - name: value, +cfg::alloc! { + impl From> for DebugName { + #[cfg_attr( + not(feature = "debug"), + expect( + unused_variables, + reason = "The value will be ignored if the `debug` feature is not enabled" + ) + )] + fn from(value: Cow<'static, str>) -> Self { + Self { + #[cfg(feature = "debug")] + name: value, + } } } -} -impl From for DebugName { - fn from(value: String) -> Self { - Self::owned(value) + impl From for DebugName { + fn from(value: String) -> Self { + Self::owned(value) + } } } diff --git a/crates/bevy_window/src/window.rs b/crates/bevy_window/src/window.rs index 6f61eae2de..4fc039d7c7 100644 --- a/crates/bevy_window/src/window.rs +++ b/crates/bevy_window/src/window.rs @@ -1,4 +1,6 @@ -use alloc::{borrow::ToOwned, format, string::String}; +#[cfg(feature = "std")] +use alloc::format; +use alloc::{borrow::ToOwned, string::String}; use core::num::NonZero; use bevy_ecs::{ @@ -750,9 +752,10 @@ pub struct CursorOptions { /// ## Platform-specific /// /// - **`macOS`** doesn't support [`CursorGrabMode::Confined`] + /// - **`X11`** doesn't support [`CursorGrabMode::Locked`] /// - **`iOS/Android`** don't have cursors. /// - /// Since `macOS` doesn't have full [`CursorGrabMode`] support, we first try to set the grab mode that was asked for. If it doesn't work then use the alternate grab mode. + /// Since `macOS` and `X11` don't have full [`CursorGrabMode`] support, we first try to set the grab mode that was asked for. If it doesn't work then use the alternate grab mode. pub grab_mode: CursorGrabMode, /// Set whether or not mouse events within *this* window are captured or fall through to the Window below. @@ -1062,9 +1065,10 @@ impl From for WindowResolution { /// ## Platform-specific /// /// - **`macOS`** doesn't support [`CursorGrabMode::Confined`] +/// - **`X11`** doesn't support [`CursorGrabMode::Locked`] /// - **`iOS/Android`** don't have cursors. /// -/// Since `macOS` doesn't have full [`CursorGrabMode`] support, we first try to set the grab mode that was asked for. If it doesn't work then use the alternate grab mode. +/// Since `macOS` and `X11` don't have full [`CursorGrabMode`] support, we first try to set the grab mode that was asked for. If it doesn't work then use the alternate grab mode. #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", @@ -1469,7 +1473,8 @@ pub struct ClosingWindow; /// - Only used on iOS. /// /// [`winit::platform::ios::ScreenEdge`]: https://docs.rs/winit/latest/x86_64-apple-darwin/winit/platform/ios/struct.ScreenEdge.html -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub enum ScreenEdge { #[default] diff --git a/crates/bevy_winit/src/winit_windows.rs b/crates/bevy_winit/src/winit_windows.rs index f238da2e20..8cc5dcfac7 100644 --- a/crates/bevy_winit/src/winit_windows.rs +++ b/crates/bevy_winit/src/winit_windows.rs @@ -190,11 +190,16 @@ impl WinitWindows { bevy_log::debug!("{display_info}"); #[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd", + all( + any(feature = "wayland", feature = "x11"), + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + ) + ), target_os = "windows" ))] if let Some(name) = &window.name { @@ -285,7 +290,7 @@ impl WinitWindows { let canvas = canvas.dyn_into::().ok(); winit_window_attributes = winit_window_attributes.with_canvas(canvas); } else { - panic!("Cannot find element: {}.", selector); + panic!("Cannot find element: {selector}."); } } diff --git a/docs/cargo_features.md b/docs/cargo_features.md index 0f9bdc507a..70030ce26d 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -81,6 +81,7 @@ The default feature set enables most of the expected features of a game engine, |detailed_trace|Enable detailed trace event logging. These trace events are expensive even when off, thus they require compile time opt-in| |dynamic_linking|Force dynamic linking, which improves iterative compile times| |embedded_watcher|Enables watching in memory asset providers for Bevy Asset hot-reloading| +|experimental_bevy_feathers|Feathers widget collection.| |experimental_pbr_pcss|Enable support for PCSS, at the risk of blowing past the global, per-shader sampler limit on older/lower-end GPUs| |exr|EXR image format support| |ff|Farbfeld image format support| @@ -89,6 +90,7 @@ The default feature set enables most of the expected features of a game engine, |ghost_nodes|Experimental support for nodes that are ignored for UI layouting| |gif|GIF image format support| |glam_assert|Enable assertions to check the validity of parameters passed to glam| +|gltf_convert_coordinates_default|Enable converting glTF coordinates to Bevy's coordinate system by default. This will be Bevy's default behavior starting in 0.18.| |hotpatching|Enable hotpatching of Bevy systems| |ico|ICO image format support| |jpeg|JPEG image format support| @@ -98,6 +100,8 @@ The default feature set enables most of the expected features of a game engine, |minimp3|MP3 audio format support (through minimp3)| |mp3|MP3 audio format support| |pbr_anisotropy_texture|Enable support for anisotropy texture in the `StandardMaterial`, at the risk of blowing past the global, per-shader texture limit on older/lower-end GPUs| +|pbr_clustered_decals|Enable support for Clustered Decals| +|pbr_light_textures|Enable support for Light Textures| |pbr_multi_layer_material_textures|Enable support for multi-layer material textures in the `StandardMaterial`, at the risk of blowing past the global, per-shader texture limit on older/lower-end GPUs| |pbr_specular_textures|Enable support for specular textures in the `StandardMaterial`, at the risk of blowing past the global, per-shader texture limit on older/lower-end GPUs| |pbr_transmission_textures|Enable support for transmission-related textures in the `StandardMaterial`, at the risk of blowing past the global, per-shader texture limit on older/lower-end GPUs| diff --git a/docs/profiling.md b/docs/profiling.md index ef6a46a608..fe55b048af 100644 --- a/docs/profiling.md +++ b/docs/profiling.md @@ -10,6 +10,7 @@ - [Perf flame graph](#perf-flame-graph) - [GPU runtime](#gpu-runtime) - [Vendor tools](#vendor-tools) + - [Xcode's Metal debugger](#xcodes-metal-debugger) - [Tracy RenderQueue](#tracy-renderqueue) - [Compile time](#compile-time) @@ -147,6 +148,33 @@ For profiling GPU work, you should use the tool corresponding to your GPU's vend Note that while RenderDoc is a great debugging tool, it is _not_ a profiler, and should not be used for this purpose. +#### Xcode's Metal debugger + +Follow the steps below to start GPU debugging on macOS. There is no need to create an Xcode project. + +1. In the menu bar click on Debug > Debug Executable… + + ![Xcode's menu bar open to Debug > Debug Executable...](https://github.com/user-attachments/assets/efdc5037-0957-4227-b29d-9a789ba17a0a) + +2. Select your executable from your project’s target folder. +3. The Scheme Editor will open. If your assets are not located next to your executable, you can go to the Arguments tab and set `BEVY_ASSET_ROOT` to the absolute path for your project (the parent of your assets folder). The rest of the defaults should be fine. + + ![Xcode's Schema Editor opened to an environment variable configuration](https://github.com/user-attachments/assets/29cafb05-0c49-4777-8d41-8643812e8f6a) + +4. Click the play button in the top left and this should start your bevy app. + + ![A cursor hovering over the play button in XCode](https://github.com/user-attachments/assets/859580e2-779b-4db8-8ea6-73cf4ef696c9) + +5. Go back to Xcode and click on the Metal icon in the bottom drawer and then Capture in the following the popup menu. + + ![A cursor hovering over the Capture button in the Metal debugging popup menu](https://github.com/user-attachments/assets/c0ce1591-0a53-499b-bd1b-4d89538ea248) + +6. Start debugging and profiling! + +![Xcode open to the Performance tab in the Debug Navigator.](https://github.com/user-attachments/assets/52732391-9306-44a9-ae01-dcf4573f77ab) + +These instructions were created for Xcode 16.4. + ### Tracy RenderQueue While it doesn't provide as much detail as vendor-specific tooling, Tracy can also be used to coarsely measure GPU performance. diff --git a/examples/2d/mesh2d_manual.rs b/examples/2d/mesh2d_manual.rs index 6354aa468c..7012e61d92 100644 --- a/examples/2d/mesh2d_manual.rs +++ b/examples/2d/mesh2d_manual.rs @@ -6,7 +6,7 @@ //! [`Material2d`]: bevy::sprite::Material2d use bevy::{ - asset::weak_handle, + asset::uuid_handle, color::palettes::basic::YELLOW, core_pipeline::core_2d::{Transparent2d, CORE_2D_DEPTH_FORMAT}, math::{ops, FloatOrd}, @@ -20,10 +20,10 @@ use bevy::{ }, render_resource::{ BlendState, ColorTargetState, ColorWrites, CompareFunction, DepthBiasState, - DepthStencilState, Face, FragmentState, FrontFace, MultisampleState, PipelineCache, - PolygonMode, PrimitiveState, PrimitiveTopology, RenderPipelineDescriptor, - SpecializedRenderPipeline, SpecializedRenderPipelines, StencilFaceState, StencilState, - TextureFormat, VertexBufferLayout, VertexFormat, VertexState, VertexStepMode, + DepthStencilState, Face, FragmentState, MultisampleState, PipelineCache, + PrimitiveState, PrimitiveTopology, RenderPipelineDescriptor, SpecializedRenderPipeline, + SpecializedRenderPipelines, StencilFaceState, StencilState, TextureFormat, + VertexBufferLayout, VertexFormat, VertexState, VertexStepMode, }, sync_component::SyncComponentPlugin, sync_world::{MainEntityHashMap, RenderEntity}, @@ -165,21 +165,19 @@ impl SpecializedRenderPipeline for ColoredMesh2dPipeline { vertex: VertexState { // Use our custom shader shader: COLORED_MESH2D_SHADER_HANDLE, - entry_point: "vertex".into(), - shader_defs: vec![], // Use our custom vertex buffer buffers: vec![vertex_layout], + ..default() }, fragment: Some(FragmentState { // Use our custom shader shader: COLORED_MESH2D_SHADER_HANDLE, - shader_defs: vec![], - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format, blend: Some(BlendState::ALPHA_BLENDING), write_mask: ColorWrites::ALL, })], + ..default() }), // Use the two standard uniforms for 2d meshes layout: vec![ @@ -188,15 +186,10 @@ impl SpecializedRenderPipeline for ColoredMesh2dPipeline { // Bind group 1 is the mesh uniform self.mesh2d_pipeline.mesh_layout.clone(), ], - push_constant_ranges: vec![], primitive: PrimitiveState { - front_face: FrontFace::Ccw, cull_mode: Some(Face::Back), - unclipped_depth: false, - polygon_mode: PolygonMode::Fill, - conservative: false, topology: key.primitive_topology(), - strip_index_format: None, + ..default() }, depth_stencil: Some(DepthStencilState { format: CORE_2D_DEPTH_FORMAT, @@ -220,7 +213,7 @@ impl SpecializedRenderPipeline for ColoredMesh2dPipeline { alpha_to_coverage_enabled: false, }, label: Some("colored_mesh2d_pipeline".into()), - zero_initialize_workgroup_memory: false, + ..default() } } } @@ -287,7 +280,7 @@ pub struct ColoredMesh2dPlugin; /// Handle to the custom shader with a unique random ID pub const COLORED_MESH2D_SHADER_HANDLE: Handle = - weak_handle!("f48b148f-7373-4638-9900-392b3b3ccc66"); + uuid_handle!("f48b148f-7373-4638-9900-392b3b3ccc66"); /// Our custom pipeline needs its own instance storage #[derive(Resource, Deref, DerefMut, Default)] diff --git a/examples/3d/clustered_decals.rs b/examples/3d/clustered_decals.rs index 108efe586e..60a445b483 100644 --- a/examples/3d/clustered_decals.rs +++ b/examples/3d/clustered_decals.rs @@ -2,7 +2,6 @@ use std::f32::consts::{FRAC_PI_3, PI}; use std::fmt::{self, Formatter}; -use std::process; use bevy::{ color::palettes::css::{LIME, ORANGE_RED, SILVER}, @@ -165,8 +164,8 @@ fn setup( ) { // Error out if clustered decals aren't supported on the current platform. if !decal::clustered::clustered_decals_are_usable(&render_device, &render_adapter) { - eprintln!("Clustered decals aren't usable on this platform."); - process::exit(1); + error!("Clustered decals aren't usable on this platform."); + commands.send_event(AppExit::error()); } spawn_cube(&mut commands, &mut meshes, &mut materials); diff --git a/examples/3d/fog.rs b/examples/3d/fog.rs index 82bce0b15a..f1fb3f1ed5 100644 --- a/examples/3d/fog.rs +++ b/examples/3d/fog.rs @@ -4,7 +4,7 @@ //! //! The [`FogFalloff`] field controls most of the behavior of the fog through different descriptions of fog "curves". I.e. [`FogFalloff::Linear`] lets us define a start and end distance where up until the start distance none of the fog color is mixed in and by the end distance the fog color is as mixed in as it can be. [`FogFalloff::Exponential`] on the other hand uses an exponential curve to drive how "visible" things are with a density value. //! -//! [Atmospheric fog](https://bevyengine.org/examples/3d-rendering/atmospheric-fog/) is another fog type that uses this same method of setup, but isn't covered here as it is a kind of fog that is most often used to imply distance and size in clear weather, while the ones shown off here are much more "dense". +//! [Atmospheric fog](https://bevy.org/examples/3d-rendering/atmospheric-fog/) is another fog type that uses this same method of setup, but isn't covered here as it is a kind of fog that is most often used to imply distance and size in clear weather, while the ones shown off here are much more "dense". //! //! The bulk of this example is spent building a scene that suites showing off that the fog is working as intended by creating a pyramid (a 3D structure with clear delineations), a light source, input handling to modify fog settings, and UI to show what the current fog settings are. //! diff --git a/examples/3d/light_textures.rs b/examples/3d/light_textures.rs new file mode 100644 index 0000000000..94ddd159f2 --- /dev/null +++ b/examples/3d/light_textures.rs @@ -0,0 +1,699 @@ +//! Demonstrates light textures, which modulate light sources. + +use std::f32::consts::{FRAC_PI_2, FRAC_PI_3, FRAC_PI_4, PI}; +use std::fmt::{self, Formatter}; + +use bevy::{ + color::palettes::css::{SILVER, YELLOW}, + input::mouse::AccumulatedMouseMotion, + pbr::{ + decal::{ + self, + clustered::{DirectionalLightTexture, PointLightTexture, SpotLightTexture}, + }, + NotShadowCaster, + }, + prelude::*, + render::renderer::{RenderAdapter, RenderDevice}, + window::SystemCursorIcon, + winit::cursor::CursorIcon, +}; +use light_consts::lux::{AMBIENT_DAYLIGHT, CLEAR_SUNRISE}; +use ops::{acos, cos, sin}; +use widgets::{ + WidgetClickEvent, WidgetClickSender, BUTTON_BORDER, BUTTON_BORDER_COLOR, + BUTTON_BORDER_RADIUS_SIZE, BUTTON_PADDING, +}; + +#[path = "../helpers/widgets.rs"] +mod widgets; + +/// The speed at which the cube rotates, in radians per frame. +const CUBE_ROTATION_SPEED: f32 = 0.02; + +/// The speed at which the selection can be moved, in spherical coordinate +/// radians per mouse unit. +const MOVE_SPEED: f32 = 0.008; +/// The speed at which the selection can be scaled, in reciprocal mouse units. +const SCALE_SPEED: f32 = 0.05; +/// The speed at which the selection can be scaled, in radians per mouse unit. +const ROLL_SPEED: f32 = 0.01; + +/// Various settings for the demo. +#[derive(Resource, Default)] +struct AppStatus { + /// The object that will be moved, scaled, or rotated when the mouse is + /// dragged. + selection: Selection, + /// What happens when the mouse is dragged: one of a move, rotate, or scale + /// operation. + drag_mode: DragMode, +} + +/// The object that will be moved, scaled, or rotated when the mouse is dragged. +#[derive(Clone, Copy, Component, Default, PartialEq)] +enum Selection { + /// The camera. + /// + /// The camera can only be moved, not scaled or rotated. + #[default] + Camera, + /// The spotlight, which uses a torch-like light texture + SpotLight, + /// The point light, which uses a light texture cubemap constructed from the faces mesh + PointLight, + /// The directional light, which uses a caustic-like texture + DirectionalLight, +} + +impl fmt::Display for Selection { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match *self { + Selection::Camera => f.write_str("camera"), + Selection::SpotLight => f.write_str("spotlight"), + Selection::PointLight => f.write_str("point light"), + Selection::DirectionalLight => f.write_str("directional light"), + } + } +} + +/// What happens when the mouse is dragged: one of a move, rotate, or scale +/// operation. +#[derive(Clone, Copy, Component, Default, PartialEq, Debug)] +enum DragMode { + /// The mouse moves the current selection. + #[default] + Move, + /// The mouse scales the current selection. + /// + /// This only applies to decals, not cameras. + Scale, + /// The mouse rotates the current selection around its local Z axis. + /// + /// This only applies to decals, not cameras. + Roll, +} + +impl fmt::Display for DragMode { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match *self { + DragMode::Move => f.write_str("move"), + DragMode::Scale => f.write_str("scale"), + DragMode::Roll => f.write_str("roll"), + } + } +} + +/// A marker component for the help text in the top left corner of the window. +#[derive(Clone, Copy, Component)] +struct HelpText; + +/// Entry point. +fn main() { + App::new() + .add_plugins(DefaultPlugins.set(WindowPlugin { + primary_window: Some(Window { + title: "Bevy Light Textures Example".into(), + ..default() + }), + ..default() + })) + .init_resource::() + .add_event::>() + .add_event::>() + .add_systems(Startup, setup) + .add_systems(Update, draw_gizmos) + .add_systems(Update, rotate_cube) + .add_systems(Update, hide_shadows) + .add_systems(Update, widgets::handle_ui_interactions::) + .add_systems(Update, widgets::handle_ui_interactions::) + .add_systems( + Update, + (handle_selection_change, update_radio_buttons) + .after(widgets::handle_ui_interactions::) + .after(widgets::handle_ui_interactions::), + ) + .add_systems(Update, toggle_visibility) + .add_systems(Update, update_directional_light) + .add_systems(Update, process_move_input) + .add_systems(Update, process_scale_input) + .add_systems(Update, process_roll_input) + .add_systems(Update, switch_drag_mode) + .add_systems(Update, update_help_text) + .add_systems(Update, update_button_visibility) + .run(); +} + +/// Creates the scene. +fn setup( + mut commands: Commands, + asset_server: Res, + app_status: Res, + render_device: Res, + render_adapter: Res, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + // Error out if clustered decals (and so light textures) aren't supported on the current platform. + if !decal::clustered::clustered_decals_are_usable(&render_device, &render_adapter) { + error!("Light textures aren't usable on this platform."); + commands.send_event(AppExit::error()); + } + + spawn_cubes(&mut commands, &mut meshes, &mut materials); + spawn_camera(&mut commands); + spawn_light(&mut commands, &asset_server); + spawn_buttons(&mut commands); + spawn_help_text(&mut commands, &app_status); + spawn_light_textures(&mut commands, &asset_server, &mut meshes, &mut materials); +} + +#[derive(Component)] +struct Rotate; + +/// Spawns the cube onto which the decals are projected. +fn spawn_cubes( + commands: &mut Commands, + meshes: &mut Assets, + materials: &mut Assets, +) { + // Rotate the cube a bit just to make it more interesting. + let mut transform = Transform::IDENTITY; + transform.rotate_y(FRAC_PI_3); + + commands.spawn(( + Mesh3d(meshes.add(Cuboid::new(3.0, 3.0, 3.0))), + MeshMaterial3d(materials.add(StandardMaterial { + base_color: SILVER.into(), + ..default() + })), + transform, + Rotate, + )); + + commands.spawn(( + Mesh3d(meshes.add(Cuboid::new(-13.0, -13.0, -13.0))), + MeshMaterial3d(materials.add(StandardMaterial { + base_color: SILVER.into(), + ..default() + })), + transform, + )); +} + +/// Spawns the directional light. +fn spawn_light(commands: &mut Commands, asset_server: &AssetServer) { + commands + .spawn(( + Visibility::Hidden, + Transform::from_xyz(8.0, 8.0, 4.0).looking_at(Vec3::ZERO, Vec3::Y), + Selection::DirectionalLight, + )) + .with_child(( + DirectionalLight { + illuminance: AMBIENT_DAYLIGHT, + ..default() + }, + DirectionalLightTexture { + image: asset_server.load("lightmaps/caustic_directional_texture.png"), + tiled: true, + }, + Visibility::Visible, + )); +} + +/// Spawns the camera. +fn spawn_camera(commands: &mut Commands) { + commands + .spawn(Camera3d::default()) + .insert(Transform::from_xyz(0.0, 2.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y)) + // Tag the camera with `Selection::Camera`. + .insert(Selection::Camera); +} + +fn spawn_light_textures( + commands: &mut Commands, + asset_server: &AssetServer, + meshes: &mut Assets, + materials: &mut Assets, +) { + commands.spawn(( + SpotLight { + color: Color::srgb(1.0, 1.0, 0.8), + intensity: 10e6, + outer_angle: 0.25, + inner_angle: 0.25, + shadows_enabled: true, + ..default() + }, + Transform::from_translation(Vec3::new(6.0, 1.0, 2.0)).looking_at(Vec3::ZERO, Vec3::Y), + SpotLightTexture { + image: asset_server.load("lightmaps/torch_spotlight_texture.png"), + }, + Visibility::Inherited, + Selection::SpotLight, + )); + + commands + .spawn(( + Visibility::Hidden, + Transform::from_translation(Vec3::new(0.0, 1.8, 0.01)).with_scale(Vec3::splat(0.1)), + Selection::PointLight, + )) + .with_children(|parent| { + parent.spawn(SceneRoot( + asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/Faces/faces.glb")), + )); + + parent.spawn(( + Mesh3d(meshes.add(Sphere::new(1.0))), + MeshMaterial3d(materials.add(StandardMaterial { + emissive: Color::srgb(0.0, 0.0, 300.0).to_linear(), + ..default() + })), + )); + + parent.spawn(( + PointLight { + color: Color::srgb(0.0, 0.0, 1.0), + intensity: 1e6, + shadows_enabled: true, + ..default() + }, + PointLightTexture { + image: asset_server.load("lightmaps/faces_pointlight_texture_blurred.png"), + cubemap_layout: decal::clustered::CubemapLayout::CrossVertical, + }, + )); + }); +} + +/// Spawns the buttons at the bottom of the screen. +fn spawn_buttons(commands: &mut Commands) { + // Spawn the radio buttons that allow the user to select an object to + // control. + commands + .spawn(widgets::main_ui_node()) + .with_children(|parent| { + widgets::spawn_option_buttons( + parent, + "Drag to Move", + &[ + (Selection::Camera, "Camera"), + (Selection::SpotLight, "Spotlight"), + (Selection::PointLight, "Point Light"), + (Selection::DirectionalLight, "Directional Light"), + ], + ); + }); + + // Spawn the drag buttons that allow the user to control the scale and roll + // of the selected object. + commands + .spawn(Node { + flex_direction: FlexDirection::Row, + position_type: PositionType::Absolute, + right: Val::Px(10.0), + bottom: Val::Px(10.0), + column_gap: Val::Px(6.0), + ..default() + }) + .with_children(|parent| { + widgets::spawn_option_buttons( + parent, + "", + &[ + (Visibility::Inherited, "Show"), + (Visibility::Hidden, "Hide"), + ], + ); + spawn_drag_button(parent, "Scale").insert(DragMode::Scale); + spawn_drag_button(parent, "Roll").insert(DragMode::Roll); + }); +} + +/// Spawns a button that the user can drag to change a parameter. +fn spawn_drag_button<'a>( + commands: &'a mut ChildSpawnerCommands, + label: &str, +) -> EntityCommands<'a> { + let mut kid = commands.spawn(Node { + border: BUTTON_BORDER, + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + padding: BUTTON_PADDING, + ..default() + }); + kid.insert(( + Button, + BackgroundColor(Color::BLACK), + BorderRadius::all(BUTTON_BORDER_RADIUS_SIZE), + BUTTON_BORDER_COLOR, + )) + .with_children(|parent| { + widgets::spawn_ui_text(parent, label, Color::WHITE); + }); + kid +} + +/// Spawns the help text at the top of the screen. +fn spawn_help_text(commands: &mut Commands, app_status: &AppStatus) { + commands.spawn(( + Text::new(create_help_string(app_status)), + Node { + position_type: PositionType::Absolute, + top: Val::Px(12.0), + left: Val::Px(12.0), + ..default() + }, + HelpText, + )); +} + +/// Draws the outlines that show the bounds of the spotlight. +fn draw_gizmos(mut gizmos: Gizmos, spotlight: Query<(&GlobalTransform, &SpotLight, &Visibility)>) { + if let Ok((global_transform, spotlight, visibility)) = spotlight.single() { + if visibility != Visibility::Hidden { + gizmos.primitive_3d( + &Cone::new(7.0 * spotlight.outer_angle, 7.0), + Isometry3d { + rotation: global_transform.rotation() * Quat::from_rotation_x(FRAC_PI_2), + translation: global_transform.translation_vec3a() * 0.5, + }, + YELLOW, + ); + } + } +} + +/// Rotates the cube a bit every frame. +fn rotate_cube(mut meshes: Query<&mut Transform, With>) { + for mut transform in &mut meshes { + transform.rotate_y(CUBE_ROTATION_SPEED); + } +} + +/// Hide shadows on all meshes except the main cube +fn hide_shadows( + mut commands: Commands, + meshes: Query, Without, Without)>, +) { + for ent in &meshes { + commands.entity(ent).insert(NotShadowCaster); + } +} + +/// Updates the state of the radio buttons when the user clicks on one. +fn update_radio_buttons( + mut widgets: Query<( + Entity, + Option<&mut BackgroundColor>, + Has, + &WidgetClickSender, + )>, + app_status: Res, + mut writer: TextUiWriter, + visible: Query<(&Visibility, &Selection)>, + mut visibility_widgets: Query< + ( + Entity, + Option<&mut BackgroundColor>, + Has, + &WidgetClickSender, + ), + Without>, + >, +) { + for (entity, maybe_bg_color, has_text, sender) in &mut widgets { + let selected = app_status.selection == **sender; + if let Some(mut bg_color) = maybe_bg_color { + widgets::update_ui_radio_button(&mut bg_color, selected); + } + if has_text { + widgets::update_ui_radio_button_text(entity, &mut writer, selected); + } + } + + let visibility = visible + .iter() + .filter(|(_, selection)| **selection == app_status.selection) + .map(|(visibility, _)| *visibility) + .next() + .unwrap_or_default(); + for (entity, maybe_bg_color, has_text, sender) in &mut visibility_widgets { + if let Some(mut bg_color) = maybe_bg_color { + widgets::update_ui_radio_button(&mut bg_color, **sender == visibility); + } + if has_text { + widgets::update_ui_radio_button_text(entity, &mut writer, **sender == visibility); + } + } +} + +/// Changes the selection when the user clicks a radio button. +fn handle_selection_change( + mut events: EventReader>, + mut app_status: ResMut, +) { + for event in events.read() { + app_status.selection = **event; + } +} + +fn toggle_visibility( + mut events: EventReader>, + app_status: Res, + mut visibility: Query<(&mut Visibility, &Selection)>, +) { + if let Some(vis) = events.read().last() { + for (mut visibility, selection) in visibility.iter_mut() { + if selection == &app_status.selection { + *visibility = **vis; + } + } + } +} + +/// Process a drag event that moves the selected object. +fn process_move_input( + mut selections: Query<(&mut Transform, &Selection)>, + mouse_buttons: Res>, + mouse_motion: Res, + app_status: Res, +) { + // Only process drags when movement is selected. + if !mouse_buttons.pressed(MouseButton::Left) || app_status.drag_mode != DragMode::Move { + return; + } + + for (mut transform, selection) in &mut selections { + if app_status.selection != *selection { + continue; + } + + // use simple movement for the point light + if *selection == Selection::PointLight { + transform.translation += + (mouse_motion.delta * Vec2::new(1.0, -1.0) * MOVE_SPEED).extend(0.0); + return; + } + + let position = transform.translation; + + // Convert to spherical coordinates. + let radius = position.length(); + let mut theta = acos(position.y / radius); + let mut phi = position.z.signum() * acos(position.x * position.xz().length_recip()); + + // Camera movement is the inverse of object movement. + let (phi_factor, theta_factor) = match *selection { + Selection::Camera => (1.0, -1.0), + _ => (-1.0, 1.0), + }; + + // Adjust the spherical coordinates. Clamp the inclination to (0, π). + phi += phi_factor * mouse_motion.delta.x * MOVE_SPEED; + theta = f32::clamp( + theta + theta_factor * mouse_motion.delta.y * MOVE_SPEED, + 0.001, + PI - 0.001, + ); + + // Convert spherical coordinates back to Cartesian coordinates. + transform.translation = + radius * vec3(sin(theta) * cos(phi), cos(theta), sin(theta) * sin(phi)); + + // Look at the center, but preserve the previous roll angle. + let roll = transform.rotation.to_euler(EulerRot::YXZ).2; + transform.look_at(Vec3::ZERO, Vec3::Y); + let (yaw, pitch, _) = transform.rotation.to_euler(EulerRot::YXZ); + transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch, roll); + } +} + +/// Processes a drag event that scales the selected target. +fn process_scale_input( + mut scale_selections: Query<(&mut Transform, &Selection)>, + mut spotlight_selections: Query<(&mut SpotLight, &Selection)>, + mouse_buttons: Res>, + mouse_motion: Res, + app_status: Res, +) { + // Only process drags when the scaling operation is selected. + if !mouse_buttons.pressed(MouseButton::Left) || app_status.drag_mode != DragMode::Scale { + return; + } + + for (mut transform, selection) in &mut scale_selections { + if app_status.selection == *selection { + transform.scale *= 1.0 + mouse_motion.delta.x * SCALE_SPEED; + } + } + + for (mut spotlight, selection) in &mut spotlight_selections { + if app_status.selection == *selection { + spotlight.outer_angle = + (spotlight.outer_angle * (1.0 + mouse_motion.delta.x * SCALE_SPEED)).min(FRAC_PI_4); + spotlight.inner_angle = spotlight.outer_angle; + } + } +} + +/// Processes a drag event that rotates the selected target along its local Z +/// axis. +fn process_roll_input( + mut selections: Query<(&mut Transform, &Selection)>, + mouse_buttons: Res>, + mouse_motion: Res, + app_status: Res, +) { + // Only process drags when the rolling operation is selected. + if !mouse_buttons.pressed(MouseButton::Left) || app_status.drag_mode != DragMode::Roll { + return; + } + + for (mut transform, selection) in &mut selections { + if app_status.selection != *selection { + continue; + } + + let (yaw, pitch, mut roll) = transform.rotation.to_euler(EulerRot::YXZ); + roll += mouse_motion.delta.x * ROLL_SPEED; + transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch, roll); + } +} + +/// Creates the help string at the top left of the screen. +fn create_help_string(app_status: &AppStatus) -> String { + format!( + "Click and drag to {} {}", + app_status.drag_mode, app_status.selection + ) +} + +/// Changes the drag mode when the user hovers over the "Scale" and "Roll" +/// buttons in the lower right. +/// +/// If the user is hovering over no such button, this system changes the drag +/// mode back to its default value of [`DragMode::Move`]. +fn switch_drag_mode( + mut commands: Commands, + mut interactions: Query<(&Interaction, &DragMode)>, + mut windows: Query>, + mouse_buttons: Res>, + mut app_status: ResMut, +) { + if mouse_buttons.pressed(MouseButton::Left) { + return; + } + + for (interaction, drag_mode) in &mut interactions { + if *interaction != Interaction::Hovered { + continue; + } + + app_status.drag_mode = *drag_mode; + + // Set the cursor to provide the user with a nice visual hint. + for window in &mut windows { + commands + .entity(window) + .insert(CursorIcon::from(SystemCursorIcon::EwResize)); + } + return; + } + + app_status.drag_mode = DragMode::Move; + + for window in &mut windows { + commands.entity(window).remove::(); + } +} + +/// Updates the help text in the top left of the screen to reflect the current +/// selection and drag mode. +fn update_help_text(mut help_text: Query<&mut Text, With>, app_status: Res) { + for mut text in &mut help_text { + text.0 = create_help_string(&app_status); + } +} + +/// Updates the visibility of the drag mode buttons so that they aren't visible +/// if the camera is selected. +fn update_button_visibility( + mut nodes: Query<&mut Visibility, Or<(With, With>)>>, + app_status: Res, +) { + for mut visibility in &mut nodes { + *visibility = match app_status.selection { + Selection::Camera => Visibility::Hidden, + _ => Visibility::Visible, + }; + } +} + +fn update_directional_light( + mut commands: Commands, + asset_server: Res, + selections: Query<(&Selection, &Visibility)>, + mut light: Query<( + Entity, + &mut DirectionalLight, + Option<&DirectionalLightTexture>, + )>, +) { + let directional_visible = selections + .iter() + .filter(|(selection, _)| **selection == Selection::DirectionalLight) + .any(|(_, visibility)| visibility != Visibility::Hidden); + let any_texture_light_visible = selections + .iter() + .filter(|(selection, _)| { + **selection == Selection::PointLight || **selection == Selection::SpotLight + }) + .any(|(_, visibility)| visibility != Visibility::Hidden); + + let (entity, mut light, maybe_texture) = light + .single_mut() + .expect("there should be a single directional light"); + + if directional_visible { + light.illuminance = AMBIENT_DAYLIGHT; + if maybe_texture.is_none() { + commands.entity(entity).insert(DirectionalLightTexture { + image: asset_server.load("lightmaps/caustic_directional_texture.png"), + tiled: true, + }); + } + } else if any_texture_light_visible { + light.illuminance = CLEAR_SUNRISE; + if maybe_texture.is_some() { + commands.entity(entity).remove::(); + } + } else { + light.illuminance = AMBIENT_DAYLIGHT; + if maybe_texture.is_some() { + commands.entity(entity).remove::(); + } + } +} diff --git a/examples/3d/lines.rs b/examples/3d/lines.rs index d755aa434a..0ee62f599c 100644 --- a/examples/3d/lines.rs +++ b/examples/3d/lines.rs @@ -77,7 +77,7 @@ impl Material for LineMaterial { } fn specialize( - _pipeline: &MaterialPipeline, + _pipeline: &MaterialPipeline, descriptor: &mut RenderPipelineDescriptor, _layout: &MeshVertexBufferLayoutRef, _key: MaterialPipelineKey, diff --git a/examples/3d/manual_material.rs b/examples/3d/manual_material.rs new file mode 100644 index 0000000000..f93265a50a --- /dev/null +++ b/examples/3d/manual_material.rs @@ -0,0 +1,315 @@ +//! A simple 3D scene with light shining over a cube sitting on a plane. + +use bevy::{ + asset::{AsAssetId, AssetEventSystems}, + core_pipeline::core_3d::Opaque3d, + ecs::system::{ + lifetimeless::{SRes, SResMut}, + SystemChangeTick, SystemParamItem, + }, + pbr::{ + DrawMaterial, EntitiesNeedingSpecialization, EntitySpecializationTicks, + MaterialBindGroupAllocator, MaterialBindGroupAllocators, MaterialDrawFunction, + MaterialFragmentShader, MaterialProperties, PreparedMaterial, RenderMaterialBindings, + RenderMaterialInstance, RenderMaterialInstances, SpecializedMaterialPipelineCache, + }, + platform::collections::hash_map::Entry, + prelude::*, + render::{ + erased_render_asset::{ErasedRenderAsset, ErasedRenderAssetPlugin, PrepareAssetError}, + render_asset::RenderAssets, + render_phase::DrawFunctions, + render_resource::{ + binding_types::{sampler, texture_2d}, + AsBindGroup, BindGroupLayout, BindGroupLayoutEntries, BindingResources, + OwnedBindingResource, Sampler, SamplerBindingType, SamplerDescriptor, ShaderStages, + TextureSampleType, TextureViewDimension, UnpreparedBindGroup, + }, + renderer::RenderDevice, + sync_world::MainEntity, + texture::GpuImage, + view::ExtractedView, + Extract, RenderApp, + }, + utils::Parallel, +}; +use std::{any::TypeId, sync::Arc}; + +const SHADER_ASSET_PATH: &str = "shaders/manual_material.wgsl"; + +fn main() { + App::new() + .add_plugins((DefaultPlugins, ImageMaterialPlugin)) + .add_systems(Startup, setup) + .run(); +} + +struct ImageMaterialPlugin; + +impl Plugin for ImageMaterialPlugin { + fn build(&self, app: &mut App) { + app.init_asset::() + .add_plugins(ErasedRenderAssetPlugin::::default()) + .add_systems( + PostUpdate, + check_entities_needing_specialization.after(AssetEventSystems), + ) + .init_resource::>(); + } + + fn finish(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app.add_systems( + ExtractSchedule, + ( + extract_image_materials, + extract_image_materials_needing_specialization, + ), + ); + + render_app.world_mut().resource_scope( + |world: &mut World, mut bind_group_allocators: Mut| { + world.resource_scope(|world: &mut World, render_device: Mut| { + let bind_group_layout = render_device.create_bind_group_layout( + "image_material_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + texture_2d(TextureSampleType::Float { filterable: false }), + sampler(SamplerBindingType::NonFiltering), + ), + ), + ); + let sampler = render_device.create_sampler(&SamplerDescriptor::default()); + world.insert_resource(ImageMaterialBindGroupLayout(bind_group_layout.clone())); + world.insert_resource(ImageMaterialBindGroupSampler(sampler)); + + bind_group_allocators.insert( + TypeId::of::(), + MaterialBindGroupAllocator::new( + &render_device, + None, + None, + bind_group_layout, + None, + ), + ); + }); + }, + ); + } +} + +#[derive(Resource)] +struct ImageMaterialBindGroupLayout(BindGroupLayout); + +#[derive(Resource)] +struct ImageMaterialBindGroupSampler(Sampler); + +#[derive(Component)] +struct ImageMaterial3d(Handle); + +impl AsAssetId for ImageMaterial3d { + type Asset = ImageMaterial; + + fn as_asset_id(&self) -> AssetId { + self.0.id() + } +} + +#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)] +struct ImageMaterial { + image: Handle, +} + +impl ErasedRenderAsset for ImageMaterial { + type SourceAsset = ImageMaterial; + type ErasedAsset = PreparedMaterial; + type Param = ( + SRes>, + SRes, + SRes, + SResMut, + SResMut, + SRes>, + SRes, + ); + + fn prepare_asset( + source_asset: Self::SourceAsset, + asset_id: AssetId, + ( + opaque_draw_functions, + material_layout, + asset_server, + bind_group_allocators, + render_material_bindings, + gpu_images, + image_material_sampler, + ): &mut SystemParamItem, + ) -> std::result::Result> { + let material_layout = material_layout.0.clone(); + let draw_function_id = opaque_draw_functions.read().id::(); + let bind_group_allocator = bind_group_allocators + .get_mut(&TypeId::of::()) + .unwrap(); + let Some(image) = gpu_images.get(&source_asset.image) else { + return Err(PrepareAssetError::RetryNextUpdate(source_asset)); + }; + let unprepared = UnpreparedBindGroup { + bindings: BindingResources(vec![ + ( + 0, + OwnedBindingResource::TextureView( + TextureViewDimension::D2, + image.texture_view.clone(), + ), + ), + ( + 1, + OwnedBindingResource::Sampler( + SamplerBindingType::NonFiltering, + image_material_sampler.0.clone(), + ), + ), + ]), + }; + let binding = match render_material_bindings.entry(asset_id.into()) { + Entry::Occupied(mut occupied_entry) => { + bind_group_allocator.free(*occupied_entry.get()); + let new_binding = + bind_group_allocator.allocate_unprepared(unprepared, &material_layout); + *occupied_entry.get_mut() = new_binding; + new_binding + } + Entry::Vacant(vacant_entry) => *vacant_entry + .insert(bind_group_allocator.allocate_unprepared(unprepared, &material_layout)), + }; + + let mut properties = MaterialProperties { + material_layout: Some(material_layout), + ..Default::default() + }; + properties.add_draw_function(MaterialDrawFunction, draw_function_id); + properties.add_shader(MaterialFragmentShader, asset_server.load(SHADER_ASSET_PATH)); + + Ok(PreparedMaterial { + binding, + properties: Arc::new(properties), + }) + } +} + +/// set up a simple 3D scene +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + asset_server: Res, +) { + // cube + commands.spawn(( + Mesh3d(meshes.add(Cuboid::new(2.0, 2.0, 2.0))), + ImageMaterial3d(materials.add(ImageMaterial { + image: asset_server.load("branding/icon.png"), + })), + Transform::from_xyz(0.0, 0.5, 0.0), + )); + // light + commands.spawn(( + PointLight { + shadows_enabled: true, + ..default() + }, + Transform::from_xyz(4.0, 8.0, 4.0), + )); + // camera + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y), + )); +} + +fn extract_image_materials( + mut material_instances: ResMut, + changed_meshes_query: Extract< + Query< + (Entity, &ViewVisibility, &ImageMaterial3d), + Or<(Changed, Changed)>, + >, + >, +) { + let last_change_tick = material_instances.current_change_tick; + + for (entity, view_visibility, material) in &changed_meshes_query { + if view_visibility.get() { + material_instances.instances.insert( + entity.into(), + RenderMaterialInstance { + asset_id: material.0.id().untyped(), + last_change_tick, + }, + ); + } else { + material_instances + .instances + .remove(&MainEntity::from(entity)); + } + } +} + +fn check_entities_needing_specialization( + needs_specialization: Query< + Entity, + ( + Or<( + Changed, + AssetChanged, + Changed, + AssetChanged, + )>, + With, + ), + >, + mut par_local: Local>>, + mut entities_needing_specialization: ResMut>, +) { + entities_needing_specialization.clear(); + + needs_specialization + .par_iter() + .for_each(|entity| par_local.borrow_local_mut().push(entity)); + + par_local.drain_into(&mut entities_needing_specialization); +} + +fn extract_image_materials_needing_specialization( + entities_needing_specialization: Extract>>, + mut entity_specialization_ticks: ResMut, + mut removed_mesh_material_components: Extract>, + mut specialized_material_pipeline_cache: ResMut, + views: Query<&ExtractedView>, + ticks: SystemChangeTick, +) { + // Clean up any despawned entities, we do this first in case the removed material was re-added + // the same frame, thus will appear both in the removed components list and have been added to + // the `EntitiesNeedingSpecialization` collection by triggering the `Changed` filter + for entity in removed_mesh_material_components.read() { + entity_specialization_ticks.remove(&MainEntity::from(entity)); + for view in views { + if let Some(cache) = + specialized_material_pipeline_cache.get_mut(&view.retained_view_entity) + { + cache.remove(&MainEntity::from(entity)); + } + } + } + + for entity in entities_needing_specialization.iter() { + // Update the entity's specialization tick with this run's tick + entity_specialization_ticks.insert((*entity).into(), ticks.this_run()); + } +} diff --git a/examples/3d/meshlet.rs b/examples/3d/meshlet.rs index 88678b90ac..8cab04ae99 100644 --- a/examples/3d/meshlet.rs +++ b/examples/3d/meshlet.rs @@ -17,7 +17,7 @@ use camera_controller::{CameraController, CameraControllerPlugin}; use std::{f32::consts::PI, path::Path, process::ExitCode}; const ASSET_URL: &str = - "https://raw.githubusercontent.com/JMS55/bevy_meshlet_asset/7a7c14138021f63904b584d5f7b73b695c7f4bbf/bunny.meshlet_mesh"; + "https://raw.githubusercontent.com/atlv24/assets/69bb39164fd35aadf863f6009520d4981eafcea0/bunny.meshlet_mesh"; fn main() -> ExitCode { if !Path::new("./assets/external/models/bunny.meshlet_mesh").exists() { @@ -30,7 +30,7 @@ fn main() -> ExitCode { .add_plugins(( DefaultPlugins, MeshletPlugin { - cluster_buffer_slots: 8192, + cluster_buffer_slots: 1 << 14, }, MaterialPlugin::::default(), CameraControllerPlugin, diff --git a/examples/3d/occlusion_culling.rs b/examples/3d/occlusion_culling.rs index f0cdbffa2a..0783a28948 100644 --- a/examples/3d/occlusion_culling.rs +++ b/examples/3d/occlusion_culling.rs @@ -30,7 +30,7 @@ use bevy::{ IndirectParametersIndexed, }, experimental::occlusion_culling::OcclusionCulling, - render_graph::{self, NodeRunError, RenderGraphApp, RenderGraphContext, RenderLabel}, + render_graph::{self, NodeRunError, RenderGraphContext, RenderGraphExt, RenderLabel}, render_resource::{Buffer, BufferDescriptor, BufferUsages, MapMode}, renderer::{RenderContext, RenderDevice}, settings::WgpuFeatures, diff --git a/examples/README.md b/examples/README.md index b966e75d1d..993299dfc6 100644 --- a/examples/README.md +++ b/examples/README.md @@ -160,11 +160,13 @@ Example | Description [Fog volumes](../examples/3d/fog_volumes.rs) | Demonstrates fog volumes [Generate Custom Mesh](../examples/3d/generate_custom_mesh.rs) | Simple showcase of how to generate a custom mesh with a custom texture [Irradiance Volumes](../examples/3d/irradiance_volumes.rs) | Demonstrates irradiance volumes +[Light Textures](../examples/3d/light_textures.rs) | Demonstrates light textures [Lighting](../examples/3d/lighting.rs) | Illustrates various lighting options in a simple scene [Lightmaps](../examples/3d/lightmaps.rs) | Rendering a scene with baked lightmaps [Lines](../examples/3d/lines.rs) | Create a custom material to draw 3d lines [Load glTF](../examples/3d/load_gltf.rs) | Loads and renders a glTF file as a scene [Load glTF extras](../examples/3d/load_gltf_extras.rs) | Loads and renders a glTF file as a scene, including the gltf extras +[Manual Material Implementation](../examples/3d/manual_material.rs) | Demonstrates how to implement a material manually using the mid-level render APIs [Mesh Ray Cast](../examples/3d/mesh_ray_cast.rs) | Demonstrates ray casting with the `MeshRayCast` system parameter [Meshlet](../examples/3d/meshlet.rs) | Meshlet rendering for dense high-poly scenes (experimental) [Mixed lighting](../examples/3d/mixed_lighting.rs) | Demonstrates how to combine baked and dynamic lighting @@ -561,6 +563,7 @@ Example | Description [Relative Cursor Position](../examples/ui/relative_cursor_position.rs) | Showcases the RelativeCursorPosition component [Render UI to Texture](../examples/ui/render_ui_to_texture.rs) | An example of rendering UI as a part of a 3D world [Scroll](../examples/ui/scroll.rs) | Demonstrates scrolling UI containers +[Scrollbars](../examples/ui/scrollbars.rs) | Demonstrates use of core scrollbar in Bevy UI [Size Constraints](../examples/ui/size_constraints.rs) | Demonstrates how the to use the size constraints to control the size of a UI node. [Stacked Gradients](../examples/ui/stacked_gradients.rs) | An example demonstrating stacked gradients [Tab Navigation](../examples/ui/tab_navigation.rs) | Demonstration of Tab Navigation between UI elements diff --git a/examples/animation/animation_graph.rs b/examples/animation/animation_graph.rs index f420b05f38..f33c3850df 100644 --- a/examples/animation/animation_graph.rs +++ b/examples/animation/animation_graph.rs @@ -182,6 +182,10 @@ fn setup_assets_programmatically( .spawn(async move { use std::io::Write; + let animation_graph: SerializedAnimationGraph = animation_graph + .try_into() + .expect("The animation graph failed to convert to its serialized form"); + let serialized_graph = ron::ser::to_string_pretty(&animation_graph, PrettyConfig::default()) .expect("Failed to serialize the animation graph"); diff --git a/examples/app/log_layers_ecs.rs b/examples/app/log_layers_ecs.rs index 55e57081ff..bcf5e5571e 100644 --- a/examples/app/log_layers_ecs.rs +++ b/examples/app/log_layers_ecs.rs @@ -150,13 +150,16 @@ fn print_logs( commands.entity(root_entity).with_children(|child| { for event in events.read() { - child.spawn(Text::default()).with_children(|child| { - child.spawn(( - TextSpan::new(format!("{:5} ", event.level)), - TextColor(level_color(&event.level)), - )); - child.spawn(TextSpan::new(&event.message)); - }); + child.spawn(( + Text::default(), + children![ + ( + TextSpan::new(format!("{:5} ", event.level)), + TextColor(level_color(&event.level)), + ), + TextSpan::new(&event.message), + ], + )); } }); } diff --git a/examples/shader/compute_shader_game_of_life.rs b/examples/shader/compute_shader_game_of_life.rs index cb7d283d1e..aa10ccf4bf 100644 --- a/examples/shader/compute_shader_game_of_life.rs +++ b/examples/shader/compute_shader_game_of_life.rs @@ -86,9 +86,9 @@ fn setup(mut commands: Commands, mut images: ResMut>) { // Switch texture to display every frame to show the one that was written to most recently. fn switch_textures(images: Res, mut sprite: Single<&mut Sprite>) { if sprite.image == images.texture_a { - sprite.image = images.texture_b.clone_weak(); + sprite.image = images.texture_b.clone(); } else { - sprite.image = images.texture_a.clone_weak(); + sprite.image = images.texture_a.clone(); } } @@ -173,22 +173,16 @@ impl FromWorld for GameOfLifePipeline { let shader = world.load_asset(SHADER_ASSET_PATH); let pipeline_cache = world.resource::(); let init_pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { - label: None, layout: vec![texture_bind_group_layout.clone()], - push_constant_ranges: Vec::new(), shader: shader.clone(), - shader_defs: vec![], - entry_point: Cow::from("init"), - zero_initialize_workgroup_memory: false, + entry_point: Some(Cow::from("init")), + ..default() }); let update_pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { - label: None, layout: vec![texture_bind_group_layout.clone()], - push_constant_ranges: Vec::new(), shader, - shader_defs: vec![], - entry_point: Cow::from("update"), - zero_initialize_workgroup_memory: false, + entry_point: Some(Cow::from("update")), + ..default() }); GameOfLifePipeline { diff --git a/examples/shader/custom_phase_item.rs b/examples/shader/custom_phase_item.rs index b363a7c27f..fd8dc063d0 100644 --- a/examples/shader/custom_phase_item.rs +++ b/examples/shader/custom_phase_item.rs @@ -24,11 +24,11 @@ use bevy::{ ViewBinnedRenderPhases, }, render_resource::{ - BufferUsages, ColorTargetState, ColorWrites, CompareFunction, DepthStencilState, - FragmentState, IndexFormat, MultisampleState, PipelineCache, PrimitiveState, - RawBufferVec, RenderPipelineDescriptor, SpecializedRenderPipeline, - SpecializedRenderPipelines, TextureFormat, VertexAttribute, VertexBufferLayout, - VertexFormat, VertexState, VertexStepMode, + BufferUsages, Canonical, ColorTargetState, ColorWrites, CompareFunction, + DepthStencilState, FragmentState, GetBaseDescriptor, IndexFormat, PipelineCache, + RawBufferVec, RenderPipeline, RenderPipelineDescriptor, SpecializedCache, Specializer, + SpecializerKey, TextureFormat, VertexAttribute, VertexBufferLayout, VertexFormat, + VertexState, VertexStepMode, }, renderer::{RenderDevice, RenderQueue}, view::{self, ExtractedView, RenderVisibleEntities, VisibilityClass}, @@ -49,14 +49,6 @@ use bytemuck::{Pod, Zeroable}; #[component(on_add = view::add_visibility_class::)] struct CustomRenderedEntity; -/// Holds a reference to our shader. -/// -/// This is loaded at app creation time. -#[derive(Resource)] -struct CustomPhasePipeline { - shader: Handle, -} - /// A [`RenderCommand`] that binds the vertex and index buffers and issues the /// draw command for our custom phase item. struct DrawCustomPhaseItem; @@ -175,8 +167,7 @@ fn main() { // We make sure to add these to the render app, not the main app. app.get_sub_app_mut(RenderApp) .unwrap() - .init_resource::() - .init_resource::>() + .init_resource::>() .add_render_command::() .add_systems( Render, @@ -221,10 +212,9 @@ fn prepare_custom_phase_item_buffers(mut commands: Commands) { /// the opaque render phases of each view. fn queue_custom_phase_item( pipeline_cache: Res, - custom_phase_pipeline: Res, mut opaque_render_phases: ResMut>, opaque_draw_functions: Res>, - mut specialized_render_pipelines: ResMut>, + mut specializer: ResMut>, views: Query<(&ExtractedView, &RenderVisibleEntities, &Msaa)>, mut next_tick: Local, ) { @@ -247,11 +237,10 @@ fn queue_custom_phase_item( // some per-view settings, such as whether the view is HDR, but for // simplicity's sake we simply hard-code the view's characteristics, // with the exception of number of MSAA samples. - let pipeline_id = specialized_render_pipelines.specialize( - &pipeline_cache, - &custom_phase_pipeline, - *msaa, - ); + let Ok(pipeline_id) = specializer.specialize(&pipeline_cache, CustomPhaseKey(*msaa)) + else { + continue; + }; // Bump the change tick in order to force Bevy to rebuild the bin. let this_tick = next_tick.get() + 1; @@ -286,18 +275,44 @@ fn queue_custom_phase_item( } } -impl SpecializedRenderPipeline for CustomPhasePipeline { - type Key = Msaa; +/// Holds a reference to our shader. +/// +/// This is loaded at app creation time. +struct CustomPhaseSpecializer { + shader: Handle, +} - fn specialize(&self, msaa: Self::Key) -> RenderPipelineDescriptor { +impl FromWorld for CustomPhaseSpecializer { + fn from_world(world: &mut World) -> Self { + let asset_server = world.resource::(); + Self { + shader: asset_server.load("shaders/custom_phase_item.wgsl"), + } + } +} + +#[derive(Copy, Clone, PartialEq, Eq, Hash, SpecializerKey)] +struct CustomPhaseKey(Msaa); + +impl Specializer for CustomPhaseSpecializer { + type Key = CustomPhaseKey; + + fn specialize( + &self, + key: Self::Key, + descriptor: &mut RenderPipelineDescriptor, + ) -> Result, BevyError> { + descriptor.multisample.count = key.0.samples(); + Ok(key) + } +} + +impl GetBaseDescriptor for CustomPhaseSpecializer { + fn get_base_descriptor(&self) -> RenderPipelineDescriptor { RenderPipelineDescriptor { label: Some("custom render pipeline".into()), - layout: vec![], - push_constant_ranges: vec![], vertex: VertexState { shader: self.shader.clone(), - shader_defs: vec![], - entry_point: "vertex".into(), buffers: vec![VertexBufferLayout { array_stride: size_of::() as u64, step_mode: VertexStepMode::Vertex, @@ -315,11 +330,10 @@ impl SpecializedRenderPipeline for CustomPhasePipeline { }, ], }], + ..default() }, fragment: Some(FragmentState { shader: self.shader.clone(), - shader_defs: vec![], - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { // Ordinarily, you'd want to check whether the view has the // HDR format and substitute the appropriate texture format @@ -328,8 +342,8 @@ impl SpecializedRenderPipeline for CustomPhasePipeline { blend: None, write_mask: ColorWrites::ALL, })], + ..default() }), - primitive: PrimitiveState::default(), // Note that if your view has no depth buffer this will need to be // changed. depth_stencil: Some(DepthStencilState { @@ -339,12 +353,7 @@ impl SpecializedRenderPipeline for CustomPhasePipeline { stencil: default(), bias: default(), }), - multisample: MultisampleState { - count: msaa.samples(), - mask: !0, - alpha_to_coverage_enabled: false, - }, - zero_initialize_workgroup_memory: false, + ..default() } } } @@ -375,14 +384,3 @@ impl FromWorld for CustomPhaseItemBuffers { } } } - -impl FromWorld for CustomPhasePipeline { - fn from_world(world: &mut World) -> Self { - // Load and compile the shader in the background. - let asset_server = world.resource::(); - - CustomPhasePipeline { - shader: asset_server.load("shaders/custom_phase_item.wgsl"), - } - } -} diff --git a/examples/shader/custom_post_processing.rs b/examples/shader/custom_post_processing.rs index 64bf312c03..81e2b7e17b 100644 --- a/examples/shader/custom_post_processing.rs +++ b/examples/shader/custom_post_processing.rs @@ -19,7 +19,7 @@ use bevy::{ UniformComponentPlugin, }, render_graph::{ - NodeRunError, RenderGraphApp, RenderGraphContext, RenderLabel, ViewNode, ViewNodeRunner, + NodeRunError, RenderGraphContext, RenderGraphExt, RenderLabel, ViewNode, ViewNodeRunner, }, render_resource::{ binding_types::{sampler, texture_2d, uniform_buffer}, @@ -27,7 +27,7 @@ use bevy::{ }, renderer::{RenderContext, RenderDevice}, view::ViewTarget, - RenderApp, + RenderApp, RenderStartup, }, }; @@ -66,6 +66,10 @@ impl Plugin for PostProcessPlugin { return; }; + // RenderStartup runs once on startup after all plugins are built + // It is useful to initialize data that will only live in the RenderApp + render_app.add_systems(RenderStartup, setup_pipeline); + render_app // Bevy's renderer uses a render graph which is a collection of nodes in a directed acyclic graph. // It currently runs on each view/camera and executes each node in the specified order. @@ -97,17 +101,6 @@ impl Plugin for PostProcessPlugin { ), ); } - - fn finish(&self, app: &mut App) { - // We need to get the render app from the main app - let Some(render_app) = app.get_sub_app_mut(RenderApp) else { - return; - }; - - render_app - // Initialize the pipeline - .init_resource::(); - } } #[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)] @@ -233,69 +226,60 @@ struct PostProcessPipeline { pipeline_id: CachedRenderPipelineId, } -impl FromWorld for PostProcessPipeline { - fn from_world(world: &mut World) -> Self { - let render_device = world.resource::(); - - // We need to define the bind group layout used for our pipeline - let layout = render_device.create_bind_group_layout( - "post_process_bind_group_layout", - &BindGroupLayoutEntries::sequential( - // The layout entries will only be visible in the fragment stage - ShaderStages::FRAGMENT, - ( - // The screen texture - texture_2d(TextureSampleType::Float { filterable: true }), - // The sampler that will be used to sample the screen texture - sampler(SamplerBindingType::Filtering), - // The settings uniform that will control the effect - uniform_buffer::(true), - ), +fn setup_pipeline( + mut commands: Commands, + render_device: Res, + asset_server: Res, + fullscreen_shader: Res, + pipeline_cache: Res, +) { + // We need to define the bind group layout used for our pipeline + let layout = render_device.create_bind_group_layout( + "post_process_bind_group_layout", + &BindGroupLayoutEntries::sequential( + // The layout entries will only be visible in the fragment stage + ShaderStages::FRAGMENT, + ( + // The screen texture + texture_2d(TextureSampleType::Float { filterable: true }), + // The sampler that will be used to sample the screen texture + sampler(SamplerBindingType::Filtering), + // The settings uniform that will control the effect + uniform_buffer::(true), ), - ); + ), + ); + // We can create the sampler here since it won't change at runtime and doesn't depend on the view + let sampler = render_device.create_sampler(&SamplerDescriptor::default()); - // We can create the sampler here since it won't change at runtime and doesn't depend on the view - let sampler = render_device.create_sampler(&SamplerDescriptor::default()); - - // Get the shader handle - let shader = world.load_asset(SHADER_ASSET_PATH); - // This will setup a fullscreen triangle for the vertex state. - let vertex_state = world.resource::().to_vertex_state(); - - let pipeline_id = world - .resource_mut::() - // This will add the pipeline to the cache and queue its creation - .queue_render_pipeline(RenderPipelineDescriptor { - label: Some("post_process_pipeline".into()), - layout: vec![layout.clone()], - vertex: vertex_state, - fragment: Some(FragmentState { - shader, - shader_defs: vec![], - // Make sure this matches the entry point of your shader. - // It can be anything as long as it matches here and in the shader. - entry_point: "fragment".into(), - targets: vec![Some(ColorTargetState { - format: TextureFormat::bevy_default(), - blend: None, - write_mask: ColorWrites::ALL, - })], - }), - // All of the following properties are not important for this effect so just use the default values. - // This struct doesn't have the Default trait implemented because not all fields can have a default value. - primitive: PrimitiveState::default(), - depth_stencil: None, - multisample: MultisampleState::default(), - push_constant_ranges: vec![], - zero_initialize_workgroup_memory: false, - }); - - Self { - layout, - sampler, - pipeline_id, - } - } + // Get the shader handle + let shader = asset_server.load(SHADER_ASSET_PATH); + // This will setup a fullscreen triangle for the vertex state. + let vertex_state = fullscreen_shader.to_vertex_state(); + let pipeline_id = pipeline_cache + // This will add the pipeline to the cache and queue its creation + .queue_render_pipeline(RenderPipelineDescriptor { + label: Some("post_process_pipeline".into()), + layout: vec![layout.clone()], + vertex: vertex_state, + fragment: Some(FragmentState { + shader, + // Make sure this matches the entry point of your shader. + // It can be anything as long as it matches here and in the shader. + targets: vec![Some(ColorTargetState { + format: TextureFormat::bevy_default(), + blend: None, + write_mask: ColorWrites::ALL, + })], + ..default() + }), + ..default() + }); + commands.insert_resource(PostProcessPipeline { + layout, + sampler, + pipeline_id, + }); } // This is the component that will get passed to the shader diff --git a/examples/shader/custom_render_phase.rs b/examples/shader/custom_render_phase.rs index 309954bbe3..89f5ce6d67 100644 --- a/examples/shader/custom_render_phase.rs +++ b/examples/shader/custom_render_phase.rs @@ -34,12 +34,12 @@ use bevy::{ }, GetBatchData, GetFullBatchData, }, - camera::ExtractedCamera, + camera::{ExtractedCamera, MainPassResolutionOverride}, extract_component::{ExtractComponent, ExtractComponentPlugin}, mesh::{allocator::MeshAllocator, MeshVertexBufferLayoutRef, RenderMesh}, render_asset::RenderAssets, render_graph::{ - NodeRunError, RenderGraphApp, RenderGraphContext, RenderLabel, ViewNode, ViewNodeRunner, + NodeRunError, RenderGraphContext, RenderGraphExt, RenderLabel, ViewNode, ViewNodeRunner, }, render_phase::{ sort_phase_system, AddRenderCommand, CachedRenderPipelinePhaseItem, DrawFunctionId, @@ -47,10 +47,10 @@ use bevy::{ SortedRenderPhasePlugin, ViewSortedRenderPhases, }, render_resource::{ - CachedRenderPipelineId, ColorTargetState, ColorWrites, Face, FragmentState, FrontFace, - MultisampleState, PipelineCache, PolygonMode, PrimitiveState, RenderPassDescriptor, - RenderPipelineDescriptor, SpecializedMeshPipeline, SpecializedMeshPipelineError, - SpecializedMeshPipelines, TextureFormat, VertexState, + CachedRenderPipelineId, ColorTargetState, ColorWrites, Face, FragmentState, + PipelineCache, PrimitiveState, RenderPassDescriptor, RenderPipelineDescriptor, + SpecializedMeshPipeline, SpecializedMeshPipelineError, SpecializedMeshPipelines, + TextureFormat, VertexState, }, renderer::RenderContext, sync_world::MainEntity, @@ -209,35 +209,28 @@ impl SpecializedMeshPipeline for StencilPipeline { // Bind group 2 is the mesh uniform self.mesh_pipeline.mesh_layouts.model_only.clone(), ], - push_constant_ranges: vec![], vertex: VertexState { shader: self.shader_handle.clone(), - shader_defs: vec![], - entry_point: "vertex".into(), buffers: vec![vertex_buffer_layout], + ..default() }, fragment: Some(FragmentState { shader: self.shader_handle.clone(), - shader_defs: vec![], - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format: TextureFormat::bevy_default(), blend: None, write_mask: ColorWrites::ALL, })], + ..default() }), primitive: PrimitiveState { topology: key.primitive_topology(), - front_face: FrontFace::Ccw, cull_mode: Some(Face::Back), - polygon_mode: PolygonMode::Fill, ..default() }, - depth_stencil: None, // It's generally recommended to specialize your pipeline for MSAA, // but it's not always possible - multisample: MultisampleState::default(), - zero_initialize_workgroup_memory: false, + ..default() }) } } @@ -589,13 +582,14 @@ impl ViewNode for CustomDrawNode { &'static ExtractedCamera, &'static ExtractedView, &'static ViewTarget, + Option<&'static MainPassResolutionOverride>, ); fn run<'w>( &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - (camera, view, target): QueryItem<'w, '_, Self::ViewQuery>, + (camera, view, target, resolution_override): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { // First, we need to get our phases resource @@ -625,7 +619,7 @@ impl ViewNode for CustomDrawNode { }); if let Some(viewport) = camera.viewport.as_ref() { - render_pass.set_camera_viewport(viewport); + render_pass.set_camera_viewport(&viewport.with_override(resolution_override)); } // Render the phase diff --git a/examples/shader/custom_vertex_attribute.rs b/examples/shader/custom_vertex_attribute.rs index 4cc8ea7dc5..46ab930851 100644 --- a/examples/shader/custom_vertex_attribute.rs +++ b/examples/shader/custom_vertex_attribute.rs @@ -74,7 +74,7 @@ impl Material for CustomMaterial { } fn specialize( - _pipeline: &MaterialPipeline, + _pipeline: &MaterialPipeline, descriptor: &mut RenderPipelineDescriptor, layout: &MeshVertexBufferLayoutRef, _key: MaterialPipelineKey, diff --git a/examples/shader/gpu_readback.rs b/examples/shader/gpu_readback.rs index 4db5795c89..964776291e 100644 --- a/examples/shader/gpu_readback.rs +++ b/examples/shader/gpu_readback.rs @@ -176,11 +176,8 @@ impl FromWorld for ComputePipeline { let pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { label: Some("GPU readback compute shader".into()), layout: vec![layout.clone()], - push_constant_ranges: Vec::new(), shader: shader.clone(), - shader_defs: Vec::new(), - entry_point: "main".into(), - zero_initialize_workgroup_memory: false, + ..default() }); ComputePipeline { layout, pipeline } } diff --git a/examples/shader/shader_defs.rs b/examples/shader/shader_defs.rs index 950ec182b4..1944040142 100644 --- a/examples/shader/shader_defs.rs +++ b/examples/shader/shader_defs.rs @@ -61,12 +61,12 @@ impl Material for CustomMaterial { } fn specialize( - _pipeline: &MaterialPipeline, + _pipeline: &MaterialPipeline, descriptor: &mut RenderPipelineDescriptor, _layout: &MeshVertexBufferLayoutRef, key: MaterialPipelineKey, ) -> Result<(), SpecializedMeshPipelineError> { - if key.bind_group_data.is_red { + if key.bind_group_data.is_red == 1 { let fragment = descriptor.fragment.as_mut().unwrap(); fragment.shader_defs.push("IS_RED".into()); } @@ -86,16 +86,19 @@ struct CustomMaterial { // This key is used to identify a specific permutation of this material pipeline. // In this case, we specialize on whether or not to configure the "IS_RED" shader def. // Specialization keys should be kept as small / cheap to hash as possible, -// as they will be used to look up the pipeline for each drawn entity with this material type. -#[derive(Eq, PartialEq, Hash, Clone)] +// as they will be used to look up the pipeline for each drawn entity with this material type, +// Which is why they are required to be `bytemuck::Pod` and `bytemuck::Zeroable` for materials +// that use the `AsBindGroup` derive macro. +#[repr(C)] +#[derive(Eq, PartialEq, Hash, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] struct CustomMaterialKey { - is_red: bool, + is_red: u32, } impl From<&CustomMaterial> for CustomMaterialKey { fn from(material: &CustomMaterial) -> Self { Self { - is_red: material.is_red, + is_red: material.is_red as u32, } } } diff --git a/examples/shader/shader_material_glsl.rs b/examples/shader/shader_material_glsl.rs index 1f25302a72..790f6637da 100644 --- a/examples/shader/shader_material_glsl.rs +++ b/examples/shader/shader_material_glsl.rs @@ -1,15 +1,9 @@ //! A shader that uses the GLSL shading language. use bevy::{ - pbr::{MaterialPipeline, MaterialPipelineKey}, prelude::*, reflect::TypePath, - render::{ - mesh::MeshVertexBufferLayoutRef, - render_resource::{ - AsBindGroup, RenderPipelineDescriptor, ShaderRef, SpecializedMeshPipelineError, - }, - }, + render::render_resource::{AsBindGroup, ShaderRef}, }; /// This example uses shader source files from the assets subdirectory @@ -74,18 +68,4 @@ impl Material for CustomMaterial { fn alpha_mode(&self) -> AlphaMode { self.alpha_mode } - - // Bevy assumes by default that vertex shaders use the "vertex" entry point - // and fragment shaders use the "fragment" entry point (for WGSL shaders). - // GLSL uses "main" as the entry point, so we must override the defaults here - fn specialize( - _pipeline: &MaterialPipeline, - descriptor: &mut RenderPipelineDescriptor, - _layout: &MeshVertexBufferLayoutRef, - _key: MaterialPipelineKey, - ) -> Result<(), SpecializedMeshPipelineError> { - descriptor.vertex.entry_point = "main".into(); - descriptor.fragment.as_mut().unwrap().entry_point = "main".into(); - Ok(()) - } } diff --git a/examples/shader/shader_material_wesl.rs b/examples/shader/shader_material_wesl.rs index 108093de78..c6db617542 100644 --- a/examples/shader/shader_material_wesl.rs +++ b/examples/shader/shader_material_wesl.rs @@ -101,15 +101,16 @@ struct CustomMaterial { party_mode: bool, } -#[derive(Eq, PartialEq, Hash, Clone)] +#[repr(C)] +#[derive(Eq, PartialEq, Hash, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] struct CustomMaterialKey { - party_mode: bool, + party_mode: u32, } impl From<&CustomMaterial> for CustomMaterialKey { fn from(material: &CustomMaterial) -> Self { Self { - party_mode: material.party_mode, + party_mode: material.party_mode as u32, } } } @@ -120,7 +121,7 @@ impl Material for CustomMaterial { } fn specialize( - _pipeline: &MaterialPipeline, + _pipeline: &MaterialPipeline, descriptor: &mut RenderPipelineDescriptor, _layout: &MeshVertexBufferLayoutRef, key: MaterialPipelineKey, @@ -128,7 +129,7 @@ impl Material for CustomMaterial { let fragment = descriptor.fragment.as_mut().unwrap(); fragment.shader_defs.push(ShaderDefVal::Bool( "PARTY_MODE".to_string(), - key.bind_group_data.party_mode, + key.bind_group_data.party_mode == 1, )); Ok(()) } diff --git a/examples/shader/specialized_mesh_pipeline.rs b/examples/shader/specialized_mesh_pipeline.rs index eeeeeb4a1e..f9aebcda7e 100644 --- a/examples/shader/specialized_mesh_pipeline.rs +++ b/examples/shader/specialized_mesh_pipeline.rs @@ -223,18 +223,14 @@ impl SpecializedMeshPipeline for CustomMeshPipeline { view_layout.empty_layout.clone(), self.mesh_pipeline.mesh_layouts.model_only.clone(), ], - push_constant_ranges: vec![], vertex: VertexState { shader: self.shader_handle.clone(), - shader_defs: vec![], - entry_point: "vertex".into(), // Customize how to store the meshes' vertex attributes in the vertex buffer buffers: vec![vertex_buffer_layout], + ..default() }, fragment: Some(FragmentState { shader: self.shader_handle.clone(), - shader_defs: vec![], - entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { // This isn't required, but bevy supports HDR and non-HDR rendering // so it's generally recommended to specialize the pipeline for that @@ -248,6 +244,7 @@ impl SpecializedMeshPipeline for CustomMeshPipeline { blend: None, write_mask: ColorWrites::ALL, })], + ..default() }), primitive: PrimitiveState { topology: mesh_key.primitive_topology(), @@ -269,9 +266,10 @@ impl SpecializedMeshPipeline for CustomMeshPipeline { // but it's not always possible multisample: MultisampleState { count: mesh_key.msaa_samples(), - ..MultisampleState::default() + ..default() }, - zero_initialize_workgroup_memory: false, + + ..default() }) } } diff --git a/examples/shader/texture_binding_array.rs b/examples/shader/texture_binding_array.rs index e4939edf6a..bfb439d81f 100644 --- a/examples/shader/texture_binding_array.rs +++ b/examples/shader/texture_binding_array.rs @@ -104,7 +104,7 @@ impl AsBindGroup for BindlessMaterial { layout: &BindGroupLayout, render_device: &RenderDevice, (image_assets, fallback_image): &mut SystemParamItem<'_, '_, Self::Param>, - ) -> Result, AsBindGroupError> { + ) -> Result { // retrieve the render resources from handles let mut images = vec![]; for handle in self.textures.iter().take(MAX_TEXTURE_COUNT) { @@ -135,17 +135,18 @@ impl AsBindGroup for BindlessMaterial { Ok(PreparedBindGroup { bindings: BindingResources(vec![]), bind_group, - data: (), }) } + fn bind_group_data(&self) -> Self::Data {} + fn unprepared_bind_group( &self, _layout: &BindGroupLayout, _render_device: &RenderDevice, _param: &mut SystemParamItem<'_, '_, Self::Param>, _force_no_bindless: bool, - ) -> Result, AsBindGroupError> { + ) -> Result { // We implement `as_bind_group`` directly because bindless texture // arrays can't be owned. // Or rather, they can be owned, but then you can't make a `&'a [&'a diff --git a/examples/testbed/full_ui.rs b/examples/testbed/full_ui.rs index 634945f057..da42dd9732 100644 --- a/examples/testbed/full_ui.rs +++ b/examples/testbed/full_ui.rs @@ -454,8 +454,8 @@ pub fn update_scroll_position( for (_pointer, pointer_map) in hover_map.iter() { for (entity, _hit) in pointer_map.iter() { if let Ok(mut scroll_position) = scrolled_node_query.get_mut(*entity) { - scroll_position.offset_x -= dx; - scroll_position.offset_y -= dy; + scroll_position.x -= dx; + scroll_position.y -= dy; } } } diff --git a/examples/testbed/ui.rs b/examples/testbed/ui.rs index 10e4e8dc8f..a2f6d6a14a 100644 --- a/examples/testbed/ui.rs +++ b/examples/testbed/ui.rs @@ -484,6 +484,26 @@ mod slice { }, )); } + + parent.spawn(( + ImageNode { + image: asset_server + .load("textures/fantasy_ui_borders/panel-border-010.png"), + image_mode: NodeImageMode::Sliced(TextureSlicer { + border: BorderRect::all(22.0), + center_scale_mode: SliceScaleMode::Stretch, + sides_scale_mode: SliceScaleMode::Stretch, + max_corner_scale: 1.0, + }), + ..Default::default() + }, + Node { + width: Val::Px(100.), + height: Val::Px(100.), + ..default() + }, + BackgroundColor(bevy::color::palettes::css::NAVY.into()), + )); }); } } diff --git a/examples/tools/scene_viewer/scene_viewer_plugin.rs b/examples/tools/scene_viewer/scene_viewer_plugin.rs index 49f4805d06..d3c5d2d74d 100644 --- a/examples/tools/scene_viewer/scene_viewer_plugin.rs +++ b/examples/tools/scene_viewer/scene_viewer_plugin.rs @@ -125,8 +125,7 @@ fn scene_load_check( maybe_directional_light.is_some() || maybe_point_light.is_some() }); - scene_handle.instance_id = - Some(scene_spawner.spawn(gltf_scene_handle.clone_weak())); + scene_handle.instance_id = Some(scene_spawner.spawn(gltf_scene_handle.clone())); info!("Spawning scene..."); } diff --git a/examples/ui/core_widgets.rs b/examples/ui/core_widgets.rs index ca91605206..318b824d0e 100644 --- a/examples/ui/core_widgets.rs +++ b/examples/ui/core_widgets.rs @@ -3,10 +3,10 @@ use bevy::{ color::palettes::basic::*, core_widgets::{ - CoreButton, CoreCheckbox, CoreRadio, CoreRadioGroup, CoreSlider, CoreSliderThumb, - CoreWidgetsPlugin, SliderRange, SliderValue, TrackClick, + Callback, CoreButton, CoreCheckbox, CoreRadio, CoreRadioGroup, CoreSlider, + CoreSliderDragState, CoreSliderThumb, CoreWidgetsPlugin, SliderRange, SliderValue, + TrackClick, }, - ecs::system::SystemId, input_focus::{ tab_navigation::{TabGroup, TabIndex, TabNavigationPlugin}, InputDispatchPlugin, @@ -146,17 +146,17 @@ fn setup(mut commands: Commands, assets: Res) { commands.spawn(Camera2d); commands.spawn(demo_root( &assets, - on_click, - on_change_value, - on_change_radio, + Callback::System(on_click), + Callback::System(on_change_value), + Callback::System(on_change_radio), )); } fn demo_root( asset_server: &AssetServer, - on_click: SystemId, - on_change_value: SystemId>, - on_change_radio: SystemId>, + on_click: Callback, + on_change_value: Callback>, + on_change_radio: Callback>, ) -> impl Bundle { ( Node { @@ -172,15 +172,15 @@ fn demo_root( TabGroup::default(), children![ button(asset_server, on_click), - slider(0.0, 100.0, 50.0, Some(on_change_value)), - checkbox(asset_server, "Checkbox", None), - radio_group(asset_server, Some(on_change_radio)), + slider(0.0, 100.0, 50.0, on_change_value), + checkbox(asset_server, "Checkbox", Callback::Ignore), + radio_group(asset_server, on_change_radio), Text::new("Press 'D' to toggle widget disabled states"), ], ) } -fn button(asset_server: &AssetServer, on_click: SystemId) -> impl Bundle { +fn button(asset_server: &AssetServer, on_click: Callback) -> impl Bundle { ( Node { width: Val::Px(150.0), @@ -192,7 +192,7 @@ fn button(asset_server: &AssetServer, on_click: SystemId) -> impl Bundle { }, DemoButton, CoreButton { - on_click: Some(on_click), + on_activate: on_click, }, Hovered::default(), TabIndex(0), @@ -323,7 +323,7 @@ fn set_button_style( } /// Create a demo slider -fn slider(min: f32, max: f32, value: f32, on_change: Option>>) -> impl Bundle { +fn slider(min: f32, max: f32, value: f32, on_change: Callback>) -> impl Bundle { ( Node { display: Display::Flex, @@ -398,6 +398,7 @@ fn update_slider_style( &SliderValue, &SliderRange, &Hovered, + &CoreSliderDragState, Has, ), ( @@ -405,6 +406,7 @@ fn update_slider_style( Changed, Changed, Changed, + Changed, Added, )>, With, @@ -413,12 +415,12 @@ fn update_slider_style( children: Query<&Children>, mut thumbs: Query<(&mut Node, &mut BackgroundColor, Has), Without>, ) { - for (slider_ent, value, range, hovered, disabled) in sliders.iter() { + for (slider_ent, value, range, hovered, drag_state, disabled) in sliders.iter() { for child in children.iter_descendants(slider_ent) { if let Ok((mut thumb_node, mut thumb_bg, is_thumb)) = thumbs.get_mut(child) { if is_thumb { thumb_node.left = Val::Percent(range.thumb_position(value.0) * 100.0); - thumb_bg.0 = thumb_color(disabled, hovered.0); + thumb_bg.0 = thumb_color(disabled, hovered.0 | drag_state.dragging); } } } @@ -426,17 +428,25 @@ fn update_slider_style( } fn update_slider_style2( - sliders: Query<(Entity, &Hovered, Has), With>, + sliders: Query< + ( + Entity, + &Hovered, + &CoreSliderDragState, + Has, + ), + With, + >, children: Query<&Children>, mut thumbs: Query<(&mut BackgroundColor, Has), Without>, mut removed_disabled: RemovedComponents, ) { removed_disabled.read().for_each(|entity| { - if let Ok((slider_ent, hovered, disabled)) = sliders.get(entity) { + if let Ok((slider_ent, hovered, drag_state, disabled)) = sliders.get(entity) { for child in children.iter_descendants(slider_ent) { if let Ok((mut thumb_bg, is_thumb)) = thumbs.get_mut(child) { if is_thumb { - thumb_bg.0 = thumb_color(disabled, hovered.0); + thumb_bg.0 = thumb_color(disabled, hovered.0 | drag_state.dragging); } } } @@ -458,7 +468,7 @@ fn thumb_color(disabled: bool, hovered: bool) -> Color { fn checkbox( asset_server: &AssetServer, caption: &str, - on_change: Option>>, + on_change: Callback>, ) -> impl Bundle { ( Node { @@ -651,7 +661,7 @@ fn set_checkbox_or_radio_style( } /// Create a demo radio group -fn radio_group(asset_server: &AssetServer, on_change: Option>>) -> impl Bundle { +fn radio_group(asset_server: &AssetServer, on_change: Callback>) -> impl Bundle { ( Node { display: Display::Flex, diff --git a/examples/ui/core_widgets_observers.rs b/examples/ui/core_widgets_observers.rs index 4e24a646b2..1ab4cda3b0 100644 --- a/examples/ui/core_widgets_observers.rs +++ b/examples/ui/core_widgets_observers.rs @@ -3,8 +3,8 @@ use bevy::{ color::palettes::basic::*, core_widgets::{ - CoreButton, CoreCheckbox, CoreSlider, CoreSliderThumb, CoreWidgetsPlugin, SliderRange, - SliderValue, + Callback, CoreButton, CoreCheckbox, CoreSlider, CoreSliderThumb, CoreWidgetsPlugin, + SliderRange, SliderValue, }, ecs::system::SystemId, input_focus::{ @@ -120,15 +120,15 @@ fn demo_root( }, TabGroup::default(), children![ - button(asset_server, on_click), - slider(0.0, 100.0, 50.0, Some(on_change_value)), - checkbox(asset_server, "Checkbox", None), + button(asset_server, Callback::System(on_click)), + slider(0.0, 100.0, 50.0, Callback::System(on_change_value)), + checkbox(asset_server, "Checkbox", Callback::Ignore), Text::new("Press 'D' to toggle widget disabled states"), ], ) } -fn button(asset_server: &AssetServer, on_click: SystemId) -> impl Bundle { +fn button(asset_server: &AssetServer, on_click: Callback) -> impl Bundle { ( Node { width: Val::Px(150.0), @@ -140,7 +140,7 @@ fn button(asset_server: &AssetServer, on_click: SystemId) -> impl Bundle { }, DemoButton, CoreButton { - on_click: Some(on_click), + on_activate: on_click, }, Hovered::default(), TabIndex(0), @@ -351,7 +351,7 @@ fn set_button_style( } /// Create a demo slider -fn slider(min: f32, max: f32, value: f32, on_change: Option>>) -> impl Bundle { +fn slider(min: f32, max: f32, value: f32, on_change: Callback>) -> impl Bundle { ( Node { display: Display::Flex, @@ -517,7 +517,7 @@ fn thumb_color(disabled: bool, hovered: bool) -> Color { fn checkbox( asset_server: &AssetServer, caption: &str, - on_change: Option>>, + on_change: Callback>, ) -> impl Bundle { ( Node { diff --git a/examples/ui/feathers.rs b/examples/ui/feathers.rs new file mode 100644 index 0000000000..45f959e1c4 --- /dev/null +++ b/examples/ui/feathers.rs @@ -0,0 +1,236 @@ +//! This example shows off the various Bevy Feathers widgets. + +use bevy::{ + core_widgets::{Callback, CoreRadio, CoreRadioGroup, CoreWidgetsPlugin, SliderStep}, + feathers::{ + controls::{ + button, checkbox, radio, slider, ButtonProps, ButtonVariant, CheckboxProps, SliderProps, + }, + dark_theme::create_dark_theme, + rounded_corners::RoundedCorners, + theme::{ThemeBackgroundColor, ThemedText, UiTheme}, + tokens, FeathersPlugin, + }, + input_focus::{ + tab_navigation::{TabGroup, TabNavigationPlugin}, + InputDispatchPlugin, + }, + prelude::*, + ui::{Checked, InteractionDisabled}, + winit::WinitSettings, +}; + +fn main() { + App::new() + .add_plugins(( + DefaultPlugins, + CoreWidgetsPlugin, + InputDispatchPlugin, + TabNavigationPlugin, + FeathersPlugin, + )) + .insert_resource(UiTheme(create_dark_theme())) + // Only run the app when there is user input. This will significantly reduce CPU/GPU use. + .insert_resource(WinitSettings::desktop_app()) + .add_systems(Startup, setup) + .run(); +} + +fn setup(mut commands: Commands) { + // ui camera + commands.spawn(Camera2d); + let root = demo_root(&mut commands); + commands.spawn(root); +} + +fn demo_root(commands: &mut Commands) -> impl Bundle { + // Update radio button states based on notification from radio group. + let radio_exclusion = commands.register_system( + |ent: In, q_radio: Query>, mut commands: Commands| { + for radio in q_radio.iter() { + if radio == *ent { + commands.entity(radio).insert(Checked); + } else { + commands.entity(radio).remove::(); + } + } + }, + ); + + ( + Node { + width: Val::Percent(100.0), + height: Val::Percent(100.0), + align_items: AlignItems::Start, + justify_content: JustifyContent::Start, + display: Display::Flex, + flex_direction: FlexDirection::Column, + row_gap: Val::Px(10.0), + ..default() + }, + TabGroup::default(), + ThemeBackgroundColor(tokens::WINDOW_BG), + children![( + Node { + display: Display::Flex, + flex_direction: FlexDirection::Column, + align_items: AlignItems::Stretch, + justify_content: JustifyContent::Start, + padding: UiRect::all(Val::Px(8.0)), + row_gap: Val::Px(8.0), + width: Val::Percent(30.), + min_width: Val::Px(200.), + ..default() + }, + children![ + ( + Node { + display: Display::Flex, + flex_direction: FlexDirection::Row, + align_items: AlignItems::Center, + justify_content: JustifyContent::Start, + column_gap: Val::Px(8.0), + ..default() + }, + children![ + button( + ButtonProps { + on_click: Callback::System(commands.register_system(|| { + info!("Normal button clicked!"); + })), + ..default() + }, + (), + Spawn((Text::new("Normal"), ThemedText)) + ), + button( + ButtonProps { + on_click: Callback::System(commands.register_system(|| { + info!("Disabled button clicked!"); + })), + ..default() + }, + InteractionDisabled, + Spawn((Text::new("Disabled"), ThemedText)) + ), + button( + ButtonProps { + on_click: Callback::System(commands.register_system(|| { + info!("Primary button clicked!"); + })), + variant: ButtonVariant::Primary, + ..default() + }, + (), + Spawn((Text::new("Primary"), ThemedText)) + ), + ] + ), + ( + Node { + display: Display::Flex, + flex_direction: FlexDirection::Row, + align_items: AlignItems::Center, + justify_content: JustifyContent::Start, + column_gap: Val::Px(1.0), + ..default() + }, + children![ + button( + ButtonProps { + on_click: Callback::System(commands.register_system(|| { + info!("Left button clicked!"); + })), + corners: RoundedCorners::Left, + ..default() + }, + (), + Spawn((Text::new("Left"), ThemedText)) + ), + button( + ButtonProps { + on_click: Callback::System(commands.register_system(|| { + info!("Center button clicked!"); + })), + corners: RoundedCorners::None, + ..default() + }, + (), + Spawn((Text::new("Center"), ThemedText)) + ), + button( + ButtonProps { + on_click: Callback::System(commands.register_system(|| { + info!("Right button clicked!"); + })), + variant: ButtonVariant::Primary, + corners: RoundedCorners::Right, + }, + (), + Spawn((Text::new("Right"), ThemedText)) + ), + ] + ), + button( + ButtonProps { + on_click: Callback::System(commands.register_system(|| { + info!("Wide button clicked!"); + })), + ..default() + }, + (), + Spawn((Text::new("Button"), ThemedText)) + ), + checkbox( + CheckboxProps { + on_change: Callback::Ignore, + }, + Checked, + Spawn((Text::new("Checkbox"), ThemedText)) + ), + checkbox( + CheckboxProps { + on_change: Callback::Ignore, + }, + InteractionDisabled, + Spawn((Text::new("Disabled"), ThemedText)) + ), + checkbox( + CheckboxProps { + on_change: Callback::Ignore, + }, + (InteractionDisabled, Checked), + Spawn((Text::new("Disabled+Checked"), ThemedText)) + ), + ( + Node { + display: Display::Flex, + flex_direction: FlexDirection::Column, + row_gap: Val::Px(4.0), + ..default() + }, + CoreRadioGroup { + on_change: Callback::System(radio_exclusion), + }, + children![ + radio(Checked, Spawn((Text::new("One"), ThemedText))), + radio((), Spawn((Text::new("Two"), ThemedText))), + radio((), Spawn((Text::new("Three"), ThemedText))), + radio( + InteractionDisabled, + Spawn((Text::new("Disabled"), ThemedText)) + ), + ] + ), + slider( + SliderProps { + max: 100.0, + value: 20.0, + ..default() + }, + SliderStep(10.) + ), + ] + ),], + ) +} diff --git a/examples/ui/scroll.rs b/examples/ui/scroll.rs index 594c89a3a5..9bedb719d2 100644 --- a/examples/ui/scroll.rs +++ b/examples/ui/scroll.rs @@ -338,8 +338,8 @@ pub fn update_scroll_position( for (_pointer, pointer_map) in hover_map.iter() { for (entity, _hit) in pointer_map.iter() { if let Ok(mut scroll_position) = scrolled_node_query.get_mut(*entity) { - scroll_position.offset_x -= dx; - scroll_position.offset_y -= dy; + scroll_position.x -= dx; + scroll_position.y -= dy; } } } diff --git a/examples/ui/scrollbars.rs b/examples/ui/scrollbars.rs new file mode 100644 index 0000000000..72cdfd8229 --- /dev/null +++ b/examples/ui/scrollbars.rs @@ -0,0 +1,205 @@ +//! Demonstrations of scrolling and scrollbars. + +use bevy::{ + core_widgets::{ + ControlOrientation, CoreScrollbar, CoreScrollbarDragState, CoreScrollbarPlugin, + CoreScrollbarThumb, + }, + ecs::{relationship::RelatedSpawner, spawn::SpawnWith}, + input_focus::{ + tab_navigation::{TabGroup, TabNavigationPlugin}, + InputDispatchPlugin, + }, + picking::hover::Hovered, + prelude::*, +}; + +fn main() { + App::new() + .add_plugins(( + DefaultPlugins, + CoreScrollbarPlugin, + InputDispatchPlugin, + TabNavigationPlugin, + )) + .insert_resource(UiScale(1.25)) + .add_systems(Startup, setup_view_root) + .add_systems(Update, update_scrollbar_thumb) + .run(); +} + +fn setup_view_root(mut commands: Commands) { + let camera = commands.spawn((Camera::default(), Camera2d)).id(); + + commands.spawn(( + Node { + display: Display::Flex, + flex_direction: FlexDirection::Column, + position_type: PositionType::Absolute, + left: Val::Px(0.), + top: Val::Px(0.), + right: Val::Px(0.), + bottom: Val::Px(0.), + padding: UiRect::all(Val::Px(3.)), + row_gap: Val::Px(6.), + ..Default::default() + }, + BackgroundColor(Color::srgb(0.1, 0.1, 0.1)), + UiTargetCamera(camera), + TabGroup::default(), + Children::spawn((Spawn(Text::new("Scrolling")), Spawn(scroll_area_demo()))), + )); +} + +/// Create a scrolling area. +/// +/// The "scroll area" is a container that can be scrolled. It has a nested structure which is +/// three levels deep: +/// - The outermost node is a grid that contains the scroll area and the scrollbars. +/// - The scroll area is a flex container that contains the scrollable content. This +/// is the element that has the `overflow: scroll` property. +/// - The scrollable content consists of the elements actually displayed in the scrolling area. +fn scroll_area_demo() -> impl Bundle { + ( + // Frame element which contains the scroll area and scrollbars. + Node { + display: Display::Grid, + width: Val::Px(200.0), + height: Val::Px(150.0), + grid_template_columns: vec![RepeatedGridTrack::flex(1, 1.), RepeatedGridTrack::auto(1)], + grid_template_rows: vec![RepeatedGridTrack::flex(1, 1.), RepeatedGridTrack::auto(1)], + row_gap: Val::Px(2.0), + column_gap: Val::Px(2.0), + ..default() + }, + Children::spawn((SpawnWith(|parent: &mut RelatedSpawner| { + // The actual scrolling area. + // Note that we're using `SpawnWith` here because we need to get the entity id of the + // scroll area in order to set the target of the scrollbars. + let scroll_area_id = parent + .spawn(( + Node { + display: Display::Flex, + flex_direction: FlexDirection::Column, + padding: UiRect::all(Val::Px(4.0)), + overflow: Overflow::scroll(), + ..default() + }, + BackgroundColor(colors::GRAY1.into()), + ScrollPosition(Vec2::new(0.0, 10.0)), + Children::spawn(( + // The actual content of the scrolling area + Spawn(text_row("Alpha Wolf")), + Spawn(text_row("Beta Blocker")), + Spawn(text_row("Delta Sleep")), + Spawn(text_row("Gamma Ray")), + Spawn(text_row("Epsilon Eridani")), + Spawn(text_row("Zeta Function")), + Spawn(text_row("Lambda Calculus")), + Spawn(text_row("Nu Metal")), + Spawn(text_row("Pi Day")), + Spawn(text_row("Chi Pants")), + Spawn(text_row("Psi Powers")), + Spawn(text_row("Omega Fatty Acid")), + )), + )) + .id(); + + // Vertical scrollbar + parent.spawn(( + Node { + min_width: Val::Px(8.0), + grid_row: GridPlacement::start(1), + grid_column: GridPlacement::start(2), + ..default() + }, + CoreScrollbar { + orientation: ControlOrientation::Vertical, + target: scroll_area_id, + min_thumb_length: 8.0, + }, + Children::spawn(Spawn(( + Node { + position_type: PositionType::Absolute, + ..default() + }, + Hovered::default(), + BackgroundColor(colors::GRAY2.into()), + BorderRadius::all(Val::Px(4.0)), + CoreScrollbarThumb, + ))), + )); + + // Horizontal scrollbar + parent.spawn(( + Node { + min_height: Val::Px(8.0), + grid_row: GridPlacement::start(2), + grid_column: GridPlacement::start(1), + ..default() + }, + CoreScrollbar { + orientation: ControlOrientation::Horizontal, + target: scroll_area_id, + min_thumb_length: 8.0, + }, + Children::spawn(Spawn(( + Node { + position_type: PositionType::Absolute, + ..default() + }, + Hovered::default(), + BackgroundColor(colors::GRAY2.into()), + BorderRadius::all(Val::Px(4.0)), + CoreScrollbarThumb, + ))), + )); + }),)), + ) +} + +/// Create a list row +fn text_row(caption: &str) -> impl Bundle { + ( + Text::new(caption), + TextFont { + font_size: 14.0, + ..default() + }, + ) +} + +// Update the color of the scrollbar thumb. +fn update_scrollbar_thumb( + mut q_thumb: Query< + (&mut BackgroundColor, &Hovered, &CoreScrollbarDragState), + ( + With, + Or<(Changed, Changed)>, + ), + >, +) { + for (mut thumb_bg, Hovered(is_hovering), drag) in q_thumb.iter_mut() { + let color: Color = if *is_hovering || drag.dragging { + // If hovering, use a lighter color + colors::GRAY3 + } else { + // Default color for the slider + colors::GRAY2 + } + .into(); + + if thumb_bg.0 != color { + // Update the color of the thumb + thumb_bg.0 = color; + } + } +} + +mod colors { + use bevy::color::Srgba; + + pub const GRAY1: Srgba = Srgba::new(0.224, 0.224, 0.243, 1.0); + pub const GRAY2: Srgba = Srgba::new(0.486, 0.486, 0.529, 1.0); + pub const GRAY3: Srgba = Srgba::new(1.0, 1.0, 1.0, 1.0); +} diff --git a/release-content/migration-guides/Newtype_ScrollPosition.md b/release-content/migration-guides/Newtype_ScrollPosition.md new file mode 100644 index 0000000000..5aa3196646 --- /dev/null +++ b/release-content/migration-guides/Newtype_ScrollPosition.md @@ -0,0 +1,6 @@ +--- +title: Make `ScrollPosition` newtype `Vec2` +pull_requests: [19881] +--- + +`ScrollPosition` now newtypes `Vec2`, its `offset_x` and `offset_y` fields have been removed. diff --git a/release-content/migration-guides/animation_graph_no_more_asset_ids.md b/release-content/migration-guides/animation_graph_no_more_asset_ids.md new file mode 100644 index 0000000000..068405614a --- /dev/null +++ b/release-content/migration-guides/animation_graph_no_more_asset_ids.md @@ -0,0 +1,20 @@ +--- +title: `AnimationGraph` no longer supports raw AssetIds. +pull_requests: [] +--- + +In previous versions of Bevy, `AnimationGraph` would serialize `Handle` as an asset +path, and if that wasn't available it would fallback to serializing `AssetId`. In +practice, this was not very useful. `AssetId` is (usually) a runtime-generated ID. This means for an +arbitrary `Handle`, it was incredibly unlikely that your handle before serialization +would correspond to the same asset as after serialization. + +This confusing behavior has been removed. As a side-effect, any `AnimationGraph`s you previously +saved (via `AnimationGraph::save`) will need to be re-saved. These legacy `AnimationGraph`s can +still be loaded until the next Bevy version. Loading and then saving the `AnimationGraph` again will +automatically migrate the `AnimationGraph`. + +If your `AnimationGraph` contained serialized `AssetId`s, you will need to manually load the bytes +of the saved graph, deserialize it into `SerializedAnimationGraph`, and then manually decide how to +migrate those `AssetId`s. Alternatively, you could simply rebuild the graph from scratch and save a +new instance. We expect this to be a very rare situation. diff --git a/release-content/migration-guides/composable_specialization.md b/release-content/migration-guides/composable_specialization.md new file mode 100644 index 0000000000..f87beef8cb --- /dev/null +++ b/release-content/migration-guides/composable_specialization.md @@ -0,0 +1,153 @@ +--- +title: Composable Specialization +pull_requests: [17373] +--- + +The existing pipeline specialization APIs (`SpecializedRenderPipeline` etc.) have +been replaced with a single `Specializer` trait and `SpecializedCache` collection: + +```rs +pub trait Specializer: Send + Sync + 'static { + type Key: SpecializerKey; + fn specialize( + &self, + key: Self::Key, + descriptor: &mut T::Descriptor, + ) -> Result, BevyError>; +} + +pub struct SpecializedCache>{ ... }; +``` + +The main difference is the change from *producing* a pipeline descriptor to +*mutating* one based on a key. The "base descriptor" that the `SpecializedCache` +passes to the `Specializer` can either be specified manually with `Specializer::new` +or by implementing `GetBaseDescriptor`. There's also a new trait for specialization +keys, `SpecializeKey`, that can be derived with the included macro in most cases. + +Composing multiple different specializers together with the `derive(Specializer)` +macro can be a lot more powerful (see the `Specialize` docs), but migrating +individual specializers is fairly simple. All static parts of the pipeline +should be specified in the base descriptor, while the `Specializer` impl +should mutate the key as little as necessary to match the key. + +```rs +pub struct MySpecializer { + layout: BindGroupLayout, + layout_msaa: BindGroupLayout, + vertex: Handle, + fragment: Handle, +} + +// before +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +// after +#[derive(Clone, Copy, PartialEq, Eq, Hash, SpecializerKey)] + +pub struct MyKey { + blend_state: BlendState, + msaa: Msaa, +} + +impl FromWorld for MySpecializer { + fn from_world(&mut World) -> Self { + ... + } +} + +// before +impl SpecializedRenderPipeline for MySpecializer { + type Key = MyKey; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + RenderPipelineDescriptor { + label: Some("my_pipeline".into()), + layout: vec![ + if key.msaa.samples() > 0 { + self.layout_msaa.clone() + } else { + self.layout.clone() + } + ], + push_constant_ranges: vec![], + vertex: VertexState { + shader: self.vertex.clone(), + shader_defs: vec![], + entry_point: "vertex".into(), + buffers: vec![], + }, + primitive: Default::default(), + depth_stencil: None, + multisample: MultisampleState { + count: key.msaa.samples(), + ..Default::default() + }, + fragment: Some(FragmentState { + shader: self.fragment.clone(), + shader_defs: vec![], + entry_point: "fragment".into(), + targets: vec![Some(ColorTargetState { + format: TextureFormat::Rgba8Unorm, + blend: Some(key.blend_state), + write_mask: ColorWrites::all(), + })], + }), + zero_initialize_workgroup_memory: false, + }, + } +} + +app.init_resource::>(); + +// after +impl Specializer for MySpecializer { + type Key = MyKey; + + fn specialize( + &self, + key: Self::Key, + descriptor: &mut RenderPipeline, + ) -> Result, BevyError> { + descriptor.multisample.count = key.msaa.samples(); + descriptor.layout[0] = if key.msaa.samples() > 0 { + self.layout_msaa.clone() + } else { + self.layout.clone() + }; + descriptor.fragment.targets[0].as_mut().unwrap().blend_mode = key.blend_state; + Ok(key) + } +} + +impl GetBaseDescriptor for MySpecializer { + fn get_base_descriptor(&self) -> RenderPipelineDescriptor { + RenderPipelineDescriptor { + label: Some("my_pipeline".into()), + layout: vec![self.layout.clone()], + push_constant_ranges: vec![], + vertex: VertexState { + shader: self.vertex.clone(), + shader_defs: vec![], + entry_point: "vertex".into(), + buffers: vec![], + }, + primitive: Default::default(), + depth_stencil: None, + multisample: MultiSampleState::default(), + fragment: Some(FragmentState { + shader: self.fragment.clone(), + shader_defs: vec![], + entry_point: "fragment".into(), + targets: vec![Some(ColorTargetState { + format: TextureFormat::Rgba8Unorm, + blend: None, + write_mask: ColorWrites::all(), + })], + }), + zero_initialize_workgroup_memory: false, + }, + } +} + +app.init_resource::>(); +``` diff --git a/release-content/release-notes/convert-coordinates.md b/release-content/migration-guides/convert-coordinates.md similarity index 58% rename from release-content/release-notes/convert-coordinates.md rename to release-content/migration-guides/convert-coordinates.md index 957508e15b..85ab80ed20 100644 --- a/release-content/release-notes/convert-coordinates.md +++ b/release-content/migration-guides/convert-coordinates.md @@ -1,7 +1,7 @@ --- title: Allow importing glTFs with a corrected coordinate system authors: ["@janhohenheim"] -pull_requests: [19633, 19685] +pull_requests: [19633, 19685, 19816] --- glTF uses the following coordinate system: @@ -24,7 +24,27 @@ Long-term, we'd like to fix our glTF imports to use the correct coordinate syste But changing the import behavior would mean that *all* imported glTFs of *all* users would suddenly look different, breaking their scenes! Not to mention that any bugs in the conversion code would be incredibly frustating for users. -This is why we are now gradually rolling out support for corrected glTF imports. Starting now you can opt into the new behavior by setting `convert_coordinates` on `GltfPlugin`: +This is why we are now gradually rolling out support for corrected glTF imports. You will now be greeted by the following warning when using the old behavior: + +> Starting from Bevy 0.18, by default all imported glTF models will be rotated by 180 degrees around the Y axis to align with Bevy's coordinate system. +> You are currently importing glTF files using the old behavior. Consider opting-in to the new import behavior by enabling the `gltf_convert_coordinates_default` feature. +> If you encounter any issues please file a bug! +> If you want to continue using the old behavior going forward (even when the default changes in 0.18), manually set the corresponding option in the `GltfPlugin` or `GltfLoaderSettings`. +> See the migration guide for more details. + +As the warning says, you can opt into the new behavior by enabling the `gltf_convert_coordinates_default` feature in your `Cargo.toml`: + +```toml +# old behavior, ignores glTF's coordinate system +[dependencies] +bevy = "0.17.0" + +# new behavior, converts the coordinate system of all glTF assets into Bevy's coordinate system +[dependencies] +bevy = { version = "0.17.0", features = ["gltf_convert_coordinates_default"] } +``` + +If you prefer, you can also do this in code by setting `convert_coordinates` on `GltfPlugin`: ```rust // old behavior, ignores glTF's coordinate system @@ -41,6 +61,9 @@ App::new() .run(); ``` +If you want to continue using the old behavior in the future, you can silence the warning by enabling the `gltf_convert_coordinates_default` feature +and explicitly setting `convert_coordinates: false` on `GltfPlugin`. + You can also control this on a per-asset-level: ```rust @@ -56,7 +79,7 @@ let handle = asset_server.load_with_settings( ); ``` -Afterwards, your scene will be oriented such that your modeling software's forward direction correctly corresponds to Bevy's forward direction. +After opting into the new behavior, your scene will be oriented such that your modeling software's forward direction correctly corresponds to Bevy's forward direction. For example, Blender assumes -Y to be forward, so exporting the following model to glTF and loading it in Bevy with the new settings will ensure everything is oriented the right way across all programs in your pipeline: diff --git a/release-content/migration-guides/handle_weak_replaced_with_handle_uuid.md b/release-content/migration-guides/handle_weak_replaced_with_handle_uuid.md new file mode 100644 index 0000000000..d6bd6b9890 --- /dev/null +++ b/release-content/migration-guides/handle_weak_replaced_with_handle_uuid.md @@ -0,0 +1,40 @@ +--- +title: `Handle::Weak` has been replaced by `Handle::Uuid`. +pull_requests: [19896] +--- + +`Handle::Weak` had some weird behavior. It allowed for a sprite to be given a handle that is dropped +**while the sprite is still using it**. This also resulted in more complexity in the asset system. +The primary remaining use for `Handle::Weak` is to store asset UUIDs initialized through the +`weak_handle!` macro. To address this, `Handle::Weak` has been replaced by `Handle::Uuid`! + +Users using the `weak_handle!` macro should switch to the `uuid_handle!` macro. + +```rust +// Before +const IMAGE: Handle = weak_handle!("b20988e9-b1b9-4176-b5f3-a6fa73aa617f"); + +// After +const IMAGE: Handle = uuid_handle!("b20988e9-b1b9-4176-b5f3-a6fa73aa617f"); +``` + +Users using `Handle::clone_weak` can (most likely) just call `Handle::clone` instead. + +```rust +// Somewhere in some startup system. +let my_sprite_image = asset_server.load("monster.png"); + +// In game code... +// This sprite could be unloaded even if the sprite is still using it! +commands.spawn(Sprite::from_image(my_sprite_image.clone_weak())); + +// Just do this instead! +commands.spawn(Sprite::from_image(my_sprite_image.clone())); +``` + +Users using the `Handle::Weak` variant directly should consider replacing it with `AssetId` instead, +accessible through `Handle::id`. These situations are very case-by-case migrations. + +P.S., for users of the `weak_handle!` macro: If you are using it for shaders, consider switching to +`load_shader_library`/`load_embedded_asset` instead (especially replacing `load_internal_asset`). +This enables hot reloading for your shaders - which Bevy internally has done this cycle! diff --git a/release-content/migration-guides/render_graph_app_to_ext.md b/release-content/migration-guides/render_graph_app_to_ext.md new file mode 100644 index 0000000000..511e60ce05 --- /dev/null +++ b/release-content/migration-guides/render_graph_app_to_ext.md @@ -0,0 +1,7 @@ +--- +title: `RenderGraphApp` renamed to `RenderGraphExt`. +pull_requests: [19912] +--- + +`RenderGraphApp` has been renamed to `RenderGraphExt`. Rename this for cases where you are +explicitly importing this trait. diff --git a/release-content/migration-guides/stack_z_offsets_changes.md b/release-content/migration-guides/stack_z_offsets_changes.md new file mode 100644 index 0000000000..04187978d7 --- /dev/null +++ b/release-content/migration-guides/stack_z_offsets_changes.md @@ -0,0 +1,34 @@ +--- +title: Fixed UI draw order and `stack_z_offsets` changes +pull_requests: [19691] +--- + +The draw order of some renderable UI elements relative to others wasn't fixed and depended on system ordering. +In particular the ordering of background colors and texture sliced images was sometimes swapped. + +The UI draw order is now fixed. +The new order is (back-to-front): + +1. Box shadows + +2. Node background colors + +3. Node borders + +4. Gradients + +5. Border Gradients + +6. Images (including texture-sliced images) + +7. Materials + +8. Text (including text shadows) + +The values of the `stack_z_offsets` constants have been updated to enforce the new ordering. Other changes: + +* `NODE` is renamed to `BACKGROUND_COLOR` + +* `TEXTURE_SLICE` is removed, use `IMAGE`. + +* New `BORDER`, `BORDER_GRADIENT` and `TEXT` constants. diff --git a/release-content/migration-guides/textshadow_is_moved_to_widget_text_module.md b/release-content/migration-guides/textshadow_is_moved_to_widget_text_module.md new file mode 100644 index 0000000000..4e23abc36b --- /dev/null +++ b/release-content/migration-guides/textshadow_is_moved_to_widget_text_module.md @@ -0,0 +1,6 @@ +--- +title: `TextShadow` has been moved to `bevy::ui::widget::text` +pull_requests: [] +--- + +`TextShadow` has been moved to `bevy::ui::widget::text`. diff --git a/release-content/release-notes/bevy_solari.md b/release-content/release-notes/bevy_solari.md index 7e7d36ac31..ac1b1abe1c 100644 --- a/release-content/release-notes/bevy_solari.md +++ b/release-content/release-notes/bevy_solari.md @@ -1,7 +1,7 @@ --- title: Initial raytraced lighting progress (bevy_solari) authors: ["@JMS55"] -pull_requests: [19058] +pull_requests: [19058, 19620, 19790] --- (TODO: Embed solari example screenshot here) @@ -25,7 +25,7 @@ The problem with these methods is that they all have large downsides: Bevy Solari is intended as a completely alternate, high-end lighting solution for Bevy that uses GPU-accelerated raytracing to fix all of the above problems. Emissive meshes will properly cast light and shadows, you will be able to have hundreds of shadow casting lights, quality will be much better, it will require no baking time, and it will support _fully_ dynamic scenes! -While Bevy 0.17 adds the bevy_solari crate, it's intended as a long-term project. Currently there is only a non-realtime path tracer intended as a reference and testbed for developing Bevy Solari. There is nothing usable yet for game developers. However, feel free to run the solari example to see the path tracer in action, and look forward to more work on Bevy Solari in future releases! (TODO: Is this burying the lede?) +While Bevy 0.17 adds the bevy_solari crate, it's intended as a long-term project. It is not yet usable by game developers. However, feel free to run the solari example (`cargo run --release --example solari --features bevy_solari` (realtime direct lighting, no denoising) or `cargo run --release --example solari --features bevy_solari -- --pathtracer` (non-realtime pathtracing)) to check out the progress we've made, and look forward to more work on Bevy Solari in future releases! (TODO: Embed bevy_solari logo here, or somewhere else that looks good) diff --git a/release-content/release-notes/feathers.md b/release-content/release-notes/feathers.md new file mode 100644 index 0000000000..754c57ebc2 --- /dev/null +++ b/release-content/release-notes/feathers.md @@ -0,0 +1,26 @@ +--- +title: Bevy Feathers +authors: ["@viridia", "@Atlas16A"] +pull_requests: [19730, 19900] +--- + +To make it easier for Bevy engine developers and third-party tool creators to make comfortable, visually cohesive tooling, +we're pleased to introduce "Feathers" - a comprehensive widget set that offers: + +- Standard widgets designed to match the look and feel of the planned Bevy Editor +- Components that can be leveraged to build custom editors, inspectors, and utility interfaces +- Essential UI elements including buttons, sliders, checkboxes, menu buttons, and more +- Layout containers for organizing and structuring UI elements +- Decorative elements such as icons for visual enhancement +- Robust theming support ensuring consistent visual styling across applications +- Accessibility features with built-in screen reader and assistive technology support +- Interactive cursor behavior that changes appropriately when hovering over widgets + +Feathers isn't meant as a toolkit for building exciting and cool game UIs: it has a somewhat plain +and utilitarian look and feel suitable for editors and graphical utilities. That being said, using +the themeing framework, you can spice up the colors quite a bit. +It can also serve as a helpful base to understand how to extend and style `bevy_ui` and our new core widgets; +copy the code into your project and start hacking! + +Feathers is still in development, and is currently hidden behind an experimental feature flag, +`experimental_bevy_feathers`. diff --git a/release-content/release-notes/headless-widgets.md b/release-content/release-notes/headless-widgets.md index e28c44ee9e..5e2a91c556 100644 --- a/release-content/release-notes/headless-widgets.md +++ b/release-content/release-notes/headless-widgets.md @@ -1,7 +1,7 @@ --- title: Headless Widgets -authors: ["@viridia"] -pull_requests: [19366, 19584, 19665, 19778] +authors: ["@viridia", "@ickshonpe", "@alice-i-cecile"] +pull_requests: [19366, 19584, 19665, 19778, 19803] --- Bevy's `Button` and `Interaction` components have been around for a long time. Unfortunately @@ -34,9 +34,11 @@ sliders, checkboxes and radio buttons. - `CoreButton` is a push button. It emits an activation event when clicked. - `CoreSlider` is a standard slider, which lets you edit an `f32` value in a given range. +- `CoreScrollbar` can be used to implement scrollbars. - `CoreCheckbox` can be used for checkboxes and toggle switches. +- `CoreRadio` and `CoreRadioGroup` can be used for radio buttons. -## Widget Interaction States +## Widget Interaction Marker Components Many of the core widgets will define supplementary ECS components that are used to store the widget's state, similar to how the old `Interaction` component worked, but in a way that is more flexible. @@ -63,13 +65,18 @@ Applications need a way to be notified when the user interacts with a widget. On is using Bevy observers. This approach is useful in cases where you want the widget notifications to bubble up the hierarchy. -However, in UI work it's often desirable to connect widget interactions in ways that cut across the -hierarchy. For these kinds of situations, the core widgets offer a different approach: one-shot -systems. You can register a function as a one-shot system and get the resulting `SystemId`. This can -then be passed as a parameter to the widget when it is constructed, so when the button subsequently +However, in UI work it's often desirable to send notifications "point-to-point" in ways that cut +across the hierarchy. For these kinds of situations, the core widgets offer a different +approach: callbacks. The `Callback` enum allows different options for triggering a notification +when a widget's state is updated. For example, you can pass in the `SystemId` of a registered +one-shot system as a widget parameter when it is constructed. When the button subsequently gets clicked or the slider is dragged, the system gets run. Because it's an ECS system, it can inject any additional parameters it needs to update the Bevy world in response to the interaction. +## State Management + +See the [Wikipedia Article on State Management](https://en.wikipedia.org/wiki/State_management). + Most of the core widgets support "external state management" - something that is referred to in the React.js world as "controlled" widgets. This means that for widgets that edit a parameter value (such as checkboxes and sliders), the widget doesn't automatically update its own internal value, @@ -84,9 +91,10 @@ interacting with that widget. Externalizing the state avoids the need for two-wa instead allows simpler one-way data binding that aligns well with the traditional "Model / View / Controller" (MVC) design pattern. -That being said, the choice of internal or external state management is up to you: if the widget -has an `on_change` callback that is not `None`, then the callback is used. If the callback -is `None`, however, the widget will update its own state. (This is similar to how React.js does it.) +That being said, the choice of internal or external state management is up to you: if the widget has +an `on_change` callback that is not `Callback::Ignore`, then the callback is used. If the callback +is `Callback::Ignore`, however, the widget will update its own state automatically. (This is similar +to how React.js does it.) There are two exceptions to this rule about external state management. First, widgets which don't edit a value, but which merely trigger an event (such as buttons), don't fall under this rule. diff --git a/tools/compile_fail_utils/Cargo.toml b/tools/compile_fail_utils/Cargo.toml index 453b7ea7ad..8668fdaa9e 100644 --- a/tools/compile_fail_utils/Cargo.toml +++ b/tools/compile_fail_utils/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT OR Apache-2.0" publish = false [dependencies] -ui_test = "0.29.1" +ui_test = "0.30.1" [[test]] name = "example" diff --git a/tools/compile_fail_utils/src/lib.rs b/tools/compile_fail_utils/src/lib.rs index ecd4f34c68..28157400a7 100644 --- a/tools/compile_fail_utils/src/lib.rs +++ b/tools/compile_fail_utils/src/lib.rs @@ -123,7 +123,8 @@ pub fn test_with_multiple_configs( let emitter: Box = if env::var_os("CI").is_some() { Box::new(( Text::verbose(), - Gha:: { + Gha { + group: true, name: test_name.into(), }, ))