From eb6e97c18e1ec7eca9afe8f950bd54edde7c4255 Mon Sep 17 00:00:00 2001 From: aecsocket <43144841+aecsocket@users.noreply.github.com> Date: Sun, 25 Aug 2024 14:44:52 +0100 Subject: [PATCH 01/53] Make ActiveAnimation::set_weight return &mut Self (#14914) # Objective Fixes #14907. ## Solution Changes `ActiveAnimation::set_weight` to return `&mut Self`. ## Testing Simple API change, I don't think this needs explicit testing. --- crates/bevy_animation/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index e8e394b957df68..70c49b3c3938a1 100755 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -442,8 +442,9 @@ impl ActiveAnimation { } /// Sets the weight of this animation. - pub fn set_weight(&mut self, weight: f32) { + pub fn set_weight(&mut self, weight: f32) -> &mut Self { self.weight = weight; + self } /// Pause the animation. From 28faafdc417be1a24ed649fc1c30048ee543fa3f Mon Sep 17 00:00:00 2001 From: Robin KAY Date: Sun, 25 Aug 2024 15:11:58 +0100 Subject: [PATCH 02/53] Fix tiny seam in Annulus geometry. (#14913) # Objective There is a tiny seam at the top of the annulus caused by normal floating-point error in calculating the coordinates. When generating the last pair of triangles, given `n == i` then `(TAU / n) * i` does not equal `TAU` exactly. Fixes https://github.com/komadori/bevy_mod_outline/issues/42 ## Solution This can be fixed by changing the calculation so that `(TAU / n) * (i % n) == 0.0`, which is equivalent for trigonometric purposes. ## Testing Added the unit test `bevy_render::mesh::primitives::dim2::tests::test_annulus`. --- .../bevy_render/src/mesh/primitives/dim2.rs | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/crates/bevy_render/src/mesh/primitives/dim2.rs b/crates/bevy_render/src/mesh/primitives/dim2.rs index 3bbd2be6d219e7..3b2d42b6c95456 100644 --- a/crates/bevy_render/src/mesh/primitives/dim2.rs +++ b/crates/bevy_render/src/mesh/primitives/dim2.rs @@ -598,7 +598,7 @@ impl MeshBuilder for AnnulusMeshBuilder { let start_angle = FRAC_PI_2; let step = std::f32::consts::TAU / self.resolution as f32; for i in 0..=self.resolution { - let theta = start_angle + i as f32 * step; + let theta = start_angle + (i % self.resolution) as f32 * step; let (sin, cos) = theta.sin_cos(); let inner_pos = [cos * inner_radius, sin * inner_radius, 0.]; let outer_pos = [cos * outer_radius, sin * outer_radius, 0.]; @@ -1005,9 +1005,33 @@ impl From for Mesh { #[cfg(test)] mod tests { - use bevy_math::primitives::RegularPolygon; + use bevy_math::{prelude::Annulus, primitives::RegularPolygon, FloatOrd}; + use bevy_utils::HashSet; + + use crate::mesh::{Mesh, MeshBuilder, Meshable, VertexAttributeValues}; + + fn count_distinct_positions(points: &[[f32; 3]]) -> usize { + let mut map = HashSet::new(); + for point in points { + map.insert(point.map(FloatOrd)); + } + map.len() + } - use crate::mesh::{Mesh, VertexAttributeValues}; + #[test] + fn test_annulus() { + let mesh = Annulus::new(1.0, 1.2).mesh().resolution(16).build(); + + assert_eq!( + 32, + count_distinct_positions( + mesh.attribute(Mesh::ATTRIBUTE_POSITION) + .unwrap() + .as_float3() + .unwrap() + ) + ); + } /// Sin/cos and multiplication computations result in numbers like 0.4999999. /// Round these to numbers we expect like 0.5. From 48bd81045174b014318b177515746757214e6f60 Mon Sep 17 00:00:00 2001 From: Ben Frankel Date: Sun, 25 Aug 2024 17:12:13 +0300 Subject: [PATCH 03/53] Rename `Commands::register_one_shot_system` -> `register_system` (#14910) # Objective Improve naming consistency for functions that deal with one-shot systems via `SystemId`: - `App::register_system` - `SubApp::register_system` - `World::run_system` - `World::register_system` - `Commands::run_system` - :x: `Commands::register_one_shot_system` ## Solution Rename `Commands::register_one_shot_system` -> `register_system`. ## Testing Not tested besides CI. ## Migration Guide `Commands::register_one_shot_system` has been renamed to `register_system`. --- crates/bevy_ecs/src/system/commands/mod.rs | 6 +++--- examples/ecs/one_shot_systems.rs | 2 +- examples/games/loading_screen.rs | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index fe5eda1b53fb3b..14748ac070eb7e 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -687,7 +687,7 @@ impl<'w, 's> Commands<'w, 's> { /// if let Some(system) = *local_system { /// commands.run_system(system); /// } else { - /// *local_system = Some(commands.register_one_shot_system(increment_counter)); + /// *local_system = Some(commands.register_system(increment_counter)); /// } /// } /// @@ -700,7 +700,7 @@ impl<'w, 's> Commands<'w, 's> { /// # let mut queue_1 = CommandQueue::default(); /// # let systemid = { /// # let mut commands = Commands::new(&mut queue_1, &world); - /// # commands.register_one_shot_system(increment_counter) + /// # commands.register_system(increment_counter) /// # }; /// # let mut queue_2 = CommandQueue::default(); /// # { @@ -712,7 +712,7 @@ impl<'w, 's> Commands<'w, 's> { /// # assert_eq!(1, world.resource::().0); /// # bevy_ecs::system::assert_is_system(register_system); /// ``` - pub fn register_one_shot_system< + pub fn register_system< I: 'static + Send, O: 'static + Send, M, diff --git a/examples/ecs/one_shot_systems.rs b/examples/ecs/one_shot_systems.rs index 74839fe234b32a..cc082828cb5b40 100644 --- a/examples/ecs/one_shot_systems.rs +++ b/examples/ecs/one_shot_systems.rs @@ -40,7 +40,7 @@ struct A; struct B; fn setup_with_commands(mut commands: Commands) { - let system_id = commands.register_one_shot_system(system_a); + let system_id = commands.register_system(system_a); commands.spawn((Callback(system_id), A)); } diff --git a/examples/games/loading_screen.rs b/examples/games/loading_screen.rs index adfb201e42d847..d695d094f1880c 100644 --- a/examples/games/loading_screen.rs +++ b/examples/games/loading_screen.rs @@ -70,9 +70,9 @@ struct LevelData { fn setup(mut commands: Commands) { let level_data = LevelData { - unload_level_id: commands.register_one_shot_system(unload_current_level), - level_1_id: commands.register_one_shot_system(load_level_1), - level_2_id: commands.register_one_shot_system(load_level_2), + unload_level_id: commands.register_system(unload_current_level), + level_1_id: commands.register_system(load_level_1), + level_2_id: commands.register_system(load_level_2), }; commands.insert_resource(level_data); From 01cce9b11c6afd6ae16638aefeafbc06f633b35e Mon Sep 17 00:00:00 2001 From: Chris Russell <8494645+chescock@users.noreply.github.com> Date: Sun, 25 Aug 2024 10:12:24 -0400 Subject: [PATCH 04/53] Make the field of `ParamSetBuilder` pub so it's actually usable. (#14896) # Objective `ParamSetBuilder` is supposed to be used as a tuple constructor, but the field was not marked `pub` so it's not actually usable outside of its module. ## Solution Mark the field `pub`. Realize one advantage of doc tests over unit tests is that they test the public API. Add a doc test example that uses the field so that this would have been caught. --- crates/bevy_ecs/src/system/builder.rs | 2 +- crates/bevy_ecs/src/system/system_param.rs | 40 ++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/system/builder.rs b/crates/bevy_ecs/src/system/builder.rs index 520d73f4e9cdc4..2cecceeb3c2c03 100644 --- a/crates/bevy_ecs/src/system/builder.rs +++ b/crates/bevy_ecs/src/system/builder.rs @@ -212,7 +212,7 @@ all_tuples!(impl_system_param_builder_tuple, 0, 16, P, B); /// A [`SystemParamBuilder`] for a [`ParamSet`]. /// To build a [`ParamSet`] with a tuple of system parameters, pass a tuple of matching [`SystemParamBuilder`]s. /// To build a [`ParamSet`] with a `Vec` of system parameters, pass a `Vec` of matching [`SystemParamBuilder`]s. -pub struct ParamSetBuilder(T); +pub struct ParamSetBuilder(pub T); macro_rules! impl_param_set_builder_tuple { ($(($param: ident, $builder: ident, $meta: ident)),*) => { diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index 3bf032590aa8d4..acc876a556b3ca 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -390,6 +390,46 @@ fn assert_component_access_compatibility( /// } /// # bevy_ecs::system::assert_is_system(event_system); /// ``` +/// +/// If you want to use `ParamSet` with a [`SystemParamBuilder`](crate::system::SystemParamBuilder), use [`ParamSetBuilder`](crate::system::ParamSetBuilder) and pass a builder for each param. +/// ``` +/// # use bevy_ecs::{prelude::*, system::*}; +/// # +/// # #[derive(Component)] +/// # struct Health; +/// # +/// # #[derive(Component)] +/// # struct Enemy; +/// # +/// # #[derive(Component)] +/// # struct Ally; +/// # +/// let mut world = World::new(); +/// +/// let system = (ParamSetBuilder(( +/// QueryParamBuilder::new(|builder| { +/// builder.with::(); +/// }), +/// QueryParamBuilder::new(|builder| { +/// builder.with::(); +/// }), +/// ParamBuilder, +/// )),) +/// .build_state(&mut world) +/// .build_system(buildable_system); +///world.run_system_once(system); +/// +/// fn buildable_system(mut set: ParamSet<(Query<&mut Health>, Query<&mut Health>, &World)>) { +/// // The first parameter is built from the first builder, +/// // so this will iterate over enemies. +/// for mut health in set.p0().iter_mut() {} +/// // And the second parameter is built from the second builder, +/// // so this will iterate over allies. +/// for mut health in set.p1().iter_mut() {} +/// // Parameters that don't need special building can use `ParamBuilder`. +/// let entities = set.p2().entities(); +/// } +/// ``` pub struct ParamSet<'w, 's, T: SystemParam> { param_states: &'s mut T::State, world: UnsafeWorldCell<'w>, From 9a2eb878a2acc4b4f9bb304109de7c66db11cfa8 Mon Sep 17 00:00:00 2001 From: Kumikaya <3411015214@qq.com> Date: Sun, 25 Aug 2024 22:13:23 +0800 Subject: [PATCH 05/53] Fix underflow panic in `InitTriInfo` (#14893) # Objective - Fix #14874 ## Solution - Change the place where a panic occurs from `t < iNrTrianglesIn - 1` to `t + 1 < iNrTrianglesIn`. ## Testing - After the fix, the following code runs successfully without any panic. ```rust use bevy::prelude::Mesh; use bevy_render::{ mesh::{Indices, PrimitiveTopology}, render_asset::RenderAssetUsages, }; const POSITIONS: &[[f32; 3]] = &[[0.0, 1.0, 0.0], [0.0, 0.0, 0.0], [0.0, 1.0, 0.0]]; const NORMALS: &[[f32; 3]] = &[[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]]; const INDICES: &[u32] = &[0, 1, 2]; const UVS: &[[f32; 2]] = &[[0.0, 1.0], [0.0, 0.0], [0.0, 1.0]]; fn main() { let mut mesh = Mesh::new( PrimitiveTopology::TriangleList, RenderAssetUsages::default(), ); mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, POSITIONS.to_vec()); mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, UVS.to_vec()); mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, NORMALS.to_vec()); mesh.insert_indices(Indices::U32(INDICES.to_vec())); mesh.generate_tangents().ok(); } ``` ## Migration Guide - No breaking changes introduced. --- crates/bevy_mikktspace/src/generated.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/bevy_mikktspace/src/generated.rs b/crates/bevy_mikktspace/src/generated.rs index 592e1679e51685..2589e3930b1a4f 100644 --- a/crates/bevy_mikktspace/src/generated.rs +++ b/crates/bevy_mikktspace/src/generated.rs @@ -253,6 +253,10 @@ pub unsafe fn genTangSpace(geometry: &mut I, fAngularThreshold: f32 t += 1 } iNrTrianglesIn = iTotTris - iDegenTriangles; + + if iNrTrianglesIn <= 0 { + return false; + } DegenPrologue( pTriInfos.as_mut_ptr(), piTriListIn.as_mut_ptr(), @@ -1030,7 +1034,7 @@ unsafe fn InitTriInfo( } f += 1 } - while t < iNrTrianglesIn - 1 { + while t + 1 < iNrTrianglesIn { let iFO_a: i32 = (*pTriInfos.offset(t as isize)).iOrgFaceNumber; let iFO_b: i32 = (*pTriInfos.offset((t + 1) as isize)).iOrgFaceNumber; if iFO_a == iFO_b { From d9527c101c2f49c6884763cce36ea1d27dd6a597 Mon Sep 17 00:00:00 2001 From: charlotte Date: Sun, 25 Aug 2024 07:14:32 -0700 Subject: [PATCH 06/53] Rewrite screenshots. (#14833) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective Rewrite screenshotting to be able to accept any `RenderTarget`. Closes #12478 ## Solution Previously, screenshotting relied on setting a variety of state on the requested window. When extracted, the window's `swap_chain_texture_view` property would be swapped out with a texture_view created that frame for the screenshot pipeline to write back to the cpu. Besides being tightly coupled to window in a way that prevented screenshotting other render targets, this approach had the drawback of relying on the implicit state of `swap_chain_texture_view` being returned from a `NormalizedRenderTarget` when view targets were prepared. Because property is set every frame for windows, that wasn't a problem, but poses a problem for render target images. Namely, to do the equivalent trick, we'd have to replace the `GpuImage`'s texture view, and somehow restore it later. As such, this PR creates a new `prepare_view_textures` system which runs before `prepare_view_targets` that allows a new `prepare_screenshots` system to be sandwiched between and overwrite the render targets texture view if a screenshot has been requested that frame for the given target. Additionally, screenshotting itself has been changed to use a component + observer pattern. We now spawn a `Screenshot` component into the world, whose lifetime is tracked with a series of marker components. When the screenshot is read back to the CPU, we send the image over a channel back to the main world where an observer fires on the screenshot entity before being despawned the next frame. This allows the user to access resources in their save callback that might be useful (e.g. uploading the screenshot over the network, etc.). ## Testing ![image](https://github.com/user-attachments/assets/48f19aed-d9e1-4058-bb17-82b37f992b7b) TODO: - [x] Web - [ ] Manual texture view --- ## Showcase render to texture example: web saving still works: ## Migration Guide `ScreenshotManager` has been removed. To take a screenshot, spawn a `Screenshot` entity with the specified render target and provide an observer targeting the `ScreenshotCaptured` event. See the `window/screenshot` example to see an example. --------- Co-authored-by: Kristoffer Søholm --- crates/bevy_dev_tools/src/ci_testing/mod.rs | 5 +- .../bevy_dev_tools/src/ci_testing/systems.rs | 22 +- crates/bevy_render/src/view/mod.rs | 68 +- crates/bevy_render/src/view/window/mod.rs | 83 +-- .../bevy_render/src/view/window/screenshot.rs | 679 ++++++++++++++---- examples/window/screenshot.rs | 35 +- 6 files changed, 617 insertions(+), 275 deletions(-) diff --git a/crates/bevy_dev_tools/src/ci_testing/mod.rs b/crates/bevy_dev_tools/src/ci_testing/mod.rs index f6510f68a131d5..59949fa0bf94f0 100644 --- a/crates/bevy_dev_tools/src/ci_testing/mod.rs +++ b/crates/bevy_dev_tools/src/ci_testing/mod.rs @@ -7,6 +7,7 @@ pub use self::config::*; use bevy_app::prelude::*; use bevy_ecs::schedule::IntoSystemConfigs; +use bevy_render::view::screenshot::trigger_screenshots; use bevy_time::TimeUpdateStrategy; use std::time::Duration; @@ -51,7 +52,9 @@ impl Plugin for CiTestingPlugin { .insert_resource(config) .add_systems( Update, - systems::send_events.before(bevy_window::close_when_requested), + systems::send_events + .before(trigger_screenshots) + .before(bevy_window::close_when_requested), ); } } diff --git a/crates/bevy_dev_tools/src/ci_testing/systems.rs b/crates/bevy_dev_tools/src/ci_testing/systems.rs index abb2b28bccfd5a..2b652abf96db69 100644 --- a/crates/bevy_dev_tools/src/ci_testing/systems.rs +++ b/crates/bevy_dev_tools/src/ci_testing/systems.rs @@ -1,9 +1,8 @@ use super::config::*; use bevy_app::AppExit; use bevy_ecs::prelude::*; -use bevy_render::view::screenshot::ScreenshotManager; -use bevy_utils::tracing::{debug, info, warn}; -use bevy_window::PrimaryWindow; +use bevy_render::view::screenshot::{save_to_disk, Screenshot}; +use bevy_utils::tracing::{debug, info}; pub(crate) fn send_events(world: &mut World, mut current_frame: Local) { let mut config = world.resource_mut::(); @@ -23,21 +22,10 @@ pub(crate) fn send_events(world: &mut World, mut current_frame: Local) { info!("Exiting after {} frames. Test successful!", *current_frame); } CiTestingEvent::Screenshot => { - let mut primary_window_query = - world.query_filtered::>(); - let Ok(main_window) = primary_window_query.get_single(world) else { - warn!("Requesting screenshot, but PrimaryWindow is not available"); - continue; - }; - let Some(mut screenshot_manager) = world.get_resource_mut::() - else { - warn!("Requesting screenshot, but ScreenshotManager is not available"); - continue; - }; let path = format!("./screenshot-{}.png", *current_frame); - screenshot_manager - .save_screenshot_to_disk(main_window, path) - .unwrap(); + world + .spawn(Screenshot::primary_window()) + .observe(save_to_disk(path)); info!("Took a screenshot at frame {}.", *current_frame); } // Custom events are forwarded to the world. diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index f9c4a5c59b4d74..3729f4048ff214 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -5,6 +5,7 @@ use bevy_asset::{load_internal_asset, Handle}; pub use visibility::*; pub use window::*; +use crate::camera::NormalizedRenderTarget; use crate::extract_component::ExtractComponentPlugin; use crate::{ camera::{ @@ -25,12 +26,13 @@ use crate::{ }; use bevy_app::{App, Plugin}; use bevy_color::LinearRgba; +use bevy_derive::{Deref, DerefMut}; use bevy_ecs::prelude::*; use bevy_math::{mat3, vec2, vec3, Mat3, Mat4, UVec4, Vec2, Vec3, Vec4, Vec4Swizzles}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render_macros::ExtractComponent; use bevy_transform::components::GlobalTransform; -use bevy_utils::HashMap; +use bevy_utils::{hashbrown::hash_map::Entry, HashMap}; use std::{ ops::Range, sync::{ @@ -119,6 +121,9 @@ impl Plugin for ViewPlugin { render_app.add_systems( Render, ( + prepare_view_attachments + .in_set(RenderSet::ManageViews) + .before(prepare_view_targets), prepare_view_targets .in_set(RenderSet::ManageViews) .after(prepare_windows) @@ -132,7 +137,9 @@ impl Plugin for ViewPlugin { fn finish(&self, app: &mut App) { if let Some(render_app) = app.get_sub_app_mut(RenderApp) { - render_app.init_resource::(); + render_app + .init_resource::() + .init_resource::(); } } } @@ -458,6 +465,13 @@ pub struct ViewTarget { out_texture: OutputColorAttachment, } +/// Contains [`OutputColorAttachment`] used for each target present on any view in the current +/// frame, after being prepared by [`prepare_view_attachments`]. Users that want to override +/// the default output color attachment for a specific target can do so by adding a +/// [`OutputColorAttachment`] to this resource before [`prepare_view_targets`] is called. +#[derive(Resource, Default, Deref, DerefMut)] +pub struct ViewTargetAttachments(HashMap); + pub struct PostProcessWrite<'a> { pub source: &'a TextureView, pub destination: &'a TextureView, @@ -794,11 +808,41 @@ struct MainTargetTextures { main_texture: Arc, } -#[allow(clippy::too_many_arguments)] -pub fn prepare_view_targets( - mut commands: Commands, +/// Prepares the view target [`OutputColorAttachment`] for each view in the current frame. +pub fn prepare_view_attachments( windows: Res, images: Res>, + manual_texture_views: Res, + cameras: Query<&ExtractedCamera>, + mut view_target_attachments: ResMut, +) { + view_target_attachments.clear(); + for camera in cameras.iter() { + let Some(target) = &camera.target else { + continue; + }; + + match view_target_attachments.entry(target.clone()) { + Entry::Occupied(_) => {} + Entry::Vacant(entry) => { + let Some(attachment) = target + .get_texture_view(&windows, &images, &manual_texture_views) + .cloned() + .zip(target.get_texture_format(&windows, &images, &manual_texture_views)) + .map(|(view, format)| { + OutputColorAttachment::new(view.clone(), format.add_srgb_suffix()) + }) + else { + continue; + }; + entry.insert(attachment); + } + }; + } +} + +pub fn prepare_view_targets( + mut commands: Commands, clear_color_global: Res, render_device: Res, mut texture_cache: ResMut, @@ -809,24 +853,16 @@ pub fn prepare_view_targets( &CameraMainTextureUsages, &Msaa, )>, - manual_texture_views: Res, + view_target_attachments: Res, ) { let mut textures = HashMap::default(); - let mut output_textures = HashMap::default(); for (entity, camera, view, texture_usage, msaa) in cameras.iter() { let (Some(target_size), Some(target)) = (camera.physical_target_size, &camera.target) else { continue; }; - let Some(out_texture) = output_textures.entry(target.clone()).or_insert_with(|| { - target - .get_texture_view(&windows, &images, &manual_texture_views) - .zip(target.get_texture_format(&windows, &images, &manual_texture_views)) - .map(|(view, format)| { - OutputColorAttachment::new(view.clone(), format.add_srgb_suffix()) - }) - }) else { + let Some(out_attachment) = view_target_attachments.get(target) else { continue; }; @@ -913,7 +949,7 @@ pub fn prepare_view_targets( main_texture: main_textures.main_texture.clone(), main_textures, main_texture_format, - out_texture: out_texture.clone(), + out_texture: out_attachment.clone(), }); } } diff --git a/crates/bevy_render/src/view/window/mod.rs b/crates/bevy_render/src/view/window/mod.rs index 027a15e467d9b7..816d8c4e8dfd79 100644 --- a/crates/bevy_render/src/view/window/mod.rs +++ b/crates/bevy_render/src/view/window/mod.rs @@ -1,9 +1,6 @@ use crate::{ - render_resource::{ - BindGroupEntries, PipelineCache, SpecializedRenderPipelines, SurfaceTexture, TextureView, - }, + render_resource::{SurfaceTexture, TextureView}, renderer::{RenderAdapter, RenderDevice, RenderInstance}, - texture::TextureFormatPixelInfo, Extract, ExtractSchedule, Render, RenderApp, RenderSet, WgpuWrapper, }; use bevy_app::{App, Last, Plugin}; @@ -18,21 +15,16 @@ use bevy_winit::CustomCursorCache; use std::{ num::NonZeroU32, ops::{Deref, DerefMut}, - sync::PoisonError, }; use wgpu::{ - BufferUsages, SurfaceConfiguration, SurfaceTargetUnsafe, TextureFormat, TextureUsages, - TextureViewDescriptor, + SurfaceConfiguration, SurfaceTargetUnsafe, TextureFormat, TextureUsages, TextureViewDescriptor, }; pub mod cursor; pub mod screenshot; -use screenshot::{ - ScreenshotManager, ScreenshotPlugin, ScreenshotPreparedState, ScreenshotToScreenPipeline, -}; - use self::cursor::update_cursors; +use screenshot::{ScreenshotPlugin, ScreenshotToScreenPipeline}; pub struct WindowRenderPlugin; @@ -78,11 +70,9 @@ pub struct ExtractedWindow { pub swap_chain_texture_view: Option, pub swap_chain_texture: Option, pub swap_chain_texture_format: Option, - pub screenshot_memory: Option, pub size_changed: bool, pub present_mode_changed: bool, pub alpha_mode: CompositeAlphaMode, - pub screenshot_func: Option, } impl ExtractedWindow { @@ -120,7 +110,6 @@ impl DerefMut for ExtractedWindows { fn extract_windows( mut extracted_windows: ResMut, - screenshot_manager: Extract>, mut closing: Extract>, windows: Extract)>>, mut removed: Extract>, @@ -149,8 +138,6 @@ fn extract_windows( swap_chain_texture_format: None, present_mode_changed: false, alpha_mode: window.composite_alpha_mode, - screenshot_func: None, - screenshot_memory: None, }); // NOTE: Drop the swap chain frame here @@ -189,20 +176,6 @@ fn extract_windows( extracted_windows.remove(&removed_window); window_surfaces.remove(&removed_window); } - // This lock will never block because `callbacks` is `pub(crate)` and this is the singular callsite where it's locked. - // Even if a user had multiple copies of this system, since the system has a mutable resource access the two systems would never run - // at the same time - // TODO: since this is guaranteed, should the lock be replaced with an UnsafeCell to remove the overhead, or is it minor enough to be ignored? - for (window, screenshot_func) in screenshot_manager - .callbacks - .lock() - .unwrap_or_else(PoisonError::into_inner) - .drain() - { - if let Some(window) = extracted_windows.get_mut(&window) { - window.screenshot_func = Some(screenshot_func); - } - } } struct SurfaceData { @@ -254,9 +227,6 @@ pub fn prepare_windows( mut windows: ResMut, mut window_surfaces: ResMut, render_device: Res, - screenshot_pipeline: Res, - pipeline_cache: Res, - mut pipelines: ResMut>, #[cfg(target_os = "linux")] render_instance: Res, ) { for window in windows.windows.values_mut() { @@ -340,53 +310,6 @@ pub fn prepare_windows( } }; window.swap_chain_texture_format = Some(surface_data.configuration.format); - - if window.screenshot_func.is_some() { - let texture = render_device.create_texture(&wgpu::TextureDescriptor { - label: Some("screenshot-capture-rendertarget"), - size: wgpu::Extent3d { - width: surface_data.configuration.width, - height: surface_data.configuration.height, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: surface_data.configuration.format.add_srgb_suffix(), - usage: TextureUsages::RENDER_ATTACHMENT - | TextureUsages::COPY_SRC - | TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }); - let texture_view = texture.create_view(&Default::default()); - let buffer = render_device.create_buffer(&wgpu::BufferDescriptor { - label: Some("screenshot-transfer-buffer"), - size: screenshot::get_aligned_size( - window.physical_width, - window.physical_height, - surface_data.configuration.format.pixel_size() as u32, - ) as u64, - usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - let bind_group = render_device.create_bind_group( - "screenshot-to-screen-bind-group", - &screenshot_pipeline.bind_group_layout, - &BindGroupEntries::single(&texture_view), - ); - let pipeline_id = pipelines.specialize( - &pipeline_cache, - &screenshot_pipeline, - surface_data.configuration.format, - ); - window.swap_chain_texture_view = Some(texture_view); - window.screenshot_memory = Some(ScreenshotPreparedState { - texture, - buffer, - bind_group, - pipeline_id, - }); - } } } diff --git a/crates/bevy_render/src/view/window/screenshot.rs b/crates/bevy_render/src/view/window/screenshot.rs index 21f6f6a6bfe9d6..bc53fc9880f82a 100644 --- a/crates/bevy_render/src/view/window/screenshot.rs +++ b/crates/bevy_render/src/view/window/screenshot.rs @@ -1,16 +1,13 @@ -use std::{borrow::Cow, path::Path, sync::PoisonError}; - -use bevy_app::Plugin; -use bevy_asset::{load_internal_asset, Handle}; -use bevy_ecs::{entity::EntityHashMap, prelude::*}; -use bevy_tasks::AsyncComputeTaskPool; -use bevy_utils::tracing::{error, info, info_span}; -use std::sync::Mutex; -use thiserror::Error; -use wgpu::{ - CommandEncoder, Extent3d, ImageDataLayout, TextureFormat, COPY_BYTES_PER_ROW_ALIGNMENT, +use super::ExtractedWindows; +use crate::camera::{ + ManualTextureViewHandle, ManualTextureViews, NormalizedRenderTarget, RenderTarget, +}; +use crate::render_asset::RenderAssets; +use crate::render_resource::{BindGroupEntries, BufferUsages, TextureUsages, TextureView}; +use crate::texture::{GpuImage, OutputColorAttachment}; +use crate::view::{ + prepare_view_attachments, prepare_view_targets, ViewTargetAttachments, WindowSurfaces, }; - use crate::{ prelude::{Image, Shader}, render_asset::RenderAssetUsages, @@ -21,51 +18,116 @@ use crate::{ }, renderer::RenderDevice, texture::TextureFormatPixelInfo, - RenderApp, + ExtractSchedule, MainWorld, Render, RenderApp, RenderSet, +}; +use bevy_app::{First, Plugin, Update}; +use bevy_asset::{load_internal_asset, Handle}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::event::event_update_system; +use bevy_ecs::system::SystemState; +use bevy_ecs::{entity::EntityHashMap, prelude::*}; +use bevy_hierarchy::DespawnRecursiveExt; +use bevy_reflect::Reflect; +use bevy_tasks::AsyncComputeTaskPool; +use bevy_utils::tracing::{error, info, warn}; +use bevy_utils::{default, HashSet}; +use bevy_window::{PrimaryWindow, WindowRef}; +use std::ops::Deref; +use std::sync::mpsc::{Receiver, Sender}; +use std::sync::{Arc, Mutex}; +use std::{borrow::Cow, path::Path}; +use wgpu::{ + CommandEncoder, Extent3d, ImageDataLayout, TextureFormat, COPY_BYTES_PER_ROW_ALIGNMENT, }; -use super::ExtractedWindows; +#[derive(Event, Deref, DerefMut, Reflect, Debug)] +#[reflect(Debug)] +pub struct ScreenshotCaptured(pub Image); + +/// A component that signals to the renderer to capture a screenshot this frame. +/// +/// This component should be spawned on a new entity with an observer that will trigger +/// with [`ScreenshotCaptured`] when the screenshot is ready. +/// +/// Screenshots are captured asynchronously and may not be available immediately after the frame +/// that the component is spawned on. The observer should be used to handle the screenshot when it +/// is ready. +/// +/// Note that the screenshot entity will be despawned after the screenshot is captured and the +/// observer is triggered. +/// +/// # Usage +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # use bevy_render::view::screenshot::{save_to_disk, Screenshot}; +/// +/// fn take_screenshot(mut commands: Commands) { +/// commands.spawn(Screenshot::primary_window()) +/// .observe(save_to_disk("screenshot.png")); +/// } +/// ``` +#[derive(Component, Deref, DerefMut, Reflect, Debug)] +#[reflect(Component, Debug)] +pub struct Screenshot(pub RenderTarget); + +/// A marker component that indicates that a screenshot is currently being captured. +#[derive(Component)] +pub struct Capturing; + +/// A marker component that indicates that a screenshot has been captured, the image is ready, and +/// the screenshot entity can be despawned. +#[derive(Component)] +pub struct Captured; + +impl Screenshot { + /// Capture a screenshot of the provided window entity. + pub fn window(window: Entity) -> Self { + Self(RenderTarget::Window(WindowRef::Entity(window))) + } + + /// Capture a screenshot of the primary window, if one exists. + pub fn primary_window() -> Self { + Self(RenderTarget::Window(WindowRef::Primary)) + } -pub type ScreenshotFn = Box; + /// Capture a screenshot of the provided render target image. + pub fn image(image: Handle) -> Self { + Self(RenderTarget::Image(image)) + } -/// A resource which allows for taking screenshots of the window. -#[derive(Resource, Default)] -pub struct ScreenshotManager { - // this is in a mutex to enable extraction with only an immutable reference - pub(crate) callbacks: Mutex>, + /// Capture a screenshot of the provided manual texture view. + pub fn texture_view(texture_view: ManualTextureViewHandle) -> Self { + Self(RenderTarget::TextureView(texture_view)) + } } -#[derive(Error, Debug)] -#[error("A screenshot for this window has already been requested.")] -pub struct ScreenshotAlreadyRequestedError; - -impl ScreenshotManager { - /// Signals the renderer to take a screenshot of this frame. - /// - /// The given callback will eventually be called on one of the [`AsyncComputeTaskPool`]s threads. - pub fn take_screenshot( - &mut self, - window: Entity, - callback: impl FnOnce(Image) + Send + Sync + 'static, - ) -> Result<(), ScreenshotAlreadyRequestedError> { - self.callbacks - .get_mut() - .unwrap_or_else(PoisonError::into_inner) - .try_insert(window, Box::new(callback)) - .map(|_| ()) - .map_err(|_| ScreenshotAlreadyRequestedError) - } +struct ScreenshotPreparedState { + pub texture: Texture, + pub buffer: Buffer, + pub bind_group: BindGroup, + pub pipeline_id: CachedRenderPipelineId, + pub size: Extent3d, +} + +#[derive(Resource, Deref, DerefMut)] +pub struct CapturedScreenshots(pub Arc>>); + +#[derive(Resource, Deref, DerefMut, Default)] +struct RenderScreenshotTargets(EntityHashMap); + +#[derive(Resource, Deref, DerefMut, Default)] +struct RenderScreenshotsPrepared(EntityHashMap); - /// Signals the renderer to take a screenshot of this frame. - /// - /// The screenshot will eventually be saved to the given path, and the format will be derived from the extension. - pub fn save_screenshot_to_disk( - &mut self, - window: Entity, - path: impl AsRef, - ) -> Result<(), ScreenshotAlreadyRequestedError> { - let path = path.as_ref().to_owned(); - self.take_screenshot(window, move |img| match img.try_into_dynamic() { +#[derive(Resource, Deref, DerefMut)] +struct RenderScreenshotsSender(Sender<(Entity, Image)>); + +/// Saves the captured screenshot to disk at the provided path. +pub fn save_to_disk(path: impl AsRef) -> impl FnMut(Trigger) { + let path = path.as_ref().to_owned(); + move |trigger| { + let img = trigger.event().deref().clone(); + match img.try_into_dynamic() { Ok(dyn_img) => match image::ImageFormat::from_path(&path) { Ok(format) => { // discard the alpha channel which stores brightness values when HDR is enabled to make sure @@ -118,17 +180,238 @@ impl ScreenshotManager { Err(e) => error!("Cannot save screenshot, requested format not recognized: {e}"), }, Err(e) => error!("Cannot save screenshot, screen format cannot be understood: {e}"), - }) + } + } +} + +fn clear_screenshots(mut commands: Commands, screenshots: Query>) { + for entity in screenshots.iter() { + commands.entity(entity).despawn_recursive(); + } +} + +pub fn trigger_screenshots( + mut commands: Commands, + captured_screenshots: ResMut, +) { + let captured_screenshots = captured_screenshots.lock().unwrap(); + while let Ok((entity, image)) = captured_screenshots.try_recv() { + commands.entity(entity).insert(Captured); + commands.trigger_targets(ScreenshotCaptured(image), entity); } } +fn extract_screenshots( + mut targets: ResMut, + mut main_world: ResMut, + mut system_state: Local< + Option< + SystemState<( + Commands, + Query>, + Query<(Entity, &Screenshot), Without>, + )>, + >, + >, + mut seen_targets: Local>, +) { + if system_state.is_none() { + *system_state = Some(SystemState::new(&mut main_world)); + } + let system_state = system_state.as_mut().unwrap(); + let (mut commands, primary_window, screenshots) = system_state.get_mut(&mut main_world); + + targets.clear(); + seen_targets.clear(); + + let primary_window = primary_window.iter().next(); + + for (entity, screenshot) in screenshots.iter() { + let render_target = screenshot.0.clone(); + let Some(render_target) = render_target.normalize(primary_window) else { + warn!( + "Unknown render target for screenshot, skipping: {:?}", + render_target + ); + continue; + }; + if seen_targets.contains(&render_target) { + warn!( + "Duplicate render target for screenshot, skipping entity {:?}: {:?}", + entity, render_target + ); + // If we don't despawn the entity here, it will be captured again in the next frame + commands.entity(entity).despawn_recursive(); + continue; + } + seen_targets.insert(render_target.clone()); + targets.insert(entity, render_target); + commands.entity(entity).insert(Capturing); + } + + system_state.apply(&mut main_world); +} + +#[allow(clippy::too_many_arguments)] +fn prepare_screenshots( + targets: Res, + mut prepared: ResMut, + window_surfaces: Res, + render_device: Res, + screenshot_pipeline: Res, + pipeline_cache: Res, + mut pipelines: ResMut>, + images: Res>, + manual_texture_views: Res, + mut view_target_attachments: ResMut, +) { + prepared.clear(); + for (entity, target) in targets.iter() { + match target { + NormalizedRenderTarget::Window(window) => { + let window = window.entity(); + let Some(surface_data) = window_surfaces.surfaces.get(&window) else { + warn!("Unknown window for screenshot, skipping: {:?}", window); + continue; + }; + let format = surface_data.configuration.format.add_srgb_suffix(); + let size = Extent3d { + width: surface_data.configuration.width, + height: surface_data.configuration.height, + ..default() + }; + let (texture_view, state) = prepare_screenshot_state( + size, + format, + &render_device, + &screenshot_pipeline, + &pipeline_cache, + &mut pipelines, + ); + prepared.insert(*entity, state); + view_target_attachments.insert( + target.clone(), + OutputColorAttachment::new(texture_view.clone(), format.add_srgb_suffix()), + ); + } + NormalizedRenderTarget::Image(image) => { + let Some(gpu_image) = images.get(image) else { + warn!("Unknown image for screenshot, skipping: {:?}", image); + continue; + }; + let format = gpu_image.texture_format; + let size = Extent3d { + width: gpu_image.size.x, + height: gpu_image.size.y, + ..default() + }; + let (texture_view, state) = prepare_screenshot_state( + size, + format, + &render_device, + &screenshot_pipeline, + &pipeline_cache, + &mut pipelines, + ); + prepared.insert(*entity, state); + view_target_attachments.insert( + target.clone(), + OutputColorAttachment::new(texture_view.clone(), format.add_srgb_suffix()), + ); + } + NormalizedRenderTarget::TextureView(texture_view) => { + let Some(manual_texture_view) = manual_texture_views.get(texture_view) else { + warn!( + "Unknown manual texture view for screenshot, skipping: {:?}", + texture_view + ); + continue; + }; + let format = manual_texture_view.format; + let size = Extent3d { + width: manual_texture_view.size.x, + height: manual_texture_view.size.y, + ..default() + }; + let (texture_view, state) = prepare_screenshot_state( + size, + format, + &render_device, + &screenshot_pipeline, + &pipeline_cache, + &mut pipelines, + ); + prepared.insert(*entity, state); + view_target_attachments.insert( + target.clone(), + OutputColorAttachment::new(texture_view.clone(), format.add_srgb_suffix()), + ); + } + } + } +} + +fn prepare_screenshot_state( + size: Extent3d, + format: TextureFormat, + render_device: &RenderDevice, + pipeline: &ScreenshotToScreenPipeline, + pipeline_cache: &PipelineCache, + pipelines: &mut SpecializedRenderPipelines, +) -> (TextureView, ScreenshotPreparedState) { + let texture = render_device.create_texture(&wgpu::TextureDescriptor { + label: Some("screenshot-capture-rendertarget"), + size, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format, + usage: TextureUsages::RENDER_ATTACHMENT + | TextureUsages::COPY_SRC + | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }); + let texture_view = texture.create_view(&Default::default()); + let buffer = render_device.create_buffer(&wgpu::BufferDescriptor { + label: Some("screenshot-transfer-buffer"), + size: get_aligned_size(size.width, size.height, format.pixel_size() as u32) as u64, + usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + let bind_group = render_device.create_bind_group( + "screenshot-to-screen-bind-group", + &pipeline.bind_group_layout, + &BindGroupEntries::single(&texture_view), + ); + let pipeline_id = pipelines.specialize(pipeline_cache, pipeline, format); + + ( + texture_view, + ScreenshotPreparedState { + texture, + buffer, + bind_group, + pipeline_id, + size, + }, + ) +} + pub struct ScreenshotPlugin; const SCREENSHOT_SHADER_HANDLE: Handle = Handle::weak_from_u128(11918575842344596158); impl Plugin for ScreenshotPlugin { fn build(&self, app: &mut bevy_app::App) { - app.init_resource::(); + app.add_systems( + First, + clear_screenshots + .after(event_update_system) + .before(apply_deferred), + ) + .add_systems(Update, trigger_screenshots) + .register_type::() + .register_type::(); load_internal_asset!( app, @@ -139,8 +422,23 @@ impl Plugin for ScreenshotPlugin { } fn finish(&self, app: &mut bevy_app::App) { + let (tx, rx) = std::sync::mpsc::channel(); + app.insert_resource(CapturedScreenshots(Arc::new(Mutex::new(rx)))); + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { - render_app.init_resource::>(); + render_app + .insert_resource(RenderScreenshotsSender(tx)) + .init_resource::() + .init_resource::() + .init_resource::>() + .add_systems(ExtractSchedule, extract_screenshots.ambiguous_with_all()) + .add_systems( + Render, + prepare_screenshots + .after(prepare_view_attachments) + .before(prepare_view_targets) + .in_set(RenderSet::ManageViews), + ); } } } @@ -221,114 +519,187 @@ impl SpecializedRenderPipeline for ScreenshotToScreenPipeline { } } -pub struct ScreenshotPreparedState { - pub texture: Texture, - pub buffer: Buffer, - pub bind_group: BindGroup, - pub pipeline_id: CachedRenderPipelineId, -} - pub(crate) fn submit_screenshot_commands(world: &World, encoder: &mut CommandEncoder) { - let windows = world.resource::(); + let targets = world.resource::(); + let prepared = world.resource::(); let pipelines = world.resource::(); - - for window in windows.values() { - if let Some(memory) = &window.screenshot_memory { - let width = window.physical_width; - let height = window.physical_height; - let texture_format = window.swap_chain_texture_format.unwrap(); - - encoder.copy_texture_to_buffer( - memory.texture.as_image_copy(), - wgpu::ImageCopyBuffer { - buffer: &memory.buffer, - layout: layout_data(width, height, texture_format), - }, - Extent3d { + let gpu_images = world.resource::>(); + let windows = world.resource::(); + let manual_texture_views = world.resource::(); + + for (entity, render_target) in targets.iter() { + match render_target { + NormalizedRenderTarget::Window(window) => { + let window = window.entity(); + let Some(window) = windows.get(&window) else { + continue; + }; + let width = window.physical_width; + let height = window.physical_height; + let Some(texture_format) = window.swap_chain_texture_format else { + continue; + }; + let Some(swap_chain_texture) = window.swap_chain_texture.as_ref() else { + continue; + }; + let texture_view = swap_chain_texture.texture.create_view(&Default::default()); + render_screenshot( + encoder, + prepared, + pipelines, + entity, width, height, - ..Default::default() - }, - ); - if let Some(pipeline) = pipelines.get_render_pipeline(memory.pipeline_id) { - let true_swapchain_texture_view = window - .swap_chain_texture - .as_ref() - .unwrap() - .texture - .create_view(&Default::default()); - let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("screenshot_to_screen_pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &true_swapchain_texture_view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }); - pass.set_pipeline(pipeline); - pass.set_bind_group(0, &memory.bind_group, &[]); - pass.draw(0..3, 0..1); + texture_format, + &texture_view, + ); } + NormalizedRenderTarget::Image(image) => { + let Some(gpu_image) = gpu_images.get(image) else { + warn!("Unknown image for screenshot, skipping: {:?}", image); + continue; + }; + let width = gpu_image.size.x; + let height = gpu_image.size.y; + let texture_format = gpu_image.texture_format; + let texture_view = gpu_image.texture_view.deref(); + render_screenshot( + encoder, + prepared, + pipelines, + entity, + width, + height, + texture_format, + texture_view, + ); + } + NormalizedRenderTarget::TextureView(texture_view) => { + let Some(texture_view) = manual_texture_views.get(texture_view) else { + warn!( + "Unknown manual texture view for screenshot, skipping: {:?}", + texture_view + ); + continue; + }; + let width = texture_view.size.x; + let height = texture_view.size.y; + let texture_format = texture_view.format; + let texture_view = texture_view.texture_view.deref(); + render_screenshot( + encoder, + prepared, + pipelines, + entity, + width, + height, + texture_format, + texture_view, + ); + } + }; + } +} + +#[allow(clippy::too_many_arguments)] +fn render_screenshot( + encoder: &mut CommandEncoder, + prepared: &RenderScreenshotsPrepared, + pipelines: &PipelineCache, + entity: &Entity, + width: u32, + height: u32, + texture_format: TextureFormat, + texture_view: &wgpu::TextureView, +) { + if let Some(prepared_state) = &prepared.get(entity) { + encoder.copy_texture_to_buffer( + prepared_state.texture.as_image_copy(), + wgpu::ImageCopyBuffer { + buffer: &prepared_state.buffer, + layout: layout_data(width, height, texture_format), + }, + Extent3d { + width, + height, + ..Default::default() + }, + ); + + if let Some(pipeline) = pipelines.get_render_pipeline(prepared_state.pipeline_id) { + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("screenshot_to_screen_pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: texture_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + pass.set_pipeline(pipeline); + pass.set_bind_group(0, &prepared_state.bind_group, &[]); + pass.draw(0..3, 0..1); } } } pub(crate) fn collect_screenshots(world: &mut World) { - let _span = info_span!("collect_screenshots"); - - let mut windows = world.resource_mut::(); - for window in windows.values_mut() { - if let Some(screenshot_func) = window.screenshot_func.take() { - let width = window.physical_width; - let height = window.physical_height; - let texture_format = window.swap_chain_texture_format.unwrap(); - let pixel_size = texture_format.pixel_size(); - let ScreenshotPreparedState { buffer, .. } = window.screenshot_memory.take().unwrap(); - - let finish = async move { - let (tx, rx) = async_channel::bounded(1); - let buffer_slice = buffer.slice(..); - // The polling for this map call is done every frame when the command queue is submitted. - buffer_slice.map_async(wgpu::MapMode::Read, move |result| { - let err = result.err(); - if err.is_some() { - panic!("{}", err.unwrap().to_string()); - } - tx.try_send(()).unwrap(); - }); - rx.recv().await.unwrap(); - let data = buffer_slice.get_mapped_range(); - // we immediately move the data to CPU memory to avoid holding the mapped view for long - let mut result = Vec::from(&*data); - drop(data); - drop(buffer); - - if result.len() != ((width * height) as usize * pixel_size) { - // Our buffer has been padded because we needed to align to a multiple of 256. - // We remove this padding here - let initial_row_bytes = width as usize * pixel_size; - let buffered_row_bytes = align_byte_size(width * pixel_size as u32) as usize; - - let mut take_offset = buffered_row_bytes; - let mut place_offset = initial_row_bytes; - for _ in 1..height { - result.copy_within( - take_offset..take_offset + buffered_row_bytes, - place_offset, - ); - take_offset += buffered_row_bytes; - place_offset += initial_row_bytes; - } - result.truncate(initial_row_bytes * height as usize); + #[cfg(feature = "trace")] + let _span = bevy_utils::tracing::info_span!("collect_screenshots").entered(); + + let sender = world.resource::().deref().clone(); + let prepared = world.resource::(); + + for (entity, prepared) in prepared.iter() { + let entity = *entity; + let sender = sender.clone(); + let width = prepared.size.width; + let height = prepared.size.height; + let texture_format = prepared.texture.format(); + let pixel_size = texture_format.pixel_size(); + let buffer = prepared.buffer.clone(); + + let finish = async move { + let (tx, rx) = async_channel::bounded(1); + let buffer_slice = buffer.slice(..); + // The polling for this map call is done every frame when the command queue is submitted. + buffer_slice.map_async(wgpu::MapMode::Read, move |result| { + let err = result.err(); + if err.is_some() { + panic!("{}", err.unwrap().to_string()); + } + tx.try_send(()).unwrap(); + }); + rx.recv().await.unwrap(); + let data = buffer_slice.get_mapped_range(); + // we immediately move the data to CPU memory to avoid holding the mapped view for long + let mut result = Vec::from(&*data); + drop(data); + + if result.len() != ((width * height) as usize * pixel_size) { + // Our buffer has been padded because we needed to align to a multiple of 256. + // We remove this padding here + let initial_row_bytes = width as usize * pixel_size; + let buffered_row_bytes = align_byte_size(width * pixel_size as u32) as usize; + + let mut take_offset = buffered_row_bytes; + let mut place_offset = initial_row_bytes; + for _ in 1..height { + result.copy_within(take_offset..take_offset + buffered_row_bytes, place_offset); + take_offset += buffered_row_bytes; + place_offset += initial_row_bytes; } + result.truncate(initial_row_bytes * height as usize); + } - screenshot_func(Image::new( + if let Err(e) = sender.send(( + entity, + Image::new( Extent3d { width, height, @@ -338,10 +709,12 @@ pub(crate) fn collect_screenshots(world: &mut World) { result, texture_format, RenderAssetUsages::RENDER_WORLD, - )); - }; + ), + )) { + error!("Failed to send screenshot: {:?}", e); + } + }; - AsyncComputeTaskPool::get().spawn(finish).detach(); - } + AsyncComputeTaskPool::get().spawn(finish).detach(); } } diff --git a/examples/window/screenshot.rs b/examples/window/screenshot.rs index 66591ac053a381..1bf800d18c16cc 100644 --- a/examples/window/screenshot.rs +++ b/examples/window/screenshot.rs @@ -1,29 +1,48 @@ //! An example showing how to save screenshots to disk use bevy::prelude::*; -use bevy::render::view::screenshot::ScreenshotManager; -use bevy::window::PrimaryWindow; +use bevy::window::SystemCursorIcon; +use bevy_render::view::cursor::CursorIcon; +use bevy_render::view::screenshot::{save_to_disk, Capturing, Screenshot}; fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) - .add_systems(Update, screenshot_on_spacebar) + .add_systems(Update, (screenshot_on_spacebar, screenshot_saving)) .run(); } fn screenshot_on_spacebar( + mut commands: Commands, input: Res>, - main_window: Query>, - mut screenshot_manager: ResMut, mut counter: Local, ) { if input.just_pressed(KeyCode::Space) { let path = format!("./screenshot-{}.png", *counter); *counter += 1; - screenshot_manager - .save_screenshot_to_disk(main_window.single(), path) - .unwrap(); + commands + .spawn(Screenshot::primary_window()) + .observe(save_to_disk(path)); + } +} + +fn screenshot_saving( + mut commands: Commands, + screenshot_saving: Query>, + windows: Query>, +) { + let window = windows.single(); + match screenshot_saving.iter().count() { + 0 => { + commands.entity(window).remove::(); + } + x if x > 0 => { + commands + .entity(window) + .insert(CursorIcon::from(SystemCursorIcon::Progress)); + } + _ => {} } } From d46a05e387234116c12754fde5f8a7c07a057cf6 Mon Sep 17 00:00:00 2001 From: IceSentry Date: Sun, 25 Aug 2024 10:15:11 -0400 Subject: [PATCH 07/53] Simplify render_to_texture examples (#14855) # Objective - The examples use a more verbose than necessary way to initialize the image - The order of the camera doesn't need to be specified. At least I didn't see a difference in my testing ## Solution - Use `Image::new_fill()` to fill the image instead of abusing `resize()` - Remove the camera ordering --- examples/3d/render_to_texture.rs | 35 ++++++++++------------------- examples/ui/render_ui_to_texture.rs | 35 ++++++++++------------------- 2 files changed, 24 insertions(+), 46 deletions(-) diff --git a/examples/3d/render_to_texture.rs b/examples/3d/render_to_texture.rs index 3fd76f00c899f7..17ab23a7277a02 100644 --- a/examples/3d/render_to_texture.rs +++ b/examples/3d/render_to_texture.rs @@ -5,12 +5,11 @@ use std::f32::consts::PI; use bevy::{ prelude::*, render::{ - render_resource::{ - Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, - }, + render_resource::{Extent3d, TextureDimension, TextureFormat, TextureUsages}, view::RenderLayers, }, }; +use bevy_render::render_asset::RenderAssetUsages; fn main() { App::new() @@ -41,24 +40,16 @@ fn setup( }; // This is the texture that will be rendered to. - let mut image = Image { - texture_descriptor: TextureDescriptor { - label: None, - size, - dimension: TextureDimension::D2, - format: TextureFormat::Bgra8UnormSrgb, - mip_level_count: 1, - sample_count: 1, - usage: TextureUsages::TEXTURE_BINDING - | TextureUsages::COPY_DST - | TextureUsages::RENDER_ATTACHMENT, - view_formats: &[], - }, - ..default() - }; - - // fill image.data with zeroes - image.resize(size); + let mut image = Image::new_fill( + size, + TextureDimension::D2, + &[0, 0, 0, 0], + TextureFormat::Bgra8UnormSrgb, + RenderAssetUsages::default(), + ); + // You need to set these texture usage flags in order to use the image as a render target + image.texture_descriptor.usage = + TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST | TextureUsages::RENDER_ATTACHMENT; let image_handle = images.add(image); @@ -100,8 +91,6 @@ fn setup( commands.spawn(( Camera3dBundle { camera: Camera { - // render before the "main pass" camera - order: -1, target: image_handle.clone().into(), clear_color: Color::WHITE.into(), ..default() diff --git a/examples/ui/render_ui_to_texture.rs b/examples/ui/render_ui_to_texture.rs index 639347d024828a..5c1648f3f92419 100644 --- a/examples/ui/render_ui_to_texture.rs +++ b/examples/ui/render_ui_to_texture.rs @@ -7,11 +7,10 @@ use bevy::{ prelude::*, render::{ camera::RenderTarget, - render_resource::{ - Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, - }, + render_resource::{Extent3d, TextureDimension, TextureFormat, TextureUsages}, }, }; +use bevy_render::render_asset::RenderAssetUsages; fn main() { App::new() @@ -38,24 +37,16 @@ fn setup( }; // This is the texture that will be rendered to. - let mut image = Image { - texture_descriptor: TextureDescriptor { - label: None, - size, - dimension: TextureDimension::D2, - format: TextureFormat::Bgra8UnormSrgb, - mip_level_count: 1, - sample_count: 1, - usage: TextureUsages::TEXTURE_BINDING - | TextureUsages::COPY_DST - | TextureUsages::RENDER_ATTACHMENT, - view_formats: &[], - }, - ..default() - }; - - // fill image.data with zeroes - image.resize(size); + let mut image = Image::new_fill( + size, + TextureDimension::D2, + &[0, 0, 0, 0], + TextureFormat::Bgra8UnormSrgb, + RenderAssetUsages::default(), + ); + // You need to set these texture usage flags in order to use the image as a render target + image.texture_descriptor.usage = + TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST | TextureUsages::RENDER_ATTACHMENT; let image_handle = images.add(image); @@ -65,8 +56,6 @@ fn setup( let texture_camera = commands .spawn(Camera2dBundle { camera: Camera { - // render before the "main pass" camera - order: -1, target: RenderTarget::Image(image_handle.clone()), ..default() }, From 6250698b56bcd81d1d8898ec205ea3be3c0593fc Mon Sep 17 00:00:00 2001 From: Zachary Harrold Date: Mon, 26 Aug 2024 00:15:49 +1000 Subject: [PATCH 08/53] Added `on_unimplemented` Diagnostic for `IntoObserverSystem` (#14840) # Objective - Fixes #14658. ## Solution - Added `on_unimplemented` Diagnostic for `IntoObserverSystem` calling out argument ordering in a `note` - Added an example to the documentation on `App::observe` to provide some explanation to users. ## Testing - Ran CI locally - Deliberately introduced a parameter order error in the `ecs/observers.rs` example as a test. --- ## Showcase
Error Before ``` error[E0277]: the trait bound `{closure@examples/ecs/observers.rs:19:13: 22:37}: IntoObserverSystem<_, _, _>` is not satisfied --> examples/ecs/observers.rs:19:13 | 18 | .observe( | ------- required by a bound introduced by this call 19 | / |mines: Query<&Mine>, 20 | | trigger: Trigger, 21 | | index: Res, 22 | | mut commands: Commands| { ... | 34 | | } 35 | | }, | |_____________^ the trait `bevy::prelude::IntoSystem, (), _>` is not implemented for closure `{closure@examples/ecs/observers.rs:19:13: 22:37}`, which is required by `{closure@examples/ecs/observers.rs:19:13: 22:37}: IntoObserverSystem<_, _, _>` | = note: required for `{closure@examples/ecs/observers.rs:19:13: 22:37}` to implement `IntoObserverSystem<_, _, _>` note: required by a bound in `bevy::prelude::App::observe` --> C:\Users\Zac\Documents\GitHub\bevy\crates\bevy_app\src\app.rs:995:24 | 993 | pub fn observe( | ------- required by a bound in this associated function 994 | &mut self, 995 | observer: impl IntoObserverSystem, | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `App::observe` For more information about this error, try `rustc --explain E0277`. error: could not compile `bevy` (example "observers") due to 1 previous error ```
Error After ``` error[E0277]: `{closure@examples/ecs/observers.rs:19:13: 22:37}` cannot become an `ObserverSystem` --> examples/ecs/observers.rs:19:13 | 18 | .observe( | ------- required by a bound introduced by this call 19 | / |mines: Query<&Mine>, 20 | | trigger: Trigger, 21 | | index: Res, 22 | | mut commands: Commands| { ... | 34 | | } 35 | | }, | |_____________^ the trait `IntoObserverSystem` is not implemented | = help: the trait `bevy::prelude::IntoSystem, (), _>` is not implemented for closure `{closure@examples/ecs/observers.rs:19:13: 22:37}`, which is required by `{closure@examples/ecs/observers.rs:19:13: 22:37}: IntoObserverSystem<_, _, _>` = note: for function `ObserverSystem`s, ensure the first argument is a `Trigger` and any subsequent ones are `SystemParam` = note: required for `{closure@examples/ecs/observers.rs:19:13: 22:37}` to implement `IntoObserverSystem<_, _, _>` note: required by a bound in `bevy::prelude::App::observe` --> C:\Users\Zac\Documents\GitHub\bevy\crates\bevy_app\src\app.rs:1025:24 | 1023 | pub fn observe( | ------- required by a bound in this associated function 1024 | &mut self, 1025 | observer: impl IntoObserverSystem, | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `App::observe` For more information about this error, try `rustc --explain E0277`. error: could not compile `bevy` (example "observers") due to 1 previous error ```
--- crates/bevy_app/src/app.rs | 30 +++++++++++++++++++ crates/bevy_ecs/src/system/observer_system.rs | 5 ++++ 2 files changed, 35 insertions(+) diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index f79bc6d52a55e9..07a29800f93df2 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -990,6 +990,36 @@ impl App { } /// Spawns an [`Observer`] entity, which will watch for and respond to the given event. + /// + /// # Examples + /// + /// ```rust + /// # use bevy_app::prelude::*; + /// # use bevy_ecs::prelude::*; + /// # use bevy_utils::default; + /// # + /// # let mut app = App::new(); + /// # + /// # #[derive(Event)] + /// # struct Party { + /// # friends_allowed: bool, + /// # }; + /// # + /// # #[derive(Event)] + /// # struct Invite; + /// # + /// # #[derive(Component)] + /// # struct Friend; + /// # + /// // An observer system can be any system where the first parameter is a trigger + /// app.observe(|trigger: Trigger, friends: Query>, mut commands: Commands| { + /// if trigger.event().friends_allowed { + /// for friend in friends.iter() { + /// commands.trigger_targets(Invite, friend); + /// } + /// } + /// }); + /// ``` pub fn observe( &mut self, observer: impl IntoObserverSystem, diff --git a/crates/bevy_ecs/src/system/observer_system.rs b/crates/bevy_ecs/src/system/observer_system.rs index c5a04f25dd4eb6..28ea902b1aa4df 100644 --- a/crates/bevy_ecs/src/system/observer_system.rs +++ b/crates/bevy_ecs/src/system/observer_system.rs @@ -25,6 +25,11 @@ impl< } /// Implemented for systems that convert into [`ObserverSystem`]. +#[diagnostic::on_unimplemented( + message = "`{Self}` cannot become an `ObserverSystem`", + label = "the trait `IntoObserverSystem` is not implemented", + note = "for function `ObserverSystem`s, ensure the first argument is a `Trigger` and any subsequent ones are `SystemParam`" +)] pub trait IntoObserverSystem: Send + 'static { /// The type of [`System`] that this instance converts into. type System: ObserverSystem; From 2c3f5a00ac44ec41b575489c27b79c6ccd5b6354 Mon Sep 17 00:00:00 2001 From: Rob Parrett Date: Sun, 25 Aug 2024 07:16:04 -0700 Subject: [PATCH 09/53] Add `AnimationGraph::from_clips` and simplify `animated_fox` example (#14853) # Objective Add a convenience constructor to make simple animation graphs easier to build. I've had some notes about attempting this since #11989 that I just remembered after seeing #14852. This partially addresses #14852, but I don't really know animation well enough to write all of the documentation it's asking for. ## Solution Add `AnimationGraph::from_clips` and use it to simplify `animated_fox`. Do some other little bits of incidental cleanup and documentation . ## Testing I ran `cargo run --example animated_fox`. --- crates/bevy_animation/src/graph.rs | 17 ++++++++++++++++ crates/bevy_animation/src/lib.rs | 5 ++++- examples/animation/animated_fox.rs | 32 ++++++++++++------------------ 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/crates/bevy_animation/src/graph.rs b/crates/bevy_animation/src/graph.rs index 3f065b79111832..aeeed9fcdf6314 100644 --- a/crates/bevy_animation/src/graph.rs +++ b/crates/bevy_animation/src/graph.rs @@ -192,6 +192,23 @@ impl AnimationGraph { (graph, node_index) } + /// A convenience method to create an [`AnimationGraph`]s with an iterator + /// of clips. + /// + /// All of the animation clips will be direct children of the root with + /// weight 1.0. + /// + /// Returns the the graph and indices of the new nodes. + pub fn from_clips<'a, I>(clips: I) -> (Self, Vec) + where + I: IntoIterator>, + ::IntoIter: 'a, + { + let mut graph = Self::new(); + let indices = graph.add_clips(clips, 1.0, graph.root).collect(); + (graph, indices) + } + /// Adds an [`AnimationClip`] to the animation graph with the given weight /// and returns its index. /// diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 70c49b3c3938a1..bf2904440ce5be 100755 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -529,7 +529,10 @@ impl ActiveAnimation { } } -/// Animation controls +/// Animation controls. +/// +/// Automatically added to any root animations of a `SceneBundle` when it is +/// spawned. #[derive(Component, Default, Reflect)] #[reflect(Component)] pub struct AnimationPlayer { diff --git a/examples/animation/animated_fox.rs b/examples/animation/animated_fox.rs index da90484f7bb7fd..a167e2b171c6d0 100644 --- a/examples/animation/animated_fox.rs +++ b/examples/animation/animated_fox.rs @@ -9,6 +9,8 @@ use bevy::{ prelude::*, }; +const FOX_PATH: &str = "models/animated/Fox.glb"; + fn main() { App::new() .insert_resource(AmbientLight { @@ -37,26 +39,17 @@ fn setup( mut graphs: ResMut>, ) { // Build the animation graph - let mut graph = AnimationGraph::new(); - let animations = graph - .add_clips( - [ - GltfAssetLabel::Animation(2).from_asset("models/animated/Fox.glb"), - GltfAssetLabel::Animation(1).from_asset("models/animated/Fox.glb"), - GltfAssetLabel::Animation(0).from_asset("models/animated/Fox.glb"), - ] - .into_iter() - .map(|path| asset_server.load(path)), - 1.0, - graph.root, - ) - .collect(); + let (graph, node_indices) = AnimationGraph::from_clips([ + asset_server.load(GltfAssetLabel::Animation(2).from_asset(FOX_PATH)), + asset_server.load(GltfAssetLabel::Animation(1).from_asset(FOX_PATH)), + asset_server.load(GltfAssetLabel::Animation(0).from_asset(FOX_PATH)), + ]); // Insert a resource with the current scene information - let graph = graphs.add(graph); + let graph_handle = graphs.add(graph); commands.insert_resource(Animations { - animations, - graph: graph.clone(), + animations: node_indices, + graph: graph_handle, }); // Camera @@ -91,7 +84,7 @@ fn setup( // Fox commands.spawn(SceneBundle { - scene: asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/animated/Fox.glb")), + scene: asset_server.load(GltfAssetLabel::Scene(0).from_asset(FOX_PATH)), ..default() }); @@ -104,7 +97,8 @@ fn setup( println!(" - return: change animation"); } -// Once the scene is loaded, start the animation +// An `AnimationPlayer` is automatically added to the scene when it's ready. +// When the player is added, start the animation. fn setup_scene_once_loaded( mut commands: Commands, animations: Res, From 94d40d206eb6aece9f4f775e1394da335219a5df Mon Sep 17 00:00:00 2001 From: MichiRecRoom <1008889+LikeLakers2@users.noreply.github.com> Date: Sun, 25 Aug 2024 10:16:11 -0400 Subject: [PATCH 10/53] Replace the `wgpu_trace` feature with a field in `bevy_render::settings::WgpuSettings` (#14842) # Objective - Remove the `wgpu_trace` feature while still making it easy/possible to record wgpu traces for debugging. - Close #14725. - Get a taste of the bevy codebase. :P ## Solution This PR performs the above objective by removing the `wgpu_trace` feature from all `Cargo.toml` files. However, wgpu traces are still useful for debugging - but to record them, you need to pass in a directory path to store the traces in. To avoid forcing users into manually creating the renderer, `bevy_render::settings::WgpuSettings` now has a `trace_path` field, so that all of Bevy's automatic initialization can happen while still allowing for tracing. ## Testing - Did you test these changes? If so, how? - I have tested these changes, but only via running `cargo run -p ci`. I am hoping the Github Actions workflows will catch anything I missed. - Are there any parts that need more testing? - I do not believe so. - How can other people (reviewers) test your changes? Is there anything specific they need to know? - If you want to test these changes, I have updated the debugging guide (`docs/debugging.md`) section on WGPU Tracing. - If relevant, what platforms did you test these changes on, and are there any important ones you can't test? - I ran the above command on a Windows 10 64-bit (x64) machine, using the `stable-x86_64-pc-windows-msvc` toolchain. I do not have anything set up for other platforms or targets (though I can't imagine this needs testing on other platforms). --- ## Migration Guide 1. The `bevy/wgpu_trace`, `bevy_render/wgpu_trace`, and `bevy_internal/wgpu_trace` features no longer exist. Remove them from your `Cargo.toml`, CI, tooling, and what-not. 2. Follow the instructions in the updated `docs/debugging.md` file in the repository, under the WGPU Tracing section. Because of the changes made, you can now generate traces to any path, rather than the hardcoded `%WorkspaceRoot%/wgpu_trace` (where `%WorkspaceRoot%` is... the root of your crate's workspace) folder. (If WGPU hasn't restored tracing functionality...) Do note that WGPU has not yet restored tracing functionality. However, once it does, the above should be sufficient to generate new traces. --------- Co-authored-by: TrialDragon <31419708+TrialDragon@users.noreply.github.com> --- Cargo.toml | 3 --- crates/bevy_internal/Cargo.toml | 1 - crates/bevy_render/Cargo.toml | 1 - crates/bevy_render/src/renderer/mod.rs | 12 +----------- crates/bevy_render/src/settings.rs | 5 ++++- docs/cargo_features.md | 1 - docs/debugging.md | 12 +++++++++--- 7 files changed, 14 insertions(+), 21 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 474058b45f4476..16a933183d7e6e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -180,9 +180,6 @@ trace_tracy_memory = [ # Tracing support trace = ["bevy_internal/trace"] -# Save a trace of all wgpu calls -wgpu_trace = ["bevy_internal/wgpu_trace"] - # EXR image format support exr = ["bevy_internal/exr"] diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 7505c8f0be3691..424a1416beca20 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -24,7 +24,6 @@ trace = [ trace_chrome = ["bevy_log/tracing-chrome"] trace_tracy = ["bevy_render?/tracing-tracy", "bevy_log/tracing-tracy"] trace_tracy_memory = ["bevy_log/trace_tracy_memory"] -wgpu_trace = ["bevy_render/wgpu_trace"] detailed_trace = ["bevy_utils/detailed_trace"] sysinfo_plugin = ["bevy_diagnostic/sysinfo_plugin"] diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index 9ebd06678bfc3b..834fca03f102a1 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -29,7 +29,6 @@ zstd = ["ruzstd"] trace = ["profiling"] tracing-tracy = [] -wgpu_trace = [] ci_limits = [] webgl = ["wgpu/webgl"] webgpu = ["wgpu/webgpu"] diff --git a/crates/bevy_render/src/renderer/mod.rs b/crates/bevy_render/src/renderer/mod.rs index 478128f2198c36..582ad2d18150be 100644 --- a/crates/bevy_render/src/renderer/mod.rs +++ b/crates/bevy_render/src/renderer/mod.rs @@ -195,16 +195,6 @@ pub async fn initialize_renderer( ); } - #[cfg(feature = "wgpu_trace")] - let trace_path = { - let path = std::path::Path::new("wgpu_trace"); - // ignore potential error, wgpu will log it - let _ = std::fs::create_dir(path); - Some(path) - }; - #[cfg(not(feature = "wgpu_trace"))] - let trace_path = None; - // Maybe get features and limits based on what is supported by the adapter/backend let mut features = wgpu::Features::empty(); let mut limits = options.limits.clone(); @@ -357,7 +347,7 @@ pub async fn initialize_renderer( required_limits: limits, memory_hints: options.memory_hints.clone(), }, - trace_path, + options.trace_path.as_deref(), ) .await .unwrap(); diff --git a/crates/bevy_render/src/settings.rs b/crates/bevy_render/src/settings.rs index 1f9df24a78de5c..7dbf016a8e4c23 100644 --- a/crates/bevy_render/src/settings.rs +++ b/crates/bevy_render/src/settings.rs @@ -1,7 +1,7 @@ use crate::renderer::{ RenderAdapter, RenderAdapterInfo, RenderDevice, RenderInstance, RenderQueue, }; -use std::borrow::Cow; +use std::{borrow::Cow, path::PathBuf}; pub use wgpu::{ Backends, Dx12Compiler, Features as WgpuFeatures, Gles3MinorVersion, InstanceFlags, @@ -52,6 +52,8 @@ pub struct WgpuSettings { pub instance_flags: InstanceFlags, /// This hints to the WGPU device about the preferred memory allocation strategy. pub memory_hints: MemoryHints, + /// The path to pass to wgpu for API call tracing. This only has an effect if wgpu's tracing functionality is enabled. + pub trace_path: Option, } impl Default for WgpuSettings { @@ -116,6 +118,7 @@ impl Default for WgpuSettings { gles3_minor_version, instance_flags, memory_hints: MemoryHints::default(), + trace_path: None, } } } diff --git a/docs/cargo_features.md b/docs/cargo_features.md index d2cbf06a23b642..c360c2eb8cfba9 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -94,5 +94,4 @@ The default feature set enables most of the expected features of a game engine, |wayland|Wayland display server support| |webgpu|Enable support for WebGPU in Wasm. When enabled, this feature will override the `webgl2` feature and you won't be able to run Wasm builds with WebGL2, only with WebGPU.| |webp|WebP image format support| -|wgpu_trace|Save a trace of all wgpu calls| |zlib|For KTX2 supercompression| diff --git a/docs/debugging.md b/docs/debugging.md index f791e7d21b7d0c..60ce21b918b970 100644 --- a/docs/debugging.md +++ b/docs/debugging.md @@ -13,6 +13,12 @@ When a suspected wgpu error occurs, you should capture a wgpu trace so that Bevy To capture a wgpu trace: -1. Create a new `wgpu_trace` folder in the root of your cargo workspace -2. Add the "wgpu_trace" feature to the bevy crate. (ex: `cargo run --example features wgpu_trace`) -3. Zip up the wgpu_trace folder and attach it to the relevant issue. New wgpu issues should generally be created [in the wgpu repository](https://github.com/gfx-rs/wgpu). Please include the wgpu revision in your bug reports. You can find the revision in the `Cargo.lock` file in your workspace. +1. Create a new folder in which to store your wgpu trace. +2. Pass the path to the folder you created for your wgpu trace to `bevy_render::RenderPlugin`, using the `render_creation` field. + * If you're manually creating the renderer resources, pass the path to wgpu when creating the `RenderDevice` and `RenderQueue`. + * Otherwise, pass the path to Bevy via the `trace_path` field in `bevy_render::settings::WgpuSettings`. +3. Add `wgpu = { version = "*", features = ["trace"]}` to your Cargo.toml. + * `version = "*"` tells Rust that this can be *any* version of the wgpu crate, so it will not try to pull in a different version of wgpu than what is already pulled in by Bevy. +4. Compile and run your application, performing any in-app actions necessary to replicate the wgpu error. + +Once you've captured a wgpu trace, zip up the folder and attach it to the relevant issue. New wgpu issues should generally be created [in the wgpu repository](https://github.com/gfx-rs/wgpu). Please include the wgpu revision in your bug reports. You can find the revision in the `Cargo.lock` file in your workspace. From 335f2903d99e44b7c5f3db061799033978d55523 Mon Sep 17 00:00:00 2001 From: Chris Russell <8494645+chescock@users.noreply.github.com> Date: Sun, 25 Aug 2024 10:23:44 -0400 Subject: [PATCH 11/53] SystemParamBuilder - Support dynamic system parameters (#14817) # Objective Support building systems with parameters whose types can be determined at runtime. ## Solution Create a `DynSystemParam` type that can be built using a `SystemParamBuilder` of any type and then downcast to the appropriate type dynamically. ## Example ```rust let system = ( DynParamBuilder::new(LocalBuilder(3_usize)), DynParamBuilder::new::>(QueryParamBuilder::new(|builder| { builder.with::(); })), DynParamBuilder::new::<&Entities>(ParamBuilder), ) .build_state(&mut world) .build_system( |mut p0: DynSystemParam, mut p1: DynSystemParam, mut p2: DynSystemParam| { let local = p0.downcast_mut::>().unwrap(); let query_count = p1.downcast_mut::>().unwrap(); let entities = p2.downcast_mut::<&Entities>().unwrap(); }, ); ``` --------- Co-authored-by: Alice Cecile Co-authored-by: Periwink --- crates/bevy_ecs/src/system/builder.rs | 60 ++++- crates/bevy_ecs/src/system/system_param.rs | 248 +++++++++++++++++++++ 2 files changed, 307 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/system/builder.rs b/crates/bevy_ecs/src/system/builder.rs index 2cecceeb3c2c03..861f915a70145c 100644 --- a/crates/bevy_ecs/src/system/builder.rs +++ b/crates/bevy_ecs/src/system/builder.rs @@ -4,7 +4,7 @@ use crate::{ prelude::QueryBuilder, query::{QueryData, QueryFilter, QueryState}, system::{ - system_param::{Local, ParamSet, SystemParam}, + system_param::{DynSystemParam, DynSystemParamState, Local, ParamSet, SystemParam}, Query, SystemMeta, }, world::{FromWorld, World}, @@ -251,6 +251,34 @@ macro_rules! impl_param_set_builder_tuple { all_tuples!(impl_param_set_builder_tuple, 1, 8, P, B, meta); +/// A [`SystemParamBuilder`] for a [`DynSystemParam`]. +pub struct DynParamBuilder<'a>( + Box DynSystemParamState + 'a>, +); + +impl<'a> DynParamBuilder<'a> { + /// Creates a new [`DynParamBuilder`] by wrapping a [`SystemParamBuilder`] of any type. + /// The built [`DynSystemParam`] can be downcast to `T`. + pub fn new(builder: impl SystemParamBuilder + 'a) -> Self { + Self(Box::new(|world, meta| { + DynSystemParamState::new::(builder.build(world, meta)) + })) + } +} + +// SAFETY: `DynSystemParam::get_param` will call `get_param` on the boxed `DynSystemParamState`, +// and the boxed builder was a valid implementation of `SystemParamBuilder` for that type. +// The resulting `DynSystemParam` can only perform access by downcasting to that param type. +unsafe impl<'a, 'w, 's> SystemParamBuilder> for DynParamBuilder<'a> { + fn build( + self, + world: &mut World, + meta: &mut SystemMeta, + ) -> as SystemParam>::State { + (self.0)(world, meta) + } +} + /// A [`SystemParamBuilder`] for a [`Local`]. /// The provided value will be used as the initial value of the `Local`. pub struct LocalBuilder(pub T); @@ -271,6 +299,7 @@ unsafe impl<'s, T: FromWorld + Send + 'static> SystemParamBuilder> #[cfg(test)] mod tests { use crate as bevy_ecs; + use crate::entity::Entities; use crate::prelude::{Component, Query}; use crate::system::{Local, RunSystemOnce}; @@ -382,4 +411,33 @@ mod tests { let result = world.run_system_once(system); assert_eq!(result, 5); } + + #[test] + fn dyn_builder() { + let mut world = World::new(); + + world.spawn(A); + world.spawn_empty(); + + let system = ( + DynParamBuilder::new(LocalBuilder(3_usize)), + DynParamBuilder::new::>(QueryParamBuilder::new(|builder| { + builder.with::(); + })), + DynParamBuilder::new::<&Entities>(ParamBuilder), + ) + .build_state(&mut world) + .build_system( + |mut p0: DynSystemParam, mut p1: DynSystemParam, mut p2: DynSystemParam| { + let local = *p0.downcast_mut::>().unwrap(); + let query_count = p1.downcast_mut::>().unwrap().iter().count(); + let _entities = p2.downcast_mut::<&Entities>().unwrap(); + assert!(p0.downcast_mut::>().is_none()); + local + query_count + }, + ); + + let result = world.run_system_once(system); + assert_eq!(result, 4); + } } diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index acc876a556b3ca..7968338764b146 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -22,6 +22,7 @@ use bevy_utils::{all_tuples, synccell::SyncCell}; #[cfg(feature = "track_change_detection")] use std::panic::Location; use std::{ + any::Any, fmt::Debug, marker::PhantomData, ops::{Deref, DerefMut}, @@ -1674,6 +1675,253 @@ unsafe impl SystemParam for PhantomData { // SAFETY: No world access. unsafe impl ReadOnlySystemParam for PhantomData {} +/// A [`SystemParam`] with a type that can be configured at runtime. +/// To be useful, this must be configured using a [`DynParamBuilder`](crate::system::DynParamBuilder) to build the system using a [`SystemParamBuilder`](crate::prelude::SystemParamBuilder). +/// +/// # Examples +/// +/// ``` +/// # use bevy_ecs::{prelude::*, system::*}; +/// # +/// # #[derive(Default, Resource)] +/// # struct A; +/// # +/// # #[derive(Default, Resource)] +/// # struct B; +/// # +/// let mut world = World::new(); +/// world.init_resource::(); +/// world.init_resource::(); +/// +/// // If the inner parameter doesn't require any special building, use `ParamBuilder`. +/// // Either specify the type parameter on `DynParamBuilder::new()` ... +/// let system = (DynParamBuilder::new::>(ParamBuilder),) +/// .build_state(&mut world) +/// .build_system(expects_res_a); +/// world.run_system_once(system); +/// +/// // ... or use a factory method on `ParamBuilder` that returns a specific type. +/// let system = (DynParamBuilder::new(ParamBuilder::resource::()),) +/// .build_state(&mut world) +/// .build_system(expects_res_a); +/// world.run_system_once(system); +/// +/// fn expects_res_a(mut param: DynSystemParam) { +/// // Use the `downcast` methods to retrieve the inner parameter. +/// // They will return `None` if the type does not match. +/// assert!(param.is::>()); +/// assert!(!param.is::>()); +/// assert!(param.downcast_mut::>().is_none()); +/// let foo: Res = param.downcast::>().unwrap(); +/// } +/// +/// let system = ( +/// // If the inner parameter also requires building, +/// // pass the appropriate `SystemParamBuilder`. +/// DynParamBuilder::new(LocalBuilder(10usize)), +/// // `DynSystemParam` is just an ordinary `SystemParam`, +/// // and can be combined with other parameters as usual! +/// ParamBuilder::query(), +/// ) +/// .build_state(&mut world) +/// .build_system(|param: DynSystemParam, query: Query<()>| { +/// let local: Local = param.downcast::>().unwrap(); +/// assert_eq!(*local, 10); +/// }); +/// world.run_system_once(system); +/// ``` +pub struct DynSystemParam<'w, 's> { + /// A `ParamState` wrapping the state for the underlying system param. + state: &'s mut dyn Any, + world: UnsafeWorldCell<'w>, + system_meta: SystemMeta, + change_tick: Tick, +} + +impl<'w, 's> DynSystemParam<'w, 's> { + /// # SAFETY + /// - `state` must be a `ParamState` for some inner `T: SystemParam`. + /// - The passed [`UnsafeWorldCell`] must have access to any world data registered + /// in [`init_state`](SystemParam::init_state) for the inner system param. + /// - `world` must be the same `World` that was used to initialize + /// [`state`](SystemParam::init_state) for the inner system param. + unsafe fn new( + state: &'s mut dyn Any, + world: UnsafeWorldCell<'w>, + system_meta: SystemMeta, + change_tick: Tick, + ) -> Self { + Self { + state, + world, + system_meta, + change_tick, + } + } + + /// Returns `true` if the inner system param is the same as `T`. + pub fn is(&self) -> bool { + self.state.is::>() + } + + /// Returns the inner system param if it is the correct type. + /// This consumes the dyn param, so the returned param can have its original world and state lifetimes. + pub fn downcast(self) -> Option> { + // SAFETY: + // - `DynSystemParam::new()` ensures `state` is a `ParamState`, that the world matches, + // and that it has access required by the inner system param. + // - This consumes the `DynSystemParam`, so it is the only use of `world` with this access and it is available for `'w`. + unsafe { downcast::(self.state, &self.system_meta, self.world, self.change_tick) } + } + + /// Returns the inner system parameter if it is the correct type. + /// This borrows the dyn param, so the returned param is only valid for the duration of that borrow. + pub fn downcast_mut(&mut self) -> Option> { + // SAFETY: + // - `DynSystemParam::new()` ensures `state` is a `ParamState`, that the world matches, + // and that it has access required by the inner system param. + // - This exclusively borrows the `DynSystemParam` for `'_`, so it is the only use of `world` with this access for `'_`. + unsafe { downcast::(self.state, &self.system_meta, self.world, self.change_tick) } + } + + /// Returns the inner system parameter if it is the correct type. + /// This borrows the dyn param, so the returned param is only valid for the duration of that borrow, + /// but since it only performs read access it can keep the original world lifetime. + /// This can be useful with methods like [`Query::iter_inner()`] or [`Res::into_inner()`] + /// to obtain references with the original world lifetime. + pub fn downcast_mut_inner( + &mut self, + ) -> Option> { + // SAFETY: + // - `DynSystemParam::new()` ensures `state` is a `ParamState`, that the world matches, + // and that it has access required by the inner system param. + // - The inner system param only performs read access, so it's safe to copy that access for the full `'w` lifetime. + unsafe { downcast::(self.state, &self.system_meta, self.world, self.change_tick) } + } +} + +/// # SAFETY +/// - `state` must be a `ParamState` for some inner `T: SystemParam`. +/// - The passed [`UnsafeWorldCell`] must have access to any world data registered +/// in [`init_state`](SystemParam::init_state) for the inner system param. +/// - `world` must be the same `World` that was used to initialize +/// [`state`](SystemParam::init_state) for the inner system param. +unsafe fn downcast<'w, 's, T: SystemParam + 'static>( + state: &'s mut dyn Any, + system_meta: &SystemMeta, + world: UnsafeWorldCell<'w>, + change_tick: Tick, +) -> Option> { + state.downcast_mut::>().map(|state| { + // SAFETY: + // - The caller ensures the world has access for the underlying system param, + // and since the downcast succeeded, the underlying system param is T. + // - The caller ensures the `world` matches. + unsafe { T::get_param(&mut state.0, system_meta, world, change_tick) } + }) +} + +/// The [`SystemParam::State`] for a [`DynSystemParam`]. +pub struct DynSystemParamState(Box); + +impl DynSystemParamState { + pub(crate) fn new(state: T::State) -> Self { + Self(Box::new(ParamState::(state))) + } +} + +/// Allows a [`SystemParam::State`] to be used as a trait object for implementing [`DynSystemParam`]. +trait DynParamState: Sync + Send { + /// Casts the underlying `ParamState` to an `Any` so it can be downcast. + fn as_any_mut(&mut self) -> &mut dyn Any; + + /// For the specified [`Archetype`], registers the components accessed by this [`SystemParam`] (if applicable).a + /// + /// # Safety + /// `archetype` must be from the [`World`] used to initialize `state` in `init_state`. + unsafe fn new_archetype(&mut self, archetype: &Archetype, system_meta: &mut SystemMeta); + + /// Applies any deferred mutations stored in this [`SystemParam`]'s state. + /// This is used to apply [`Commands`] during [`apply_deferred`](crate::prelude::apply_deferred). + /// + /// [`Commands`]: crate::prelude::Commands + fn apply(&mut self, system_meta: &SystemMeta, world: &mut World); + + /// Queues any deferred mutations to be applied at the next [`apply_deferred`](crate::prelude::apply_deferred). + fn queue(&mut self, system_meta: &SystemMeta, world: DeferredWorld); +} + +/// A wrapper around a [`SystemParam::State`] that can be used as a trait object in a [`DynSystemParam`]. +struct ParamState(T::State); + +impl DynParamState for ParamState { + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + unsafe fn new_archetype(&mut self, archetype: &Archetype, system_meta: &mut SystemMeta) { + // SAFETY: The caller ensures that `archetype` is from the World the state was initialized from in `init_state`. + unsafe { T::new_archetype(&mut self.0, archetype, system_meta) }; + } + + fn apply(&mut self, system_meta: &SystemMeta, world: &mut World) { + T::apply(&mut self.0, system_meta, world); + } + + fn queue(&mut self, system_meta: &SystemMeta, world: DeferredWorld) { + T::queue(&mut self.0, system_meta, world); + } +} + +// SAFETY: `init_state` creates a state of (), which performs no access. The interesting safety checks are on the `SystemParamBuilder`. +unsafe impl SystemParam for DynSystemParam<'_, '_> { + type State = DynSystemParamState; + + type Item<'world, 'state> = DynSystemParam<'world, 'state>; + + fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State { + DynSystemParamState::new::<()>(()) + } + + unsafe fn get_param<'world, 'state>( + state: &'state mut Self::State, + system_meta: &SystemMeta, + world: UnsafeWorldCell<'world>, + change_tick: Tick, + ) -> Self::Item<'world, 'state> { + // SAFETY: + // - `state.0` is a boxed `ParamState`, and its implementation of `as_any_mut` returns `self`. + // - The state was obtained from `SystemParamBuilder::build()`, which registers all [`World`] accesses used + // by [`SystemParam::get_param`] with the provided [`system_meta`](SystemMeta). + // - The caller ensures that the provided world is the same and has the required access. + unsafe { + DynSystemParam::new( + state.0.as_any_mut(), + world, + system_meta.clone(), + change_tick, + ) + } + } + + unsafe fn new_archetype( + state: &mut Self::State, + archetype: &Archetype, + system_meta: &mut SystemMeta, + ) { + // SAFETY: The caller ensures that `archetype` is from the World the state was initialized from in `init_state`. + unsafe { state.0.new_archetype(archetype, system_meta) }; + } + + fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { + state.0.apply(system_meta, world); + } + + fn queue(state: &mut Self::State, system_meta: &SystemMeta, world: DeferredWorld) { + state.0.queue(system_meta, world); + } +} + #[cfg(test)] mod tests { use super::*; From 89a5c741f77a28efb1cbe4b25408cd27b51ca2c9 Mon Sep 17 00:00:00 2001 From: akimakinai <105044389+akimakinai@users.noreply.github.com> Date: Sun, 25 Aug 2024 23:52:03 +0900 Subject: [PATCH 12/53] Fix Gizmo joint rendering in webgpu (#14721) # Objective - Gizmo rendering on WebGPU has been fixed by #14653, but gizmo joints still cause error (https://github.com/bevyengine/bevy/issues/14696#issuecomment-2283689669) when enabled. ## Solution - Applies the same fix as #14653 to Gizmo joints. I'm noob and just copied their solution, please correct me if I did something wrong. ## Testing - Tested 2d-gizmos and 3d-gizmos examples in WebGPU on Chrome. No rendering errors, and the gizmo joints are apparently rendered ok. --- crates/bevy_gizmos/src/lib.rs | 25 ++++++++++++++++++------- crates/bevy_gizmos/src/pipeline_2d.rs | 2 ++ crates/bevy_gizmos/src/pipeline_3d.rs | 2 ++ 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/crates/bevy_gizmos/src/lib.rs b/crates/bevy_gizmos/src/lib.rs index 1d42c589470ba3..3e28516b608b75 100644 --- a/crates/bevy_gizmos/src/lib.rs +++ b/crates/bevy_gizmos/src/lib.rs @@ -669,11 +669,24 @@ impl RenderCommand

for DrawLineJointGizmo { }; let instances = { - pass.set_vertex_buffer(0, line_gizmo.position_buffer.slice(..)); - pass.set_vertex_buffer(1, line_gizmo.position_buffer.slice(..)); - pass.set_vertex_buffer(2, line_gizmo.position_buffer.slice(..)); + let item_size = VertexFormat::Float32x3.size(); + // position_a + let buffer_size_a = line_gizmo.position_buffer.size() - item_size * 2; + pass.set_vertex_buffer(0, line_gizmo.position_buffer.slice(..buffer_size_a)); + // position_b + let buffer_size_b = line_gizmo.position_buffer.size() - item_size; + pass.set_vertex_buffer( + 1, + line_gizmo.position_buffer.slice(item_size..buffer_size_b), + ); + // position_c + pass.set_vertex_buffer(2, line_gizmo.position_buffer.slice(item_size * 2..)); - pass.set_vertex_buffer(3, line_gizmo.color_buffer.slice(..)); + // color + let item_size = VertexFormat::Float32x4.size(); + let buffer_size = line_gizmo.color_buffer.size() - item_size; + // This corresponds to the color of position_b, hence starts from `item_size` + pass.set_vertex_buffer(3, line_gizmo.color_buffer.slice(item_size..buffer_size)); u32::max(line_gizmo.vertex_count, 2) - 2 }; @@ -770,7 +783,7 @@ fn line_joint_gizmo_vertex_buffer_layouts() -> Vec { step_mode: VertexStepMode::Instance, attributes: vec![VertexAttribute { format: Float32x4, - offset: Float32x4.size(), + offset: 0, shader_location: 3, }], }; @@ -779,12 +792,10 @@ fn line_joint_gizmo_vertex_buffer_layouts() -> Vec { position_layout.clone(), { position_layout.attributes[0].shader_location = 1; - position_layout.attributes[0].offset = Float32x3.size(); position_layout.clone() }, { position_layout.attributes[0].shader_location = 2; - position_layout.attributes[0].offset = 2 * Float32x3.size(); position_layout }, color_layout.clone(), diff --git a/crates/bevy_gizmos/src/pipeline_2d.rs b/crates/bevy_gizmos/src/pipeline_2d.rs index 0f6552f7874065..6154d8edc9e063 100644 --- a/crates/bevy_gizmos/src/pipeline_2d.rs +++ b/crates/bevy_gizmos/src/pipeline_2d.rs @@ -54,7 +54,9 @@ impl Plugin for LineGizmo2dPlugin { ) .add_systems( Render, + // FIXME: added `chain()` to workaround vertex buffer being not updated when sliced size changed (queue_line_gizmos_2d, queue_line_joint_gizmos_2d) + .chain() .in_set(GizmoRenderSystem::QueueLineGizmos2d) .after(prepare_assets::), ); diff --git a/crates/bevy_gizmos/src/pipeline_3d.rs b/crates/bevy_gizmos/src/pipeline_3d.rs index 8197623b3618cf..37cf9b01db6629 100644 --- a/crates/bevy_gizmos/src/pipeline_3d.rs +++ b/crates/bevy_gizmos/src/pipeline_3d.rs @@ -53,7 +53,9 @@ impl Plugin for LineGizmo3dPlugin { ) .add_systems( Render, + // FIXME: added `chain()` to workaround vertex buffer being not updated when sliced size changed (queue_line_gizmos_3d, queue_line_joint_gizmos_3d) + .chain() .in_set(GizmoRenderSystem::QueueLineGizmos3d) .after(prepare_assets::), ); From f9d7a2ca028c9ef37e85603312eac58eb299b756 Mon Sep 17 00:00:00 2001 From: Sorseg Date: Sun, 25 Aug 2024 19:55:54 +0300 Subject: [PATCH 13/53] Implement `std::fmt::Debug` for `ecs::observer::Trigger` (#14857) # Objective I tried writing something like this in my project ```rust .observe(|e: Trigger| { panic!("Skeletoned! {e:?}"); }); ``` and it didn't compile. Having `Debug` trait defined on `Trigger` event will ease debugging the observers a little bit. ## Solution Add a bespoke `Debug` implementation when both the bundle and the event have `Debug` implemented for them. ## Testing I've added `println!("{trigger:#?}");` to the [observers example](https://github.com/bevyengine/bevy/blob/938d810766d34f1a300beb440273c3db1635ee5c/examples/ecs/observers.rs#L124) and it compiled! Caveats with this PR are: - removing this implementation if for any reason we will need it, will be a breaking change - the implementation is manually generated, which adds potential toil when changing the `Trigger` structure ## Showcase Log output: ```rust on_add_mine: Trigger { event: OnAdd, propagate: false, trigger: ObserverTrigger { observer: 2v1#4294967298, event_type: ComponentId( 0, ), entity: 454v1#4294967750, }, _marker: PhantomData, } ``` Thank you for maintaining this engine! :orange_heart: --- crates/bevy_ecs/src/observer/mod.rs | 13 ++++++++++++- crates/bevy_ecs/src/world/component_constants.rs | 12 ++++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index 600384d4784389..3ef2d6b28d63dc 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -12,7 +12,7 @@ use crate::{archetype::ArchetypeFlags, system::IntoObserverSystem, world::*}; use crate::{component::ComponentId, prelude::*, world::DeferredWorld}; use bevy_ptr::Ptr; use bevy_utils::{EntityHashMap, HashMap}; -use std::marker::PhantomData; +use std::{fmt::Debug, marker::PhantomData}; /// Type containing triggered [`Event`] information for a given run of an [`Observer`]. This contains the /// [`Event`] data itself. If it was triggered for a specific [`Entity`], it includes that as well. It also @@ -84,6 +84,17 @@ impl<'w, E, B: Bundle> Trigger<'w, E, B> { } } +impl<'w, E: Debug, B: Bundle> Debug for Trigger<'w, E, B> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Trigger") + .field("event", &self.event) + .field("propagate", &self.propagate) + .field("trigger", &self.trigger) + .field("_marker", &self._marker) + .finish() + } +} + /// A description of what an [`Observer`] observes. #[derive(Default, Clone)] pub struct ObserverDescriptor { diff --git a/crates/bevy_ecs/src/world/component_constants.rs b/crates/bevy_ecs/src/world/component_constants.rs index 92c1572b0935bb..ead0e5ac9f676c 100644 --- a/crates/bevy_ecs/src/world/component_constants.rs +++ b/crates/bevy_ecs/src/world/component_constants.rs @@ -16,24 +16,28 @@ pub const ON_REMOVE: ComponentId = ComponentId::new(3); /// Trigger emitted when a component is added to an entity. See [`crate::component::ComponentHooks::on_add`] /// for more information. -#[derive(Event)] +#[derive(Event, Debug)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] pub struct OnAdd; /// Trigger emitted when a component is inserted onto an entity. See [`crate::component::ComponentHooks::on_insert`] /// for more information. -#[derive(Event)] +#[derive(Event, Debug)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] pub struct OnInsert; /// Trigger emitted when a component is replaced on an entity. See [`crate::component::ComponentHooks::on_replace`] /// for more information. -#[derive(Event)] +#[derive(Event, Debug)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] pub struct OnReplace; /// Trigger emitted when a component is removed from an entity. See [`crate::component::ComponentHooks::on_remove`] /// for more information. -#[derive(Event)] +#[derive(Event, Debug)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] pub struct OnRemove; From 3892adcb47551bb42706cf199ef6ebb2514cb623 Mon Sep 17 00:00:00 2001 From: Gino Valente <49806985+MrGVSV@users.noreply.github.com> Date: Sun, 25 Aug 2024 10:57:07 -0700 Subject: [PATCH 14/53] bevy_reflect: Add `Type` type (#14838) # Objective Closes #7622. I was working on adding support for reflecting generic functions and found that I wanted to use an argument's `TypeId` for hashing and comparison, but its `TypePath` for debugging and error messaging. While I could just keep them separate, place them in a tuple or a local struct or something, I think I see an opportunity to make a dedicate type for this. Additionally, we can use this type to clean up some duplication amongst the type info structs in a manner similar to #7622. ## Solution Added the `Type` type. This should be seen as the most basic representation of a type apart from `TypeId`. It stores both the `TypeId` of the type as well as its `TypePathTable`. The `Hash` and `PartialEq` implementations rely on the `TypeId`, while the `Debug` implementation relies on the `TypePath`. This makes it especially useful as a key in a `HashMap` since we get the speed of the `TypeId` hashing/comparisons with the readability of `TypePath`. With this type, we're able to reduce the duplication across the type info structs by removing individual fields for `TypeId` and `TypePathTable`, replacing them with a single `Type` field. Similarly, we can remove many duplicate methods and replace it with a macro that delegates to the stored `Type`. ### Caveats It should be noted that this type is currently 3x larger than `TypeId`. On my machine, it's 48 bytes compared to `TypeId`'s 16. While this doesn't matter for `TypeInfo` since it would contain that data regardless, it is something to keep in mind when using elsewhere. ## Testing All tests should pass as normal: ``` cargo test --package bevy_reflect ``` --- ## Showcase `bevy_reflect` now exports a `Type` struct. This type contains both the `TypeId` and the `TypePathTable` of the given type, allowing it to be used like `TypeId` but have the debuggability of `TypePath`. ```rust // We can create this for any type implementing `TypePath`: let ty = Type::of::(); // It has `Hash` and `Eq` impls powered by `TypeId`, making it useful for maps: let mut map = HashMap::::new(); map.insert(ty, 25); // And it has a human-readable `Debug` representation: let debug = format!("{:?}", map); assert_eq!(debug, "{alloc::string::String: 25}"); ``` ## Migration Guide Certain type info structs now only return their item types as `Type` instead of exposing direct methods on them. The following methods have been removed: - `ArrayInfo::item_type_path_table` - `ArrayInfo::item_type_id` - `ArrayInfo::item_is` - `ListInfo::item_type_path_table` - `ListInfo::item_type_id` - `ListInfo::item_is` - `SetInfo::value_type_path_table` - `SetInfo::value_type_id` - `SetInfo::value_is` - `MapInfo::key_type_path_table` - `MapInfo::key_type_id` - `MapInfo::key_is` - `MapInfo::value_type_path_table` - `MapInfo::value_type_id` - `MapInfo::value_is` Instead, access the `Type` directly using one of the new methods: - `ArrayInfo::item_ty` - `ListInfo::item_ty` - `SetInfo::value_ty` - `MapInfo::key_ty` - `MapInfo::value_ty` For example: ```rust // BEFORE let type_id = array_info.item_type_id(); // AFTER let type_id = array_info.item_ty().id(); ``` --- crates/bevy_reflect/src/array.rs | 62 +---- crates/bevy_reflect/src/enums/enum_trait.rs | 37 +-- crates/bevy_reflect/src/fields.rs | 70 +---- crates/bevy_reflect/src/func/args/info.rs | 18 +- crates/bevy_reflect/src/func/info.rs | 14 +- crates/bevy_reflect/src/lib.rs | 27 +- crates/bevy_reflect/src/list.rs | 62 +---- crates/bevy_reflect/src/map.rs | 85 ++---- crates/bevy_reflect/src/serde/de.rs | 20 +- crates/bevy_reflect/src/set.rs | 61 +---- crates/bevy_reflect/src/struct_trait.rs | 42 +-- crates/bevy_reflect/src/tuple.rs | 40 +-- crates/bevy_reflect/src/tuple_struct.rs | 37 +-- crates/bevy_reflect/src/type_info.rs | 279 ++++++++++++++++---- 14 files changed, 342 insertions(+), 512 deletions(-) diff --git a/crates/bevy_reflect/src/array.rs b/crates/bevy_reflect/src/array.rs index a7d6ef8828aff3..42498da3b62031 100644 --- a/crates/bevy_reflect/src/array.rs +++ b/crates/bevy_reflect/src/array.rs @@ -1,10 +1,11 @@ +use crate::type_info::impl_type_methods; use crate::{ self as bevy_reflect, utility::reflect_hasher, ApplyError, MaybeTyped, PartialReflect, Reflect, - ReflectKind, ReflectMut, ReflectOwned, ReflectRef, TypeInfo, TypePath, TypePathTable, + ReflectKind, ReflectMut, ReflectOwned, ReflectRef, Type, TypeInfo, TypePath, }; use bevy_reflect_derive::impl_type_path; use std::{ - any::{Any, TypeId}, + any::Any, fmt::{Debug, Formatter}, hash::{Hash, Hasher}, }; @@ -77,11 +78,9 @@ pub trait Array: PartialReflect { /// A container for compile-time array info. #[derive(Clone, Debug)] pub struct ArrayInfo { - type_path: TypePathTable, - type_id: TypeId, + ty: Type, item_info: fn() -> Option<&'static TypeInfo>, - item_type_path: TypePathTable, - item_type_id: TypeId, + item_ty: Type, capacity: usize, #[cfg(feature = "documentation")] docs: Option<&'static str>, @@ -98,11 +97,9 @@ impl ArrayInfo { capacity: usize, ) -> Self { Self { - type_path: TypePathTable::of::(), - type_id: TypeId::of::(), + ty: Type::of::(), item_info: TItem::maybe_type_info, - item_type_path: TypePathTable::of::(), - item_type_id: TypeId::of::(), + item_ty: Type::of::(), capacity, #[cfg(feature = "documentation")] docs: None, @@ -120,32 +117,7 @@ impl ArrayInfo { self.capacity } - /// A representation of the type path of the array. - /// - /// Provides dynamic access to all methods on [`TypePath`]. - pub fn type_path_table(&self) -> &TypePathTable { - &self.type_path - } - - /// The [stable, full type path] of the array. - /// - /// Use [`type_path_table`] if you need access to the other methods on [`TypePath`]. - /// - /// [stable, full type path]: TypePath - /// [`type_path_table`]: Self::type_path_table - pub fn type_path(&self) -> &'static str { - self.type_path_table().path() - } - - /// The [`TypeId`] of the array. - pub fn type_id(&self) -> TypeId { - self.type_id - } - - /// Check if the given type matches the array type. - pub fn is(&self) -> bool { - TypeId::of::() == self.type_id - } + impl_type_methods!(ty); /// The [`TypeInfo`] of the array item. /// @@ -155,21 +127,11 @@ impl ArrayInfo { (self.item_info)() } - /// A representation of the type path of the array item. + /// The [type] of the array item. /// - /// Provides dynamic access to all methods on [`TypePath`]. - pub fn item_type_path_table(&self) -> &TypePathTable { - &self.item_type_path - } - - /// The [`TypeId`] of the array item. - pub fn item_type_id(&self) -> TypeId { - self.item_type_id - } - - /// Check if the given type matches the array item type. - pub fn item_is(&self) -> bool { - TypeId::of::() == self.item_type_id + /// [type]: Type + pub fn item_ty(&self) -> Type { + self.item_ty } /// The docstring of this array, if any. diff --git a/crates/bevy_reflect/src/enums/enum_trait.rs b/crates/bevy_reflect/src/enums/enum_trait.rs index 993f08a7c110ff..f3fe336fccb556 100644 --- a/crates/bevy_reflect/src/enums/enum_trait.rs +++ b/crates/bevy_reflect/src/enums/enum_trait.rs @@ -1,7 +1,7 @@ use crate::attributes::{impl_custom_attribute_methods, CustomAttributes}; -use crate::{DynamicEnum, PartialReflect, TypePath, TypePathTable, VariantInfo, VariantType}; +use crate::type_info::impl_type_methods; +use crate::{DynamicEnum, PartialReflect, Type, TypePath, VariantInfo, VariantType}; use bevy_utils::HashMap; -use std::any::{Any, TypeId}; use std::slice::Iter; use std::sync::Arc; @@ -135,8 +135,7 @@ pub trait Enum: PartialReflect { /// A container for compile-time enum info, used by [`TypeInfo`](crate::TypeInfo). #[derive(Clone, Debug)] pub struct EnumInfo { - type_path: TypePathTable, - type_id: TypeId, + ty: Type, variants: Box<[VariantInfo]>, variant_names: Box<[&'static str]>, variant_indices: HashMap<&'static str, usize>, @@ -162,8 +161,7 @@ impl EnumInfo { let variant_names = variants.iter().map(VariantInfo::name).collect(); Self { - type_path: TypePathTable::of::(), - type_id: TypeId::of::(), + ty: Type::of::(), variants: variants.to_vec().into_boxed_slice(), variant_names, variant_indices, @@ -231,32 +229,7 @@ impl EnumInfo { self.variants.len() } - /// A representation of the type path of the value. - /// - /// Provides dynamic access to all methods on [`TypePath`]. - pub fn type_path_table(&self) -> &TypePathTable { - &self.type_path - } - - /// The [stable, full type path] of the value. - /// - /// Use [`type_path_table`] if you need access to the other methods on [`TypePath`]. - /// - /// [stable, full type path]: TypePath - /// [`type_path_table`]: Self::type_path_table - pub fn type_path(&self) -> &'static str { - self.type_path_table().path() - } - - /// The [`TypeId`] of the enum. - pub fn type_id(&self) -> TypeId { - self.type_id - } - - /// Check if the given type matches the enum type. - pub fn is(&self) -> bool { - TypeId::of::() == self.type_id - } + impl_type_methods!(ty); /// The docstring of this enum, if any. #[cfg(feature = "documentation")] diff --git a/crates/bevy_reflect/src/fields.rs b/crates/bevy_reflect/src/fields.rs index e20d952ac50978..56f6a25879098f 100644 --- a/crates/bevy_reflect/src/fields.rs +++ b/crates/bevy_reflect/src/fields.rs @@ -1,6 +1,6 @@ use crate::attributes::{impl_custom_attribute_methods, CustomAttributes}; -use crate::{MaybeTyped, PartialReflect, TypeInfo, TypePath, TypePathTable}; -use std::any::{Any, TypeId}; +use crate::type_info::impl_type_methods; +use crate::{MaybeTyped, PartialReflect, Type, TypeInfo, TypePath}; use std::sync::Arc; /// The named field of a reflected struct. @@ -8,8 +8,7 @@ use std::sync::Arc; pub struct NamedField { name: &'static str, type_info: fn() -> Option<&'static TypeInfo>, - type_path: TypePathTable, - type_id: TypeId, + ty: Type, custom_attributes: Arc, #[cfg(feature = "documentation")] docs: Option<&'static str>, @@ -21,8 +20,7 @@ impl NamedField { Self { name, type_info: T::maybe_type_info, - type_path: TypePathTable::of::(), - type_id: TypeId::of::(), + ty: Type::of::(), custom_attributes: Arc::new(CustomAttributes::default()), #[cfg(feature = "documentation")] docs: None, @@ -57,32 +55,7 @@ impl NamedField { (self.type_info)() } - /// A representation of the type path of the field. - /// - /// Provides dynamic access to all methods on [`TypePath`]. - pub fn type_path_table(&self) -> &TypePathTable { - &self.type_path - } - - /// The [stable, full type path] of the field. - /// - /// Use [`type_path_table`] if you need access to the other methods on [`TypePath`]. - /// - /// [stable, full type path]: TypePath - /// [`type_path_table`]: Self::type_path_table - pub fn type_path(&self) -> &'static str { - self.type_path_table().path() - } - - /// The [`TypeId`] of the field. - pub fn type_id(&self) -> TypeId { - self.type_id - } - - /// Check if the given type matches the field type. - pub fn is(&self) -> bool { - TypeId::of::() == self.type_id - } + impl_type_methods!(ty); /// The docstring of this field, if any. #[cfg(feature = "documentation")] @@ -98,8 +71,7 @@ impl NamedField { pub struct UnnamedField { index: usize, type_info: fn() -> Option<&'static TypeInfo>, - type_path: TypePathTable, - type_id: TypeId, + ty: Type, custom_attributes: Arc, #[cfg(feature = "documentation")] docs: Option<&'static str>, @@ -110,8 +82,7 @@ impl UnnamedField { Self { index, type_info: T::maybe_type_info, - type_path: TypePathTable::of::(), - type_id: TypeId::of::(), + ty: Type::of::(), custom_attributes: Arc::new(CustomAttributes::default()), #[cfg(feature = "documentation")] docs: None, @@ -146,32 +117,7 @@ impl UnnamedField { (self.type_info)() } - /// A representation of the type path of the field. - /// - /// Provides dynamic access to all methods on [`TypePath`]. - pub fn type_path_table(&self) -> &TypePathTable { - &self.type_path - } - - /// The [stable, full type path] of the field. - /// - /// Use [`type_path_table`] if you need access to the other methods on [`TypePath`]. - /// - /// [stable, full type path]: TypePath - /// [`type_path_table`]: Self::type_path_table - pub fn type_path(&self) -> &'static str { - self.type_path_table().path() - } - - /// The [`TypeId`] of the field. - pub fn type_id(&self) -> TypeId { - self.type_id - } - - /// Check if the given type matches the field type. - pub fn is(&self) -> bool { - TypeId::of::() == self.type_id - } + impl_type_methods!(ty); /// The docstring of this field, if any. #[cfg(feature = "documentation")] diff --git a/crates/bevy_reflect/src/func/args/info.rs b/crates/bevy_reflect/src/func/args/info.rs index e932e77be3f33b..3c1098637c17e5 100644 --- a/crates/bevy_reflect/src/func/args/info.rs +++ b/crates/bevy_reflect/src/func/args/info.rs @@ -1,7 +1,8 @@ use alloc::borrow::Cow; use crate::func::args::{GetOwnership, Ownership}; -use crate::TypePath; +use crate::type_info::impl_type_methods; +use crate::{Type, TypePath}; /// Type information for an [`Arg`] used in a [`DynamicFunction`] or [`DynamicFunctionMut`]. /// @@ -16,10 +17,10 @@ pub struct ArgInfo { name: Option>, /// The ownership of the argument. ownership: Ownership, - /// The [type path] of the argument. + /// The [type] of the argument. /// - /// [type path]: TypePath::type_path - type_path: &'static str, + /// [type]: Type + ty: Type, } impl ArgInfo { @@ -31,7 +32,7 @@ impl ArgInfo { index, name: None, ownership: T::ownership(), - type_path: T::type_path(), + ty: Type::of::(), } } @@ -72,12 +73,7 @@ impl ArgInfo { self.ownership } - /// The [type path] of the argument. - /// - /// [type path]: TypePath::type_path - pub fn type_path(&self) -> &'static str { - self.type_path - } + impl_type_methods!(ty); /// Get an ID representing the argument. /// diff --git a/crates/bevy_reflect/src/func/info.rs b/crates/bevy_reflect/src/func/info.rs index 413af8a3ed5110..8627424dd01881 100644 --- a/crates/bevy_reflect/src/func/info.rs +++ b/crates/bevy_reflect/src/func/info.rs @@ -3,7 +3,8 @@ use alloc::borrow::Cow; use bevy_utils::all_tuples; use crate::func::args::{ArgInfo, GetOwnership, Ownership}; -use crate::TypePath; +use crate::type_info::impl_type_methods; +use crate::{Type, TypePath}; /// Type information for a [`DynamicFunction`] or [`DynamicFunctionMut`]. /// @@ -140,7 +141,7 @@ impl FunctionInfo { /// [`DynamicFunctionMut`]: crate::func::DynamicFunctionMut #[derive(Debug, Clone)] pub struct ReturnInfo { - type_path: &'static str, + ty: Type, ownership: Ownership, } @@ -148,17 +149,14 @@ impl ReturnInfo { /// Create a new [`ReturnInfo`] representing the given type, `T`. pub fn new() -> Self { Self { - type_path: T::type_path(), + ty: Type::of::(), ownership: T::ownership(), } } - /// The type path of the return type. - pub fn type_path(&self) -> &'static str { - self.type_path - } + impl_type_methods!(ty); - /// The ownership of the return type. + /// The ownership of this type. pub fn ownership(&self) -> Ownership { self.ownership } diff --git a/crates/bevy_reflect/src/lib.rs b/crates/bevy_reflect/src/lib.rs index 941105a851e992..29829130feeac3 100644 --- a/crates/bevy_reflect/src/lib.rs +++ b/crates/bevy_reflect/src/lib.rs @@ -1719,10 +1719,10 @@ mod tests { let info = MyList::type_info().as_list().unwrap(); assert!(info.is::()); - assert!(info.item_is::()); + assert!(info.item_ty().is::()); assert!(info.item_info().unwrap().is::()); assert_eq!(MyList::type_path(), info.type_path()); - assert_eq!(usize::type_path(), info.item_type_path_table().path()); + assert_eq!(usize::type_path(), info.item_ty().path()); let value: &dyn Reflect = &vec![123_usize]; let info = value.get_represented_type_info().unwrap(); @@ -1735,10 +1735,10 @@ mod tests { let info = MySmallVec::type_info().as_list().unwrap(); assert!(info.is::()); - assert!(info.item_is::()); + assert!(info.item_ty().is::()); assert!(info.item_info().unwrap().is::()); assert_eq!(MySmallVec::type_path(), info.type_path()); - assert_eq!(String::type_path(), info.item_type_path_table().path()); + assert_eq!(String::type_path(), info.item_ty().path()); let value: MySmallVec = smallvec::smallvec![String::default(); 2]; let value: &dyn Reflect = &value; @@ -1751,10 +1751,10 @@ mod tests { let info = MyArray::type_info().as_array().unwrap(); assert!(info.is::()); - assert!(info.item_is::()); + assert!(info.item_ty().is::()); assert!(info.item_info().unwrap().is::()); assert_eq!(MyArray::type_path(), info.type_path()); - assert_eq!(usize::type_path(), info.item_type_path_table().path()); + assert_eq!(usize::type_path(), info.item_ty().path()); assert_eq!(3, info.capacity()); let value: &dyn Reflect = &[1usize, 2usize, 3usize]; @@ -1779,13 +1779,10 @@ mod tests { let info = MyCowSlice::type_info().as_list().unwrap(); assert!(info.is::()); - assert!(info.item_is::()); + assert!(info.item_ty().is::()); assert!(info.item_info().unwrap().is::()); assert_eq!(std::any::type_name::(), info.type_path()); - assert_eq!( - std::any::type_name::(), - info.item_type_path_table().path() - ); + assert_eq!(std::any::type_name::(), info.item_ty().path()); let value: &dyn Reflect = &Cow::<'static, [u8]>::Owned(vec![0, 1, 2, 3]); let info = value.get_represented_type_info().unwrap(); @@ -1797,13 +1794,13 @@ mod tests { let info = MyMap::type_info().as_map().unwrap(); assert!(info.is::()); - assert!(info.key_is::()); - assert!(info.value_is::()); + assert!(info.key_ty().is::()); + assert!(info.value_ty().is::()); assert!(info.key_info().unwrap().is::()); assert!(info.value_info().unwrap().is::()); assert_eq!(MyMap::type_path(), info.type_path()); - assert_eq!(usize::type_path(), info.key_type_path_table().path()); - assert_eq!(f32::type_path(), info.value_type_path_table().path()); + assert_eq!(usize::type_path(), info.key_ty().path()); + assert_eq!(f32::type_path(), info.value_ty().path()); let value: &dyn Reflect = &MyMap::new(); let info = value.get_represented_type_info().unwrap(); diff --git a/crates/bevy_reflect/src/list.rs b/crates/bevy_reflect/src/list.rs index 3a29c5733e6591..b44a7e1c0db78f 100644 --- a/crates/bevy_reflect/src/list.rs +++ b/crates/bevy_reflect/src/list.rs @@ -1,13 +1,14 @@ -use std::any::{Any, TypeId}; +use std::any::Any; use std::fmt::{Debug, Formatter}; use std::hash::{Hash, Hasher}; use bevy_reflect_derive::impl_type_path; +use crate::type_info::impl_type_methods; use crate::utility::reflect_hasher; use crate::{ self as bevy_reflect, ApplyError, FromReflect, MaybeTyped, PartialReflect, Reflect, - ReflectKind, ReflectMut, ReflectOwned, ReflectRef, TypeInfo, TypePath, TypePathTable, + ReflectKind, ReflectMut, ReflectOwned, ReflectRef, Type, TypeInfo, TypePath, }; /// A trait used to power [list-like] operations via [reflection]. @@ -108,11 +109,9 @@ pub trait List: PartialReflect { /// A container for compile-time list info. #[derive(Clone, Debug)] pub struct ListInfo { - type_path: TypePathTable, - type_id: TypeId, + ty: Type, item_info: fn() -> Option<&'static TypeInfo>, - item_type_path: TypePathTable, - item_type_id: TypeId, + item_ty: Type, #[cfg(feature = "documentation")] docs: Option<&'static str>, } @@ -121,11 +120,9 @@ impl ListInfo { /// Create a new [`ListInfo`]. pub fn new() -> Self { Self { - type_path: TypePathTable::of::(), - type_id: TypeId::of::(), + ty: Type::of::(), item_info: TItem::maybe_type_info, - item_type_path: TypePathTable::of::(), - item_type_id: TypeId::of::(), + item_ty: Type::of::(), #[cfg(feature = "documentation")] docs: None, } @@ -137,32 +134,7 @@ impl ListInfo { Self { docs, ..self } } - /// A representation of the type path of the list. - /// - /// Provides dynamic access to all methods on [`TypePath`]. - pub fn type_path_table(&self) -> &TypePathTable { - &self.type_path - } - - /// The [stable, full type path] of the list. - /// - /// Use [`type_path_table`] if you need access to the other methods on [`TypePath`]. - /// - /// [stable, full type path]: TypePath - /// [`type_path_table`]: Self::type_path_table - pub fn type_path(&self) -> &'static str { - self.type_path_table().path() - } - - /// The [`TypeId`] of the list. - pub fn type_id(&self) -> TypeId { - self.type_id - } - - /// Check if the given type matches the list type. - pub fn is(&self) -> bool { - TypeId::of::() == self.type_id - } + impl_type_methods!(ty); /// The [`TypeInfo`] of the list item. /// @@ -172,21 +144,11 @@ impl ListInfo { (self.item_info)() } - /// A representation of the type path of the list item. + /// The [type] of the list item. /// - /// Provides dynamic access to all methods on [`TypePath`]. - pub fn item_type_path_table(&self) -> &TypePathTable { - &self.item_type_path - } - - /// The [`TypeId`] of the list item. - pub fn item_type_id(&self) -> TypeId { - self.item_type_id - } - - /// Check if the given type matches the list item type. - pub fn item_is(&self) -> bool { - TypeId::of::() == self.item_type_id + /// [type]: Type + pub fn item_ty(&self) -> Type { + self.item_ty } /// The docstring of this list, if any. diff --git a/crates/bevy_reflect/src/map.rs b/crates/bevy_reflect/src/map.rs index b619a091eff2c9..4b192d6b75c97f 100644 --- a/crates/bevy_reflect/src/map.rs +++ b/crates/bevy_reflect/src/map.rs @@ -1,12 +1,12 @@ -use std::any::{Any, TypeId}; use std::fmt::{Debug, Formatter}; use bevy_reflect_derive::impl_type_path; use bevy_utils::{Entry, HashMap}; +use crate::type_info::impl_type_methods; use crate::{ self as bevy_reflect, ApplyError, MaybeTyped, PartialReflect, Reflect, ReflectKind, ReflectMut, - ReflectOwned, ReflectRef, TypeInfo, TypePath, TypePathTable, + ReflectOwned, ReflectRef, Type, TypeInfo, TypePath, }; /// A trait used to power [map-like] operations via [reflection]. @@ -97,14 +97,11 @@ pub trait Map: PartialReflect { /// A container for compile-time map info. #[derive(Clone, Debug)] pub struct MapInfo { - type_path: TypePathTable, - type_id: TypeId, + ty: Type, key_info: fn() -> Option<&'static TypeInfo>, - key_type_path: TypePathTable, - key_type_id: TypeId, + key_ty: Type, value_info: fn() -> Option<&'static TypeInfo>, - value_type_path: TypePathTable, - value_type_id: TypeId, + value_ty: Type, #[cfg(feature = "documentation")] docs: Option<&'static str>, } @@ -117,14 +114,11 @@ impl MapInfo { TValue: Reflect + MaybeTyped + TypePath, >() -> Self { Self { - type_path: TypePathTable::of::(), - type_id: TypeId::of::(), + ty: Type::of::(), key_info: TKey::maybe_type_info, - key_type_path: TypePathTable::of::(), - key_type_id: TypeId::of::(), + key_ty: Type::of::(), value_info: TValue::maybe_type_info, - value_type_path: TypePathTable::of::(), - value_type_id: TypeId::of::(), + value_ty: Type::of::(), #[cfg(feature = "documentation")] docs: None, } @@ -136,32 +130,7 @@ impl MapInfo { Self { docs, ..self } } - /// A representation of the type path of the map. - /// - /// Provides dynamic access to all methods on [`TypePath`]. - pub fn type_path_table(&self) -> &TypePathTable { - &self.type_path - } - - /// The [stable, full type path] of the map. - /// - /// Use [`type_path_table`] if you need access to the other methods on [`TypePath`]. - /// - /// [stable, full type path]: TypePath - /// [`type_path_table`]: Self::type_path_table - pub fn type_path(&self) -> &'static str { - self.type_path_table().path() - } - - /// The [`TypeId`] of the map. - pub fn type_id(&self) -> TypeId { - self.type_id - } - - /// Check if the given type matches the map type. - pub fn is(&self) -> bool { - TypeId::of::() == self.type_id - } + impl_type_methods!(ty); /// The [`TypeInfo`] of the key type. /// @@ -171,21 +140,11 @@ impl MapInfo { (self.key_info)() } - /// A representation of the type path of the key type. + /// The [type] of the key type. /// - /// Provides dynamic access to all methods on [`TypePath`]. - pub fn key_type_path_table(&self) -> &TypePathTable { - &self.key_type_path - } - - /// The [`TypeId`] of the key. - pub fn key_type_id(&self) -> TypeId { - self.key_type_id - } - - /// Check if the given type matches the key type. - pub fn key_is(&self) -> bool { - TypeId::of::() == self.key_type_id + /// [type]: Type + pub fn key_ty(&self) -> Type { + self.key_ty } /// The [`TypeInfo`] of the value type. @@ -196,21 +155,11 @@ impl MapInfo { (self.value_info)() } - /// A representation of the type path of the value type. + /// The [type] of the value type. /// - /// Provides dynamic access to all methods on [`TypePath`]. - pub fn value_type_path_table(&self) -> &TypePathTable { - &self.value_type_path - } - - /// The [`TypeId`] of the value. - pub fn value_type_id(&self) -> TypeId { - self.value_type_id - } - - /// Check if the given type matches the value type. - pub fn value_is(&self) -> bool { - TypeId::of::() == self.value_type_id + /// [type]: Type + pub fn value_ty(&self) -> Type { + self.value_ty } /// The docstring of this map, if any. diff --git a/crates/bevy_reflect/src/serde/de.rs b/crates/bevy_reflect/src/serde/de.rs index 26fff47100e48a..5563d9192f5e95 100644 --- a/crates/bevy_reflect/src/serde/de.rs +++ b/crates/bevy_reflect/src/serde/de.rs @@ -730,8 +730,8 @@ impl<'a, 'de> Visitor<'de> for ArrayVisitor<'a> { { let mut vec = Vec::with_capacity(seq.size_hint().unwrap_or_default()); let registration = get_registration( - self.array_info.item_type_id(), - self.array_info.item_type_path_table().path(), + self.array_info.item_ty().id(), + self.array_info.item_ty().path(), self.registry, )?; while let Some(value) = seq.next_element_seed(TypedReflectDeserializer { @@ -770,8 +770,8 @@ impl<'a, 'de> Visitor<'de> for ListVisitor<'a> { { let mut list = DynamicList::default(); let registration = get_registration( - self.list_info.item_type_id(), - self.list_info.item_type_path_table().path(), + self.list_info.item_ty().id(), + self.list_info.item_ty().path(), self.registry, )?; while let Some(value) = seq.next_element_seed(TypedReflectDeserializer { @@ -802,13 +802,13 @@ impl<'a, 'de> Visitor<'de> for MapVisitor<'a> { { let mut dynamic_map = DynamicMap::default(); let key_registration = get_registration( - self.map_info.key_type_id(), - self.map_info.key_type_path_table().path(), + self.map_info.key_ty().id(), + self.map_info.key_ty().path(), self.registry, )?; let value_registration = get_registration( - self.map_info.value_type_id(), - self.map_info.value_type_path_table().path(), + self.map_info.value_ty().id(), + self.map_info.value_ty().path(), self.registry, )?; while let Some(key) = map.next_key_seed(TypedReflectDeserializer { @@ -844,8 +844,8 @@ impl<'a, 'de> Visitor<'de> for SetVisitor<'a> { { let mut dynamic_set = DynamicSet::default(); let value_registration = get_registration( - self.set_info.value_type_id(), - self.set_info.value_type_path_table().path(), + self.set_info.value_ty().id(), + self.set_info.value_ty().path(), self.registry, )?; while let Some(value) = set.next_element_seed(TypedReflectDeserializer { diff --git a/crates/bevy_reflect/src/set.rs b/crates/bevy_reflect/src/set.rs index 08bcf4c163eced..3eb92470c84b5f 100644 --- a/crates/bevy_reflect/src/set.rs +++ b/crates/bevy_reflect/src/set.rs @@ -1,13 +1,13 @@ -use std::any::{Any, TypeId}; use std::fmt::{Debug, Formatter}; use bevy_reflect_derive::impl_type_path; use bevy_utils::hashbrown::hash_table::OccupiedEntry as HashTableOccupiedEntry; use bevy_utils::hashbrown::HashTable; +use crate::type_info::impl_type_methods; use crate::{ self as bevy_reflect, hash_error, ApplyError, PartialReflect, Reflect, ReflectKind, ReflectMut, - ReflectOwned, ReflectRef, TypeInfo, TypePath, TypePathTable, + ReflectOwned, ReflectRef, Type, TypeInfo, TypePath, }; /// A trait used to power [set-like] operations via [reflection]. @@ -82,10 +82,8 @@ pub trait Set: PartialReflect { /// A container for compile-time set info. #[derive(Clone, Debug)] pub struct SetInfo { - type_path: TypePathTable, - type_id: TypeId, - value_type_path: TypePathTable, - value_type_id: TypeId, + ty: Type, + value_ty: Type, #[cfg(feature = "documentation")] docs: Option<&'static str>, } @@ -94,10 +92,8 @@ impl SetInfo { /// Create a new [`SetInfo`]. pub fn new() -> Self { Self { - type_path: TypePathTable::of::(), - type_id: TypeId::of::(), - value_type_path: TypePathTable::of::(), - value_type_id: TypeId::of::(), + ty: Type::of::(), + value_ty: Type::of::(), #[cfg(feature = "documentation")] docs: None, } @@ -109,48 +105,13 @@ impl SetInfo { Self { docs, ..self } } - /// A representation of the type path of the set. - /// - /// Provides dynamic access to all methods on [`TypePath`]. - pub fn type_path_table(&self) -> &TypePathTable { - &self.type_path - } - - /// The [stable, full type path] of the set. - /// - /// Use [`type_path_table`] if you need access to the other methods on [`TypePath`]. - /// - /// [stable, full type path]: TypePath - /// [`type_path_table`]: Self::type_path_table - pub fn type_path(&self) -> &'static str { - self.type_path_table().path() - } - - /// The [`TypeId`] of the set. - pub fn type_id(&self) -> TypeId { - self.type_id - } - - /// Check if the given type matches the set type. - pub fn is(&self) -> bool { - TypeId::of::() == self.type_id - } + impl_type_methods!(ty); - /// A representation of the type path of the value type. + /// The [type] of the value. /// - /// Provides dynamic access to all methods on [`TypePath`]. - pub fn value_type_path_table(&self) -> &TypePathTable { - &self.value_type_path - } - - /// The [`TypeId`] of the value. - pub fn value_type_id(&self) -> TypeId { - self.value_type_id - } - - /// Check if the given type matches the value type. - pub fn value_is(&self) -> bool { - TypeId::of::() == self.value_type_id + /// [type]: Type + pub fn value_ty(&self) -> Type { + self.value_ty } /// The docstring of this set, if any. diff --git a/crates/bevy_reflect/src/struct_trait.rs b/crates/bevy_reflect/src/struct_trait.rs index 52dce37f91b945..d3c94933862094 100644 --- a/crates/bevy_reflect/src/struct_trait.rs +++ b/crates/bevy_reflect/src/struct_trait.rs @@ -1,17 +1,14 @@ use crate::attributes::{impl_custom_attribute_methods, CustomAttributes}; +use crate::type_info::impl_type_methods; use crate::{ self as bevy_reflect, ApplyError, NamedField, PartialReflect, Reflect, ReflectKind, ReflectMut, - ReflectOwned, ReflectRef, TypeInfo, TypePath, TypePathTable, + ReflectOwned, ReflectRef, Type, TypeInfo, TypePath, }; use bevy_reflect_derive::impl_type_path; use bevy_utils::HashMap; use std::fmt::{Debug, Formatter}; use std::sync::Arc; -use std::{ - any::{Any, TypeId}, - borrow::Cow, - slice::Iter, -}; +use std::{borrow::Cow, slice::Iter}; /// A trait used to power [struct-like] operations via [reflection]. /// @@ -78,8 +75,7 @@ pub trait Struct: PartialReflect { /// A container for compile-time named struct info. #[derive(Clone, Debug)] pub struct StructInfo { - type_path: TypePathTable, - type_id: TypeId, + ty: Type, fields: Box<[NamedField]>, field_names: Box<[&'static str]>, field_indices: HashMap<&'static str, usize>, @@ -105,8 +101,7 @@ impl StructInfo { let field_names = fields.iter().map(NamedField::name).collect(); Self { - type_path: TypePathTable::of::(), - type_id: TypeId::of::(), + ty: Type::of::(), fields: fields.to_vec().into_boxed_slice(), field_names, field_indices, @@ -162,32 +157,7 @@ impl StructInfo { self.fields.len() } - /// A representation of the type path of the struct. - /// - /// Provides dynamic access to all methods on [`TypePath`]. - pub fn type_path_table(&self) -> &TypePathTable { - &self.type_path - } - - /// The [stable, full type path] of the struct. - /// - /// Use [`type_path_table`] if you need access to the other methods on [`TypePath`]. - /// - /// [stable, full type path]: TypePath - /// [`type_path_table`]: Self::type_path_table - pub fn type_path(&self) -> &'static str { - self.type_path_table().path() - } - - /// The [`TypeId`] of the struct. - pub fn type_id(&self) -> TypeId { - self.type_id - } - - /// Check if the given type matches the struct type. - pub fn is(&self) -> bool { - TypeId::of::() == self.type_id - } + impl_type_methods!(ty); /// The docstring of this struct, if any. #[cfg(feature = "documentation")] diff --git a/crates/bevy_reflect/src/tuple.rs b/crates/bevy_reflect/src/tuple.rs index b63a5f6c777e83..fe200ca1197bc6 100644 --- a/crates/bevy_reflect/src/tuple.rs +++ b/crates/bevy_reflect/src/tuple.rs @@ -1,13 +1,14 @@ use bevy_reflect_derive::impl_type_path; use bevy_utils::all_tuples; +use crate::type_info::impl_type_methods; use crate::{ self as bevy_reflect, utility::GenericTypePathCell, ApplyError, FromReflect, - GetTypeRegistration, MaybeTyped, Reflect, ReflectMut, ReflectOwned, ReflectRef, TypeInfo, + GetTypeRegistration, MaybeTyped, Reflect, ReflectMut, ReflectOwned, ReflectRef, Type, TypeInfo, TypePath, TypeRegistration, TypeRegistry, Typed, UnnamedField, }; -use crate::{PartialReflect, ReflectKind, TypePathTable}; -use std::any::{Any, TypeId}; +use crate::{PartialReflect, ReflectKind}; +use std::any::Any; use std::fmt::{Debug, Formatter}; use std::slice::Iter; @@ -139,8 +140,7 @@ impl GetTupleField for dyn Tuple { /// A container for compile-time tuple info. #[derive(Clone, Debug)] pub struct TupleInfo { - type_path: TypePathTable, - type_id: TypeId, + ty: Type, fields: Box<[UnnamedField]>, #[cfg(feature = "documentation")] docs: Option<&'static str>, @@ -155,8 +155,7 @@ impl TupleInfo { /// pub fn new(fields: &[UnnamedField]) -> Self { Self { - type_path: TypePathTable::of::(), - type_id: TypeId::of::(), + ty: Type::of::(), fields: fields.to_vec().into_boxed_slice(), #[cfg(feature = "documentation")] docs: None, @@ -184,32 +183,7 @@ impl TupleInfo { self.fields.len() } - /// A representation of the type path of the tuple. - /// - /// Provides dynamic access to all methods on [`TypePath`]. - pub fn type_path_table(&self) -> &TypePathTable { - &self.type_path - } - - /// The [stable, full type path] of the tuple. - /// - /// Use [`type_path_table`] if you need access to the other methods on [`TypePath`]. - /// - /// [stable, full type path]: TypePath - /// [`type_path_table`]: Self::type_path_table - pub fn type_path(&self) -> &'static str { - self.type_path_table().path() - } - - /// The [`TypeId`] of the tuple. - pub fn type_id(&self) -> TypeId { - self.type_id - } - - /// Check if the given type matches the tuple type. - pub fn is(&self) -> bool { - TypeId::of::() == self.type_id - } + impl_type_methods!(ty); /// The docstring of this tuple, if any. #[cfg(feature = "documentation")] diff --git a/crates/bevy_reflect/src/tuple_struct.rs b/crates/bevy_reflect/src/tuple_struct.rs index dd923e47be7950..6cdd3031f04898 100644 --- a/crates/bevy_reflect/src/tuple_struct.rs +++ b/crates/bevy_reflect/src/tuple_struct.rs @@ -1,11 +1,11 @@ use bevy_reflect_derive::impl_type_path; use crate::attributes::{impl_custom_attribute_methods, CustomAttributes}; +use crate::type_info::impl_type_methods; use crate::{ self as bevy_reflect, ApplyError, DynamicTuple, PartialReflect, Reflect, ReflectKind, - ReflectMut, ReflectOwned, ReflectRef, Tuple, TypeInfo, TypePath, TypePathTable, UnnamedField, + ReflectMut, ReflectOwned, ReflectRef, Tuple, Type, TypeInfo, TypePath, UnnamedField, }; -use std::any::{Any, TypeId}; use std::fmt::{Debug, Formatter}; use std::slice::Iter; use std::sync::Arc; @@ -58,8 +58,7 @@ pub trait TupleStruct: PartialReflect { /// A container for compile-time tuple struct info. #[derive(Clone, Debug)] pub struct TupleStructInfo { - type_path: TypePathTable, - type_id: TypeId, + ty: Type, fields: Box<[UnnamedField]>, custom_attributes: Arc, #[cfg(feature = "documentation")] @@ -75,8 +74,7 @@ impl TupleStructInfo { /// pub fn new(fields: &[UnnamedField]) -> Self { Self { - type_path: TypePathTable::of::(), - type_id: TypeId::of::(), + ty: Type::of::(), fields: fields.to_vec().into_boxed_slice(), custom_attributes: Arc::new(CustomAttributes::default()), #[cfg(feature = "documentation")] @@ -113,32 +111,7 @@ impl TupleStructInfo { self.fields.len() } - /// A representation of the type path of the struct. - /// - /// Provides dynamic access to all methods on [`TypePath`]. - pub fn type_path_table(&self) -> &TypePathTable { - &self.type_path - } - - /// The [stable, full type path] of the struct. - /// - /// Use [`type_path_table`] if you need access to the other methods on [`TypePath`]. - /// - /// [stable, full type path]: TypePath - /// [`type_path_table`]: Self::type_path_table - pub fn type_path(&self) -> &'static str { - self.type_path_table().path() - } - - /// The [`TypeId`] of the tuple struct. - pub fn type_id(&self) -> TypeId { - self.type_id - } - - /// Check if the given type matches the tuple struct type. - pub fn is(&self) -> bool { - TypeId::of::() == self.type_id - } + impl_type_methods!(ty); /// The docstring of this struct, if any. #[cfg(feature = "documentation")] diff --git a/crates/bevy_reflect/src/type_info.rs b/crates/bevy_reflect/src/type_info.rs index a6a7ceb2404241..d3450239e6d3f7 100644 --- a/crates/bevy_reflect/src/type_info.rs +++ b/crates/bevy_reflect/src/type_info.rs @@ -3,8 +3,10 @@ use crate::{ DynamicTupleStruct, EnumInfo, ListInfo, MapInfo, PartialReflect, Reflect, ReflectKind, SetInfo, StructInfo, TupleInfo, TupleStructInfo, TypePath, TypePathTable, }; +use core::fmt::Formatter; use std::any::{Any, TypeId}; use std::fmt::Debug; +use std::hash::Hash; use thiserror::Error; /// A static accessor to compile-time type information. @@ -178,36 +180,33 @@ pub enum TypeInfo { } impl TypeInfo { - /// The [`TypeId`] of the underlying type. - pub fn type_id(&self) -> TypeId { + /// The underlying Rust [type]. + /// + /// [type]: Type + pub fn ty(&self) -> &Type { match self { - Self::Struct(info) => info.type_id(), - Self::TupleStruct(info) => info.type_id(), - Self::Tuple(info) => info.type_id(), - Self::List(info) => info.type_id(), - Self::Array(info) => info.type_id(), - Self::Map(info) => info.type_id(), - Self::Set(info) => info.type_id(), - Self::Enum(info) => info.type_id(), - Self::Value(info) => info.type_id(), + Self::Struct(info) => info.ty(), + Self::TupleStruct(info) => info.ty(), + Self::Tuple(info) => info.ty(), + Self::List(info) => info.ty(), + Self::Array(info) => info.ty(), + Self::Map(info) => info.ty(), + Self::Set(info) => info.ty(), + Self::Enum(info) => info.ty(), + Self::Value(info) => info.ty(), } } + /// The [`TypeId`] of the underlying type. + pub fn type_id(&self) -> TypeId { + self.ty().id() + } + /// A representation of the type path of the underlying type. /// /// Provides dynamic access to all methods on [`TypePath`]. pub fn type_path_table(&self) -> &TypePathTable { - match self { - Self::Struct(info) => info.type_path_table(), - Self::TupleStruct(info) => info.type_path_table(), - Self::Tuple(info) => info.type_path_table(), - Self::List(info) => info.type_path_table(), - Self::Array(info) => info.type_path_table(), - Self::Map(info) => info.type_path_table(), - Self::Set(info) => info.type_path_table(), - Self::Enum(info) => info.type_path_table(), - Self::Value(info) => info.type_path_table(), - } + self.ty().type_path_table() } /// The [stable, full type path] of the underlying type. @@ -217,12 +216,16 @@ impl TypeInfo { /// [stable, full type path]: TypePath /// [`type_path_table`]: Self::type_path_table pub fn type_path(&self) -> &'static str { - self.type_path_table().path() + self.ty().path() } - /// Check if the given type matches the underlying type. + /// Check if the given type matches this one. + /// + /// This only compares the [`TypeId`] of the types + /// and does not verify they share the same [`TypePath`] + /// (though it implies they do). pub fn is(&self) -> bool { - TypeId::of::() == self.type_id() + self.ty().is::() } /// The docstring of the underlying type, if any. @@ -287,6 +290,199 @@ impl TypeInfo { impl_cast_method!(as_value: Value => ValueInfo); } +/// The base representation of a Rust type. +/// +/// When possible, it is recommended to use [`&'static TypeInfo`] instead of this +/// as it provides more information as well as being smaller +/// (since a reference only takes the same number of bytes as a `usize`). +/// +/// However, where a static reference to [`TypeInfo`] is not possible, +/// such as with trait objects and other types that can't implement [`Typed`], +/// this type can be used instead. +/// +/// It only requires that the type implements [`TypePath`]. +/// +/// And unlike [`TypeInfo`], this type implements [`Copy`], [`Eq`], and [`Hash`], +/// making it useful as a key type. +/// +/// It's especially helpful when compared to [`TypeId`] as it can provide the +/// actual [type path] when debugging, while still having the same performance +/// as hashing/comparing [`TypeId`] directly—at the cost of a little more memory. +/// +/// # Examples +/// +/// ``` +/// use bevy_reflect::{Type, TypePath}; +/// +/// fn assert_char(t: &T) -> Result<(), String> { +/// let ty = Type::of::(); +/// if Type::of::() == ty { +/// Ok(()) +/// } else { +/// Err(format!("expected `char`, got `{}`", ty.path())) +/// } +/// } +/// +/// assert_eq!( +/// assert_char(&'a'), +/// Ok(()) +/// ); +/// assert_eq!( +/// assert_char(&String::from("Hello, world!")), +/// Err(String::from("expected `char`, got `alloc::string::String`")) +/// ); +/// ``` +/// +/// [`&'static TypeInfo`]: TypeInfo +#[derive(Copy, Clone)] +pub struct Type { + type_path_table: TypePathTable, + type_id: TypeId, +} + +impl Type { + /// Create a new [`Type`] from a type that implements [`TypePath`]. + pub fn of() -> Self { + Self { + type_path_table: TypePathTable::of::(), + type_id: TypeId::of::(), + } + } + + /// Returns the [`TypeId`] of the type. + pub fn id(&self) -> TypeId { + self.type_id + } + + /// See [`TypePath::type_path`]. + pub fn path(&self) -> &'static str { + self.type_path_table.path() + } + + /// See [`TypePath::short_type_path`]. + pub fn short_path(&self) -> &'static str { + self.type_path_table.short_path() + } + + /// See [`TypePath::type_ident`]. + pub fn ident(&self) -> Option<&'static str> { + self.type_path_table.ident() + } + + /// See [`TypePath::crate_name`]. + pub fn crate_name(&self) -> Option<&'static str> { + self.type_path_table.crate_name() + } + + /// See [`TypePath::module_path`]. + pub fn module_path(&self) -> Option<&'static str> { + self.type_path_table.module_path() + } + + /// A representation of the type path of this. + /// + /// Provides dynamic access to all methods on [`TypePath`]. + pub fn type_path_table(&self) -> &TypePathTable { + &self.type_path_table + } + + /// Check if the given type matches this one. + /// + /// This only compares the [`TypeId`] of the types + /// and does not verify they share the same [`TypePath`] + /// (though it implies they do). + pub fn is(&self) -> bool { + TypeId::of::() == self.type_id + } +} + +/// This implementation will only output the [type path] of the type. +/// +/// If you need to include the [`TypeId`] in the output, +/// you can access it through [`Type::id`]. +/// +/// [type path]: TypePath +impl Debug for Type { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + write!(f, "{}", self.type_path_table.path()) + } +} + +impl Eq for Type {} + +/// This implementation purely relies on the [`TypeId`] of the type, +/// and not on the [type path]. +/// +/// [type path]: TypePath +impl PartialEq for Type { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.type_id == other.type_id + } +} + +/// This implementation purely relies on the [`TypeId`] of the type, +/// and not on the [type path]. +/// +/// [type path]: TypePath +impl Hash for Type { + #[inline] + fn hash(&self, state: &mut H) { + self.type_id.hash(state); + } +} + +macro_rules! impl_type_methods { + ($field:ident) => { + /// The underlying Rust [type]. + /// + /// [type]: crate::type_info::Type + pub fn ty(&self) -> &$crate::type_info::Type { + &self.$field + } + + /// The [`TypeId`] of this type. + /// + /// [`TypeId`]: std::any::TypeId + pub fn type_id(&self) -> ::std::any::TypeId { + self.$field.id() + } + + /// The [stable, full type path] of this type. + /// + /// Use [`type_path_table`] if you need access to the other methods on [`TypePath`]. + /// + /// [stable, full type path]: TypePath + /// [`type_path_table`]: Self::type_path_table + pub fn type_path(&self) -> &'static str { + self.$field.path() + } + + /// A representation of the type path of this type. + /// + /// Provides dynamic access to all methods on [`TypePath`]. + /// + /// [`TypePath`]: crate::type_path::TypePath + pub fn type_path_table(&self) -> &$crate::type_path::TypePathTable { + &self.$field.type_path_table() + } + + /// Check if the given type matches this one. + /// + /// This only compares the [`TypeId`] of the types + /// and does not verify they share the same [`TypePath`] + /// (though it implies they do). + /// + /// [`TypeId`]: std::any::TypeId + /// [`TypePath`]: crate::type_path::TypePath + pub fn is(&self) -> bool { + self.$field.is::() + } + }; +} + +pub(crate) use impl_type_methods; + /// A container for compile-time info related to general value types, including primitives. /// /// This typically represents a type which cannot be broken down any further. This is often @@ -297,8 +493,7 @@ impl TypeInfo { /// it _as_ a struct. It therefore makes more sense to represent it as a [`ValueInfo`]. #[derive(Debug, Clone)] pub struct ValueInfo { - type_path: TypePathTable, - type_id: TypeId, + ty: Type, #[cfg(feature = "documentation")] docs: Option<&'static str>, } @@ -306,8 +501,7 @@ pub struct ValueInfo { impl ValueInfo { pub fn new() -> Self { Self { - type_path: TypePathTable::of::(), - type_id: TypeId::of::(), + ty: Type::of::(), #[cfg(feature = "documentation")] docs: None, } @@ -319,32 +513,7 @@ impl ValueInfo { Self { docs: doc, ..self } } - /// A representation of the type path of the value. - /// - /// Provides dynamic access to all methods on [`TypePath`]. - pub fn type_path_table(&self) -> &TypePathTable { - &self.type_path - } - - /// The [stable, full type path] of the value. - /// - /// Use [`type_path_table`] if you need access to the other methods on [`TypePath`]. - /// - /// [stable, full type path]: TypePath - /// [`type_path_table`]: Self::type_path_table - pub fn type_path(&self) -> &'static str { - self.type_path_table().path() - } - - /// The [`TypeId`] of the value. - pub fn type_id(&self) -> TypeId { - self.type_id - } - - /// Check if the given type matches the value type. - pub fn is(&self) -> bool { - TypeId::of::() == self.type_id - } + impl_type_methods!(ty); /// The docstring of this dynamic value, if any. #[cfg(feature = "documentation")] From 1caa64d94820d7148fa4b9a04e5436d17098ab1b Mon Sep 17 00:00:00 2001 From: charlotte Date: Sun, 25 Aug 2024 13:16:34 -0700 Subject: [PATCH 15/53] Refactor `AsBindGroup` to use a associated `SystemParam`. (#14909) # Objective Adding more features to `AsBindGroup` proc macro means making the trait arguments uglier. Downstream implementors of the trait without the proc macro might want to do different things than our default arguments. ## Solution Make `AsBindGroup` take an associated `Param` type. ## Migration Guide `AsBindGroup` now allows the user to specify a `SystemParam` to be used for creating bind groups. --- crates/bevy_pbr/src/extended_material.rs | 18 +++++---------- crates/bevy_pbr/src/material.rs | 13 +++-------- .../bevy_render/macros/src/as_bind_group.rs | 11 ++++++--- .../src/render_resource/bind_group.rs | 23 ++++++++++--------- crates/bevy_sprite/src/mesh2d/material.rs | 17 +++----------- .../src/render/ui_material_pipeline.rs | 13 ++++------- examples/shader/texture_binding_array.rs | 14 ++++++----- 7 files changed, 44 insertions(+), 65 deletions(-) diff --git a/crates/bevy_pbr/src/extended_material.rs b/crates/bevy_pbr/src/extended_material.rs index ff21314f36a818..81b3c6ebbbfd7d 100644 --- a/crates/bevy_pbr/src/extended_material.rs +++ b/crates/bevy_pbr/src/extended_material.rs @@ -1,14 +1,13 @@ use bevy_asset::{Asset, Handle}; +use bevy_ecs::system::SystemParamItem; use bevy_reflect::{impl_type_path, Reflect}; use bevy_render::{ mesh::MeshVertexBufferLayoutRef, - render_asset::RenderAssets, render_resource::{ AsBindGroup, AsBindGroupError, BindGroupLayout, RenderPipelineDescriptor, Shader, ShaderRef, SpecializedMeshPipelineError, UnpreparedBindGroup, }, renderer::RenderDevice, - texture::{FallbackImage, GpuImage}, }; use crate::{Material, MaterialPipeline, MaterialPipelineKey, MeshPipeline, MeshPipelineKey}; @@ -147,26 +146,21 @@ impl_type_path!((in bevy_pbr::extended_material) ExtendedMaterial AsBindGroup for ExtendedMaterial { type Data = (::Data, ::Data); + type Param = (::Param, ::Param); fn unprepared_bind_group( &self, layout: &BindGroupLayout, render_device: &RenderDevice, - images: &RenderAssets, - fallback_image: &FallbackImage, + (base_param, extended_param): &mut SystemParamItem<'_, '_, Self::Param>, ) -> Result, AsBindGroupError> { // add together the bindings of the base material and the user material let UnpreparedBindGroup { mut bindings, data: base_data, - } = B::unprepared_bind_group(&self.base, layout, render_device, images, fallback_image)?; - let extended_bindgroup = E::unprepared_bind_group( - &self.extension, - layout, - render_device, - images, - fallback_image, - )?; + } = B::unprepared_bind_group(&self.base, layout, render_device, base_param)?; + let extended_bindgroup = + E::unprepared_bind_group(&self.extension, layout, render_device, extended_param)?; bindings.extend(extended_bindgroup.bindings); diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index a000b19a2c6632..d5b601d6515cc6 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -30,7 +30,6 @@ use bevy_render::{ render_phase::*, render_resource::*, renderer::RenderDevice, - texture::FallbackImage, view::{ExtractedView, Msaa, RenderVisibilityRanges, VisibleEntities, WithMesh}, }; use bevy_utils::tracing::error; @@ -908,22 +907,16 @@ impl RenderAsset for PreparedMaterial { type Param = ( SRes, - SRes>, - SRes, SRes>, SRes, + M::Param, ); fn prepare_asset( material: Self::SourceAsset, - (render_device, images, fallback_image, pipeline, default_opaque_render_method): &mut SystemParamItem, + (render_device, pipeline, default_opaque_render_method, ref mut material_param): &mut SystemParamItem, ) -> Result> { - match material.as_bind_group( - &pipeline.material_layout, - render_device, - images, - fallback_image, - ) { + match material.as_bind_group(&pipeline.material_layout, render_device, material_param) { Ok(prepared) => { let method = match material.opaque_render_method() { OpaqueRendererMethod::Forward => OpaqueRendererMethod::Forward, diff --git a/crates/bevy_render/macros/src/as_bind_group.rs b/crates/bevy_render/macros/src/as_bind_group.rs index 8fa69d29f1a068..81fb920f3c4448 100644 --- a/crates/bevy_render/macros/src/as_bind_group.rs +++ b/crates/bevy_render/macros/src/as_bind_group.rs @@ -42,6 +42,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { let manifest = BevyManifest::default(); let render_path = manifest.get_path("bevy_render"); let asset_path = manifest.get_path("bevy_asset"); + let ecs_path = manifest.get_path("bevy_ecs"); let mut binding_states: Vec = Vec::new(); let mut binding_impls = Vec::new(); @@ -62,7 +63,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { binding_impls.push(quote! {{ use #render_path::render_resource::AsBindGroupShaderType; let mut buffer = #render_path::render_resource::encase::UniformBuffer::new(Vec::new()); - let converted: #converted_shader_type = self.as_bind_group_shader_type(images); + let converted: #converted_shader_type = self.as_bind_group_shader_type(&images); buffer.write(&converted).unwrap(); ( #binding_index, @@ -523,6 +524,11 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { impl #impl_generics #render_path::render_resource::AsBindGroup for #struct_name #ty_generics #where_clause { type Data = #prepared_data; + type Param = ( + #ecs_path::system::lifetimeless::SRes<#render_path::render_asset::RenderAssets<#render_path::texture::GpuImage>>, + #ecs_path::system::lifetimeless::SRes<#render_path::texture::FallbackImage>, + ); + fn label() -> Option<&'static str> { Some(#struct_name_literal) } @@ -531,8 +537,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { &self, layout: &#render_path::render_resource::BindGroupLayout, render_device: &#render_path::renderer::RenderDevice, - images: &#render_path::render_asset::RenderAssets<#render_path::texture::GpuImage>, - fallback_image: &#render_path::texture::FallbackImage, + (images, fallback_image): &mut #ecs_path::system::SystemParamItem<'_, '_, Self::Param>, ) -> Result<#render_path::render_resource::UnpreparedBindGroup, #render_path::render_resource::AsBindGroupError> { let bindings = vec![#(#binding_impls,)*]; diff --git a/crates/bevy_render/src/render_resource/bind_group.rs b/crates/bevy_render/src/render_resource/bind_group.rs index 6371678e48b3dd..4380890e17c27e 100644 --- a/crates/bevy_render/src/render_resource/bind_group.rs +++ b/crates/bevy_render/src/render_resource/bind_group.rs @@ -3,8 +3,9 @@ use crate::{ render_asset::RenderAssets, render_resource::{resource_macros::*, BindGroupLayout, Buffer, Sampler, TextureView}, renderer::RenderDevice, - texture::{FallbackImage, GpuImage}, + texture::GpuImage, }; +use bevy_ecs::system::{SystemParam, SystemParamItem}; pub use bevy_render_macros::AsBindGroup; use encase::ShaderType; use std::ops::Deref; @@ -57,7 +58,7 @@ impl Deref for BindGroup { /// /// This is an opinionated trait that is intended to make it easy to generically /// convert a type into a [`BindGroup`]. It provides access to specific render resources, -/// such as [`RenderAssets`] and [`FallbackImage`]. If a type has a [`Handle`](bevy_asset::Handle), +/// such as [`RenderAssets`] and [`crate::texture::FallbackImage`]. If a type has a [`Handle`](bevy_asset::Handle), /// these can be used to retrieve the corresponding [`Texture`](crate::render_resource::Texture) resource. /// /// [`AsBindGroup::as_bind_group`] is intended to be called once, then the result cached somewhere. It is generally @@ -115,7 +116,7 @@ impl Deref for BindGroup { /// * This field's [`Handle`](bevy_asset::Handle) will be used to look up the matching [`Texture`](crate::render_resource::Texture) /// GPU resource, which will be bound as a texture in shaders. The field will be assumed to implement [`Into>>`]. In practice, /// most fields should be a [`Handle`](bevy_asset::Handle) or [`Option>`]. If the value of an [`Option>`] is -/// [`None`], the [`FallbackImage`] resource will be used instead. This attribute can be used in conjunction with a `sampler` binding attribute +/// [`None`], the [`crate::texture::FallbackImage`] resource will be used instead. This attribute can be used in conjunction with a `sampler` binding attribute /// (with a different binding index) if a binding of the sampler for the [`Image`](crate::texture::Image) is also required. /// /// | Arguments | Values | Default | @@ -130,7 +131,7 @@ impl Deref for BindGroup { /// * This field's [`Handle`](bevy_asset::Handle) will be used to look up the matching [`Texture`](crate::render_resource::Texture) /// GPU resource, which will be bound as a storage texture in shaders. The field will be assumed to implement [`Into>>`]. In practice, /// most fields should be a [`Handle`](bevy_asset::Handle) or [`Option>`]. If the value of an [`Option>`] is -/// [`None`], the [`FallbackImage`] resource will be used instead. +/// [`None`], the [`crate::texture::FallbackImage`] resource will be used instead. /// /// | Arguments | Values | Default | /// |------------------------|--------------------------------------------------------------------------------------------|---------------| @@ -143,7 +144,7 @@ impl Deref for BindGroup { /// * This field's [`Handle`](bevy_asset::Handle) will be used to look up the matching [`Sampler`] GPU /// resource, which will be bound as a sampler in shaders. The field will be assumed to implement [`Into>>`]. In practice, /// most fields should be a [`Handle`](bevy_asset::Handle) or [`Option>`]. If the value of an [`Option>`] is -/// [`None`], the [`FallbackImage`] resource will be used instead. This attribute can be used in conjunction with a `texture` binding attribute +/// [`None`], the [`crate::texture::FallbackImage`] resource will be used instead. This attribute can be used in conjunction with a `texture` binding attribute /// (with a different binding index) if a binding of the texture for the [`Image`](crate::texture::Image) is also required. /// /// | Arguments | Values | Default | @@ -187,7 +188,7 @@ impl Deref for BindGroup { /// color_texture: Option>, /// } /// ``` -/// This is useful if you want a texture to be optional. When the value is [`None`], the [`FallbackImage`] will be used for the binding instead, which defaults +/// This is useful if you want a texture to be optional. When the value is [`None`], the [`crate::texture::FallbackImage`] will be used for the binding instead, which defaults /// to "pure white". /// /// Field uniforms with the same index will be combined into a single binding: @@ -284,6 +285,8 @@ pub trait AsBindGroup { /// Data that will be stored alongside the "prepared" bind group. type Data: Send + Sync; + type Param: SystemParam + 'static; + /// label fn label() -> Option<&'static str> { None @@ -294,11 +297,10 @@ pub trait AsBindGroup { &self, layout: &BindGroupLayout, render_device: &RenderDevice, - images: &RenderAssets, - fallback_image: &FallbackImage, + param: &mut SystemParamItem<'_, '_, Self::Param>, ) -> Result, AsBindGroupError> { let UnpreparedBindGroup { bindings, data } = - Self::unprepared_bind_group(self, layout, render_device, images, fallback_image)?; + Self::unprepared_bind_group(self, layout, render_device, param)?; let entries = bindings .iter() @@ -325,8 +327,7 @@ pub trait AsBindGroup { &self, layout: &BindGroupLayout, render_device: &RenderDevice, - images: &RenderAssets, - fallback_image: &FallbackImage, + param: &mut SystemParamItem<'_, '_, Self::Param>, ) -> Result, AsBindGroupError>; /// Creates the bind group layout matching all bind groups returned by [`AsBindGroup::as_bind_group`] diff --git a/crates/bevy_sprite/src/mesh2d/material.rs b/crates/bevy_sprite/src/mesh2d/material.rs index 849fd980fbc91c..17743f5aab693f 100644 --- a/crates/bevy_sprite/src/mesh2d/material.rs +++ b/crates/bevy_sprite/src/mesh2d/material.rs @@ -28,7 +28,6 @@ use bevy_render::{ SpecializedMeshPipeline, SpecializedMeshPipelineError, SpecializedMeshPipelines, }, renderer::RenderDevice, - texture::{FallbackImage, GpuImage}, view::{ExtractedView, InheritedVisibility, Msaa, ViewVisibility, Visibility, VisibleEntities}, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; @@ -581,23 +580,13 @@ impl PreparedMaterial2d { impl RenderAsset for PreparedMaterial2d { type SourceAsset = M; - type Param = ( - SRes, - SRes>, - SRes, - SRes>, - ); + type Param = (SRes, SRes>, M::Param); fn prepare_asset( material: Self::SourceAsset, - (render_device, images, fallback_image, pipeline): &mut SystemParamItem, + (render_device, pipeline, material_param): &mut SystemParamItem, ) -> Result> { - match material.as_bind_group( - &pipeline.material2d_layout, - render_device, - images, - fallback_image, - ) { + match material.as_bind_group(&pipeline.material2d_layout, render_device, material_param) { Ok(prepared) => { let mut mesh_pipeline_key_bits = Mesh2dPipelineKey::empty(); mesh_pipeline_key_bits.insert(alpha_mode_pipeline_key(material.alpha_mode())); diff --git a/crates/bevy_ui/src/render/ui_material_pipeline.rs b/crates/bevy_ui/src/render/ui_material_pipeline.rs index 2eaa2f583bc4da..bcdccf15fc1c89 100644 --- a/crates/bevy_ui/src/render/ui_material_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_material_pipeline.rs @@ -16,7 +16,7 @@ use bevy_render::{ render_phase::*, render_resource::{binding_types::uniform_buffer, *}, renderer::{RenderDevice, RenderQueue}, - texture::{BevyDefault, FallbackImage, GpuImage}, + texture::BevyDefault, view::*, Extract, ExtractSchedule, Render, RenderSet, }; @@ -604,18 +604,13 @@ pub struct PreparedUiMaterial { impl RenderAsset for PreparedUiMaterial { type SourceAsset = M; - type Param = ( - SRes, - SRes>, - SRes, - SRes>, - ); + type Param = (SRes, SRes>, M::Param); fn prepare_asset( material: Self::SourceAsset, - (render_device, images, fallback_image, pipeline): &mut SystemParamItem, + (render_device, pipeline, ref mut material_param): &mut SystemParamItem, ) -> Result> { - match material.as_bind_group(&pipeline.ui_layout, render_device, images, fallback_image) { + match material.as_bind_group(&pipeline.ui_layout, render_device, material_param) { Ok(prepared) => Ok(PreparedUiMaterial { bindings: prepared.bindings, bind_group: prepared.bind_group, diff --git a/examples/shader/texture_binding_array.rs b/examples/shader/texture_binding_array.rs index 0698c488bf32e4..53e774df0485ea 100644 --- a/examples/shader/texture_binding_array.rs +++ b/examples/shader/texture_binding_array.rs @@ -1,6 +1,8 @@ //! A shader that binds several textures onto one //! `binding_array>` shader binding slot and sample non-uniformly. +use bevy::ecs::system::lifetimeless::SRes; +use bevy::ecs::system::SystemParamItem; use bevy::{ prelude::*, reflect::TypePath, @@ -97,12 +99,13 @@ struct BindlessMaterial { impl AsBindGroup for BindlessMaterial { type Data = (); + type Param = (SRes>, SRes); + fn as_bind_group( &self, layout: &BindGroupLayout, render_device: &RenderDevice, - image_assets: &RenderAssets, - fallback_image: &FallbackImage, + (image_assets, fallback_image): &mut SystemParamItem<'_, '_, Self::Param>, ) -> Result, AsBindGroupError> { // retrieve the render resources from handles let mut images = vec![]; @@ -140,10 +143,9 @@ impl AsBindGroup for BindlessMaterial { fn unprepared_bind_group( &self, - _: &BindGroupLayout, - _: &RenderDevice, - _: &RenderAssets, - _: &FallbackImage, + _layout: &BindGroupLayout, + _render_device: &RenderDevice, + _param: &mut SystemParamItem<'_, '_, Self::Param>, ) -> Result, AsBindGroupError> { // we implement as_bind_group directly because panic!("bindless texture arrays can't be owned") From 7bb76ab74b1ee0609e414b054a5ea1153f587896 Mon Sep 17 00:00:00 2001 From: Sludge Date: Mon, 26 Aug 2024 19:54:33 +0200 Subject: [PATCH 16/53] Add `VertexBufferLayout::offset_locations` (#9805) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective When using instancing, 2 `VertexBufferLayout`s are needed, one for per-vertex and one for per-instance data. Shader locations of all attributes must not overlap, so one of the layouts needs to start its locations at an offset. However, `VertexBufferLayout::from_vertex_formats` will always start locations at 0, requiring manual adjustment, which is currently pretty verbose. ## Solution Add `VertexBufferLayout::offset_locations`, which adds an offset to all attribute locations. Code using this method looks like this: ```rust VertexState { shader: BACKBUFFER_SHADER_HANDLE.typed(), shader_defs: Vec::new(), entry_point: "vertex".into(), buffers: vec![ VertexBufferLayout::from_vertex_formats( VertexStepMode::Vertex, [VertexFormat::Float32x2], ), VertexBufferLayout::from_vertex_formats( VertexStepMode::Instance, [VertexFormat::Float32x2, VertexFormat::Float32x3], ) .offset_locations(1), ], } ``` Alternative solutions include: - Pass the starting location to `from_vertex_formats` – this is a bit simpler than my solution here, but most calls don't need an offset, so they'd always pass 0 there. - Do nothing and make the user hand-write this. --- ## Changelog - Add `VertexBufferLayout::offset_locations` to simplify buffer layout construction when using instancing. --------- Co-authored-by: Nicola Papale Co-authored-by: Alice Cecile --- crates/bevy_render/src/render_resource/pipeline.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/bevy_render/src/render_resource/pipeline.rs b/crates/bevy_render/src/render_resource/pipeline.rs index 8eafdaf2892a99..93f76d3f56e8a6 100644 --- a/crates/bevy_render/src/render_resource/pipeline.rs +++ b/crates/bevy_render/src/render_resource/pipeline.rs @@ -158,6 +158,15 @@ impl VertexBufferLayout { attributes, } } + + /// Returns a [`VertexBufferLayout`] with the shader location of every attribute offset by + /// `location`. + pub fn offset_locations_by(mut self, location: u32) -> Self { + self.attributes.iter_mut().for_each(|attr| { + attr.shader_location += location; + }); + self + } } /// Describes the fragment process in a render pipeline. From 6cc96f4c1fede790a6d0e3b2f345dee160abc5af Mon Sep 17 00:00:00 2001 From: JMS55 <47158642+JMS55@users.noreply.github.com> Date: Mon, 26 Aug 2024 10:54:34 -0700 Subject: [PATCH 17/53] Meshlet software raster + start of cleanup (#14623) # Objective - Faster meshlet rasterization path for small triangles - Avoid having to allocate and write out a triangle buffer - Refactor gpu_scene.rs ## Solution - Replace the 32bit visbuffer texture with a 64bit visbuffer buffer, where the left 32 bits encode depth, and the right 32 bits encode the existing cluster + triangle IDs. Can't use 64bit textures, wgpu/naga doesn't support atomic ops on textures yet. - Instead of writing out a buffer of packed cluster + triangle IDs (per triangle) to raster, the culling pass now writes out a buffer of just cluster IDs (per cluster, so less memory allocated, cheaper to write out). - Clusters for software raster are allocated from the left side - Clusters for hardware raster are allocated in the same buffer, from the right side - The buffer size is fixed at MeshletPlugin build time, and should be set to a reasonable value for your scene (no warning on overflow, and no good way to determine what value you need outside of renderdoc - I plan to fix this in a future PR adding a meshlet stats overlay) - Currently I don't have a heuristic for software vs hardware raster selection for each cluster. The existing code is just a placeholder. I need to profile on a release scene and come up with a heuristic, probably in a future PR. - The culling shader is getting pretty hard to follow at this point, but I don't want to spend time improving it as the entire shader/pass is getting rewritten/replaced in the near future. - Software raster is a compute workgroup per-cluster. Each workgroup loads and transforms the <=64 vertices of the cluster, and then rasterizes the <=64 triangles of the cluster. - Two variants are implemented: Scanline for clusters with any larger triangles (still smaller than hardware is good at), and brute-force for very very tiny triangles - Once the shader determines that a pixel should be filled in, it does an atomicMax() on the visbuffer to store the results, copying how Nanite works - On devices with a low max workgroups per dispatch limit, an extra compute pass is inserted before software raster to convert from a 1d to 2d dispatch (I don't think 3d would ever be necessary). - I haven't implemented the top-left rule or subpixel precision yet, I'm leaving that for a future PR since I get usable results without it for now - Resources used: https://kristoffer-dyrkorn.github.io/triangle-rasterizer and chapters 6-8 of https://fgiesen.wordpress.com/2013/02/17/optimizing-sw-occlusion-culling-index - Hardware raster now spawns 64*3 vertex invocations per meshlet, instead of the actual meshlet vertex count. Extra invocations just early-exit. - While this is slower than the existing system, hardware draws should be rare now that software raster is usable, and it saves a ton of memory using the unified cluster ID buffer. This would be fixed if wgpu had support for mesh shaders. - Instead of writing to a color+depth attachment, the hardware raster pass also does the same atomic visbuffer writes that software raster uses. - We have to bind a dummy render target anyways, as wgpu doesn't currently support render passes without any attachments - Material IDs are no longer written out during the main rasterization passes. - If we had async compute queues, we could overlap the software and hardware raster passes. - New material and depth resolve passes run at the end of the visbuffer node, and write out view depth and material ID depth textures ### Misc changes - Fixed cluster culling importing, but never actually using the previous view uniforms when doing occlusion culling - Fixed incorrectly adding the LOD error twice when building the meshlet mesh - Splitup gpu_scene module into meshlet_mesh_manager, instance_manager, and resource_manager - resource_manager is still too complex and inefficient (extract and prepare are way too expensive). I plan on improving this in a future PR, but for now ResourceManager is mostly a 1:1 port of the leftover MeshletGpuScene bits. - Material draw passes have been renamed to the more accurate material shade pass, as well as some other misc renaming (in the future, these will be compute shaders even, and not actual draw calls) --- ## Migration Guide - TBD (ask me at the end of the release for meshlet changes as a whole) --------- Co-authored-by: vero --- crates/bevy_pbr/src/material.rs | 6 +- crates/bevy_pbr/src/meshlet/asset.rs | 9 +- .../src/meshlet/copy_material_depth.wgsl | 10 - .../bevy_pbr/src/meshlet/cull_clusters.wgsl | 73 +- .../src/meshlet/downsample_depth.wgsl | 49 +- .../src/meshlet/fill_cluster_buffers.wgsl | 6 +- crates/bevy_pbr/src/meshlet/from_mesh.rs | 10 +- crates/bevy_pbr/src/meshlet/gpu_scene.rs | 1050 ----------------- .../bevy_pbr/src/meshlet/instance_manager.rs | 261 ++++ ...repare.rs => material_pipeline_prepare.rs} | 23 +- ..._draw_nodes.rs => material_shade_nodes.rs} | 42 +- .../src/meshlet/meshlet_bindings.wgsl | 34 +- .../src/meshlet/meshlet_mesh_manager.rs | 132 +++ crates/bevy_pbr/src/meshlet/mod.rs | 137 ++- .../src/meshlet/persistent_buffer_impls.rs | 1 + crates/bevy_pbr/src/meshlet/pipelines.rs | 383 ++++-- .../src/meshlet/remap_1d_to_2d_dispatch.wgsl | 20 + .../src/meshlet/resolve_render_targets.wgsl | 39 + .../bevy_pbr/src/meshlet/resource_manager.rs | 809 +++++++++++++ ...=> visibility_buffer_hardware_raster.wgsl} | 68 +- .../meshlet/visibility_buffer_raster_node.rs | 302 +++-- .../meshlet/visibility_buffer_resolve.wgsl | 5 +- .../visibility_buffer_software_raster.wgsl | 196 +++ crates/bevy_pbr/src/prepass/mod.rs | 4 +- crates/bevy_render/src/render_resource/mod.rs | 5 +- examples/3d/meshlet.rs | 6 +- 26 files changed, 2247 insertions(+), 1433 deletions(-) delete mode 100644 crates/bevy_pbr/src/meshlet/copy_material_depth.wgsl delete mode 100644 crates/bevy_pbr/src/meshlet/gpu_scene.rs create mode 100644 crates/bevy_pbr/src/meshlet/instance_manager.rs rename crates/bevy_pbr/src/meshlet/{material_draw_prepare.rs => material_pipeline_prepare.rs} (95%) rename crates/bevy_pbr/src/meshlet/{material_draw_nodes.rs => material_shade_nodes.rs} (91%) create mode 100644 crates/bevy_pbr/src/meshlet/meshlet_mesh_manager.rs create mode 100644 crates/bevy_pbr/src/meshlet/remap_1d_to_2d_dispatch.wgsl create mode 100644 crates/bevy_pbr/src/meshlet/resolve_render_targets.wgsl create mode 100644 crates/bevy_pbr/src/meshlet/resource_manager.rs rename crates/bevy_pbr/src/meshlet/{visibility_buffer_raster.wgsl => visibility_buffer_hardware_raster.wgsl} (56%) create mode 100644 crates/bevy_pbr/src/meshlet/visibility_buffer_software_raster.wgsl diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index d5b601d6515cc6..05bb1d7acc5e29 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -1,7 +1,7 @@ #[cfg(feature = "meshlet")] use crate::meshlet::{ prepare_material_meshlet_meshes_main_opaque_pass, queue_material_meshlet_meshes, - MeshletGpuScene, + InstanceManager, }; use crate::*; use bevy_asset::{Asset, AssetId, AssetServer}; @@ -283,7 +283,7 @@ where Render, queue_material_meshlet_meshes:: .in_set(RenderSet::QueueMeshes) - .run_if(resource_exists::), + .run_if(resource_exists::), ); #[cfg(feature = "meshlet")] @@ -293,7 +293,7 @@ where .in_set(RenderSet::QueueMeshes) .after(prepare_assets::>) .before(queue_material_meshlet_meshes::) - .run_if(resource_exists::), + .run_if(resource_exists::), ); } diff --git a/crates/bevy_pbr/src/meshlet/asset.rs b/crates/bevy_pbr/src/meshlet/asset.rs index 5701e0f2884491..108cf981515c73 100644 --- a/crates/bevy_pbr/src/meshlet/asset.rs +++ b/crates/bevy_pbr/src/meshlet/asset.rs @@ -35,8 +35,6 @@ pub const MESHLET_MESH_ASSET_VERSION: u64 = 1; /// See also [`super::MaterialMeshletMeshBundle`] and [`super::MeshletPlugin`]. #[derive(Asset, TypePath, Clone)] pub struct MeshletMesh { - /// The total amount of triangles summed across all LOD 0 meshlets in the mesh. - pub(crate) worst_case_meshlet_triangles: u64, /// Raw vertex data bytes for the overall mesh. pub(crate) vertex_data: Arc<[u8]>, /// Indices into `vertex_data`. @@ -57,6 +55,8 @@ pub struct Meshlet { pub start_vertex_id: u32, /// The offset within the parent mesh's [`MeshletMesh::indices`] buffer where the indices for this meshlet begin. pub start_index_id: u32, + /// The amount of vertices in this meshlet. + pub vertex_count: u32, /// The amount of triangles in this meshlet. pub triangle_count: u32, } @@ -107,9 +107,6 @@ impl AssetSaver for MeshletMeshSaverLoader { .await?; // Compress and write asset data - writer - .write_all(&asset.worst_case_meshlet_triangles.to_le_bytes()) - .await?; let mut writer = FrameEncoder::new(AsyncWriteSyncAdapter(writer)); write_slice(&asset.vertex_data, &mut writer)?; write_slice(&asset.vertex_ids, &mut writer)?; @@ -146,7 +143,6 @@ impl AssetLoader for MeshletMeshSaverLoader { } // Load and decompress asset data - let worst_case_meshlet_triangles = async_read_u64(reader).await?; let reader = &mut FrameDecoder::new(AsyncReadSyncAdapter(reader)); let vertex_data = read_slice(reader)?; let vertex_ids = read_slice(reader)?; @@ -155,7 +151,6 @@ impl AssetLoader for MeshletMeshSaverLoader { let bounding_spheres = read_slice(reader)?; Ok(MeshletMesh { - worst_case_meshlet_triangles, vertex_data, vertex_ids, indices, diff --git a/crates/bevy_pbr/src/meshlet/copy_material_depth.wgsl b/crates/bevy_pbr/src/meshlet/copy_material_depth.wgsl deleted file mode 100644 index 177cbc35a34243..00000000000000 --- a/crates/bevy_pbr/src/meshlet/copy_material_depth.wgsl +++ /dev/null @@ -1,10 +0,0 @@ -#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput - -@group(0) @binding(0) var material_depth: texture_2d; - -/// This pass copies the R16Uint material depth texture to an actual Depth16Unorm depth texture. - -@fragment -fn copy_material_depth(in: FullscreenVertexOutput) -> @builtin(frag_depth) f32 { - return f32(textureLoad(material_depth, vec2(in.position.xy), 0).r) / 65535.0; -} diff --git a/crates/bevy_pbr/src/meshlet/cull_clusters.wgsl b/crates/bevy_pbr/src/meshlet/cull_clusters.wgsl index b73792aa5d3f36..fe5df60f120828 100644 --- a/crates/bevy_pbr/src/meshlet/cull_clusters.wgsl +++ b/crates/bevy_pbr/src/meshlet/cull_clusters.wgsl @@ -9,9 +9,10 @@ previous_view, should_cull_instance, cluster_is_second_pass_candidate, - meshlets, - draw_indirect_args, - draw_triangle_buffer, + meshlet_software_raster_indirect_args, + meshlet_hardware_raster_indirect_args, + meshlet_raster_clusters, + meshlet_raster_cluster_rightmost_slot, } #import bevy_render::maths::affine3_to_square @@ -25,10 +26,10 @@ fn cull_clusters( @builtin(workgroup_id) workgroup_id: vec3, @builtin(num_workgroups) num_workgroups: vec3, - @builtin(local_invocation_id) local_invocation_id: vec3, + @builtin(local_invocation_index) local_invocation_index: u32, ) { // Calculate the cluster ID for this thread - let cluster_id = local_invocation_id.x + 128u * dot(workgroup_id, vec3(num_workgroups.x * num_workgroups.x, num_workgroups.x, 1u)); + let cluster_id = local_invocation_index + 128u * dot(workgroup_id, vec3(num_workgroups.x * num_workgroups.x, num_workgroups.x, 1u)); if cluster_id >= arrayLength(&meshlet_cluster_meshlet_ids) { return; } #ifdef MESHLET_SECOND_CULLING_PASS @@ -47,8 +48,8 @@ fn cull_clusters( 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]; - var culling_bounding_sphere_center = world_from_local * vec4(bounding_spheres.self_culling.center, 1.0); - var culling_bounding_sphere_radius = world_scale * bounding_spheres.self_culling.radius; + let culling_bounding_sphere_center = world_from_local * vec4(bounding_spheres.self_culling.center, 1.0); + let culling_bounding_sphere_radius = world_scale * bounding_spheres.self_culling.radius; #ifdef MESHLET_FIRST_CULLING_PASS // Frustum culling @@ -59,17 +60,17 @@ fn cull_clusters( } } - // Calculate view-space LOD bounding sphere for the meshlet + // Calculate view-space LOD bounding sphere for the cluster let lod_bounding_sphere_center = world_from_local * vec4(bounding_spheres.self_lod.center, 1.0); let lod_bounding_sphere_radius = world_scale * bounding_spheres.self_lod.radius; let lod_bounding_sphere_center_view_space = (view.view_from_world * vec4(lod_bounding_sphere_center.xyz, 1.0)).xyz; - // Calculate view-space LOD bounding sphere for the meshlet's parent + // Calculate view-space LOD bounding sphere for the cluster's parent let parent_lod_bounding_sphere_center = world_from_local * vec4(bounding_spheres.parent_lod.center, 1.0); let parent_lod_bounding_sphere_radius = world_scale * bounding_spheres.parent_lod.radius; let parent_lod_bounding_sphere_center_view_space = (view.view_from_world * vec4(parent_lod_bounding_sphere_center.xyz, 1.0)).xyz; - // Check LOD cut (meshlet error imperceptible, and parent error not imperceptible) + // Check LOD cut (cluster error imperceptible, and parent error not imperceptible) let lod_is_ok = lod_error_is_imperceptible(lod_bounding_sphere_center_view_space, lod_bounding_sphere_radius); let parent_lod_is_ok = lod_error_is_imperceptible(parent_lod_bounding_sphere_center_view_space, parent_lod_bounding_sphere_radius); if !lod_is_ok || parent_lod_is_ok { return; } @@ -79,16 +80,20 @@ fn cull_clusters( #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]))); - culling_bounding_sphere_center = previous_world_from_local * vec4(bounding_spheres.self_culling.center, 1.0); - culling_bounding_sphere_radius = previous_world_from_local_scale * bounding_spheres.self_culling.radius; + let occlusion_culling_bounding_sphere_center = previous_world_from_local * vec4(bounding_spheres.self_culling.center, 1.0); + let occlusion_culling_bounding_sphere_radius = previous_world_from_local_scale * bounding_spheres.self_culling.radius; + let occlusion_culling_bounding_sphere_center_view_space = (previous_view.view_from_world * vec4(occlusion_culling_bounding_sphere_center.xyz, 1.0)).xyz; +#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; #endif - let culling_bounding_sphere_center_view_space = (view.view_from_world * vec4(culling_bounding_sphere_center.xyz, 1.0)).xyz; - let aabb = project_view_space_sphere_to_screen_space_aabb(culling_bounding_sphere_center_view_space, culling_bounding_sphere_radius); + 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)); - let width = (aabb.z - aabb.x) * depth_pyramid_size_mip_0.x; - let height = (aabb.w - aabb.y) * depth_pyramid_size_mip_0.y; - let depth_level = max(0, i32(ceil(log2(max(width, height))))); // TODO: Naga doesn't like this being a u32 + 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); @@ -102,11 +107,11 @@ fn cull_clusters( var cluster_visible: bool; if view.clip_from_view[3][3] == 1.0 { // Orthographic - let sphere_depth = view.clip_from_view[3][2] + (culling_bounding_sphere_center_view_space.z + culling_bounding_sphere_radius) * view.clip_from_view[2][2]; + 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] / (culling_bounding_sphere_center_view_space.z + culling_bounding_sphere_radius); + 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; } @@ -118,15 +123,29 @@ fn cull_clusters( } #endif - // Append a list of this cluster's triangles to draw if not culled - if cluster_visible { - let meshlet_triangle_count = meshlets[meshlet_id].triangle_count; - let buffer_start = atomicAdd(&draw_indirect_args.vertex_count, meshlet_triangle_count * 3u) / 3u; - let cluster_id_packed = cluster_id << 6u; - for (var triangle_id = 0u; triangle_id < meshlet_triangle_count; triangle_id++) { - draw_triangle_buffer[buffer_start + triangle_id] = cluster_id_packed | triangle_id; - } + // Cluster would be occluded if drawn, so don't setup a draw for it + if !cluster_visible { return; } + + // 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(32.0)); // TODO: Nanite does something different. Come up with my own heuristic. + + // TODO: Also check if needs depth clipping + var buffer_slot: u32; + if cluster_is_small { + // Append this cluster to the list for software rasterization + buffer_slot = atomicAdd(&meshlet_software_raster_indirect_args.x, 1u); + } else { + // Append this cluster to the list for hardware rasterization + buffer_slot = atomicAdd(&meshlet_hardware_raster_indirect_args.instance_count, 1u); + buffer_slot = meshlet_raster_cluster_rightmost_slot - buffer_slot; } + meshlet_raster_clusters[buffer_slot] = cluster_id; } // https://stackoverflow.com/questions/21648630/radius-of-projected-sphere-in-screen-space/21649403#21649403 diff --git a/crates/bevy_pbr/src/meshlet/downsample_depth.wgsl b/crates/bevy_pbr/src/meshlet/downsample_depth.wgsl index 04e8f3f56af081..80dd7d4baafd42 100644 --- a/crates/bevy_pbr/src/meshlet/downsample_depth.wgsl +++ b/crates/bevy_pbr/src/meshlet/downsample_depth.wgsl @@ -1,4 +1,8 @@ -@group(0) @binding(0) var mip_0: texture_depth_2d; +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT +@group(0) @binding(0) var mip_0: array; // Per pixel +#else +@group(0) @binding(0) var mip_0: array; // Per pixel +#endif @group(0) @binding(1) var mip_1: texture_storage_2d; @group(0) @binding(2) var mip_2: texture_storage_2d; @group(0) @binding(3) var mip_3: texture_storage_2d; @@ -12,11 +16,16 @@ @group(0) @binding(11) var mip_11: texture_storage_2d; @group(0) @binding(12) var mip_12: texture_storage_2d; @group(0) @binding(13) var samplr: sampler; -var max_mip_level: u32; +struct Constants { max_mip_level: u32, view_width: u32 } +var constants: Constants; /// Generates a hierarchical depth buffer. /// Based on FidelityFX SPD v2.1 https://github.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/blob/d7531ae47d8b36a5d4025663e731a47a38be882f/sdk/include/FidelityFX/gpu/spd/ffx_spd.h#L528 +// TODO: +// * Subgroup support +// * True single pass downsampling + var intermediate_memory: array, 16>; @compute @@ -70,7 +79,7 @@ fn downsample_mips_0_and_1(x: u32, y: u32, workgroup_id: vec2u, local_invocation v[3] = reduce_load_mip_0(tex); textureStore(mip_1, pix, vec4(v[3])); - if max_mip_level <= 1u { return; } + if constants.max_mip_level <= 1u { return; } for (var i = 0u; i < 4u; i++) { intermediate_memory[x][y] = v[i]; @@ -100,19 +109,19 @@ fn downsample_mips_0_and_1(x: u32, y: u32, workgroup_id: vec2u, local_invocation } fn downsample_mips_2_to_5(x: u32, y: u32, workgroup_id: vec2u, local_invocation_index: u32) { - if max_mip_level <= 2u { return; } + if constants.max_mip_level <= 2u { return; } workgroupBarrier(); downsample_mip_2(x, y, workgroup_id, local_invocation_index); - if max_mip_level <= 3u { return; } + if constants.max_mip_level <= 3u { return; } workgroupBarrier(); downsample_mip_3(x, y, workgroup_id, local_invocation_index); - if max_mip_level <= 4u { return; } + if constants.max_mip_level <= 4u { return; } workgroupBarrier(); downsample_mip_4(x, y, workgroup_id, local_invocation_index); - if max_mip_level <= 5u { return; } + if constants.max_mip_level <= 5u { return; } workgroupBarrier(); downsample_mip_5(workgroup_id, local_invocation_index); } @@ -191,7 +200,7 @@ fn downsample_mips_6_and_7(x: u32, y: u32) { v[3] = reduce_load_mip_6(tex); textureStore(mip_7, pix, vec4(v[3])); - if max_mip_level <= 7u { return; } + if constants.max_mip_level <= 7u { return; } let vr = reduce_4(v); textureStore(mip_8, vec2(x, y), vec4(vr)); @@ -199,19 +208,19 @@ fn downsample_mips_6_and_7(x: u32, y: u32) { } fn downsample_mips_8_to_11(x: u32, y: u32, local_invocation_index: u32) { - if max_mip_level <= 8u { return; } + if constants.max_mip_level <= 8u { return; } workgroupBarrier(); downsample_mip_8(x, y, local_invocation_index); - if max_mip_level <= 9u { return; } + if constants.max_mip_level <= 9u { return; } workgroupBarrier(); downsample_mip_9(x, y, local_invocation_index); - if max_mip_level <= 10u { return; } + if constants.max_mip_level <= 10u { return; } workgroupBarrier(); downsample_mip_10(x, y, local_invocation_index); - if max_mip_level <= 11u { return; } + if constants.max_mip_level <= 11u { return; } workgroupBarrier(); downsample_mip_11(local_invocation_index); } @@ -275,8 +284,11 @@ fn remap_for_wave_reduction(a: u32) -> vec2u { } fn reduce_load_mip_0(tex: vec2u) -> f32 { - let uv = (vec2f(tex) + 0.5) / vec2f(textureDimensions(mip_0)); - return reduce_4(textureGather(mip_0, samplr, uv)); + let a = load_mip_0(tex.x, tex.y); + let b = load_mip_0(tex.x + 1u, tex.y); + let c = load_mip_0(tex.x, tex.y + 1u); + let d = load_mip_0(tex.x + 1u, tex.y + 1u); + return reduce_4(vec4(a, b, c, d)); } fn reduce_load_mip_6(tex: vec2u) -> f32 { @@ -288,6 +300,15 @@ fn reduce_load_mip_6(tex: vec2u) -> f32 { )); } +fn load_mip_0(x: u32, y: u32) -> f32 { + let i = y * constants.view_width + x; +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT + return bitcast(u32(mip_0[i] >> 32u)); +#else + return bitcast(mip_0[i]); +#endif +} + fn reduce_4(v: vec4f) -> f32 { return min(min(v.x, v.y), min(v.z, v.w)); } diff --git a/crates/bevy_pbr/src/meshlet/fill_cluster_buffers.wgsl b/crates/bevy_pbr/src/meshlet/fill_cluster_buffers.wgsl index f228ba050875f6..04af6c4ad70919 100644 --- a/crates/bevy_pbr/src/meshlet/fill_cluster_buffers.wgsl +++ b/crates/bevy_pbr/src/meshlet/fill_cluster_buffers.wgsl @@ -13,11 +13,11 @@ fn fill_cluster_buffers( @builtin(workgroup_id) workgroup_id: vec3, @builtin(num_workgroups) num_workgroups: vec3, - @builtin(local_invocation_id) local_invocation_id: vec3 + @builtin(local_invocation_index) local_invocation_index: u32, ) { // Calculate the cluster ID for this thread - let cluster_id = local_invocation_id.x + 128u * dot(workgroup_id, vec3(num_workgroups.x * num_workgroups.x, num_workgroups.x, 1u)); - if cluster_id >= cluster_count { return; } + let cluster_id = local_invocation_index + 128u * dot(workgroup_id, vec3(num_workgroups.x * num_workgroups.x, num_workgroups.x, 1u)); + if cluster_id >= cluster_count { return; } // TODO: Could be an arrayLength? // Binary search to find the instance this cluster belongs to var left = 0u; diff --git a/crates/bevy_pbr/src/meshlet/from_mesh.rs b/crates/bevy_pbr/src/meshlet/from_mesh.rs index f62e00b4340bc0..047b1a492e1eee 100644 --- a/crates/bevy_pbr/src/meshlet/from_mesh.rs +++ b/crates/bevy_pbr/src/meshlet/from_mesh.rs @@ -49,11 +49,6 @@ impl MeshletMesh { }, }) .collect::>(); - let worst_case_meshlet_triangles = meshlets - .meshlets - .iter() - .map(|m| m.triangle_count as u64) - .sum(); let mesh_scale = simplify_scale(&vertices); // Build further LODs @@ -87,7 +82,7 @@ impl MeshletMesh { // Add the maximum child error to the parent error to make parent error cumulative from LOD 0 // (we're currently building the parent from its children) - group_error += group_meshlets.iter().fold(group_error, |acc, meshlet_id| { + group_error += group_meshlets.iter().fold(0.0f32, |acc, meshlet_id| { acc.max(bounding_spheres[*meshlet_id].self_lod.radius) }); @@ -140,12 +135,12 @@ impl MeshletMesh { .map(|m| Meshlet { start_vertex_id: m.vertex_offset, start_index_id: m.triangle_offset, + vertex_count: m.vertex_count, triangle_count: m.triangle_count, }) .collect(); Ok(Self { - worst_case_meshlet_triangles, vertex_data: vertex_buffer.into(), vertex_ids: meshlets.vertices.into(), indices: meshlets.triangles.into(), @@ -294,6 +289,7 @@ fn simplify_meshlet_groups( let target_error = target_error_relative * mesh_scale; // Simplify the group to ~50% triangle count + // TODO: Simplify using vertex attributes let mut error = 0.0; let simplified_group_indices = simplify( &group_indices, diff --git a/crates/bevy_pbr/src/meshlet/gpu_scene.rs b/crates/bevy_pbr/src/meshlet/gpu_scene.rs deleted file mode 100644 index 1d4bf7ffe67011..00000000000000 --- a/crates/bevy_pbr/src/meshlet/gpu_scene.rs +++ /dev/null @@ -1,1050 +0,0 @@ -use super::{ - asset::{Meshlet, MeshletBoundingSpheres, MeshletMesh}, - persistent_buffer::PersistentGpuBuffer, -}; -use crate::{ - Material, MeshFlags, MeshTransforms, MeshUniform, NotShadowCaster, NotShadowReceiver, - PreviousGlobalTransform, RenderMaterialInstances, ShadowView, -}; -use bevy_asset::{AssetEvent, AssetId, AssetServer, Assets, Handle, UntypedAssetId}; -use bevy_core_pipeline::{ - core_3d::Camera3d, - prepass::{PreviousViewData, PreviousViewUniforms}, -}; -use bevy_ecs::{ - component::Component, - entity::{Entity, EntityHashMap}, - event::EventReader, - query::{AnyOf, Has}, - system::{Commands, Local, Query, Res, ResMut, Resource, SystemState}, - world::{FromWorld, World}, -}; -use bevy_math::{UVec2, Vec4Swizzles}; -use bevy_render::{ - render_resource::{binding_types::*, *}, - renderer::{RenderDevice, RenderQueue}, - texture::{CachedTexture, TextureCache}, - view::{ExtractedView, RenderLayers, ViewDepthTexture, ViewUniform, ViewUniforms}, - MainWorld, -}; -use bevy_transform::components::GlobalTransform; -use bevy_utils::{default, HashMap, HashSet}; -use encase::internal::WriteInto; -use std::{ - array, iter, - mem::size_of, - ops::{DerefMut, Range}, - sync::{atomic::AtomicBool, Arc}, -}; - -/// Create and queue for uploading to the GPU [`MeshUniform`] components for -/// [`MeshletMesh`] entities, as well as queuing uploads for any new meshlet mesh -/// assets that have not already been uploaded to the GPU. -pub fn extract_meshlet_meshes( - mut gpu_scene: ResMut, - // TODO: Replace main_world and system_state when Extract>> is possible - mut main_world: ResMut, - mut system_state: Local< - Option< - SystemState<( - Query<( - Entity, - &Handle, - &GlobalTransform, - Option<&PreviousGlobalTransform>, - Option<&RenderLayers>, - Has, - Has, - )>, - Res, - ResMut>, - EventReader>, - )>, - >, - >, -) { - if system_state.is_none() { - *system_state = Some(SystemState::new(&mut main_world)); - } - let system_state = system_state.as_mut().unwrap(); - - let (instances_query, asset_server, mut assets, mut asset_events) = - system_state.get_mut(&mut main_world); - - // Reset all temporary data for MeshletGpuScene - gpu_scene.reset(); - - // Free GPU buffer space for any modified or dropped MeshletMesh assets - for asset_event in asset_events.read() { - if let AssetEvent::Unused { id } | AssetEvent::Modified { id } = asset_event { - if let Some(( - [vertex_data_slice, vertex_ids_slice, indices_slice, meshlets_slice, meshlet_bounding_spheres_slice], - _, - )) = gpu_scene.meshlet_mesh_slices.remove(id) - { - gpu_scene.vertex_data.mark_slice_unused(vertex_data_slice); - gpu_scene.vertex_ids.mark_slice_unused(vertex_ids_slice); - gpu_scene.indices.mark_slice_unused(indices_slice); - gpu_scene.meshlets.mark_slice_unused(meshlets_slice); - gpu_scene - .meshlet_bounding_spheres - .mark_slice_unused(meshlet_bounding_spheres_slice); - } - } - } - - for ( - instance, - handle, - transform, - previous_transform, - render_layers, - not_shadow_receiver, - not_shadow_caster, - ) in &instances_query - { - // Skip instances with an unloaded MeshletMesh asset - if asset_server.is_managed(handle.id()) - && !asset_server.is_loaded_with_dependencies(handle.id()) - { - continue; - } - - // Upload the instance's MeshletMesh asset data, if not done already, along with other per-frame per-instance data. - gpu_scene.queue_meshlet_mesh_upload( - instance, - render_layers.cloned().unwrap_or(default()), - not_shadow_caster, - handle, - &mut assets, - ); - - // Build a MeshUniform for each instance - let transform = transform.affine(); - let previous_transform = previous_transform.map(|t| t.0).unwrap_or(transform); - let mut flags = if not_shadow_receiver { - MeshFlags::empty() - } else { - MeshFlags::SHADOW_RECEIVER - }; - if transform.matrix3.determinant().is_sign_positive() { - flags |= MeshFlags::SIGN_DETERMINANT_MODEL_3X3; - } - let transforms = MeshTransforms { - world_from_local: (&transform).into(), - previous_world_from_local: (&previous_transform).into(), - flags: flags.bits(), - }; - gpu_scene - .instance_uniforms - .get_mut() - .push(MeshUniform::new(&transforms, 0, None)); - } -} - -/// Upload all newly queued [`MeshletMesh`] asset data from [`extract_meshlet_meshes`] to the GPU. -pub fn perform_pending_meshlet_mesh_writes( - mut gpu_scene: ResMut, - render_queue: Res, - render_device: Res, -) { - gpu_scene - .vertex_data - .perform_writes(&render_queue, &render_device); - gpu_scene - .vertex_ids - .perform_writes(&render_queue, &render_device); - gpu_scene - .indices - .perform_writes(&render_queue, &render_device); - gpu_scene - .meshlets - .perform_writes(&render_queue, &render_device); - gpu_scene - .meshlet_bounding_spheres - .perform_writes(&render_queue, &render_device); -} - -/// For each entity in the scene, record what material ID (for use with depth testing during the meshlet mesh material draw nodes) -/// 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( - mut gpu_scene: ResMut, - render_material_instances: Res>, -) { - // TODO: Ideally we could parallelize this system, both between different materials, and the loop over instances - let gpu_scene = gpu_scene.deref_mut(); - - for (i, (instance, _, _)) in gpu_scene.instances.iter().enumerate() { - if let Some(material_asset_id) = render_material_instances.get(instance) { - let material_asset_id = material_asset_id.untyped(); - if let Some(material_id) = gpu_scene.material_id_lookup.get(&material_asset_id) { - gpu_scene.material_ids_present_in_scene.insert(*material_id); - gpu_scene.instance_material_ids.get_mut()[i] = *material_id; - } - } - } -} - -// 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 fn prepare_meshlet_per_frame_resources( - mut gpu_scene: ResMut, - views: Query<( - Entity, - &ExtractedView, - Option<&RenderLayers>, - AnyOf<(&Camera3d, &ShadowView)>, - )>, - mut texture_cache: ResMut, - render_queue: Res, - render_device: Res, - mut commands: Commands, -) { - if gpu_scene.scene_meshlet_count == 0 { - return; - } - - let gpu_scene = gpu_scene.as_mut(); - - gpu_scene - .instance_uniforms - .write_buffer(&render_device, &render_queue); - upload_storage_buffer( - &mut gpu_scene.instance_material_ids, - &render_device, - &render_queue, - ); - upload_storage_buffer( - &mut gpu_scene.instance_meshlet_counts_prefix_sum, - &render_device, - &render_queue, - ); - upload_storage_buffer( - &mut gpu_scene.instance_meshlet_slice_starts, - &render_device, - &render_queue, - ); - - // Early submission for GPU data uploads to start while the render graph records commands - render_queue.submit([]); - - let needed_buffer_size = 4 * gpu_scene.scene_meshlet_count as u64; - match &mut gpu_scene.cluster_instance_ids { - 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 gpu_scene.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"), - size: needed_buffer_size, - usage: BufferUsages::STORAGE, - mapped_at_creation: false, - }); - *slot = Some(buffer.clone()); - buffer - } - }; - - let needed_buffer_size = 4 * gpu_scene.scene_triangle_count; - let visibility_buffer_draw_triangle_buffer = - match &mut gpu_scene.visibility_buffer_draw_triangle_buffer { - Some(buffer) if buffer.size() >= needed_buffer_size => buffer.clone(), - slot => { - let buffer = render_device.create_buffer(&BufferDescriptor { - label: Some("meshlet_visibility_buffer_draw_triangle_buffer"), - size: needed_buffer_size, - usage: BufferUsages::STORAGE, - mapped_at_creation: false, - }); - *slot = Some(buffer.clone()); - buffer - } - }; - - let needed_buffer_size = - gpu_scene.scene_meshlet_count.div_ceil(u32::BITS) as u64 * size_of::() as u64; - for (view_entity, view, render_layers, (_, shadow_view)) in &views { - let instance_visibility = gpu_scene - .view_instance_visibility - .entry(view_entity) - .or_insert_with(|| { - let mut buffer = StorageBuffer::default(); - buffer.set_label(Some("meshlet_view_instance_visibility")); - buffer - }); - for (instance_index, (_, layers, not_shadow_caster)) in - gpu_scene.instances.iter().enumerate() - { - // If either the layers don't match the view's layers or this is a shadow view - // and the instance is not a shadow caster, hide the instance for this view - if !render_layers.unwrap_or(&default()).intersects(layers) - || (shadow_view.is_some() && *not_shadow_caster) - { - let vec = instance_visibility.get_mut(); - let index = instance_index / 32; - let bit = instance_index - index * 32; - if vec.len() <= index { - vec.extend(iter::repeat(0).take(index - vec.len() + 1)); - } - vec[index] |= 1 << bit; - } - } - upload_storage_buffer(instance_visibility, &render_device, &render_queue); - let instance_visibility = instance_visibility.buffer().unwrap().clone(); - - let second_pass_candidates_buffer = match &mut gpu_scene.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 - } - }; - - let visibility_buffer = TextureDescriptor { - label: Some("meshlet_visibility_buffer"), - size: Extent3d { - width: view.viewport.z, - height: view.viewport.w, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: TextureDimension::D2, - format: TextureFormat::R32Uint, - usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }; - - let visibility_buffer_draw_indirect_args_first = - render_device.create_buffer_with_data(&BufferInitDescriptor { - label: Some("meshlet_visibility_buffer_draw_indirect_args_first"), - contents: DrawIndirectArgs { - vertex_count: 0, - instance_count: 1, - first_vertex: 0, - first_instance: 0, - } - .as_bytes(), - usage: BufferUsages::STORAGE | BufferUsages::INDIRECT, - }); - let visibility_buffer_draw_indirect_args_second = - render_device.create_buffer_with_data(&BufferInitDescriptor { - label: Some("meshlet_visibility_buffer_draw_indirect_args_second"), - contents: DrawIndirectArgs { - vertex_count: 0, - instance_count: 1, - first_vertex: 0, - first_instance: 0, - } - .as_bytes(), - usage: BufferUsages::STORAGE | BufferUsages::INDIRECT, - }); - - let depth_pyramid_size = Extent3d { - width: view.viewport.z.div_ceil(2), - height: view.viewport.w.div_ceil(2), - depth_or_array_layers: 1, - }; - let depth_pyramid_mip_count = depth_pyramid_size.max_mips(TextureDimension::D2); - let depth_pyramid = texture_cache.get( - &render_device, - TextureDescriptor { - label: Some("meshlet_depth_pyramid"), - size: depth_pyramid_size, - mip_level_count: depth_pyramid_mip_count, - sample_count: 1, - dimension: TextureDimension::D2, - format: TextureFormat::R32Float, - usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }, - ); - let depth_pyramid_mips = array::from_fn(|i| { - if (i as u32) < depth_pyramid_mip_count { - depth_pyramid.texture.create_view(&TextureViewDescriptor { - label: Some("meshlet_depth_pyramid_texture_view"), - format: Some(TextureFormat::R32Float), - dimension: Some(TextureViewDimension::D2), - aspect: TextureAspect::All, - base_mip_level: i as u32, - mip_level_count: Some(1), - base_array_layer: 0, - array_layer_count: Some(1), - }) - } else { - gpu_scene.depth_pyramid_dummy_texture.clone() - } - }); - let depth_pyramid_all_mips = depth_pyramid.default_view.clone(); - - let previous_depth_pyramid = match gpu_scene.previous_depth_pyramids.get(&view_entity) { - Some(texture_view) => texture_view.clone(), - None => depth_pyramid_all_mips.clone(), - }; - gpu_scene - .previous_depth_pyramids - .insert(view_entity, depth_pyramid_all_mips.clone()); - - let material_depth_color = TextureDescriptor { - label: Some("meshlet_material_depth_color"), - size: Extent3d { - width: view.viewport.z, - height: view.viewport.w, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: TextureDimension::D2, - format: TextureFormat::R16Uint, - usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }; - - let material_depth = TextureDescriptor { - label: Some("meshlet_material_depth"), - size: Extent3d { - width: view.viewport.z, - height: view.viewport.w, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: TextureDimension::D2, - format: TextureFormat::Depth16Unorm, - usage: TextureUsages::RENDER_ATTACHMENT, - view_formats: &[], - }; - - let not_shadow_view = shadow_view.is_none(); - commands.entity(view_entity).insert(MeshletViewResources { - scene_meshlet_count: gpu_scene.scene_meshlet_count, - second_pass_candidates_buffer, - instance_visibility, - visibility_buffer: not_shadow_view - .then(|| texture_cache.get(&render_device, visibility_buffer)), - visibility_buffer_draw_indirect_args_first, - visibility_buffer_draw_indirect_args_second, - visibility_buffer_draw_triangle_buffer: visibility_buffer_draw_triangle_buffer.clone(), - depth_pyramid_all_mips, - depth_pyramid_mips, - depth_pyramid_mip_count, - previous_depth_pyramid, - material_depth_color: not_shadow_view - .then(|| texture_cache.get(&render_device, material_depth_color)), - material_depth: not_shadow_view - .then(|| texture_cache.get(&render_device, material_depth)), - view_size: view.viewport.zw(), - }); - } -} - -pub fn prepare_meshlet_view_bind_groups( - gpu_scene: Res, - views: Query<( - Entity, - &MeshletViewResources, - AnyOf<(&ViewDepthTexture, &ShadowView)>, - )>, - view_uniforms: Res, - previous_view_uniforms: Res, - render_device: Res, - mut commands: Commands, -) { - let ( - Some(cluster_instance_ids), - Some(cluster_meshlet_ids), - Some(view_uniforms), - Some(previous_view_uniforms), - ) = ( - gpu_scene.cluster_instance_ids.as_ref(), - gpu_scene.cluster_meshlet_ids.as_ref(), - view_uniforms.uniforms.binding(), - previous_view_uniforms.uniforms.binding(), - ) - else { - return; - }; - - let first_node = Arc::new(AtomicBool::new(true)); - - // TODO: Some of these bind groups can be reused across multiple views - for (view_entity, view_resources, view_depth) in &views { - let entries = BindGroupEntries::sequential(( - gpu_scene - .instance_meshlet_counts_prefix_sum - .binding() - .unwrap(), - gpu_scene.instance_meshlet_slice_starts.binding().unwrap(), - cluster_instance_ids.as_entire_binding(), - cluster_meshlet_ids.as_entire_binding(), - )); - let fill_cluster_buffers = render_device.create_bind_group( - "meshlet_fill_cluster_buffers", - &gpu_scene.fill_cluster_buffers_bind_group_layout, - &entries, - ); - - let entries = BindGroupEntries::sequential(( - cluster_meshlet_ids.as_entire_binding(), - gpu_scene.meshlet_bounding_spheres.binding(), - cluster_instance_ids.as_entire_binding(), - gpu_scene.instance_uniforms.binding().unwrap(), - view_resources.instance_visibility.as_entire_binding(), - view_resources - .second_pass_candidates_buffer - .as_entire_binding(), - gpu_scene.meshlets.binding(), - view_resources - .visibility_buffer_draw_indirect_args_first - .as_entire_binding(), - view_resources - .visibility_buffer_draw_triangle_buffer - .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", - &gpu_scene.culling_bind_group_layout, - &entries, - ); - - let entries = BindGroupEntries::sequential(( - cluster_meshlet_ids.as_entire_binding(), - gpu_scene.meshlet_bounding_spheres.binding(), - cluster_instance_ids.as_entire_binding(), - gpu_scene.instance_uniforms.binding().unwrap(), - view_resources.instance_visibility.as_entire_binding(), - view_resources - .second_pass_candidates_buffer - .as_entire_binding(), - gpu_scene.meshlets.binding(), - view_resources - .visibility_buffer_draw_indirect_args_second - .as_entire_binding(), - view_resources - .visibility_buffer_draw_triangle_buffer - .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", - &gpu_scene.culling_bind_group_layout, - &entries, - ); - - let view_depth_texture = match view_depth { - (Some(view_depth), None) => view_depth.view(), - (None, Some(shadow_view)) => &shadow_view.depth_attachment.view, - _ => unreachable!(), - }; - let downsample_depth = render_device.create_bind_group( - "meshlet_downsample_depth_bind_group", - &gpu_scene.downsample_depth_bind_group_layout, - &BindGroupEntries::sequential(( - view_depth_texture, - &view_resources.depth_pyramid_mips[0], - &view_resources.depth_pyramid_mips[1], - &view_resources.depth_pyramid_mips[2], - &view_resources.depth_pyramid_mips[3], - &view_resources.depth_pyramid_mips[4], - &view_resources.depth_pyramid_mips[5], - &view_resources.depth_pyramid_mips[6], - &view_resources.depth_pyramid_mips[7], - &view_resources.depth_pyramid_mips[8], - &view_resources.depth_pyramid_mips[9], - &view_resources.depth_pyramid_mips[10], - &view_resources.depth_pyramid_mips[11], - &gpu_scene.depth_pyramid_sampler, - )), - ); - - let entries = BindGroupEntries::sequential(( - cluster_meshlet_ids.as_entire_binding(), - gpu_scene.meshlets.binding(), - gpu_scene.indices.binding(), - gpu_scene.vertex_ids.binding(), - gpu_scene.vertex_data.binding(), - cluster_instance_ids.as_entire_binding(), - gpu_scene.instance_uniforms.binding().unwrap(), - gpu_scene.instance_material_ids.binding().unwrap(), - view_resources - .visibility_buffer_draw_triangle_buffer - .as_entire_binding(), - view_uniforms.clone(), - )); - let visibility_buffer_raster = render_device.create_bind_group( - "meshlet_visibility_raster_buffer_bind_group", - &gpu_scene.visibility_buffer_raster_bind_group_layout, - &entries, - ); - - let copy_material_depth = - view_resources - .material_depth_color - .as_ref() - .map(|material_depth_color| { - render_device.create_bind_group( - "meshlet_copy_material_depth_bind_group", - &gpu_scene.copy_material_depth_bind_group_layout, - &[BindGroupEntry { - binding: 0, - resource: BindingResource::TextureView( - &material_depth_color.default_view, - ), - }], - ) - }); - - let material_draw = view_resources - .visibility_buffer - .as_ref() - .map(|visibility_buffer| { - let entries = BindGroupEntries::sequential(( - &visibility_buffer.default_view, - cluster_meshlet_ids.as_entire_binding(), - gpu_scene.meshlets.binding(), - gpu_scene.indices.binding(), - gpu_scene.vertex_ids.binding(), - gpu_scene.vertex_data.binding(), - cluster_instance_ids.as_entire_binding(), - gpu_scene.instance_uniforms.binding().unwrap(), - )); - render_device.create_bind_group( - "meshlet_mesh_material_draw_bind_group", - &gpu_scene.material_draw_bind_group_layout, - &entries, - ) - }); - - commands.entity(view_entity).insert(MeshletViewBindGroups { - first_node: Arc::clone(&first_node), - fill_cluster_buffers, - culling_first, - culling_second, - downsample_depth, - visibility_buffer_raster, - copy_material_depth, - material_draw, - }); - } -} - -/// A resource that manages GPU data for rendering [`MeshletMesh`]'s. -#[derive(Resource)] -pub struct MeshletGpuScene { - vertex_data: PersistentGpuBuffer>, - vertex_ids: PersistentGpuBuffer>, - indices: PersistentGpuBuffer>, - meshlets: PersistentGpuBuffer>, - meshlet_bounding_spheres: PersistentGpuBuffer>, - meshlet_mesh_slices: HashMap, ([Range; 5], u64)>, - - scene_meshlet_count: u32, - scene_triangle_count: u64, - next_material_id: u32, - material_id_lookup: HashMap, - material_ids_present_in_scene: HashSet, - /// Per-instance [`Entity`], [`RenderLayers`], and [`NotShadowCaster`] - instances: Vec<(Entity, RenderLayers, bool)>, - /// Per-instance transforms, model matrices, and render flags - instance_uniforms: StorageBuffer>, - /// Per-view per-instance visibility bit. Used for [`RenderLayers`] and [`NotShadowCaster`] support. - view_instance_visibility: EntityHashMap>>, - instance_material_ids: StorageBuffer>, - instance_meshlet_counts_prefix_sum: StorageBuffer>, - instance_meshlet_slice_starts: StorageBuffer>, - cluster_instance_ids: Option, - cluster_meshlet_ids: Option, - second_pass_candidates_buffer: Option, - previous_depth_pyramids: EntityHashMap, - visibility_buffer_draw_triangle_buffer: Option, - - fill_cluster_buffers_bind_group_layout: BindGroupLayout, - culling_bind_group_layout: BindGroupLayout, - visibility_buffer_raster_bind_group_layout: BindGroupLayout, - downsample_depth_bind_group_layout: BindGroupLayout, - copy_material_depth_bind_group_layout: BindGroupLayout, - material_draw_bind_group_layout: BindGroupLayout, - depth_pyramid_sampler: Sampler, - depth_pyramid_dummy_texture: TextureView, -} - -impl FromWorld for MeshletGpuScene { - fn from_world(world: &mut World) -> Self { - let render_device = world.resource::(); - - Self { - vertex_data: PersistentGpuBuffer::new("meshlet_vertex_data", render_device), - vertex_ids: PersistentGpuBuffer::new("meshlet_vertex_ids", render_device), - indices: PersistentGpuBuffer::new("meshlet_indices", render_device), - meshlets: PersistentGpuBuffer::new("meshlets", render_device), - meshlet_bounding_spheres: PersistentGpuBuffer::new( - "meshlet_bounding_spheres", - render_device, - ), - meshlet_mesh_slices: HashMap::new(), - - scene_meshlet_count: 0, - scene_triangle_count: 0, - next_material_id: 0, - material_id_lookup: HashMap::new(), - material_ids_present_in_scene: HashSet::new(), - instances: Vec::new(), - instance_uniforms: { - let mut buffer = StorageBuffer::default(); - buffer.set_label(Some("meshlet_instance_uniforms")); - buffer - }, - view_instance_visibility: EntityHashMap::default(), - instance_material_ids: { - let mut buffer = StorageBuffer::default(); - buffer.set_label(Some("meshlet_instance_material_ids")); - buffer - }, - instance_meshlet_counts_prefix_sum: { - let mut buffer = StorageBuffer::default(); - buffer.set_label(Some("meshlet_instance_meshlet_counts_prefix_sum")); - buffer - }, - instance_meshlet_slice_starts: { - let mut buffer = StorageBuffer::default(); - buffer.set_label(Some("meshlet_instance_meshlet_slice_starts")); - buffer - }, - cluster_instance_ids: None, - cluster_meshlet_ids: None, - second_pass_candidates_buffer: None, - previous_depth_pyramids: EntityHashMap::default(), - visibility_buffer_draw_triangle_buffer: None, - - // 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), - ), - ), - ), - culling_bind_group_layout: render_device.create_bind_group_layout( - "meshlet_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_sized(false, None), - storage_buffer_read_only_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), - ), - ), - ), - downsample_depth_bind_group_layout: render_device.create_bind_group_layout( - "meshlet_downsample_depth_bind_group_layout", - &BindGroupLayoutEntries::sequential(ShaderStages::COMPUTE, { - let write_only_r32float = || { - texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly) - }; - ( - texture_depth_2d(), - write_only_r32float(), - write_only_r32float(), - write_only_r32float(), - write_only_r32float(), - write_only_r32float(), - texture_storage_2d( - TextureFormat::R32Float, - StorageTextureAccess::ReadWrite, - ), - write_only_r32float(), - write_only_r32float(), - write_only_r32float(), - write_only_r32float(), - write_only_r32float(), - write_only_r32float(), - sampler(SamplerBindingType::NonFiltering), - ) - }), - ), - visibility_buffer_raster_bind_group_layout: render_device.create_bind_group_layout( - "meshlet_visibility_buffer_raster_bind_group_layout", - &BindGroupLayoutEntries::sequential( - ShaderStages::VERTEX, - ( - 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_read_only_sized(false, None), - storage_buffer_read_only_sized(false, None), - storage_buffer_read_only_sized(false, None), - uniform_buffer::(true), - ), - ), - ), - copy_material_depth_bind_group_layout: render_device.create_bind_group_layout( - "meshlet_copy_material_depth_bind_group_layout", - &BindGroupLayoutEntries::single( - ShaderStages::FRAGMENT, - texture_2d(TextureSampleType::Uint), - ), - ), - material_draw_bind_group_layout: render_device.create_bind_group_layout( - "meshlet_mesh_material_draw_bind_group_layout", - &BindGroupLayoutEntries::sequential( - ShaderStages::FRAGMENT, - ( - texture_2d(TextureSampleType::Uint), - 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_read_only_sized(false, None), - ), - ), - ), - depth_pyramid_sampler: render_device.create_sampler(&SamplerDescriptor { - label: Some("meshlet_depth_pyramid_sampler"), - ..default() - }), - depth_pyramid_dummy_texture: render_device - .create_texture(&TextureDescriptor { - label: Some("meshlet_depth_pyramid_dummy_texture"), - size: Extent3d { - width: 1, - height: 1, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: TextureDimension::D2, - format: TextureFormat::R32Float, - usage: TextureUsages::STORAGE_BINDING, - view_formats: &[], - }) - .create_view(&TextureViewDescriptor { - label: Some("meshlet_depth_pyramid_dummy_texture_view"), - format: Some(TextureFormat::R32Float), - dimension: Some(TextureViewDimension::D2), - aspect: TextureAspect::All, - base_mip_level: 0, - mip_level_count: Some(1), - base_array_layer: 0, - array_layer_count: Some(1), - }), - } - } -} - -impl MeshletGpuScene { - /// Clear per-frame CPU->GPU upload buffers and reset all per-frame data. - fn reset(&mut self) { - // TODO: Shrink capacity if saturation is low - self.scene_meshlet_count = 0; - self.scene_triangle_count = 0; - self.next_material_id = 0; - self.material_id_lookup.clear(); - self.material_ids_present_in_scene.clear(); - self.instances.clear(); - self.view_instance_visibility - .values_mut() - .for_each(|b| b.get_mut().clear()); - self.instance_uniforms.get_mut().clear(); - self.instance_material_ids.get_mut().clear(); - self.instance_meshlet_counts_prefix_sum.get_mut().clear(); - self.instance_meshlet_slice_starts.get_mut().clear(); - // TODO: Remove unused entries for view_instance_visibility and previous_depth_pyramids - } - - fn queue_meshlet_mesh_upload( - &mut self, - instance: Entity, - render_layers: RenderLayers, - not_shadow_caster: bool, - handle: &Handle, - assets: &mut Assets, - ) { - 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 MeshletGpuScene", - ); - - let vertex_data_slice = self - .vertex_data - .queue_write(Arc::clone(&meshlet_mesh.vertex_data), ()); - let vertex_ids_slice = self.vertex_ids.queue_write( - Arc::clone(&meshlet_mesh.vertex_ids), - vertex_data_slice.start, - ); - let indices_slice = self - .indices - .queue_write(Arc::clone(&meshlet_mesh.indices), ()); - let meshlets_slice = self.meshlets.queue_write( - Arc::clone(&meshlet_mesh.meshlets), - (vertex_ids_slice.start, indices_slice.start), - ); - let meshlet_bounding_spheres_slice = self - .meshlet_bounding_spheres - .queue_write(Arc::clone(&meshlet_mesh.bounding_spheres), ()); - - ( - [ - vertex_data_slice, - vertex_ids_slice, - indices_slice, - meshlets_slice, - meshlet_bounding_spheres_slice, - ], - meshlet_mesh.worst_case_meshlet_triangles, - ) - }; - - // If the MeshletMesh asset has not been uploaded to the GPU yet, queue it for uploading - let ([_, _, _, meshlets_slice, _], triangle_count) = self - .meshlet_mesh_slices - .entry(handle.id()) - .or_insert_with_key(queue_meshlet_mesh) - .clone(); - - let meshlets_slice = (meshlets_slice.start as u32 / size_of::() as u32) - ..(meshlets_slice.end as u32 / size_of::() as u32); - - // Append instance data for this frame - self.instances - .push((instance, render_layers, not_shadow_caster)); - self.instance_material_ids.get_mut().push(0); - self.instance_meshlet_counts_prefix_sum - .get_mut() - .push(self.scene_meshlet_count); - self.instance_meshlet_slice_starts - .get_mut() - .push(meshlets_slice.start); - - self.scene_meshlet_count += meshlets_slice.end - meshlets_slice.start; - self.scene_triangle_count += triangle_count; - } - - /// Get the depth value for use with the material depth texture for a given [`Material`] asset. - pub fn get_material_id(&mut self, material_id: UntypedAssetId) -> u32 { - *self - .material_id_lookup - .entry(material_id) - .or_insert_with(|| { - self.next_material_id += 1; - self.next_material_id - }) - } - - pub fn material_present_in_scene(&self, material_id: &u32) -> bool { - self.material_ids_present_in_scene.contains(material_id) - } - - pub fn fill_cluster_buffers_bind_group_layout(&self) -> BindGroupLayout { - self.fill_cluster_buffers_bind_group_layout.clone() - } - - pub fn culling_bind_group_layout(&self) -> BindGroupLayout { - self.culling_bind_group_layout.clone() - } - - pub fn downsample_depth_bind_group_layout(&self) -> BindGroupLayout { - self.downsample_depth_bind_group_layout.clone() - } - - pub fn visibility_buffer_raster_bind_group_layout(&self) -> BindGroupLayout { - self.visibility_buffer_raster_bind_group_layout.clone() - } - - pub fn copy_material_depth_bind_group_layout(&self) -> BindGroupLayout { - self.copy_material_depth_bind_group_layout.clone() - } - - pub fn material_draw_bind_group_layout(&self) -> BindGroupLayout { - self.material_draw_bind_group_layout.clone() - } -} - -#[derive(Component)] -pub struct MeshletViewResources { - pub scene_meshlet_count: u32, - pub second_pass_candidates_buffer: Buffer, - instance_visibility: Buffer, - pub visibility_buffer: Option, - pub visibility_buffer_draw_indirect_args_first: Buffer, - pub visibility_buffer_draw_indirect_args_second: Buffer, - visibility_buffer_draw_triangle_buffer: Buffer, - depth_pyramid_all_mips: TextureView, - depth_pyramid_mips: [TextureView; 12], - pub depth_pyramid_mip_count: u32, - previous_depth_pyramid: TextureView, - pub material_depth_color: Option, - pub material_depth: Option, - pub view_size: UVec2, -} - -#[derive(Component)] -pub struct MeshletViewBindGroups { - pub first_node: Arc, - pub fill_cluster_buffers: BindGroup, - pub culling_first: BindGroup, - pub culling_second: BindGroup, - pub downsample_depth: BindGroup, - pub visibility_buffer_raster: BindGroup, - pub copy_material_depth: Option, - pub material_draw: Option, -} diff --git a/crates/bevy_pbr/src/meshlet/instance_manager.rs b/crates/bevy_pbr/src/meshlet/instance_manager.rs new file mode 100644 index 00000000000000..0f370f22004592 --- /dev/null +++ b/crates/bevy_pbr/src/meshlet/instance_manager.rs @@ -0,0 +1,261 @@ +use super::{meshlet_mesh_manager::MeshletMeshManager, MeshletMesh}; +use crate::{ + Material, MeshFlags, MeshTransforms, MeshUniform, NotShadowCaster, NotShadowReceiver, + PreviousGlobalTransform, RenderMaterialInstances, +}; +use bevy_asset::{AssetEvent, AssetServer, Assets, Handle, UntypedAssetId}; +use bevy_ecs::{ + entity::{Entities, Entity, EntityHashMap}, + event::EventReader, + query::Has, + system::{Local, Query, Res, ResMut, Resource, SystemState}, +}; +use bevy_render::{render_resource::StorageBuffer, view::RenderLayers, MainWorld}; +use bevy_transform::components::GlobalTransform; +use bevy_utils::{HashMap, HashSet}; +use std::ops::{DerefMut, Range}; + +/// Manages data for each entity with a [`MeshletMesh`]. +#[derive(Resource)] +pub struct InstanceManager { + /// Amount of clusters in the scene (sum of all meshlet counts across all instances) + pub scene_cluster_count: u32, + + /// Per-instance [`Entity`], [`RenderLayers`], and [`NotShadowCaster`] + pub instances: Vec<(Entity, RenderLayers, bool)>, + /// Per-instance [`MeshUniform`] + pub instance_uniforms: StorageBuffer>, + /// Per-instance material ID + pub instance_material_ids: StorageBuffer>, + /// Prefix-sum of meshlet counts per instance + pub instance_meshlet_counts_prefix_sum: StorageBuffer>, + /// Per-instance index to the start of the instance's slice of the meshlets buffer + pub instance_meshlet_slice_starts: 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: u32, + /// Map of [`Material`] to material ID + material_id_lookup: HashMap, + /// Set of material IDs used in the scene + material_ids_present_in_scene: HashSet, +} + +impl InstanceManager { + pub fn new() -> Self { + Self { + scene_cluster_count: 0, + + instances: Vec::new(), + instance_uniforms: { + let mut buffer = StorageBuffer::default(); + buffer.set_label(Some("meshlet_instance_uniforms")); + buffer + }, + instance_material_ids: { + let mut buffer = StorageBuffer::default(); + buffer.set_label(Some("meshlet_instance_material_ids")); + buffer + }, + instance_meshlet_counts_prefix_sum: { + let mut buffer = StorageBuffer::default(); + buffer.set_label(Some("meshlet_instance_meshlet_counts_prefix_sum")); + buffer + }, + instance_meshlet_slice_starts: { + let mut buffer = StorageBuffer::default(); + buffer.set_label(Some("meshlet_instance_meshlet_slice_starts")); + buffer + }, + view_instance_visibility: EntityHashMap::default(), + + next_material_id: 0, + material_id_lookup: HashMap::new(), + material_ids_present_in_scene: HashSet::new(), + } + } + + #[allow(clippy::too_many_arguments)] + pub fn add_instance( + &mut self, + instance: Entity, + meshlets_slice: Range, + transform: &GlobalTransform, + previous_transform: Option<&PreviousGlobalTransform>, + render_layers: Option<&RenderLayers>, + not_shadow_receiver: bool, + not_shadow_caster: bool, + ) { + // Build a MeshUniform for the instance + let transform = transform.affine(); + let previous_transform = previous_transform.map(|t| t.0).unwrap_or(transform); + let mut flags = if not_shadow_receiver { + MeshFlags::empty() + } else { + MeshFlags::SHADOW_RECEIVER + }; + if transform.matrix3.determinant().is_sign_positive() { + flags |= MeshFlags::SIGN_DETERMINANT_MODEL_3X3; + } + let transforms = MeshTransforms { + world_from_local: (&transform).into(), + previous_world_from_local: (&previous_transform).into(), + flags: flags.bits(), + }; + let mesh_uniform = MeshUniform::new(&transforms, 0, None); + + // Append instance data + self.instances.push(( + instance, + render_layers.cloned().unwrap_or(RenderLayers::default()), + not_shadow_caster, + )); + self.instance_uniforms.get_mut().push(mesh_uniform); + self.instance_material_ids.get_mut().push(0); + self.instance_meshlet_counts_prefix_sum + .get_mut() + .push(self.scene_cluster_count); + self.instance_meshlet_slice_starts + .get_mut() + .push(meshlets_slice.start); + + self.scene_cluster_count += meshlets_slice.end - meshlets_slice.start; + } + + /// Get the material ID for a [`crate::Material`]. + pub fn get_material_id(&mut self, material_asset_id: UntypedAssetId) -> u32 { + *self + .material_id_lookup + .entry(material_asset_id) + .or_insert_with(|| { + self.next_material_id += 1; + self.next_material_id + }) + } + + pub fn material_present_in_scene(&self, material_id: &u32) -> bool { + self.material_ids_present_in_scene.contains(material_id) + } + + pub fn reset(&mut self, entities: &Entities) { + self.scene_cluster_count = 0; + + self.instances.clear(); + self.instance_uniforms.get_mut().clear(); + self.instance_material_ids.get_mut().clear(); + self.instance_meshlet_counts_prefix_sum.get_mut().clear(); + self.instance_meshlet_slice_starts.get_mut().clear(); + self.view_instance_visibility + .retain(|view_entity, _| entities.contains(*view_entity)); + self.view_instance_visibility + .values_mut() + .for_each(|b| b.get_mut().clear()); + + self.next_material_id = 0; + self.material_id_lookup.clear(); + self.material_ids_present_in_scene.clear(); + } +} + +pub fn extract_meshlet_mesh_entities( + mut meshlet_mesh_manager: ResMut, + mut instance_manager: ResMut, + // TODO: Replace main_world and system_state when Extract>> is possible + mut main_world: ResMut, + mut system_state: Local< + Option< + SystemState<( + Query<( + Entity, + &Handle, + &GlobalTransform, + Option<&PreviousGlobalTransform>, + Option<&RenderLayers>, + Has, + Has, + )>, + Res, + ResMut>, + EventReader>, + &Entities, + )>, + >, + >, +) { + // Get instances query + if system_state.is_none() { + *system_state = Some(SystemState::new(&mut main_world)); + } + let system_state = system_state.as_mut().unwrap(); + let (instances_query, asset_server, mut assets, mut asset_events, entities) = + system_state.get_mut(&mut main_world); + + // Reset per-frame data + instance_manager.reset(entities); + + // Free GPU buffer space for any modified or dropped MeshletMesh assets + for asset_event in asset_events.read() { + if let AssetEvent::Unused { id } | AssetEvent::Modified { id } = asset_event { + meshlet_mesh_manager.remove(id); + } + } + + // Iterate over every instance + for ( + instance, + meshlet_mesh, + transform, + previous_transform, + render_layers, + not_shadow_receiver, + not_shadow_caster, + ) in &instances_query + { + // Skip instances with an unloaded MeshletMesh asset + // TODO: This is a semi-expensive check + if asset_server.is_managed(meshlet_mesh.id()) + && !asset_server.is_loaded_with_dependencies(meshlet_mesh.id()) + { + continue; + } + + // Upload the instance's MeshletMesh asset data if not done already done + let meshlets_slice = + 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, + meshlets_slice, + transform, + previous_transform, + render_layers, + not_shadow_receiver, + not_shadow_caster, + ); + } +} + +/// 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( + mut instance_manager: ResMut, + render_material_instances: Res>, +) { + let instance_manager = instance_manager.deref_mut(); + + for (i, (instance, _, _)) in instance_manager.instances.iter().enumerate() { + if let Some(material_asset_id) = render_material_instances.get(instance) { + 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; + } + } + } +} diff --git a/crates/bevy_pbr/src/meshlet/material_draw_prepare.rs b/crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs similarity index 95% rename from crates/bevy_pbr/src/meshlet/material_draw_prepare.rs rename to crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs index f572edc0c7df4b..1a5c3e2d562071 100644 --- a/crates/bevy_pbr/src/meshlet/material_draw_prepare.rs +++ b/crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs @@ -1,4 +1,7 @@ -use super::{MeshletGpuScene, MESHLET_MESH_MATERIAL_SHADER_HANDLE}; +use super::{ + instance_manager::InstanceManager, resource_manager::ResourceManager, + MESHLET_MESH_MATERIAL_SHADER_HANDLE, +}; use crate::{environment_map::EnvironmentMapLight, irradiance_volume::IrradianceVolume, *}; use bevy_asset::AssetServer; use bevy_core_pipeline::{ @@ -22,10 +25,11 @@ use std::hash::Hash; pub struct MeshletViewMaterialsMainOpaquePass(pub Vec<(u32, CachedRenderPipelineId, BindGroup)>); /// Prepare [`Material`] pipelines for [`super::MeshletMesh`] entities for use in [`super::MeshletMainOpaquePass3dNode`], -/// and register the material with [`MeshletGpuScene`]. +/// and register the material with [`InstanceManager`]. #[allow(clippy::too_many_arguments)] pub fn prepare_material_meshlet_meshes_main_opaque_pass( - mut gpu_scene: ResMut, + resource_manager: ResMut, + mut instance_manager: ResMut, mut cache: Local>, pipeline_cache: Res, material_pipeline: Res>, @@ -167,7 +171,7 @@ pub fn prepare_material_meshlet_meshes_main_opaque_pass( label: material_pipeline_descriptor.label, layout: vec![ mesh_pipeline.get_view_layout(view_key.into()).clone(), - gpu_scene.material_draw_bind_group_layout(), + resource_manager.material_shade_bind_group_layout.clone(), material_pipeline.material_layout.clone(), ], push_constant_ranges: vec![], @@ -198,7 +202,7 @@ pub fn prepare_material_meshlet_meshes_main_opaque_pass( }), }; - let material_id = gpu_scene.get_material_id(material_id.untyped()); + let material_id = instance_manager.get_material_id(material_id.untyped()); let pipeline_id = *cache.entry(view_key).or_insert_with(|| { pipeline_cache.queue_render_pipeline(pipeline_descriptor.clone()) @@ -219,10 +223,11 @@ pub struct MeshletViewMaterialsDeferredGBufferPrepass( ); /// Prepare [`Material`] pipelines for [`super::MeshletMesh`] entities for use in [`super::MeshletPrepassNode`], -/// and [`super::MeshletDeferredGBufferPrepassNode`] and register the material with [`MeshletGpuScene`]. +/// and [`super::MeshletDeferredGBufferPrepassNode`] and register the material with [`InstanceManager`]. #[allow(clippy::too_many_arguments)] pub fn prepare_material_meshlet_meshes_prepass( - mut gpu_scene: ResMut, + resource_manager: ResMut, + mut instance_manager: ResMut, mut cache: Local>, pipeline_cache: Res, prepass_pipeline: Res>, @@ -319,7 +324,7 @@ pub fn prepare_material_meshlet_meshes_prepass( label: material_pipeline_descriptor.label, layout: vec![ view_layout, - gpu_scene.material_draw_bind_group_layout(), + resource_manager.material_shade_bind_group_layout.clone(), prepass_pipeline.material_layout.clone(), ], push_constant_ranges: vec![], @@ -350,7 +355,7 @@ pub fn prepare_material_meshlet_meshes_prepass( }), }; - let material_id = gpu_scene.get_material_id(material_id.untyped()); + let material_id = instance_manager.get_material_id(material_id.untyped()); let pipeline_id = *cache.entry(view_key).or_insert_with(|| { pipeline_cache.queue_render_pipeline(pipeline_descriptor.clone()) diff --git a/crates/bevy_pbr/src/meshlet/material_draw_nodes.rs b/crates/bevy_pbr/src/meshlet/material_shade_nodes.rs similarity index 91% rename from crates/bevy_pbr/src/meshlet/material_draw_nodes.rs rename to crates/bevy_pbr/src/meshlet/material_shade_nodes.rs index e7b71ea25366d1..9c2d432d8856a6 100644 --- a/crates/bevy_pbr/src/meshlet/material_draw_nodes.rs +++ b/crates/bevy_pbr/src/meshlet/material_shade_nodes.rs @@ -1,10 +1,10 @@ use super::{ - gpu_scene::{MeshletViewBindGroups, MeshletViewResources}, - material_draw_prepare::{ + material_pipeline_prepare::{ MeshletViewMaterialsDeferredGBufferPrepass, MeshletViewMaterialsMainOpaquePass, MeshletViewMaterialsPrepass, }, - MeshletGpuScene, + resource_manager::{MeshletViewBindGroups, MeshletViewResources}, + InstanceManager, }; use crate::{ MeshViewBindGroup, PrepassViewBindGroup, ViewEnvironmentMapUniformOffset, ViewFogUniformOffset, @@ -72,15 +72,15 @@ impl ViewNode for MeshletMainOpaquePass3dNode { } let ( - Some(meshlet_gpu_scene), + Some(instance_manager), Some(pipeline_cache), Some(meshlet_material_depth), - Some(meshlet_material_draw_bind_group), + Some(meshlet_material_shade_bind_group), ) = ( - world.get_resource::(), + world.get_resource::(), world.get_resource::(), meshlet_view_resources.material_depth.as_ref(), - meshlet_view_bind_groups.material_draw.as_ref(), + meshlet_view_bind_groups.material_shade.as_ref(), ) else { return Ok(()); @@ -116,13 +116,13 @@ impl ViewNode for MeshletMainOpaquePass3dNode { **view_environment_map_offset, ], ); - render_pass.set_bind_group(1, meshlet_material_draw_bind_group, &[]); + render_pass.set_bind_group(1, meshlet_material_shade_bind_group, &[]); // 1 fullscreen triangle draw per material for (material_id, material_pipeline_id, material_bind_group) in meshlet_view_materials.iter() { - if meshlet_gpu_scene.material_present_in_scene(material_id) { + if instance_manager.material_present_in_scene(material_id) { if let Some(material_pipeline) = pipeline_cache.get_render_pipeline(*material_pipeline_id) { @@ -175,16 +175,16 @@ impl ViewNode for MeshletPrepassNode { let ( Some(prepass_view_bind_group), - Some(meshlet_gpu_scene), + Some(instance_manager), Some(pipeline_cache), Some(meshlet_material_depth), - Some(meshlet_material_draw_bind_group), + Some(meshlet_material_shade_bind_group), ) = ( world.get_resource::(), - world.get_resource::(), + world.get_resource::(), world.get_resource::(), meshlet_view_resources.material_depth.as_ref(), - meshlet_view_bind_groups.material_draw.as_ref(), + meshlet_view_bind_groups.material_shade.as_ref(), ) else { return Ok(()); @@ -239,13 +239,13 @@ impl ViewNode for MeshletPrepassNode { ); } - render_pass.set_bind_group(1, meshlet_material_draw_bind_group, &[]); + render_pass.set_bind_group(1, meshlet_material_shade_bind_group, &[]); // 1 fullscreen triangle draw per material for (material_id, material_pipeline_id, material_bind_group) in meshlet_view_materials.iter() { - if meshlet_gpu_scene.material_present_in_scene(material_id) { + if instance_manager.material_present_in_scene(material_id) { if let Some(material_pipeline) = pipeline_cache.get_render_pipeline(*material_pipeline_id) { @@ -298,16 +298,16 @@ impl ViewNode for MeshletDeferredGBufferPrepassNode { let ( Some(prepass_view_bind_group), - Some(meshlet_gpu_scene), + Some(instance_manager), Some(pipeline_cache), Some(meshlet_material_depth), - Some(meshlet_material_draw_bind_group), + Some(meshlet_material_shade_bind_group), ) = ( world.get_resource::(), - world.get_resource::(), + world.get_resource::(), world.get_resource::(), meshlet_view_resources.material_depth.as_ref(), - meshlet_view_bind_groups.material_draw.as_ref(), + meshlet_view_bind_groups.material_shade.as_ref(), ) else { return Ok(()); @@ -367,13 +367,13 @@ impl ViewNode for MeshletDeferredGBufferPrepassNode { ); } - render_pass.set_bind_group(1, meshlet_material_draw_bind_group, &[]); + render_pass.set_bind_group(1, meshlet_material_shade_bind_group, &[]); // 1 fullscreen triangle draw per material for (material_id, material_pipeline_id, material_bind_group) in meshlet_view_materials.iter() { - if meshlet_gpu_scene.material_present_in_scene(material_id) { + if instance_manager.material_present_in_scene(material_id) { if let Some(material_pipeline) = pipeline_cache.get_render_pipeline(*material_pipeline_id) { diff --git a/crates/bevy_pbr/src/meshlet/meshlet_bindings.wgsl b/crates/bevy_pbr/src/meshlet/meshlet_bindings.wgsl index a3f18cbc9b29e8..f70252b28e328b 100644 --- a/crates/bevy_pbr/src/meshlet/meshlet_bindings.wgsl +++ b/crates/bevy_pbr/src/meshlet/meshlet_bindings.wgsl @@ -30,6 +30,7 @@ fn unpack_meshlet_vertex(packed: PackedMeshletVertex) -> MeshletVertex { struct Meshlet { start_vertex_id: u32, start_index_id: u32, + vertex_count: u32, triangle_count: u32, } @@ -44,9 +45,15 @@ struct MeshletBoundingSphere { radius: f32, } +struct DispatchIndirectArgs { + x: atomic, + y: u32, + z: u32, +} + struct DrawIndirectArgs { - vertex_count: atomic, - instance_count: u32, + vertex_count: u32, + instance_count: atomic, first_vertex: u32, first_instance: u32, } @@ -60,15 +67,16 @@ var cluster_count: u32; #endif #ifdef MESHLET_CULLING_PASS +var meshlet_raster_cluster_rightmost_slot: u32; @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_cluster_instance_ids: array; // Per cluster @group(0) @binding(3) var meshlet_instance_uniforms: array; // Per entity instance @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_second_pass_candidates: array>; // 1 bit per cluster , packed as a bitmask -@group(0) @binding(6) var meshlets: array; // Per meshlet -@group(0) @binding(7) var draw_indirect_args: DrawIndirectArgs; // Single object shared between all workgroups/meshlets/triangles -@group(0) @binding(8) var draw_triangle_buffer: array; // Single object shared between all workgroups/meshlets/triangles +@group(0) @binding(6) var meshlet_software_raster_indirect_args: DispatchIndirectArgs; // Single object shared between all workgroups/clusters/triangles +@group(0) @binding(7) var meshlet_hardware_raster_indirect_args: DrawIndirectArgs; // Single object shared between all workgroups/clusters/triangles +@group(0) @binding(8) var meshlet_raster_clusters: array; // Single object shared between all workgroups/clusters/triangles @group(0) @binding(9) 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(10) var view: View; @group(0) @binding(11) var previous_view: PreviousViewUniforms; @@ -79,6 +87,7 @@ fn should_cull_instance(instance_id: u32) -> bool { return bool(extractBits(packed_visibility, bit_offset, 1u)); } +// 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; @@ -94,10 +103,16 @@ fn cluster_is_second_pass_candidate(cluster_id: u32) -> bool { @group(0) @binding(4) var meshlet_vertex_data: array; // Many per meshlet @group(0) @binding(5) var meshlet_cluster_instance_ids: array; // Per cluster @group(0) @binding(6) var meshlet_instance_uniforms: array; // Per entity instance -@group(0) @binding(7) var meshlet_instance_material_ids: array; // Per entity instance -@group(0) @binding(8) var draw_triangle_buffer: array; // Single object shared between all workgroups/meshlets/triangles -@group(0) @binding(9) var view: View; +@group(0) @binding(7) var meshlet_raster_clusters: array; // Single object shared between all workgroups/clusters/triangles +@group(0) @binding(8) var meshlet_software_raster_cluster_count: u32; +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT +@group(0) @binding(9) var meshlet_visibility_buffer: array>; // Per pixel +#else +@group(0) @binding(9) var meshlet_visibility_buffer: array>; // Per pixel +#endif +@group(0) @binding(10) var view: View; +// TODO: Load only twice, instead of 3x in cases where you load 3 indices per thread? fn get_meshlet_index(index_id: u32) -> u32 { let packed_index = meshlet_indices[index_id / 4u]; let bit_offset = (index_id % 4u) * 8u; @@ -106,7 +121,7 @@ fn get_meshlet_index(index_id: u32) -> u32 { #endif #ifdef MESHLET_MESH_MATERIAL_PASS -@group(1) @binding(0) var meshlet_visibility_buffer: texture_2d; // Generated from the meshlet raster passes +@group(1) @binding(0) var meshlet_visibility_buffer: array; // Per pixel @group(1) @binding(1) var meshlet_cluster_meshlet_ids: array; // Per cluster @group(1) @binding(2) var meshlets: array; // Per meshlet @group(1) @binding(3) var meshlet_indices: array; // Many per meshlet @@ -115,6 +130,7 @@ fn get_meshlet_index(index_id: u32) -> u32 { @group(1) @binding(6) var meshlet_cluster_instance_ids: array; // Per cluster @group(1) @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_index(index_id: u32) -> u32 { let packed_index = meshlet_indices[index_id / 4u]; let bit_offset = (index_id % 4u) * 8u; diff --git a/crates/bevy_pbr/src/meshlet/meshlet_mesh_manager.rs b/crates/bevy_pbr/src/meshlet/meshlet_mesh_manager.rs new file mode 100644 index 00000000000000..03855ec039a536 --- /dev/null +++ b/crates/bevy_pbr/src/meshlet/meshlet_mesh_manager.rs @@ -0,0 +1,132 @@ +use super::{ + asset::{Meshlet, MeshletBoundingSpheres}, + persistent_buffer::PersistentGpuBuffer, + MeshletMesh, +}; +use bevy_asset::{AssetId, Assets}; +use bevy_ecs::{ + system::{Res, ResMut, Resource}, + world::{FromWorld, World}, +}; +use bevy_render::{ + render_resource::BufferAddress, + renderer::{RenderDevice, RenderQueue}, +}; +use bevy_utils::HashMap; +use std::{mem::size_of, ops::Range, sync::Arc}; + +/// Manages uploading [`MeshletMesh`] asset data to the GPU. +#[derive(Resource)] +pub struct MeshletMeshManager { + pub vertex_data: PersistentGpuBuffer>, + pub vertex_ids: PersistentGpuBuffer>, + pub indices: PersistentGpuBuffer>, + pub meshlets: PersistentGpuBuffer>, + pub meshlet_bounding_spheres: PersistentGpuBuffer>, + meshlet_mesh_slices: HashMap, [Range; 5]>, +} + +impl FromWorld for MeshletMeshManager { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + Self { + vertex_data: PersistentGpuBuffer::new("meshlet_vertex_data", render_device), + vertex_ids: PersistentGpuBuffer::new("meshlet_vertex_ids", render_device), + indices: PersistentGpuBuffer::new("meshlet_indices", render_device), + meshlets: PersistentGpuBuffer::new("meshlets", render_device), + meshlet_bounding_spheres: PersistentGpuBuffer::new( + "meshlet_bounding_spheres", + render_device, + ), + meshlet_mesh_slices: HashMap::new(), + } + } +} + +impl MeshletMeshManager { + pub fn queue_upload_if_needed( + &mut self, + asset_id: AssetId, + assets: &mut Assets, + ) -> Range { + 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", + ); + + let vertex_data_slice = self + .vertex_data + .queue_write(Arc::clone(&meshlet_mesh.vertex_data), ()); + let vertex_ids_slice = self.vertex_ids.queue_write( + Arc::clone(&meshlet_mesh.vertex_ids), + vertex_data_slice.start, + ); + let indices_slice = self + .indices + .queue_write(Arc::clone(&meshlet_mesh.indices), ()); + let meshlets_slice = self.meshlets.queue_write( + Arc::clone(&meshlet_mesh.meshlets), + (vertex_ids_slice.start, indices_slice.start), + ); + let meshlet_bounding_spheres_slice = self + .meshlet_bounding_spheres + .queue_write(Arc::clone(&meshlet_mesh.bounding_spheres), ()); + + [ + vertex_data_slice, + vertex_ids_slice, + indices_slice, + meshlets_slice, + meshlet_bounding_spheres_slice, + ] + }; + + // If the MeshletMesh asset has not been uploaded to the GPU yet, queue it for uploading + let [_, _, _, meshlets_slice, _] = 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 + } + + pub fn remove(&mut self, asset_id: &AssetId) { + if let Some( + [vertex_data_slice, vertex_ids_slice, indices_slice, meshlets_slice, meshlet_bounding_spheres_slice], + ) = self.meshlet_mesh_slices.remove(asset_id) + { + self.vertex_data.mark_slice_unused(vertex_data_slice); + self.vertex_ids.mark_slice_unused(vertex_ids_slice); + self.indices.mark_slice_unused(indices_slice); + self.meshlets.mark_slice_unused(meshlets_slice); + self.meshlet_bounding_spheres + .mark_slice_unused(meshlet_bounding_spheres_slice); + } + } +} + +/// Upload all newly queued [`MeshletMesh`] asset data to the GPU. +pub fn perform_pending_meshlet_mesh_writes( + mut meshlet_mesh_manager: ResMut, + render_queue: Res, + render_device: Res, +) { + meshlet_mesh_manager + .vertex_data + .perform_writes(&render_queue, &render_device); + meshlet_mesh_manager + .vertex_ids + .perform_writes(&render_queue, &render_device); + meshlet_mesh_manager + .indices + .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); +} diff --git a/crates/bevy_pbr/src/meshlet/mod.rs b/crates/bevy_pbr/src/meshlet/mod.rs index a55dc42247a805..d61dc05c5671fd 100644 --- a/crates/bevy_pbr/src/meshlet/mod.rs +++ b/crates/bevy_pbr/src/meshlet/mod.rs @@ -3,12 +3,14 @@ mod asset; #[cfg(feature = "meshlet_processor")] mod from_mesh; -mod gpu_scene; -mod material_draw_nodes; -mod material_draw_prepare; +mod instance_manager; +mod material_pipeline_prepare; +mod material_shade_nodes; +mod meshlet_mesh_manager; mod persistent_buffer; mod persistent_buffer_impls; mod pipelines; +mod resource_manager; mod visibility_buffer_raster_node; pub mod graph { @@ -24,8 +26,8 @@ pub mod graph { } pub(crate) use self::{ - gpu_scene::{queue_material_meshlet_meshes, MeshletGpuScene}, - material_draw_prepare::{ + instance_manager::{queue_material_meshlet_meshes, InstanceManager}, + material_pipeline_prepare::{ prepare_material_meshlet_meshes_main_opaque_pass, prepare_material_meshlet_meshes_prepass, }, }; @@ -35,22 +37,19 @@ pub use self::asset::{MeshletMesh, MeshletMeshSaverLoader}; pub use self::from_mesh::MeshToMeshletMeshConversionError; use self::{ - gpu_scene::{ - extract_meshlet_meshes, perform_pending_meshlet_mesh_writes, - prepare_meshlet_per_frame_resources, prepare_meshlet_view_bind_groups, - }, graph::NodeMeshlet, - material_draw_nodes::{ - MeshletDeferredGBufferPrepassNode, MeshletMainOpaquePass3dNode, MeshletPrepassNode, - }, - material_draw_prepare::{ + instance_manager::extract_meshlet_mesh_entities, + material_pipeline_prepare::{ MeshletViewMaterialsDeferredGBufferPrepass, MeshletViewMaterialsMainOpaquePass, MeshletViewMaterialsPrepass, }, - pipelines::{ - MeshletPipelines, MESHLET_COPY_MATERIAL_DEPTH_SHADER_HANDLE, MESHLET_CULLING_SHADER_HANDLE, - MESHLET_DOWNSAMPLE_DEPTH_SHADER_HANDLE, MESHLET_FILL_CLUSTER_BUFFERS_SHADER_HANDLE, - MESHLET_VISIBILITY_BUFFER_RASTER_SHADER_HANDLE, + material_shade_nodes::{ + MeshletDeferredGBufferPrepassNode, MeshletMainOpaquePass3dNode, MeshletPrepassNode, + }, + meshlet_mesh_manager::{perform_pending_meshlet_mesh_writes, MeshletMeshManager}, + pipelines::*, + resource_manager::{ + prepare_meshlet_per_frame_resources, prepare_meshlet_view_bind_groups, ResourceManager, }, visibility_buffer_raster_node::MeshletVisibilityBufferRasterPassNode, }; @@ -58,10 +57,7 @@ use crate::{graph::NodePbr, Material}; use bevy_app::{App, Plugin, PostUpdate}; use bevy_asset::{load_internal_asset, AssetApp, Handle}; use bevy_core_pipeline::{ - core_3d::{ - graph::{Core3d, Node3d}, - Camera3d, - }, + core_3d::graph::{Core3d, Node3d}, prepass::{DeferredPrepass, MotionVectorPrepass, NormalPrepass}, }; use bevy_ecs::{ @@ -74,7 +70,7 @@ use bevy_ecs::{ }; use bevy_render::{ render_graph::{RenderGraphApp, ViewNodeRunner}, - render_resource::{Shader, TextureUsages}, + render_resource::Shader, renderer::RenderDevice, settings::WgpuFeatures, view::{ @@ -84,6 +80,7 @@ use bevy_render::{ ExtractSchedule, Render, RenderApp, RenderSet, }; use bevy_transform::components::{GlobalTransform, Transform}; +use bevy_utils::tracing::error; const MESHLET_BINDINGS_SHADER_HANDLE: Handle = Handle::weak_from_u128(1325134235233421); const MESHLET_MESH_MATERIAL_SHADER_HANDLE: Handle = @@ -96,26 +93,46 @@ const MESHLET_MESH_MATERIAL_SHADER_HANDLE: Handle = /// /// In comparison to Bevy's standard renderer: /// * Much more efficient culling. Meshlets can be culled individually, instead of all or nothing culling for entire meshes at a time. -/// Additionally, occlusion culling can eliminate meshlets that would cause overdraw. -/// * Much more efficient batching. All geometry can be rasterized in a single indirect draw. +/// Additionally, occlusion culling can eliminate meshlets that would cause overdraw. +/// * Much more efficient batching. All geometry can be rasterized in a single draw. /// * Scales better with large amounts of dense geometry and overdraw. Bevy's standard renderer will bottleneck sooner. /// * Near-seamless level of detail (LOD). -/// * Much greater base overhead. Rendering will be slower than Bevy's standard renderer with small amounts of geometry and overdraw. -/// * Much greater memory usage. +/// * Much greater base overhead. Rendering will be slower and use more memory than Bevy's standard renderer +/// with small amounts of geometry and overdraw. /// * Requires preprocessing meshes. See [`MeshletMesh`] for details. /// * Limitations on the kinds of materials you can use. See [`MeshletMesh`] for details. /// +/// This plugin requires a fairly recent GPU that supports [`WgpuFeatures::SHADER_INT64_ATOMIC_MIN_MAX`]. +/// +/// This plugin currently works only on the Vulkan backend. +/// /// This plugin is not compatible with [`Msaa`]. Any camera rendering a [`MeshletMesh`] must have /// [`Msaa`] set to [`Msaa::Off`]. /// -/// This plugin does not work on Wasm. -/// /// Mixing forward+prepass and deferred rendering for opaque materials is not currently supported when using this plugin. /// You must use one or the other by setting [`crate::DefaultOpaqueRendererMethod`]. /// Do not override [`crate::Material::opaque_render_method`] for any material when using this plugin. /// /// ![A render of the Stanford dragon as a `MeshletMesh`](https://raw.githubusercontent.com/bevyengine/bevy/main/crates/bevy_pbr/src/meshlet/meshlet_preview.png) -pub struct MeshletPlugin; +pub struct MeshletPlugin { + /// The maximum amount of clusters that can be processed at once, + /// used to control the size of a pre-allocated GPU buffer. + /// + /// If this number is too low, you'll see rendering artifacts like missing or blinking meshes. + /// + /// Each cluster slot costs 4 bytes of VRAM. + pub cluster_buffer_slots: u32, +} + +impl MeshletPlugin { + /// [`WgpuFeatures`] required for this plugin to function. + pub fn required_wgpu_features() -> WgpuFeatures { + WgpuFeatures::SHADER_INT64_ATOMIC_MIN_MAX + | WgpuFeatures::SHADER_INT64 + | WgpuFeatures::SUBGROUP + | WgpuFeatures::PUSH_CONSTANTS + } +} impl Plugin for MeshletPlugin { fn build(&self, app: &mut App) { @@ -154,8 +171,14 @@ impl Plugin for MeshletPlugin { ); load_internal_asset!( app, - MESHLET_VISIBILITY_BUFFER_RASTER_SHADER_HANDLE, - "visibility_buffer_raster.wgsl", + 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!( @@ -166,8 +189,14 @@ impl Plugin for MeshletPlugin { ); load_internal_asset!( app, - MESHLET_COPY_MATERIAL_DEPTH_SHADER_HANDLE, - "copy_material_depth.wgsl", + 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 ); @@ -184,13 +213,14 @@ impl Plugin for MeshletPlugin { return; }; - if !render_app - .world() - .resource::() - .features() - .contains(WgpuFeatures::PUSH_CONSTANTS) - { - panic!("MeshletPlugin can't be used. GPU lacks support: WgpuFeatures::PUSH_CONSTANTS is not supported."); + let render_device = render_app.world().resource::().clone(); + let features = render_device.features(); + if !features.contains(Self::required_wgpu_features()) { + error!( + "MeshletPlugin can't be used. GPU lacks support for required features: {:?}.", + Self::required_wgpu_features().difference(features) + ); + std::process::exit(1); } render_app @@ -213,24 +243,31 @@ impl Plugin for MeshletPlugin { .add_render_graph_edges( Core3d, ( - // Non-meshlet shading passes _must_ come before meshlet shading passes - NodePbr::ShadowPass, NodeMeshlet::VisibilityBufferRasterPass, + NodePbr::ShadowPass, + // NodeMeshlet::Prepass, Node3d::Prepass, + // NodeMeshlet::DeferredPrepass, Node3d::DeferredPrepass, Node3d::CopyDeferredLightingId, Node3d::EndPrepasses, + // Node3d::StartMainPass, NodeMeshlet::MainOpaquePass, Node3d::MainOpaquePass, Node3d::EndMainPass, ), ) - .init_resource::() + .init_resource::() + .insert_resource(InstanceManager::new()) + .insert_resource(ResourceManager::new( + self.cluster_buffer_slots, + &render_device, + )) .init_resource::() - .add_systems(ExtractSchedule, extract_meshlet_meshes) + .add_systems(ExtractSchedule, extract_meshlet_mesh_entities) .add_systems( Render, ( @@ -281,7 +318,6 @@ pub type WithMeshletMesh = With>; fn configure_meshlet_views( mut views_3d: Query<( Entity, - &mut Camera3d, &Msaa, Has, Has, @@ -289,17 +325,12 @@ fn configure_meshlet_views( )>, mut commands: Commands, ) { - for (entity, mut camera_3d, msaa, normal_prepass, motion_vector_prepass, deferred_prepass) in - &mut views_3d - { + for (entity, msaa, normal_prepass, motion_vector_prepass, deferred_prepass) in &mut views_3d { if *msaa != Msaa::Off { - panic!("MeshletPlugin can't be used. MSAA is not supported."); + error!("MeshletPlugin can't be used with MSAA. Add Msaa::Off to your camera to use this plugin."); + std::process::exit(1); } - let mut usages: TextureUsages = camera_3d.depth_texture_usages.into(); - usages |= TextureUsages::TEXTURE_BINDING; - camera_3d.depth_texture_usages = usages.into(); - if !(normal_prepass || motion_vector_prepass || deferred_prepass) { commands .entity(entity) diff --git a/crates/bevy_pbr/src/meshlet/persistent_buffer_impls.rs b/crates/bevy_pbr/src/meshlet/persistent_buffer_impls.rs index 64b6861260f60a..da341c285e895e 100644 --- a/crates/bevy_pbr/src/meshlet/persistent_buffer_impls.rs +++ b/crates/bevy_pbr/src/meshlet/persistent_buffer_impls.rs @@ -58,6 +58,7 @@ impl PersistentGpuBufferable for Arc<[Meshlet]> { let bytes = bytemuck::cast::<_, [u8; size_of::()]>(Meshlet { start_vertex_id: meshlet.start_vertex_id + vertex_offset, start_index_id: meshlet.start_index_id + index_offset, + vertex_count: meshlet.vertex_count, triangle_count: meshlet.triangle_count, }); buffer_slice[i..(i + size)].clone_from_slice(&bytes); diff --git a/crates/bevy_pbr/src/meshlet/pipelines.rs b/crates/bevy_pbr/src/meshlet/pipelines.rs index 0112dcbb676cc9..69f10e015e279e 100644 --- a/crates/bevy_pbr/src/meshlet/pipelines.rs +++ b/crates/bevy_pbr/src/meshlet/pipelines.rs @@ -1,4 +1,4 @@ -use super::gpu_scene::MeshletGpuScene; +use super::resource_manager::ResourceManager; use bevy_asset::Handle; use bevy_core_pipeline::{ core_3d::CORE_3D_DEPTH_FORMAT, fullscreen_vertex_shader::fullscreen_shader_vertex_state, @@ -14,10 +14,14 @@ pub const MESHLET_FILL_CLUSTER_BUFFERS_SHADER_HANDLE: Handle = pub const MESHLET_CULLING_SHADER_HANDLE: Handle = Handle::weak_from_u128(5325134235233421); pub const MESHLET_DOWNSAMPLE_DEPTH_SHADER_HANDLE: Handle = Handle::weak_from_u128(6325134235233421); -pub const MESHLET_VISIBILITY_BUFFER_RASTER_SHADER_HANDLE: Handle = +pub const MESHLET_VISIBILITY_BUFFER_SOFTWARE_RASTER_SHADER_HANDLE: Handle = Handle::weak_from_u128(7325134235233421); -pub const MESHLET_COPY_MATERIAL_DEPTH_SHADER_HANDLE: Handle = +pub const MESHLET_VISIBILITY_BUFFER_HARDWARE_RASTER_SHADER_HANDLE: Handle = Handle::weak_from_u128(8325134235233421); +pub const MESHLET_RESOLVE_RENDER_TARGETS_SHADER_HANDLE: Handle = + Handle::weak_from_u128(9325134235233421); +pub const MESHLET_REMAP_1D_TO_2D_DISPATCH_SHADER_HANDLE: Handle = + Handle::weak_from_u128(9425134235233421); #[derive(Resource)] pub struct MeshletPipelines { @@ -26,21 +30,38 @@ pub struct MeshletPipelines { cull_second: CachedComputePipelineId, downsample_depth_first: CachedComputePipelineId, downsample_depth_second: CachedComputePipelineId, - visibility_buffer_raster: CachedRenderPipelineId, - visibility_buffer_raster_depth_only: CachedRenderPipelineId, - visibility_buffer_raster_depth_only_clamp_ortho: CachedRenderPipelineId, - copy_material_depth: CachedRenderPipelineId, + downsample_depth_first_shadow_view: CachedComputePipelineId, + downsample_depth_second_shadow_view: CachedComputePipelineId, + visibility_buffer_software_raster: CachedComputePipelineId, + visibility_buffer_software_raster_depth_only: CachedComputePipelineId, + visibility_buffer_software_raster_depth_only_clamp_ortho: CachedComputePipelineId, + visibility_buffer_hardware_raster: CachedRenderPipelineId, + visibility_buffer_hardware_raster_depth_only: CachedRenderPipelineId, + visibility_buffer_hardware_raster_depth_only_clamp_ortho: CachedRenderPipelineId, + resolve_depth: CachedRenderPipelineId, + resolve_depth_shadow_view: CachedRenderPipelineId, + resolve_material_depth: CachedRenderPipelineId, + remap_1d_to_2d_dispatch: Option, } impl FromWorld for MeshletPipelines { fn from_world(world: &mut World) -> Self { - let gpu_scene = world.resource::(); - let fill_cluster_buffers_bind_group_layout = - gpu_scene.fill_cluster_buffers_bind_group_layout(); - let cull_layout = gpu_scene.culling_bind_group_layout(); - let downsample_depth_layout = gpu_scene.downsample_depth_bind_group_layout(); - let visibility_buffer_layout = gpu_scene.visibility_buffer_raster_bind_group_layout(); - let copy_material_depth_layout = gpu_scene.copy_material_depth_bind_group_layout(); + let resource_manager = world.resource::(); + let fill_cluster_buffers_bind_group_layout = resource_manager + .fill_cluster_buffers_bind_group_layout + .clone(); + let cull_layout = resource_manager.culling_bind_group_layout.clone(); + let downsample_depth_layout = resource_manager.downsample_depth_bind_group_layout.clone(); + let visibility_buffer_raster_layout = resource_manager + .visibility_buffer_raster_bind_group_layout + .clone(); + let resolve_depth_layout = resource_manager.resolve_depth_bind_group_layout.clone(); + let resolve_material_depth_layout = resource_manager + .resolve_material_depth_bind_group_layout + .clone(); + let remap_1d_to_2d_dispatch_layout = resource_manager + .remap_1d_to_2d_dispatch_bind_group_layout + .clone(); let pipeline_cache = world.resource_mut::(); Self { @@ -61,7 +82,10 @@ impl FromWorld for MeshletPipelines { cull_first: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { label: Some("meshlet_culling_first_pipeline".into()), layout: vec![cull_layout.clone()], - push_constant_ranges: vec![], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..4, + }], shader: MESHLET_CULLING_SHADER_HANDLE, shader_defs: vec![ "MESHLET_CULLING_PASS".into(), @@ -73,7 +97,10 @@ impl FromWorld for MeshletPipelines { cull_second: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { label: Some("meshlet_culling_second_pipeline".into()), layout: vec![cull_layout], - push_constant_ranges: vec![], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..4, + }], shader: MESHLET_CULLING_SHADER_HANDLE, shader_defs: vec![ "MESHLET_CULLING_PASS".into(), @@ -88,21 +115,49 @@ impl FromWorld for MeshletPipelines { layout: vec![downsample_depth_layout.clone()], push_constant_ranges: vec![PushConstantRange { stages: ShaderStages::COMPUTE, - range: 0..4, + range: 0..8, }], shader: MESHLET_DOWNSAMPLE_DEPTH_SHADER_HANDLE, - shader_defs: vec![], + shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into()], entry_point: "downsample_depth_first".into(), }, ), downsample_depth_second: pipeline_cache.queue_compute_pipeline( + ComputePipelineDescriptor { + label: Some("meshlet_downsample_depth_second_pipeline".into()), + layout: vec![downsample_depth_layout.clone()], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..8, + }], + shader: MESHLET_DOWNSAMPLE_DEPTH_SHADER_HANDLE, + shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into()], + entry_point: "downsample_depth_second".into(), + }, + ), + + downsample_depth_first_shadow_view: pipeline_cache.queue_compute_pipeline( + ComputePipelineDescriptor { + label: Some("meshlet_downsample_depth_first_pipeline".into()), + layout: vec![downsample_depth_layout.clone()], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..8, + }], + shader: MESHLET_DOWNSAMPLE_DEPTH_SHADER_HANDLE, + shader_defs: vec![], + entry_point: "downsample_depth_first".into(), + }, + ), + + downsample_depth_second_shadow_view: pipeline_cache.queue_compute_pipeline( ComputePipelineDescriptor { label: Some("meshlet_downsample_depth_second_pipeline".into()), layout: vec![downsample_depth_layout], push_constant_ranges: vec![PushConstantRange { stages: ShaderStages::COMPUTE, - range: 0..4, + range: 0..8, }], shader: MESHLET_DOWNSAMPLE_DEPTH_SHADER_HANDLE, shader_defs: vec![], @@ -110,13 +165,79 @@ impl FromWorld for MeshletPipelines { }, ), - visibility_buffer_raster: pipeline_cache.queue_render_pipeline( - RenderPipelineDescriptor { - label: Some("meshlet_visibility_buffer_raster_pipeline".into()), - layout: vec![visibility_buffer_layout.clone()], + visibility_buffer_software_raster: pipeline_cache.queue_compute_pipeline( + ComputePipelineDescriptor { + 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_defs: vec![ + "MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into(), + "MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into(), + if remap_1d_to_2d_dispatch_layout.is_some() { + "MESHLET_2D_DISPATCH" + } else { + "" + } + .into(), + ], + entry_point: "rasterize_cluster".into(), + }, + ), + + visibility_buffer_software_raster_depth_only: pipeline_cache.queue_compute_pipeline( + ComputePipelineDescriptor { + label: Some( + "meshlet_visibility_buffer_software_raster_depth_only_pipeline".into(), + ), + layout: vec![visibility_buffer_raster_layout.clone()], push_constant_ranges: vec![], + shader: MESHLET_VISIBILITY_BUFFER_SOFTWARE_RASTER_SHADER_HANDLE, + shader_defs: vec![ + "MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into(), + if remap_1d_to_2d_dispatch_layout.is_some() { + "MESHLET_2D_DISPATCH" + } else { + "" + } + .into(), + ], + entry_point: "rasterize_cluster".into(), + }, + ), + + visibility_buffer_software_raster_depth_only_clamp_ortho: pipeline_cache + .queue_compute_pipeline(ComputePipelineDescriptor { + label: Some( + "meshlet_visibility_buffer_software_raster_depth_only_clamp_ortho_pipeline" + .into(), + ), + layout: vec![visibility_buffer_raster_layout.clone()], + push_constant_ranges: vec![], + shader: MESHLET_VISIBILITY_BUFFER_SOFTWARE_RASTER_SHADER_HANDLE, + shader_defs: vec![ + "MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into(), + "DEPTH_CLAMP_ORTHO".into(), + if remap_1d_to_2d_dispatch_layout.is_some() { + "MESHLET_2D_DISPATCH" + } else { + "" + } + .into(), + ], + entry_point: "rasterize_cluster".into(), + }), + + visibility_buffer_hardware_raster: pipeline_cache.queue_render_pipeline( + RenderPipelineDescriptor { + label: Some("meshlet_visibility_buffer_hardware_raster_pipeline".into()), + layout: vec![visibility_buffer_raster_layout.clone()], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::VERTEX, + range: 0..4, + }], vertex: VertexState { - shader: MESHLET_VISIBILITY_BUFFER_RASTER_SHADER_HANDLE, + shader: MESHLET_VISIBILITY_BUFFER_HARDWARE_RASTER_SHADER_HANDLE, shader_defs: vec![ "MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into(), "MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into(), @@ -133,44 +254,36 @@ impl FromWorld for MeshletPipelines { polygon_mode: PolygonMode::Fill, conservative: false, }, - depth_stencil: Some(DepthStencilState { - format: CORE_3D_DEPTH_FORMAT, - depth_write_enabled: true, - depth_compare: CompareFunction::GreaterEqual, - stencil: StencilState::default(), - bias: DepthBiasState::default(), - }), + depth_stencil: None, multisample: MultisampleState::default(), fragment: Some(FragmentState { - shader: MESHLET_VISIBILITY_BUFFER_RASTER_SHADER_HANDLE, + shader: MESHLET_VISIBILITY_BUFFER_HARDWARE_RASTER_SHADER_HANDLE, 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::R32Uint, - blend: None, - write_mask: ColorWrites::ALL, - }), - Some(ColorTargetState { - format: TextureFormat::R16Uint, - blend: None, - write_mask: ColorWrites::ALL, - }), - ], + targets: vec![Some(ColorTargetState { + format: TextureFormat::R8Uint, + blend: None, + write_mask: ColorWrites::empty(), + })], }), }, ), - visibility_buffer_raster_depth_only: pipeline_cache.queue_render_pipeline( + visibility_buffer_hardware_raster_depth_only: pipeline_cache.queue_render_pipeline( RenderPipelineDescriptor { - label: Some("meshlet_visibility_buffer_raster_depth_only_pipeline".into()), - layout: vec![visibility_buffer_layout.clone()], - push_constant_ranges: vec![], + label: Some( + "meshlet_visibility_buffer_hardware_raster_depth_only_pipeline".into(), + ), + layout: vec![visibility_buffer_raster_layout.clone()], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::VERTEX, + range: 0..4, + }], vertex: VertexState { - shader: MESHLET_VISIBILITY_BUFFER_RASTER_SHADER_HANDLE, + shader: MESHLET_VISIBILITY_BUFFER_HARDWARE_RASTER_SHADER_HANDLE, shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into()], entry_point: "vertex".into(), buffers: vec![], @@ -184,27 +297,34 @@ impl FromWorld for MeshletPipelines { polygon_mode: PolygonMode::Fill, conservative: false, }, - depth_stencil: Some(DepthStencilState { - format: CORE_3D_DEPTH_FORMAT, - depth_write_enabled: true, - depth_compare: CompareFunction::GreaterEqual, - stencil: StencilState::default(), - bias: DepthBiasState::default(), - }), + depth_stencil: None, multisample: MultisampleState::default(), - fragment: None, + fragment: Some(FragmentState { + shader: MESHLET_VISIBILITY_BUFFER_HARDWARE_RASTER_SHADER_HANDLE, + 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(), + })], + }), }, ), - visibility_buffer_raster_depth_only_clamp_ortho: pipeline_cache.queue_render_pipeline( - RenderPipelineDescriptor { + visibility_buffer_hardware_raster_depth_only_clamp_ortho: pipeline_cache + .queue_render_pipeline(RenderPipelineDescriptor { label: Some( - "meshlet_visibility_buffer_raster_depth_only_clamp_ortho_pipeline".into(), + "meshlet_visibility_buffer_hardware_raster_depth_only_clamp_ortho_pipeline" + .into(), ), - layout: vec![visibility_buffer_layout], - push_constant_ranges: vec![], + layout: vec![visibility_buffer_raster_layout], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::VERTEX, + range: 0..4, + }], vertex: VertexState { - shader: MESHLET_VISIBILITY_BUFFER_RASTER_SHADER_HANDLE, + shader: MESHLET_VISIBILITY_BUFFER_HARDWARE_RASTER_SHADER_HANDLE, shader_defs: vec![ "MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into(), "DEPTH_CLAMP_ORTHO".into(), @@ -221,47 +341,112 @@ impl FromWorld for MeshletPipelines { polygon_mode: PolygonMode::Fill, conservative: false, }, - depth_stencil: Some(DepthStencilState { - format: CORE_3D_DEPTH_FORMAT, - depth_write_enabled: true, - depth_compare: CompareFunction::GreaterEqual, - stencil: StencilState::default(), - bias: DepthBiasState::default(), - }), + depth_stencil: None, multisample: MultisampleState::default(), fragment: Some(FragmentState { - shader: MESHLET_VISIBILITY_BUFFER_RASTER_SHADER_HANDLE, + shader: MESHLET_VISIBILITY_BUFFER_HARDWARE_RASTER_SHADER_HANDLE, shader_defs: vec![ "MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into(), "DEPTH_CLAMP_ORTHO".into(), ], entry_point: "fragment".into(), - targets: vec![], + targets: vec![Some(ColorTargetState { + format: TextureFormat::R8Uint, + blend: None, + write_mask: ColorWrites::empty(), + })], }), - }, - ), + }), - copy_material_depth: pipeline_cache.queue_render_pipeline(RenderPipelineDescriptor { - label: Some("meshlet_copy_material_depth_pipeline".into()), - layout: vec![copy_material_depth_layout], - push_constant_ranges: vec![], + resolve_depth: pipeline_cache.queue_render_pipeline(RenderPipelineDescriptor { + label: Some("meshlet_resolve_depth_pipeline".into()), + layout: vec![resolve_depth_layout.clone()], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::FRAGMENT, + range: 0..4, + }], vertex: fullscreen_shader_vertex_state(), primitive: PrimitiveState::default(), depth_stencil: Some(DepthStencilState { - format: TextureFormat::Depth16Unorm, + format: CORE_3D_DEPTH_FORMAT, depth_write_enabled: true, - depth_compare: CompareFunction::Always, + depth_compare: CompareFunction::GreaterEqual, stencil: StencilState::default(), bias: DepthBiasState::default(), }), multisample: MultisampleState::default(), fragment: Some(FragmentState { - shader: MESHLET_COPY_MATERIAL_DEPTH_SHADER_HANDLE, - shader_defs: vec![], - entry_point: "copy_material_depth".into(), + shader: MESHLET_RESOLVE_RENDER_TARGETS_SHADER_HANDLE, + shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into()], + entry_point: "resolve_depth".into(), targets: vec![], }), }), + + resolve_depth_shadow_view: pipeline_cache.queue_render_pipeline( + RenderPipelineDescriptor { + label: Some("meshlet_resolve_depth_pipeline".into()), + layout: vec![resolve_depth_layout], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::FRAGMENT, + range: 0..4, + }], + vertex: fullscreen_shader_vertex_state(), + primitive: PrimitiveState::default(), + depth_stencil: Some(DepthStencilState { + format: CORE_3D_DEPTH_FORMAT, + depth_write_enabled: true, + depth_compare: CompareFunction::GreaterEqual, + 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![], + }), + }, + ), + + resolve_material_depth: pipeline_cache.queue_render_pipeline( + RenderPipelineDescriptor { + label: Some("meshlet_resolve_material_depth_pipeline".into()), + layout: vec![resolve_material_depth_layout], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::FRAGMENT, + range: 0..4, + }], + vertex: fullscreen_shader_vertex_state(), + primitive: PrimitiveState::default(), + depth_stencil: Some(DepthStencilState { + format: TextureFormat::Depth16Unorm, + depth_write_enabled: true, + depth_compare: CompareFunction::Always, + stencil: StencilState::default(), + bias: DepthBiasState::default(), + }), + multisample: MultisampleState::default(), + fragment: Some(FragmentState { + shader: MESHLET_RESOLVE_RENDER_TARGETS_SHADER_HANDLE, + shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into()], + entry_point: "resolve_material_depth".into(), + targets: vec![], + }), + }, + ), + + 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()), + layout: vec![layout], + push_constant_ranges: vec![], + shader: MESHLET_REMAP_1D_TO_2D_DISPATCH_SHADER_HANDLE, + shader_defs: vec![], + entry_point: "remap_dispatch".into(), + }) + }), } } } @@ -275,10 +460,18 @@ impl MeshletPipelines { &ComputePipeline, &ComputePipeline, &ComputePipeline, + &ComputePipeline, + &ComputePipeline, + &ComputePipeline, + &ComputePipeline, + &ComputePipeline, + &RenderPipeline, &RenderPipeline, &RenderPipeline, &RenderPipeline, &RenderPipeline, + &RenderPipeline, + Option<&ComputePipeline>, )> { let pipeline_cache = world.get_resource::()?; let pipeline = world.get_resource::()?; @@ -288,11 +481,27 @@ impl MeshletPipelines { pipeline_cache.get_compute_pipeline(pipeline.cull_second)?, pipeline_cache.get_compute_pipeline(pipeline.downsample_depth_first)?, pipeline_cache.get_compute_pipeline(pipeline.downsample_depth_second)?, - pipeline_cache.get_render_pipeline(pipeline.visibility_buffer_raster)?, - pipeline_cache.get_render_pipeline(pipeline.visibility_buffer_raster_depth_only)?, + pipeline_cache.get_compute_pipeline(pipeline.downsample_depth_first_shadow_view)?, + pipeline_cache.get_compute_pipeline(pipeline.downsample_depth_second_shadow_view)?, + pipeline_cache.get_compute_pipeline(pipeline.visibility_buffer_software_raster)?, + pipeline_cache + .get_compute_pipeline(pipeline.visibility_buffer_software_raster_depth_only)?, + pipeline_cache.get_compute_pipeline( + pipeline.visibility_buffer_software_raster_depth_only_clamp_ortho, + )?, + pipeline_cache.get_render_pipeline(pipeline.visibility_buffer_hardware_raster)?, pipeline_cache - .get_render_pipeline(pipeline.visibility_buffer_raster_depth_only_clamp_ortho)?, - pipeline_cache.get_render_pipeline(pipeline.copy_material_depth)?, + .get_render_pipeline(pipeline.visibility_buffer_hardware_raster_depth_only)?, + pipeline_cache.get_render_pipeline( + pipeline.visibility_buffer_hardware_raster_depth_only_clamp_ortho, + )?, + pipeline_cache.get_render_pipeline(pipeline.resolve_depth)?, + pipeline_cache.get_render_pipeline(pipeline.resolve_depth_shadow_view)?, + pipeline_cache.get_render_pipeline(pipeline.resolve_material_depth)?, + match pipeline.remap_1d_to_2d_dispatch { + Some(id) => Some(pipeline_cache.get_compute_pipeline(id)?), + None => None, + }, )) } } 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 new file mode 100644 index 00000000000000..6ade11b1d87e65 --- /dev/null +++ b/crates/bevy_pbr/src/meshlet/remap_1d_to_2d_dispatch.wgsl @@ -0,0 +1,20 @@ +/// Remaps an indirect 1d to 2d dispatch for devices with low dispatch size limit. + +struct DispatchIndirectArgs { + x: u32, + y: u32, + z: u32, +} + +@group(0) @binding(0) var meshlet_software_raster_indirect_args: DispatchIndirectArgs; +@group(0) @binding(1) var meshlet_software_raster_cluster_count: u32; + +@compute +@workgroup_size(1, 1, 1) +fn remap_dispatch() { + meshlet_software_raster_cluster_count = meshlet_software_raster_indirect_args.x; + + let n = u32(ceil(sqrt(f32(meshlet_software_raster_indirect_args.x)))); + meshlet_software_raster_indirect_args.x = n; + meshlet_software_raster_indirect_args.y = n; +} diff --git a/crates/bevy_pbr/src/meshlet/resolve_render_targets.wgsl b/crates/bevy_pbr/src/meshlet/resolve_render_targets.wgsl new file mode 100644 index 00000000000000..b54dbaec535853 --- /dev/null +++ b/crates/bevy_pbr/src/meshlet/resolve_render_targets.wgsl @@ -0,0 +1,39 @@ +#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput + +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT +@group(0) @binding(0) var meshlet_visibility_buffer: array; // Per pixel +#else +@group(0) @binding(0) var meshlet_visibility_buffer: array; // Per pixel +#endif +@group(0) @binding(1) var meshlet_cluster_instance_ids: array; // Per cluster +@group(0) @binding(2) var meshlet_instance_material_ids: array; // Per entity instance +var view_width: u32; + +/// This pass writes out the depth texture. +@fragment +fn resolve_depth(in: FullscreenVertexOutput) -> @builtin(frag_depth) f32 { + let frag_coord_1d = u32(in.position.y) * view_width + u32(in.position.x); + let visibility = meshlet_visibility_buffer[frag_coord_1d]; +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT + return bitcast(u32(visibility >> 32u)); +#else + return bitcast(visibility); +#endif +} + +/// This pass writes out the material depth texture. +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT +@fragment +fn resolve_material_depth(in: FullscreenVertexOutput) -> @builtin(frag_depth) f32 { + let frag_coord_1d = u32(in.position.y) * view_width + u32(in.position.x); + let visibility = meshlet_visibility_buffer[frag_coord_1d]; + + let depth = visibility >> 32u; + if depth == 0lu { return 0.0; } + + let cluster_id = u32(visibility) >> 6u; + let instance_id = meshlet_cluster_instance_ids[cluster_id]; + let material_id = meshlet_instance_material_ids[instance_id]; + return f32(material_id) / 65535.0; +} +#endif diff --git a/crates/bevy_pbr/src/meshlet/resource_manager.rs b/crates/bevy_pbr/src/meshlet/resource_manager.rs new file mode 100644 index 00000000000000..1d6ada2a2a19dd --- /dev/null +++ b/crates/bevy_pbr/src/meshlet/resource_manager.rs @@ -0,0 +1,809 @@ +use super::{instance_manager::InstanceManager, meshlet_mesh_manager::MeshletMeshManager}; +use crate::ShadowView; +use bevy_core_pipeline::{ + core_3d::Camera3d, + prepass::{PreviousViewData, PreviousViewUniforms}, +}; +use bevy_ecs::{ + component::Component, + entity::{Entity, EntityHashMap}, + query::AnyOf, + system::{Commands, Query, Res, ResMut, Resource}, +}; +use bevy_math::{UVec2, Vec4Swizzles}; +use bevy_render::{ + render_resource::*, + renderer::{RenderDevice, RenderQueue}, + texture::{CachedTexture, TextureCache}, + view::{ExtractedView, RenderLayers, ViewUniform, ViewUniforms}, +}; +use binding_types::*; +use encase::internal::WriteInto; +use std::{ + array, iter, + mem::size_of, + sync::{atomic::AtomicBool, Arc}, +}; + +/// 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 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, + + /// 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, + /// Sampler for a depth pyramid + depth_pyramid_sampler: Sampler, + /// Dummy texture view for binding depth pyramids with less than the maximum amount of mips + depth_pyramid_dummy_texture: TextureView, + + // TODO + previous_depth_pyramids: EntityHashMap, + + // Bind group layouts + pub fill_cluster_buffers_bind_group_layout: BindGroupLayout, + pub culling_bind_group_layout: BindGroupLayout, + pub visibility_buffer_raster_bind_group_layout: BindGroupLayout, + pub downsample_depth_bind_group_layout: BindGroupLayout, + pub resolve_depth_bind_group_layout: BindGroupLayout, + pub resolve_material_depth_bind_group_layout: BindGroupLayout, + pub material_shade_bind_group_layout: BindGroupLayout, + pub remap_1d_to_2d_dispatch_bind_group_layout: Option, +} + +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; + + 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, + usage: BufferUsages::STORAGE, + 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, + + cluster_instance_ids: None, + cluster_meshlet_ids: None, + second_pass_candidates_buffer: None, + depth_pyramid_sampler: render_device.create_sampler(&SamplerDescriptor { + label: Some("meshlet_depth_pyramid_sampler"), + ..SamplerDescriptor::default() + }), + depth_pyramid_dummy_texture: render_device + .create_texture(&TextureDescriptor { + label: Some("meshlet_depth_pyramid_dummy_texture"), + size: Extent3d { + width: 1, + height: 1, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::R32Float, + usage: TextureUsages::STORAGE_BINDING, + view_formats: &[], + }) + .create_view(&TextureViewDescriptor { + label: Some("meshlet_depth_pyramid_dummy_texture_view"), + format: Some(TextureFormat::R32Float), + dimension: Some(TextureViewDimension::D2), + aspect: TextureAspect::All, + base_mip_level: 0, + mip_level_count: Some(1), + base_array_layer: 0, + array_layer_count: Some(1), + }), + + 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), + ), + ), + ), + culling_bind_group_layout: render_device.create_bind_group_layout( + "meshlet_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_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), + ), + ), + ), + downsample_depth_bind_group_layout: render_device.create_bind_group_layout( + "meshlet_downsample_depth_bind_group_layout", + &BindGroupLayoutEntries::sequential(ShaderStages::COMPUTE, { + let write_only_r32float = || { + texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly) + }; + ( + storage_buffer_read_only_sized(false, None), + write_only_r32float(), + write_only_r32float(), + write_only_r32float(), + write_only_r32float(), + write_only_r32float(), + texture_storage_2d( + TextureFormat::R32Float, + StorageTextureAccess::ReadWrite, + ), + write_only_r32float(), + write_only_r32float(), + write_only_r32float(), + write_only_r32float(), + write_only_r32float(), + write_only_r32float(), + sampler(SamplerBindingType::NonFiltering), + ) + }), + ), + visibility_buffer_raster_bind_group_layout: render_device.create_bind_group_layout( + "meshlet_visibility_buffer_raster_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::all(), + ( + 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_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_sized(false, None), + uniform_buffer::(true), + ), + ), + ), + resolve_depth_bind_group_layout: render_device.create_bind_group_layout( + "meshlet_resolve_depth_bind_group_layout", + &BindGroupLayoutEntries::single( + ShaderStages::FRAGMENT, + storage_buffer_read_only_sized(false, None), + ), + ), + resolve_material_depth_bind_group_layout: render_device.create_bind_group_layout( + "meshlet_resolve_material_depth_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + ), + ), + ), + material_shade_bind_group_layout: render_device.create_bind_group_layout( + "meshlet_mesh_material_shade_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + 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_read_only_sized(false, None), + storage_buffer_read_only_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", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + ), + ), + ) + }), + } + } +} + +// ------------ TODO: Everything under here needs to be rewritten and cached ------------ + +#[derive(Component)] +pub struct MeshletViewResources { + pub scene_cluster_count: u32, + pub second_pass_candidates_buffer: Buffer, + instance_visibility: Buffer, + pub dummy_render_target: CachedTexture, + pub visibility_buffer: Buffer, + 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, + depth_pyramid_all_mips: TextureView, + depth_pyramid_mips: [TextureView; 12], + pub depth_pyramid_mip_count: u32, + previous_depth_pyramid: TextureView, + pub material_depth: Option, + pub view_size: UVec2, + pub raster_cluster_rightmost_slot: u32, +} + +#[derive(Component)] +pub struct MeshletViewBindGroups { + pub first_node: Arc, + pub fill_cluster_buffers: BindGroup, + pub culling_first: BindGroup, + pub culling_second: 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); + } +} + +// TODO: Cache things per-view and skip running this system / optimize this system +pub fn prepare_meshlet_per_frame_resources( + mut resource_manager: ResMut, + mut instance_manager: ResMut, + views: Query<( + Entity, + &ExtractedView, + Option<&RenderLayers>, + AnyOf<(&Camera3d, &ShadowView)>, + )>, + mut texture_cache: ResMut, + render_queue: Res, + render_device: Res, + mut commands: Commands, +) { + if instance_manager.scene_cluster_count == 0 { + return; + } + + let instance_manager = instance_manager.as_mut(); + + // TODO: Move this and the submit to a separate system and remove pub from the fields + 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_prefix_sum, + &render_device, + &render_queue, + ); + upload_storage_buffer( + &mut instance_manager.instance_meshlet_slice_starts, + &render_device, + &render_queue, + ); + + // Early submission for GPU data uploads to start while the render graph records commands + render_queue.submit([]); + + let needed_buffer_size = 4 * instance_manager.scene_cluster_count as u64; + match &mut resource_manager.cluster_instance_ids { + 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"), + size: needed_buffer_size, + usage: BufferUsages::STORAGE, + mapped_at_creation: false, + }); + *slot = Some(buffer.clone()); + buffer + } + }; + + 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(); + + let instance_visibility = instance_manager + .view_instance_visibility + .entry(view_entity) + .or_insert_with(|| { + let mut buffer = StorageBuffer::default(); + buffer.set_label(Some("meshlet_view_instance_visibility")); + buffer + }); + for (instance_index, (_, layers, not_shadow_caster)) in + instance_manager.instances.iter().enumerate() + { + // If either the layers don't match the view's layers or this is a shadow view + // and the instance is not a shadow caster, hide the instance for this view + if !render_layers + .unwrap_or(&RenderLayers::default()) + .intersects(layers) + || (shadow_view.is_some() && *not_shadow_caster) + { + let vec = instance_visibility.get_mut(); + let index = instance_index / 32; + let bit = instance_index - index * 32; + if vec.len() <= index { + vec.extend(iter::repeat(0).take(index - vec.len() + 1)); + } + vec[index] |= 1 << bit; + } + } + upload_storage_buffer(instance_visibility, &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, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::R8Uint, + usage: TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }, + ); + + let type_size = if not_shadow_view { + size_of::() + } else { + size_of::() + } as u64; + // TODO: Cache + let visibility_buffer = render_device.create_buffer(&BufferDescriptor { + label: Some("meshlet_visibility_buffer"), + size: type_size * (view.viewport.z * view.viewport.w) as u64, + usage: BufferUsages::STORAGE, + mapped_at_creation: false, + }); + + 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"), + 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"), + 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 + .create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_visibility_buffer_hardware_raster_indirect_args_first"), + contents: DrawIndirectArgs { + vertex_count: 64 * 3, + instance_count: 0, + first_vertex: 0, + first_instance: 0, + } + .as_bytes(), + usage: BufferUsages::STORAGE | BufferUsages::INDIRECT, + }); + let visibility_buffer_hardware_raster_indirect_args_second = render_device + .create_buffer_with_data(&BufferInitDescriptor { + label: Some("visibility_buffer_hardware_raster_indirect_args_second"), + contents: DrawIndirectArgs { + vertex_count: 64 * 3, + instance_count: 0, + first_vertex: 0, + first_instance: 0, + } + .as_bytes(), + usage: BufferUsages::STORAGE | BufferUsages::INDIRECT, + }); + + let depth_pyramid_size = Extent3d { + width: view.viewport.z.div_ceil(2), + height: view.viewport.w.div_ceil(2), + depth_or_array_layers: 1, + }; + let depth_pyramid_mip_count = depth_pyramid_size.max_mips(TextureDimension::D2); + let depth_pyramid = texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("meshlet_depth_pyramid"), + size: depth_pyramid_size, + mip_level_count: depth_pyramid_mip_count, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::R32Float, + usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + ); + let depth_pyramid_mips = array::from_fn(|i| { + if (i as u32) < depth_pyramid_mip_count { + depth_pyramid.texture.create_view(&TextureViewDescriptor { + label: Some("meshlet_depth_pyramid_texture_view"), + format: Some(TextureFormat::R32Float), + dimension: Some(TextureViewDimension::D2), + aspect: TextureAspect::All, + base_mip_level: i as u32, + mip_level_count: Some(1), + base_array_layer: 0, + array_layer_count: Some(1), + }) + } else { + resource_manager.depth_pyramid_dummy_texture.clone() + } + }); + let depth_pyramid_all_mips = depth_pyramid.default_view.clone(); + + let previous_depth_pyramid = + match resource_manager.previous_depth_pyramids.get(&view_entity) { + Some(texture_view) => texture_view.clone(), + None => depth_pyramid_all_mips.clone(), + }; + resource_manager + .previous_depth_pyramids + .insert(view_entity, depth_pyramid_all_mips.clone()); + + let material_depth = TextureDescriptor { + label: Some("meshlet_material_depth"), + size: Extent3d { + width: view.viewport.z, + height: view.viewport.w, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Depth16Unorm, + usage: TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }; + + commands.entity(view_entity).insert(MeshletViewResources { + scene_cluster_count: instance_manager.scene_cluster_count, + second_pass_candidates_buffer, + 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, + depth_pyramid_all_mips, + depth_pyramid_mips, + depth_pyramid_mip_count, + 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, + }); + } +} + +#[allow(clippy::too_many_arguments)] +pub fn prepare_meshlet_view_bind_groups( + meshlet_mesh_manager: Res, + resource_manager: Res, + instance_manager: Res, + views: Query<(Entity, &MeshletViewResources)>, + view_uniforms: Res, + previous_view_uniforms: Res, + 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(), + view_uniforms.uniforms.binding(), + previous_view_uniforms.uniforms.binding(), + ) + else { + return; + }; + + let first_node = Arc::new(AtomicBool::new(true)); + + // 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_prefix_sum + .binding() + .unwrap(), + instance_manager + .instance_meshlet_slice_starts + .binding() + .unwrap(), + cluster_instance_ids.as_entire_binding(), + cluster_meshlet_ids.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 entries = BindGroupEntries::sequential(( + cluster_meshlet_ids.as_entire_binding(), + meshlet_mesh_manager.meshlet_bounding_spheres.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 entries = BindGroupEntries::sequential(( + cluster_meshlet_ids.as_entire_binding(), + meshlet_mesh_manager.meshlet_bounding_spheres.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 downsample_depth = render_device.create_bind_group( + "meshlet_downsample_depth_bind_group", + &resource_manager.downsample_depth_bind_group_layout, + &BindGroupEntries::sequential(( + view_resources.visibility_buffer.as_entire_binding(), + &view_resources.depth_pyramid_mips[0], + &view_resources.depth_pyramid_mips[1], + &view_resources.depth_pyramid_mips[2], + &view_resources.depth_pyramid_mips[3], + &view_resources.depth_pyramid_mips[4], + &view_resources.depth_pyramid_mips[5], + &view_resources.depth_pyramid_mips[6], + &view_resources.depth_pyramid_mips[7], + &view_resources.depth_pyramid_mips[8], + &view_resources.depth_pyramid_mips[9], + &view_resources.depth_pyramid_mips[10], + &view_resources.depth_pyramid_mips[11], + &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_ids.binding(), + meshlet_mesh_manager.vertex_data.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.as_entire_binding(), + view_uniforms.clone(), + )); + let visibility_buffer_raster = render_device.create_bind_group( + "meshlet_visibility_raster_buffer_bind_group", + &resource_manager.visibility_buffer_raster_bind_group_layout, + &entries, + ); + + let resolve_depth = render_device.create_bind_group( + "meshlet_resolve_depth_bind_group", + &resource_manager.resolve_depth_bind_group_layout, + &BindGroupEntries::single(view_resources.visibility_buffer.as_entire_binding()), + ); + + let resolve_material_depth = view_resources.material_depth.as_ref().map(|_| { + let entries = BindGroupEntries::sequential(( + view_resources.visibility_buffer.as_entire_binding(), + 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, + ) + }); + + let material_shade = view_resources.material_depth.as_ref().map(|_| { + let entries = BindGroupEntries::sequential(( + view_resources.visibility_buffer.as_entire_binding(), + cluster_meshlet_ids.as_entire_binding(), + meshlet_mesh_manager.meshlets.binding(), + meshlet_mesh_manager.indices.binding(), + meshlet_mesh_manager.vertex_ids.binding(), + meshlet_mesh_manager.vertex_data.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, + ) + }); + + let remap_1d_to_2d_dispatch = resource_manager + .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(), + )), + ), + ) + }); + + commands.entity(view_entity).insert(MeshletViewBindGroups { + first_node: Arc::clone(&first_node), + fill_cluster_buffers, + culling_first, + culling_second, + downsample_depth, + visibility_buffer_raster, + resolve_depth, + resolve_material_depth, + material_shade, + remap_1d_to_2d_dispatch, + }); + } +} diff --git a/crates/bevy_pbr/src/meshlet/visibility_buffer_raster.wgsl b/crates/bevy_pbr/src/meshlet/visibility_buffer_hardware_raster.wgsl similarity index 56% rename from crates/bevy_pbr/src/meshlet/visibility_buffer_raster.wgsl rename to crates/bevy_pbr/src/meshlet/visibility_buffer_hardware_raster.wgsl index 86b34cd2f0f225..e3cf7a6fb3a22e 100644 --- a/crates/bevy_pbr/src/meshlet/visibility_buffer_raster.wgsl +++ b/crates/bevy_pbr/src/meshlet/visibility_buffer_hardware_raster.wgsl @@ -6,8 +6,8 @@ meshlet_vertex_data, meshlet_cluster_instance_ids, meshlet_instance_uniforms, - meshlet_instance_material_ids, - draw_triangle_buffer, + meshlet_raster_clusters, + meshlet_visibility_buffer, view, get_meshlet_index, unpack_meshlet_vertex, @@ -15,38 +15,33 @@ mesh_functions::mesh_position_local_to_world, } #import bevy_render::maths::affine3_to_square +var meshlet_raster_cluster_rightmost_slot: u32; -/// Vertex/fragment shader for rasterizing meshlets into a visibility buffer. +/// Vertex/fragment shader for rasterizing large clusters into a visibility buffer. struct VertexOutput { - @builtin(position) clip_position: vec4, + @builtin(position) position: vec4, #ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT - @location(0) @interpolate(flat) visibility: u32, - @location(1) @interpolate(flat) material_depth: u32, + @location(0) @interpolate(flat) packed_ids: u32, #endif #ifdef DEPTH_CLAMP_ORTHO @location(0) unclamped_clip_depth: f32, #endif } -#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT -struct FragmentOutput { - @location(0) visibility: vec4, - @location(1) material_depth: vec4, -} -#endif - @vertex -fn vertex(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { - let packed_ids = draw_triangle_buffer[vertex_index / 3u]; - let cluster_id = packed_ids >> 6u; - let triangle_id = extractBits(packed_ids, 0u, 6u); - let index_id = (triangle_id * 3u) + (vertex_index % 3u); +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]; let meshlet = meshlets[meshlet_id]; + + let triangle_id = vertex_index / 3u; + if triangle_id >= meshlet.triangle_count { return dummy_vertex(); } + let index_id = (triangle_id * 3u) + (vertex_index % 3u); let index = get_meshlet_index(meshlet.start_index_id + index_id); let vertex_id = meshlet_vertex_ids[meshlet.start_vertex_id + index]; let vertex = unpack_meshlet_vertex(meshlet_vertex_data[vertex_id]); + let instance_id = meshlet_cluster_instance_ids[cluster_id]; let instance_uniform = meshlet_instance_uniforms[instance_id]; @@ -61,8 +56,7 @@ fn vertex(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { return VertexOutput( clip_position, #ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT - packed_ids, - meshlet_instance_material_ids[instance_id], + (cluster_id << 6u) | triangle_id, #endif #ifdef DEPTH_CLAMP_ORTHO unclamped_clip_depth, @@ -70,19 +64,31 @@ fn vertex(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { ); } -#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT @fragment -fn fragment(vertex_output: VertexOutput) -> FragmentOutput { - return FragmentOutput( - vec4(vertex_output.visibility, 0u, 0u, 0u), - vec4(vertex_output.material_depth, 0u, 0u, 0u), - ); -} +fn fragment(vertex_output: VertexOutput) { + let frag_coord_1d = u32(vertex_output.position.y) * u32(view.viewport.z) + u32(vertex_output.position.x); + +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT + let depth = bitcast(vertex_output.position.z); + let visibility = (u64(depth) << 32u) | u64(vertex_output.packed_ids); + atomicMax(&meshlet_visibility_buffer[frag_coord_1d], visibility); +#else ifdef DEPTH_CLAMP_ORTHO + let depth = bitcast(vertex_output.unclamped_clip_depth); + atomicMax(&meshlet_visibility_buffer[frag_coord_1d], depth); +#else + let depth = bitcast(vertex_output.position.z); + atomicMax(&meshlet_visibility_buffer[frag_coord_1d], depth); #endif +} +fn dummy_vertex() -> VertexOutput { + return VertexOutput( + vec4(0.0), +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT + 0u, +#endif #ifdef DEPTH_CLAMP_ORTHO -@fragment -fn fragment(vertex_output: VertexOutput) -> @builtin(frag_depth) f32 { - return vertex_output.unclamped_clip_depth; -} + 0.0, #endif + ); +} 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 5af10769cc8dc5..3f2b8883e75f2e 100644 --- a/crates/bevy_pbr/src/meshlet/visibility_buffer_raster_node.rs +++ b/crates/bevy_pbr/src/meshlet/visibility_buffer_raster_node.rs @@ -1,6 +1,6 @@ use super::{ - gpu_scene::{MeshletViewBindGroups, MeshletViewResources}, pipelines::MeshletPipelines, + resource_manager::{MeshletViewBindGroups, MeshletViewResources}, }; use crate::{LightEntity, ShadowView, ViewLightEntities}; use bevy_color::LinearRgba; @@ -80,10 +80,18 @@ impl Node for MeshletVisibilityBufferRasterPassNode { culling_second_pipeline, downsample_depth_first_pipeline, downsample_depth_second_pipeline, - visibility_buffer_raster_pipeline, - visibility_buffer_raster_depth_only_pipeline, - visibility_buffer_raster_depth_only_clamp_ortho, - copy_material_depth_pipeline, + downsample_depth_first_shadow_view_pipeline, + downsample_depth_second_shadow_view_pipeline, + visibility_buffer_software_raster_pipeline, + visibility_buffer_software_raster_depth_only_pipeline, + visibility_buffer_software_raster_depth_only_clamp_ortho, + visibility_buffer_hardware_raster_pipeline, + visibility_buffer_hardware_raster_depth_only_pipeline, + visibility_buffer_hardware_raster_depth_only_clamp_ortho, + resolve_depth_pipeline, + resolve_depth_shadow_view_pipeline, + resolve_material_depth_pipeline, + remap_1d_to_2d_dispatch_pipeline, )) = MeshletPipelines::get(world) else { return Ok(()); @@ -94,7 +102,7 @@ impl Node for MeshletVisibilityBufferRasterPassNode { .fetch_and(false, Ordering::SeqCst); let thread_per_cluster_workgroups = - (meshlet_view_resources.scene_meshlet_count.div_ceil(128) as f32) + (meshlet_view_resources.scene_cluster_count.div_ceil(128) as f32) .cbrt() .ceil() as u32; @@ -112,7 +120,7 @@ impl Node for MeshletVisibilityBufferRasterPassNode { &meshlet_view_bind_groups.fill_cluster_buffers, fill_cluster_buffers_pipeline, thread_per_cluster_workgroups, - meshlet_view_resources.scene_meshlet_count, + meshlet_view_resources.scene_cluster_count, ); } cull_pass( @@ -123,17 +131,25 @@ impl Node for MeshletVisibilityBufferRasterPassNode { previous_view_offset, culling_first_pipeline, thread_per_cluster_workgroups, + meshlet_view_resources.raster_cluster_rightmost_slot, + meshlet_view_bind_groups + .remap_1d_to_2d_dispatch + .as_ref() + .map(|(bg1, _)| bg1), + remap_1d_to_2d_dispatch_pipeline, ); raster_pass( true, render_context, - meshlet_view_resources, - &meshlet_view_resources.visibility_buffer_draw_indirect_args_first, - view_depth.get_attachment(StoreOp::Store), + &meshlet_view_resources.visibility_buffer_software_raster_indirect_args_first, + &meshlet_view_resources.visibility_buffer_hardware_raster_indirect_args_first, + &meshlet_view_resources.dummy_render_target.default_view, meshlet_view_bind_groups, view_offset, - visibility_buffer_raster_pipeline, + visibility_buffer_software_raster_pipeline, + visibility_buffer_hardware_raster_pipeline, Some(camera), + meshlet_view_resources.raster_cluster_rightmost_slot, ); downsample_depth( render_context, @@ -150,23 +166,39 @@ impl Node for MeshletVisibilityBufferRasterPassNode { previous_view_offset, culling_second_pipeline, thread_per_cluster_workgroups, + meshlet_view_resources.raster_cluster_rightmost_slot, + meshlet_view_bind_groups + .remap_1d_to_2d_dispatch + .as_ref() + .map(|(_, bg2)| bg2), + remap_1d_to_2d_dispatch_pipeline, ); raster_pass( false, render_context, - meshlet_view_resources, - &meshlet_view_resources.visibility_buffer_draw_indirect_args_second, - view_depth.get_attachment(StoreOp::Store), + &meshlet_view_resources.visibility_buffer_software_raster_indirect_args_second, + &meshlet_view_resources.visibility_buffer_hardware_raster_indirect_args_second, + &meshlet_view_resources.dummy_render_target.default_view, meshlet_view_bind_groups, view_offset, - visibility_buffer_raster_pipeline, + visibility_buffer_software_raster_pipeline, + visibility_buffer_hardware_raster_pipeline, Some(camera), + meshlet_view_resources.raster_cluster_rightmost_slot, ); - copy_material_depth_pass( + resolve_depth( render_context, + view_depth.get_attachment(StoreOp::Store), meshlet_view_resources, meshlet_view_bind_groups, - copy_material_depth_pipeline, + resolve_depth_pipeline, + camera, + ); + resolve_material_depth( + render_context, + meshlet_view_resources, + meshlet_view_bind_groups, + resolve_material_depth_pipeline, camera, ); downsample_depth( @@ -191,9 +223,18 @@ impl Node for MeshletVisibilityBufferRasterPassNode { continue; }; - let shadow_visibility_buffer_pipeline = match light_type { - LightEntity::Directional { .. } => visibility_buffer_raster_depth_only_clamp_ortho, - _ => visibility_buffer_raster_depth_only_pipeline, + let ( + shadow_visibility_buffer_software_raster_pipeline, + shadow_visibility_buffer_hardware_raster_pipeline, + ) = match light_type { + LightEntity::Directional { .. } => ( + visibility_buffer_software_raster_depth_only_clamp_ortho, + visibility_buffer_hardware_raster_depth_only_clamp_ortho, + ), + _ => ( + visibility_buffer_software_raster_depth_only_pipeline, + visibility_buffer_hardware_raster_depth_only_pipeline, + ), }; render_context.command_encoder().push_debug_group(&format!( @@ -213,24 +254,32 @@ impl Node for MeshletVisibilityBufferRasterPassNode { previous_view_offset, culling_first_pipeline, thread_per_cluster_workgroups, + meshlet_view_resources.raster_cluster_rightmost_slot, + meshlet_view_bind_groups + .remap_1d_to_2d_dispatch + .as_ref() + .map(|(bg1, _)| bg1), + remap_1d_to_2d_dispatch_pipeline, ); raster_pass( true, render_context, - meshlet_view_resources, - &meshlet_view_resources.visibility_buffer_draw_indirect_args_first, - shadow_view.depth_attachment.get_attachment(StoreOp::Store), + &meshlet_view_resources.visibility_buffer_software_raster_indirect_args_first, + &meshlet_view_resources.visibility_buffer_hardware_raster_indirect_args_first, + &meshlet_view_resources.dummy_render_target.default_view, meshlet_view_bind_groups, view_offset, - shadow_visibility_buffer_pipeline, + shadow_visibility_buffer_software_raster_pipeline, + shadow_visibility_buffer_hardware_raster_pipeline, None, + meshlet_view_resources.raster_cluster_rightmost_slot, ); downsample_depth( render_context, meshlet_view_resources, meshlet_view_bind_groups, - downsample_depth_first_pipeline, - downsample_depth_second_pipeline, + downsample_depth_first_shadow_view_pipeline, + downsample_depth_second_shadow_view_pipeline, ); cull_pass( "culling_second", @@ -240,24 +289,40 @@ impl Node for MeshletVisibilityBufferRasterPassNode { previous_view_offset, culling_second_pipeline, thread_per_cluster_workgroups, + meshlet_view_resources.raster_cluster_rightmost_slot, + meshlet_view_bind_groups + .remap_1d_to_2d_dispatch + .as_ref() + .map(|(_, bg2)| bg2), + remap_1d_to_2d_dispatch_pipeline, ); raster_pass( false, render_context, - meshlet_view_resources, - &meshlet_view_resources.visibility_buffer_draw_indirect_args_second, - shadow_view.depth_attachment.get_attachment(StoreOp::Store), + &meshlet_view_resources.visibility_buffer_software_raster_indirect_args_second, + &meshlet_view_resources.visibility_buffer_hardware_raster_indirect_args_second, + &meshlet_view_resources.dummy_render_target.default_view, meshlet_view_bind_groups, view_offset, - shadow_visibility_buffer_pipeline, + shadow_visibility_buffer_software_raster_pipeline, + shadow_visibility_buffer_hardware_raster_pipeline, None, + meshlet_view_resources.raster_cluster_rightmost_slot, + ); + resolve_depth( + render_context, + shadow_view.depth_attachment.get_attachment(StoreOp::Store), + meshlet_view_resources, + meshlet_view_bind_groups, + resolve_depth_shadow_view_pipeline, + camera, ); downsample_depth( render_context, meshlet_view_resources, meshlet_view_bind_groups, - downsample_depth_first_pipeline, - downsample_depth_second_pipeline, + downsample_depth_first_shadow_view_pipeline, + downsample_depth_second_shadow_view_pipeline, ); render_context.command_encoder().pop_debug_group(); } @@ -274,20 +339,21 @@ fn fill_cluster_buffers_pass( cluster_count: u32, ) { let command_encoder = render_context.command_encoder(); - let mut cull_pass = command_encoder.begin_compute_pass(&ComputePassDescriptor { + let mut fill_pass = command_encoder.begin_compute_pass(&ComputePassDescriptor { label: Some("fill_cluster_buffers"), timestamp_writes: None, }); - cull_pass.set_pipeline(fill_cluster_buffers_pass_pipeline); - cull_pass.set_push_constants(0, &cluster_count.to_le_bytes()); - cull_pass.set_bind_group(0, fill_cluster_buffers_bind_group, &[]); - cull_pass.dispatch_workgroups( + fill_pass.set_pipeline(fill_cluster_buffers_pass_pipeline); + fill_pass.set_push_constants(0, &cluster_count.to_le_bytes()); + fill_pass.set_bind_group(0, fill_cluster_buffers_bind_group, &[]); + fill_pass.dispatch_workgroups( fill_cluster_buffers_pass_workgroups, fill_cluster_buffers_pass_workgroups, fill_cluster_buffers_pass_workgroups, ); } +#[allow(clippy::too_many_arguments)] fn cull_pass( label: &'static str, render_context: &mut RenderContext, @@ -296,6 +362,9 @@ fn cull_pass( previous_view_offset: &PreviousViewUniformOffset, culling_pipeline: &ComputePipeline, culling_workgroups: u32, + raster_cluster_rightmost_slot: u32, + remap_1d_to_2d_dispatch_bind_group: Option<&BindGroup>, + remap_1d_to_2d_dispatch_pipeline: Option<&ComputePipeline>, ) { let command_encoder = render_context.command_encoder(); let mut cull_pass = command_encoder.begin_compute_pass(&ComputePassDescriptor { @@ -303,82 +372,90 @@ fn cull_pass( timestamp_writes: None, }); cull_pass.set_pipeline(culling_pipeline); + cull_pass.set_push_constants(0, &raster_cluster_rightmost_slot.to_le_bytes()); cull_pass.set_bind_group( 0, culling_bind_group, &[view_offset.offset, previous_view_offset.offset], ); cull_pass.dispatch_workgroups(culling_workgroups, culling_workgroups, culling_workgroups); + + 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_bind_group(0, remap_1d_to_2d_dispatch_bind_group, &[]); + cull_pass.dispatch_workgroups(1, 1, 1); + } } #[allow(clippy::too_many_arguments)] fn raster_pass( first_pass: bool, render_context: &mut RenderContext, - meshlet_view_resources: &MeshletViewResources, - visibility_buffer_draw_indirect_args: &Buffer, - depth_stencil_attachment: RenderPassDepthStencilAttachment, + visibility_buffer_hardware_software_indirect_args: &Buffer, + visibility_buffer_hardware_raster_indirect_args: &Buffer, + dummy_render_target: &TextureView, meshlet_view_bind_groups: &MeshletViewBindGroups, view_offset: &ViewUniformOffset, - visibility_buffer_raster_pipeline: &RenderPipeline, + visibility_buffer_hardware_software_pipeline: &ComputePipeline, + visibility_buffer_hardware_raster_pipeline: &RenderPipeline, camera: Option<&ExtractedCamera>, + raster_cluster_rightmost_slot: u32, ) { - let mut color_attachments_filled = [None, None]; - if let (Some(visibility_buffer), Some(material_depth_color)) = ( - meshlet_view_resources.visibility_buffer.as_ref(), - meshlet_view_resources.material_depth_color.as_ref(), - ) { - let load = if first_pass { - LoadOp::Clear(LinearRgba::BLACK.into()) + 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 { - LoadOp::Load - }; - color_attachments_filled = [ - Some(RenderPassColorAttachment { - view: &visibility_buffer.default_view, - resolve_target: None, - ops: Operations { - load, - store: StoreOp::Store, - }, - }), - Some(RenderPassColorAttachment { - view: &material_depth_color.default_view, - resolve_target: None, - ops: Operations { - load, - store: StoreOp::Store, - }, - }), - ]; - } + "raster_software_second" + }), + timestamp_writes: None, + }); + software_pass.set_pipeline(visibility_buffer_hardware_software_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); + drop(software_pass); - let mut draw_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + let mut hardware_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { label: Some(if first_pass { - "raster_first" + "raster_hardware_first" } else { - "raster_second" + "raster_hardware_second" }), - color_attachments: if color_attachments_filled[0].is_none() { - &[] - } else { - &color_attachments_filled - }, - depth_stencil_attachment: Some(depth_stencil_attachment), + color_attachments: &[Some(RenderPassColorAttachment { + view: dummy_render_target, + resolve_target: None, + ops: Operations { + load: LoadOp::Clear(LinearRgba::BLACK.into()), + store: StoreOp::Discard, + }, + })], + depth_stencil_attachment: None, timestamp_writes: None, occlusion_query_set: None, }); if let Some(viewport) = camera.and_then(|camera| camera.viewport.as_ref()) { - draw_pass.set_camera_viewport(viewport); + hardware_pass.set_camera_viewport(viewport); } - - draw_pass.set_render_pipeline(visibility_buffer_raster_pipeline); - draw_pass.set_bind_group( + hardware_pass.set_render_pipeline(visibility_buffer_hardware_raster_pipeline); + hardware_pass.set_push_constants( + ShaderStages::VERTEX, + 0, + &raster_cluster_rightmost_slot.to_le_bytes(), + ); + hardware_pass.set_bind_group( 0, &meshlet_view_bind_groups.visibility_buffer_raster, &[view_offset.offset], ); - draw_pass.draw_indirect(visibility_buffer_draw_indirect_args, 0); + hardware_pass.draw_indirect(visibility_buffer_hardware_raster_indirect_args, 0); } fn downsample_depth( @@ -396,7 +473,10 @@ fn downsample_depth( downsample_pass.set_pipeline(downsample_depth_first_pipeline); downsample_pass.set_push_constants( 0, - &meshlet_view_resources.depth_pyramid_mip_count.to_le_bytes(), + bytemuck::cast_slice(&[ + meshlet_view_resources.depth_pyramid_mip_count, + meshlet_view_resources.view_size.x, + ]), ); downsample_pass.set_bind_group(0, &meshlet_view_bind_groups.downsample_depth, &[]); downsample_pass.dispatch_workgroups( @@ -411,19 +491,47 @@ fn downsample_depth( } } -fn copy_material_depth_pass( +fn resolve_depth( + render_context: &mut RenderContext, + depth_stencil_attachment: RenderPassDepthStencilAttachment, + meshlet_view_resources: &MeshletViewResources, + meshlet_view_bind_groups: &MeshletViewBindGroups, + resolve_depth_pipeline: &RenderPipeline, + camera: &ExtractedCamera, +) { + let mut resolve_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("resolve_depth"), + color_attachments: &[], + depth_stencil_attachment: Some(depth_stencil_attachment), + timestamp_writes: None, + occlusion_query_set: None, + }); + if let Some(viewport) = &camera.viewport { + resolve_pass.set_camera_viewport(viewport); + } + resolve_pass.set_render_pipeline(resolve_depth_pipeline); + resolve_pass.set_push_constants( + ShaderStages::FRAGMENT, + 0, + &meshlet_view_resources.view_size.x.to_le_bytes(), + ); + resolve_pass.set_bind_group(0, &meshlet_view_bind_groups.resolve_depth, &[]); + resolve_pass.draw(0..3, 0..1); +} + +fn resolve_material_depth( render_context: &mut RenderContext, meshlet_view_resources: &MeshletViewResources, meshlet_view_bind_groups: &MeshletViewBindGroups, - copy_material_depth_pipeline: &RenderPipeline, + resolve_material_depth_pipeline: &RenderPipeline, camera: &ExtractedCamera, ) { - if let (Some(material_depth), Some(copy_material_depth_bind_group)) = ( + if let (Some(material_depth), Some(resolve_material_depth_bind_group)) = ( meshlet_view_resources.material_depth.as_ref(), - meshlet_view_bind_groups.copy_material_depth.as_ref(), + meshlet_view_bind_groups.resolve_material_depth.as_ref(), ) { - let mut copy_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { - label: Some("copy_material_depth"), + let mut resolve_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("resolve_material_depth"), color_attachments: &[], depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { view: &material_depth.default_view, @@ -437,11 +545,15 @@ fn copy_material_depth_pass( occlusion_query_set: None, }); if let Some(viewport) = &camera.viewport { - copy_pass.set_camera_viewport(viewport); + resolve_pass.set_camera_viewport(viewport); } - - copy_pass.set_render_pipeline(copy_material_depth_pipeline); - copy_pass.set_bind_group(0, copy_material_depth_bind_group, &[]); - copy_pass.draw(0..3, 0..1); + resolve_pass.set_render_pipeline(resolve_material_depth_pipeline); + resolve_pass.set_push_constants( + ShaderStages::FRAGMENT, + 0, + &meshlet_view_resources.view_size.x.to_le_bytes(), + ); + resolve_pass.set_bind_group(0, resolve_material_depth_bind_group, &[]); + resolve_pass.draw(0..3, 0..1); } } diff --git a/crates/bevy_pbr/src/meshlet/visibility_buffer_resolve.wgsl b/crates/bevy_pbr/src/meshlet/visibility_buffer_resolve.wgsl index baf72afcc4cab5..bb35c1649734a6 100644 --- a/crates/bevy_pbr/src/meshlet/visibility_buffer_resolve.wgsl +++ b/crates/bevy_pbr/src/meshlet/visibility_buffer_resolve.wgsl @@ -95,13 +95,14 @@ struct VertexOutput { /// Load the visibility buffer texture and resolve it into a VertexOutput. fn resolve_vertex_output(frag_coord: vec4) -> VertexOutput { - let packed_ids = textureLoad(meshlet_visibility_buffer, vec2(frag_coord.xy), 0).r; + let frag_coord_1d = u32(frag_coord.y) * u32(view.viewport.z) + u32(frag_coord.x); + let packed_ids = u32(meshlet_visibility_buffer[frag_coord_1d]); // TODO: Might be faster to load the correct u32 directly let cluster_id = packed_ids >> 6u; let meshlet_id = meshlet_cluster_meshlet_ids[cluster_id]; let meshlet = meshlets[meshlet_id]; let triangle_id = extractBits(packed_ids, 0u, 6u); - let index_ids = meshlet.start_index_id + vec3(triangle_id * 3u) + vec3(0u, 1u, 2u); + let index_ids = meshlet.start_index_id + (triangle_id * 3u) + vec3(0u, 1u, 2u); let indices = meshlet.start_vertex_id + vec3(get_meshlet_index(index_ids.x), get_meshlet_index(index_ids.y), get_meshlet_index(index_ids.z)); let vertex_ids = vec3(meshlet_vertex_ids[indices.x], meshlet_vertex_ids[indices.y], meshlet_vertex_ids[indices.z]); let vertex_1 = unpack_meshlet_vertex(meshlet_vertex_data[vertex_ids.x]); diff --git a/crates/bevy_pbr/src/meshlet/visibility_buffer_software_raster.wgsl b/crates/bevy_pbr/src/meshlet/visibility_buffer_software_raster.wgsl new file mode 100644 index 00000000000000..02feaaeaeda4ca --- /dev/null +++ b/crates/bevy_pbr/src/meshlet/visibility_buffer_software_raster.wgsl @@ -0,0 +1,196 @@ +#import bevy_pbr::{ + meshlet_bindings::{ + meshlet_cluster_meshlet_ids, + meshlets, + meshlet_vertex_ids, + meshlet_vertex_data, + meshlet_cluster_instance_ids, + meshlet_instance_uniforms, + meshlet_raster_clusters, + meshlet_software_raster_cluster_count, + meshlet_visibility_buffer, + view, + get_meshlet_index, + unpack_meshlet_vertex, + }, + mesh_functions::mesh_position_local_to_world, + view_transformations::ndc_to_uv, +} +#import bevy_render::maths::affine3_to_square + +/// Compute shader for rasterizing small clusters into a visibility buffer. + +// TODO: Subpixel precision and top-left rule + +var viewport_vertices: array; + +@compute +@workgroup_size(64, 1, 1) // 64 threads per workgroup, 1 vertex/triangle per thread, 1 cluster per workgroup +fn rasterize_cluster( + @builtin(workgroup_id) workgroup_id: vec3, + @builtin(local_invocation_index) local_invocation_index: u32, +#ifdef MESHLET_2D_DISPATCH + @builtin(num_workgroups) num_workgroups: vec3, +#endif +) { + var workgroup_id_1d = workgroup_id.x; + +#ifdef MESHLET_2D_DISPATCH + workgroup_id_1d += workgroup_id.y * num_workgroups.x; + 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]; + let meshlet = meshlets[meshlet_id]; + + // Load and project 1 vertex per thread + let vertex_id = local_invocation_index; + if vertex_id < meshlet.vertex_count { + let meshlet_vertex_id = meshlet_vertex_ids[meshlet.start_vertex_id + vertex_id]; + let vertex = unpack_meshlet_vertex(meshlet_vertex_data[meshlet_vertex_id]); + + // Project vertex to viewport space + let instance_id = meshlet_cluster_instance_ids[cluster_id]; + let instance_uniform = meshlet_instance_uniforms[instance_id]; + let world_from_local = affine3_to_square(instance_uniform.world_from_local); + let world_position = mesh_position_local_to_world(world_from_local, vec4(vertex.position, 1.0)); + var clip_position = view.clip_from_world * vec4(world_position.xyz, 1.0); + var ndc_position = clip_position.xyz / clip_position.w; +#ifdef DEPTH_CLAMP_ORTHO + ndc_position.z = 1.0 / clip_position.z; +#endif + let viewport_position_xy = ndc_to_uv(ndc_position.xy) * view.viewport.zw; + + // Write vertex to workgroup shared memory + viewport_vertices[vertex_id] = vec3(viewport_position_xy, ndc_position.z); + } + + workgroupBarrier(); + + // Load 1 triangle's worth of vertex data per thread + let triangle_id = local_invocation_index; + if triangle_id >= meshlet.triangle_count { return; } + let index_ids = meshlet.start_index_id + (triangle_id * 3u) + vec3(0u, 1u, 2u); + let vertex_ids = vec3(get_meshlet_index(index_ids[0]), get_meshlet_index(index_ids[1]), get_meshlet_index(index_ids[2])); + let vertex_0 = viewport_vertices[vertex_ids[2]]; + let vertex_1 = viewport_vertices[vertex_ids[1]]; + let vertex_2 = viewport_vertices[vertex_ids[0]]; + let packed_ids = (cluster_id << 6u) | triangle_id; + + // Compute triangle bounding box + let min_x = u32(min3(vertex_0.x, vertex_1.x, vertex_2.x)); + let min_y = u32(min3(vertex_0.y, vertex_1.y, vertex_2.y)); + var max_x = u32(ceil(max3(vertex_0.x, vertex_1.x, vertex_2.x))); + var max_y = u32(ceil(max3(vertex_0.y, vertex_1.y, vertex_2.y))); + max_x = min(max_x, u32(view.viewport.z) - 1u); + max_y = min(max_y, u32(view.viewport.w) - 1u); + if any(vec2(min_x, min_y) > vec2(max_x, max_y)) { return; } + + // Setup triangle gradients + let w_x = vec3(vertex_1.y - vertex_2.y, vertex_2.y - vertex_0.y, vertex_0.y - vertex_1.y); + let w_y = vec3(vertex_2.x - vertex_1.x, vertex_0.x - vertex_2.x, vertex_1.x - vertex_0.x); + let triangle_double_area = edge_function(vertex_0.xy, vertex_1.xy, vertex_2.xy); // TODO: Reuse earlier calculations and take advantage of summing to 1 + let vertices_z = vec3(vertex_0.z, vertex_1.z, vertex_2.z) / triangle_double_area; + let z_x = dot(vertices_z, w_x); + let z_y = dot(vertices_z, w_y); + + // Setup initial triangle equations + let starting_pixel = vec2(f32(min_x), f32(min_y)) + 0.5; + var w_row = vec3( + // TODO: Reuse earlier calculations and take advantage of summing to 1 + edge_function(vertex_1.xy, vertex_2.xy, starting_pixel), + edge_function(vertex_2.xy, vertex_0.xy, starting_pixel), + edge_function(vertex_0.xy, vertex_1.xy, starting_pixel), + ); + var z_row = dot(vertices_z, w_row); + let view_width = u32(view.viewport.z); + var frag_coord_1d_row = min_y * view_width; + + // Rasterize triangle + if subgroupAny(max_x - min_x > 4u) { + // Scanline setup + let edge_012 = -w_x; + let open_edge = edge_012 < vec3(0.0); + let inverse_edge_012 = select(1.0 / edge_012, vec3(1e8), edge_012 == vec3(0.0)); + let max_x_diff = vec3(max_x - min_x); + for (var y = min_y; y <= max_y; y++) { + // Calculate start and end X interval for pixels in this row within the triangle + let cross_x = w_row * inverse_edge_012; + let min_x2 = select(vec3(0.0), cross_x, open_edge); + let max_x2 = select(cross_x, max_x_diff, open_edge); + var x0 = u32(ceil(max3(min_x2[0], min_x2[1], min_x2[2]))); + var x1 = u32(min3(max_x2[0], max_x2[1], max_x2[2])); + + var w = w_row + w_x * f32(x0); + var z = z_row + z_x * f32(x0); + x0 += min_x; + x1 += min_x; + + // Iterate scanline X interval + for (var x = x0; x <= x1; x++) { + // Check if point at pixel is within triangle (TODO: this shouldn't be needed, but there's bugs without it) + if min3(w[0], w[1], w[2]) >= 0.0 { + write_visibility_buffer_pixel(frag_coord_1d_row + x, z, packed_ids); + } + + // Increment edge functions along the X-axis + w += w_x; + z += z_x; + } + + // Increment edge functions along the Y-axis + w_row += w_y; + z_row += z_y; + frag_coord_1d_row += view_width; + } + } else { + // Iterate over every pixel in the triangle's bounding box + for (var y = min_y; y <= max_y; y++) { + var w = w_row; + var z = z_row; + + for (var x = min_x; x <= max_x; x++) { + // Check if point at pixel is within triangle + if min3(w[0], w[1], w[2]) >= 0.0 { + write_visibility_buffer_pixel(frag_coord_1d_row + x, z, packed_ids); + } + + // Increment edge functions along the X-axis + w += w_x; + z += z_x; + } + + // Increment edge functions along the Y-axis + w_row += w_y; + z_row += z_y; + frag_coord_1d_row += view_width; + } + } +} + +fn write_visibility_buffer_pixel(frag_coord_1d: u32, z: f32, packed_ids: u32) { +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT + let depth = bitcast(z); + let visibility = (u64(depth) << 32u) | u64(packed_ids); + atomicMax(&meshlet_visibility_buffer[frag_coord_1d], visibility); +#else ifdef DEPTH_CLAMP_ORTHO + let depth = bitcast(1.0 / z); + atomicMax(&meshlet_visibility_buffer[frag_coord_1d], depth); +#else + let depth = bitcast(z); + atomicMax(&meshlet_visibility_buffer[frag_coord_1d], depth); +#endif +} + +fn edge_function(a: vec2, b: vec2, c: vec2) -> f32 { + return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x); +} + +fn min3(a: f32, b: f32, c: f32) -> f32 { + return min(a, min(b, c)); +} + +fn max3(a: f32, b: f32, c: f32) -> f32 { + return max(a, max(b, c)); +} diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index a8ca69a41f176d..e545e3df3b21e5 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -31,7 +31,7 @@ use bevy_utils::tracing::error; #[cfg(feature = "meshlet")] use crate::meshlet::{ - prepare_material_meshlet_meshes_prepass, queue_material_meshlet_meshes, MeshletGpuScene, + prepare_material_meshlet_meshes_prepass, queue_material_meshlet_meshes, InstanceManager, MeshletMesh, }; use crate::*; @@ -186,7 +186,7 @@ where .in_set(RenderSet::QueueMeshes) .after(prepare_assets::>) .before(queue_material_meshlet_meshes::) - .run_if(resource_exists::), + .run_if(resource_exists::), ); } } diff --git a/crates/bevy_render/src/render_resource/mod.rs b/crates/bevy_render/src/render_resource/mod.rs index 4b914ef413556a..42aed363395d14 100644 --- a/crates/bevy_render/src/render_resource/mod.rs +++ b/crates/bevy_render/src/render_resource/mod.rs @@ -32,7 +32,10 @@ pub use uniform_buffer::*; // TODO: decide where re-exports should go pub use wgpu::{ - util::{BufferInitDescriptor, DrawIndexedIndirectArgs, DrawIndirectArgs, TextureDataOrder}, + util::{ + BufferInitDescriptor, DispatchIndirectArgs, DrawIndexedIndirectArgs, DrawIndirectArgs, + TextureDataOrder, + }, AdapterInfo as WgpuAdapterInfo, AddressMode, AstcBlock, AstcChannel, BindGroupDescriptor, BindGroupEntry, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType, BlendComponent, BlendFactor, BlendOperation, BlendState, BufferAddress, BufferAsyncError, diff --git a/examples/3d/meshlet.rs b/examples/3d/meshlet.rs index 4721e0c9819df8..20264b43a15f38 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/b6c712cfc87c65de419f856845401aba336a7bcd/bunny.meshlet_mesh"; + "https://raw.githubusercontent.com/JMS55/bevy_meshlet_asset/10bb5471c7beedfe63ad1cf269599c92b0f10aa2/bunny.meshlet_mesh"; fn main() -> ExitCode { if !Path::new("./assets/models/bunny.meshlet_mesh").exists() { @@ -29,7 +29,9 @@ fn main() -> ExitCode { .insert_resource(DirectionalLightShadowMap { size: 4096 }) .add_plugins(( DefaultPlugins, - MeshletPlugin, + MeshletPlugin { + cluster_buffer_slots: 8192, + }, MaterialPlugin::::default(), CameraControllerPlugin, )) From 2e36b2719ca9fa29b41f28f4c6126d4eb84dff35 Mon Sep 17 00:00:00 2001 From: Stepan Koltsov Date: Mon, 26 Aug 2024 18:56:37 +0100 Subject: [PATCH 18/53] ImageSampler::init_descriptor (#11113) Shortcut to avoid repetition in code like https://github.com/bevyengine/bevy/pull/11109. --------- Co-authored-by: Alice Cecile --- crates/bevy_render/src/texture/image.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/crates/bevy_render/src/texture/image.rs b/crates/bevy_render/src/texture/image.rs index 4eca33b20fa62c..8d92395b63c091 100644 --- a/crates/bevy_render/src/texture/image.rs +++ b/crates/bevy_render/src/texture/image.rs @@ -202,6 +202,23 @@ impl ImageSampler { pub fn nearest() -> ImageSampler { ImageSampler::Descriptor(ImageSamplerDescriptor::nearest()) } + + /// Initialize the descriptor if it is not already initialized. + /// + /// Descriptor is typically initialized by Bevy when the image is loaded, + /// so this is convenient shortcut for updating the descriptor. + pub fn get_or_init_descriptor(&mut self) -> &mut ImageSamplerDescriptor { + match self { + ImageSampler::Default => { + *self = ImageSampler::Descriptor(ImageSamplerDescriptor::default()); + match self { + ImageSampler::Descriptor(descriptor) => descriptor, + _ => unreachable!(), + } + } + ImageSampler::Descriptor(descriptor) => descriptor, + } + } } /// A rendering resource for the default image sampler which is set during renderer From 6819e998c078535dd4fa9e687481646d2b6df5e4 Mon Sep 17 00:00:00 2001 From: Robert Walter <26892280+RobWalt@users.noreply.github.com> Date: Mon, 26 Aug 2024 17:57:57 +0000 Subject: [PATCH 19/53] Fix `arc_2d` Gizmos (#14731) # Objective `arc_2d` wasn't actually doing what the docs were saying. The arc wasn't offset by what was previously `direction_angle` but by `direction_angle - arc_angle / 2.0`. This meant that the arcs center was laying on the `Vec2::Y` axis and then it was offset. This was probably done to fit the behavior of the `Arc2D` primitive. I would argue that this isn't desirable for the plain `arc_2d` gizmo method since - a) the docs get longer to explain the weird centering - b) the mental model the user has to know gets bigger with more implicit assumptions given the code ```rust my_gizmos.arc_2d(Vec2::ZERO, 0.0, FRAC_PI_2, 75.0, ORANGE_RED); ``` we get ![image](https://github.com/user-attachments/assets/84894c6d-42e4-451b-b3e2-811266486ede) where after the fix with ```rust my_gizmos.arc_2d(Isometry2d::IDENTITY, FRAC_PI_2, 75.0, ORANGE_RED); ``` we get ![image](https://github.com/user-attachments/assets/16b0aba0-f7b5-4600-ac49-a22be0315c40) To get the same result with the previous implementation you would have to randomly add `arc_angle / 2.0` to the `direction_angle`. ```rust my_gizmos.arc_2d(Vec2::ZERO, FRAC_PI_4, FRAC_PI_2, 75.0, ORANGE_RED); ``` This makes constructing similar helping functions as they already exist in 3D like - `long_arc_2d_between` - `short_arc_2d_between` much harder. ## Solution - Make the arc really start at `Vec2::Y * radius` in counter-clockwise direction + offset by an angle as the docs state it - Use `Isometry2d` instead of `position : Vec2` and `direction_angle : f32` to reduce the chance of messing up rotation/translation - Adjust the docs for the changes above - Adjust the gizmo rendering of some primitives ## Testing - check `2d_gizmos.rs` and `render_primitives.rs` examples ## Migration Guide - users have to adjust their usages of `arc_2d`: - before: ```rust arc_2d( pos, angle, arc_angle, radius, color ) ``` - after: ```rust arc_2d( // this `+ arc_angle * 0.5` quirk is only if you want to preserve the previous behavior // with the new API. // feel free to try to fix this though since your current calls to this function most likely // involve some computations to counter-act that quirk in the first place Isometry2d::new(pos, Rot2::radians(angle + arc_angle * 0.5), arc_angle, radius, color ) ``` --- crates/bevy_gizmos/src/arcs.rs | 55 +++++++++-------------- crates/bevy_gizmos/src/primitives/dim2.rs | 23 ++++------ crates/bevy_math/src/lib.rs | 6 +-- examples/gizmos/2d_gizmos.rs | 12 +++-- 4 files changed, 41 insertions(+), 55 deletions(-) diff --git a/crates/bevy_gizmos/src/arcs.rs b/crates/bevy_gizmos/src/arcs.rs index 14c260dd307465..1aa6b5c78a0b6e 100644 --- a/crates/bevy_gizmos/src/arcs.rs +++ b/crates/bevy_gizmos/src/arcs.rs @@ -6,8 +6,8 @@ use crate::circles::DEFAULT_CIRCLE_RESOLUTION; use crate::prelude::{GizmoConfigGroup, Gizmos}; use bevy_color::Color; -use bevy_math::{Quat, Vec2, Vec3}; -use std::f32::consts::TAU; +use bevy_math::{Isometry2d, Quat, Vec2, Vec3}; +use std::f32::consts::{FRAC_PI_2, TAU}; // === 2D === @@ -21,9 +21,9 @@ where /// This should be called for each frame the arc needs to be rendered. /// /// # Arguments - /// - `position` sets the center of this circle. - /// - `direction_angle` sets the counter-clockwise angle in radians between `Vec2::Y` and - /// the vector from `position` to the midpoint of the arc. + /// - `isometry` defines the translation and rotation of the arc. + /// - the translation specifies the center of the arc + /// - the rotation is counter-clockwise starting from `Vec2::Y` /// - `arc_angle` sets the length of this arc, in radians. /// - `radius` controls the distance from `position` to this arc, and thus its curvature. /// - `color` sets the color to draw the arc. @@ -32,15 +32,15 @@ where /// ``` /// # use bevy_gizmos::prelude::*; /// # use bevy_math::prelude::*; - /// # use std::f32::consts::PI; + /// # use std::f32::consts::FRAC_PI_4; /// # use bevy_color::palettes::basic::{GREEN, RED}; /// fn system(mut gizmos: Gizmos) { - /// gizmos.arc_2d(Vec2::ZERO, 0., PI / 4., 1., GREEN); + /// gizmos.arc_2d(Isometry2d::IDENTITY, FRAC_PI_4, 1., GREEN); /// /// // Arcs have 32 line-segments by default. /// // You may want to increase this for larger arcs. /// gizmos - /// .arc_2d(Vec2::ZERO, 0., PI / 4., 5., RED) + /// .arc_2d(Isometry2d::IDENTITY, FRAC_PI_4, 5., RED) /// .resolution(64); /// } /// # bevy_ecs::system::assert_is_system(system); @@ -48,16 +48,14 @@ where #[inline] pub fn arc_2d( &mut self, - position: Vec2, - direction_angle: f32, + isometry: Isometry2d, arc_angle: f32, radius: f32, color: impl Into, ) -> Arc2dBuilder<'_, 'w, 's, Config, Clear> { Arc2dBuilder { gizmos: self, - position, - direction_angle, + isometry, arc_angle, radius, color: color.into(), @@ -73,8 +71,7 @@ where Clear: 'static + Send + Sync, { gizmos: &'a mut Gizmos<'w, 's, Config, Clear>, - position: Vec2, - direction_angle: f32, + isometry: Isometry2d, arc_angle: f32, radius: f32, color: Color, @@ -107,31 +104,19 @@ where .resolution .unwrap_or_else(|| resolution_from_angle(self.arc_angle)); - let positions = arc_2d_inner( - self.direction_angle, - self.arc_angle, - self.radius, - resolution, - ) - .map(|vec2| (vec2 + self.position)); + let positions = + arc_2d_inner(self.arc_angle, self.radius, resolution).map(|vec2| self.isometry * vec2); self.gizmos.linestrip_2d(positions, self.color); } } -fn arc_2d_inner( - direction_angle: f32, - arc_angle: f32, - radius: f32, - resolution: u32, -) -> impl Iterator { - (0..resolution + 1).map(move |i| { - let start = direction_angle - arc_angle / 2.; - - let angle = - start + (i as f32 * (arc_angle / resolution as f32)) + std::f32::consts::FRAC_PI_2; - - Vec2::new(angle.cos(), angle.sin()) * radius - }) +fn arc_2d_inner(arc_angle: f32, radius: f32, resolution: u32) -> impl Iterator { + (0..=resolution) + .map(move |n| arc_angle * n as f32 / resolution as f32) + .map(|angle| angle + FRAC_PI_2) + .map(f32::sin_cos) + .map(|(sin, cos)| Vec2::new(cos, sin)) + .map(move |vec2| vec2 * radius) } // === 3D === diff --git a/crates/bevy_gizmos/src/primitives/dim2.rs b/crates/bevy_gizmos/src/primitives/dim2.rs index e05eaba6453e2f..f57904bb806ccf 100644 --- a/crates/bevy_gizmos/src/primitives/dim2.rs +++ b/crates/bevy_gizmos/src/primitives/dim2.rs @@ -1,6 +1,6 @@ //! A module for rendering each of the 2D [`bevy_math::primitives`] with [`Gizmos`]. -use std::f32::consts::PI; +use std::f32::consts::{FRAC_PI_2, PI}; use super::helpers::*; @@ -10,7 +10,7 @@ use bevy_math::primitives::{ CircularSegment, Ellipse, Line2d, Plane2d, Polygon, Polyline2d, Primitive2d, Rectangle, RegularPolygon, Rhombus, Segment2d, Triangle2d, }; -use bevy_math::{Dir2, Mat2, Vec2}; +use bevy_math::{Dir2, Isometry2d, Mat2, Rot2, Vec2}; use crate::prelude::{GizmoConfigGroup, Gizmos}; @@ -86,8 +86,7 @@ where } self.arc_2d( - position, - angle, + Isometry2d::new(position, Rot2::radians(angle - primitive.half_angle)), primitive.half_angle * 2.0, primitive.radius, color, @@ -139,8 +138,7 @@ where // we need to draw the arc part of the sector, and the two lines connecting the arc and the center self.arc_2d( - position, - angle, + Isometry2d::new(position, Rot2::radians(angle - primitive.arc.half_angle)), primitive.arc.half_angle * 2.0, primitive.arc.radius, color, @@ -179,8 +177,7 @@ where // we need to draw the arc part of the segment, and the line connecting the two ends self.arc_2d( - position, - angle, + Isometry2d::new(position, Rot2::radians(angle - primitive.arc.half_angle)), primitive.arc.half_angle * 2.0, primitive.arc.radius, color, @@ -386,20 +383,18 @@ where self.line_2d(bottom_left, top_left, polymorphic_color); self.line_2d(bottom_right, top_right, polymorphic_color); - let start_angle_top = angle; - let start_angle_bottom = PI + angle; + let start_angle_top = angle - FRAC_PI_2; + let start_angle_bottom = angle + FRAC_PI_2; // draw arcs self.arc_2d( - top_center, - start_angle_top, + Isometry2d::new(top_center, Rot2::radians(start_angle_top)), PI, primitive.radius, polymorphic_color, ); self.arc_2d( - bottom_center, - start_angle_bottom, + Isometry2d::new(bottom_center, Rot2::radians(start_angle_bottom)), PI, primitive.radius, polymorphic_color, diff --git a/crates/bevy_math/src/lib.rs b/crates/bevy_math/src/lib.rs index b765a2759e751c..76ad5b06a7b5a8 100644 --- a/crates/bevy_math/src/lib.rs +++ b/crates/bevy_math/src/lib.rs @@ -57,9 +57,9 @@ pub mod prelude { }, direction::{Dir2, Dir3, Dir3A}, primitives::*, - BVec2, BVec3, BVec4, EulerRot, FloatExt, IRect, IVec2, IVec3, IVec4, Mat2, Mat3, Mat4, - Quat, Ray2d, Ray3d, Rect, Rot2, StableInterpolate, URect, UVec2, UVec3, UVec4, Vec2, - Vec2Swizzles, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles, + BVec2, BVec3, BVec4, EulerRot, FloatExt, IRect, IVec2, IVec3, IVec4, Isometry2d, + Isometry3d, Mat2, Mat3, Mat4, Quat, Ray2d, Ray3d, Rect, Rot2, StableInterpolate, URect, + UVec2, UVec3, UVec4, Vec2, Vec2Swizzles, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles, }; } diff --git a/examples/gizmos/2d_gizmos.rs b/examples/gizmos/2d_gizmos.rs index 830412e536a378..b935b2fb7a2d41 100644 --- a/examples/gizmos/2d_gizmos.rs +++ b/examples/gizmos/2d_gizmos.rs @@ -1,8 +1,8 @@ //! This example demonstrates Bevy's immediate mode drawing API intended for visual debugging. -use std::f32::consts::{PI, TAU}; +use std::f32::consts::{FRAC_PI_2, PI, TAU}; -use bevy::{color::palettes::css::*, prelude::*}; +use bevy::{color::palettes::css::*, math::Isometry2d, prelude::*}; fn main() { App::new() @@ -87,7 +87,13 @@ fn draw_example_collection( // Arcs default resolution is linearly interpolated between // 1 and 32, using the arc length as scalar. - my_gizmos.arc_2d(Vec2::ZERO, sin / 10., PI / 2., 310., ORANGE_RED); + my_gizmos.arc_2d( + Isometry2d::from_rotation(Rot2::radians(sin / 10.)), + FRAC_PI_2, + 310., + ORANGE_RED, + ); + my_gizmos.arc_2d(Isometry2d::IDENTITY, FRAC_PI_2, 75.0, ORANGE_RED); gizmos.arrow_2d( Vec2::ZERO, From 3540b87e17c078285f1abfec1a02fe13cf15a57b Mon Sep 17 00:00:00 2001 From: JoshValjosh <48692273+jnhyatt@users.noreply.github.com> Date: Mon, 26 Aug 2024 12:01:32 -0600 Subject: [PATCH 20/53] Add bevy_picking sprite backend (#14757) # Objective Add `bevy_picking` sprite backend as part of the `bevy_mod_picking` upstreamening (#12365). ## Solution More or less a copy/paste from `bevy_mod_picking`, with the changes [here](https://github.com/aevyrie/bevy_mod_picking/pull/354). I'm putting that link here since those changes haven't yet made it through review, so should probably be reviewed on their own. ## Testing I couldn't find any sprite-backend-specific tests in `bevy_mod_picking` and unfortunately I'm not familiar enough with Bevy's testing patterns to write tests for code that relies on windowing and input. I'm willing to break the pointer hit system into testable blocks and add some more modular tests if that's deemed important enough to block, otherwise I can open an issue for adding tests as follow-up. ## Follow-up work - More docs/tests - Ignore pick events on transparent sprite pixels with potential opt-out --------- Co-authored-by: Aevyrie --- Cargo.toml | 11 ++ crates/bevy_internal/Cargo.toml | 6 +- crates/bevy_picking/src/backend.rs | 4 + crates/bevy_picking/src/lib.rs | 4 + crates/bevy_sprite/Cargo.toml | 3 + crates/bevy_sprite/src/lib.rs | 5 + crates/bevy_sprite/src/picking_backend.rs | 171 ++++++++++++++++++++++ examples/README.md | 1 + examples/picking/sprite_picking.rs | 160 ++++++++++++++++++++ 9 files changed, 364 insertions(+), 1 deletion(-) create mode 100644 crates/bevy_sprite/src/picking_backend.rs create mode 100644 examples/picking/sprite_picking.rs diff --git a/Cargo.toml b/Cargo.toml index 16a933183d7e6e..fde3db1f420d24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3384,6 +3384,17 @@ description = "Demonstrates how to use picking events to spawn simple objects" category = "Picking" wasm = true +[[example]] +name = "sprite_picking" +path = "examples/picking/sprite_picking.rs" +doc-scrape-examples = true + +[package.metadata.example.sprite_picking] +name = "Sprite Picking" +description = "Demonstrates picking sprites and sprite atlases" +category = "Picking" +wasm = true + [profile.wasm-release] inherits = "release" opt-level = "z" diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 424a1416beca20..2cd6e221623190 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -190,7 +190,11 @@ meshlet_processor = ["bevy_pbr?/meshlet_processor"] bevy_dev_tools = ["dep:bevy_dev_tools"] # Provides a picking functionality -bevy_picking = ["dep:bevy_picking", "bevy_ui?/bevy_picking"] +bevy_picking = [ + "dep:bevy_picking", + "bevy_ui?/bevy_picking", + "bevy_sprite?/bevy_picking", +] # Enable support for the ios_simulator by downgrading some rendering capabilities ios_simulator = ["bevy_pbr?/ios_simulator", "bevy_render?/ios_simulator"] diff --git a/crates/bevy_picking/src/backend.rs b/crates/bevy_picking/src/backend.rs index 606459b18ee1d0..fb46ac7864172e 100644 --- a/crates/bevy_picking/src/backend.rs +++ b/crates/bevy_picking/src/backend.rs @@ -50,6 +50,10 @@ pub mod prelude { /// Some backends may only support providing the topmost entity; this is a valid limitation of some /// backends. For example, a picking shader might only have data on the topmost rendered output from /// its buffer. +/// +/// Note that systems reading these events in [`PreUpdate`](bevy_app) will not report ordering +/// ambiguities with picking backends. Take care to ensure such systems are explicitly ordered +/// against [`PickSet::Backends`](crate), or better, avoid reading `PointerHits` in `PreUpdate`. #[derive(Event, Debug, Clone)] pub struct PointerHits { /// The pointer associated with this hit test. diff --git a/crates/bevy_picking/src/lib.rs b/crates/bevy_picking/src/lib.rs index ba0ef8eee98faf..cd75515c974226 100644 --- a/crates/bevy_picking/src/lib.rs +++ b/crates/bevy_picking/src/lib.rs @@ -207,6 +207,10 @@ impl Plugin for PickingPlugin { .add_event::() .add_event::() .add_event::() + // Rather than try to mark all current and future backends as ambiguous with each other, + // we allow them to send their hits in any order. These are later sorted, so submission + // order doesn't matter. See `PointerHits` docs for caveats. + .allow_ambiguous_resource::>() .add_systems( PreUpdate, ( diff --git a/crates/bevy_sprite/Cargo.toml b/crates/bevy_sprite/Cargo.toml index 367e7bb68ca12c..59d33d76e0a3ed 100644 --- a/crates/bevy_sprite/Cargo.toml +++ b/crates/bevy_sprite/Cargo.toml @@ -9,6 +9,7 @@ license = "MIT OR Apache-2.0" keywords = ["bevy"] [features] +bevy_picking = ["dep:bevy_picking", "dep:bevy_window"] webgl = [] webgpu = [] @@ -20,12 +21,14 @@ bevy_color = { path = "../bevy_color", version = "0.15.0-dev" } bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.15.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } bevy_math = { path = "../bevy_math", version = "0.15.0-dev" } +bevy_picking = { path = "../bevy_picking", version = "0.15.0-dev", optional = true } bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [ "bevy", ] } bevy_render = { path = "../bevy_render", version = "0.15.0-dev" } bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" } bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } +bevy_window = { path = "../bevy_window", version = "0.15.0-dev", optional = true } bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" } # other diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index 77a31f505e0b81..1278b73fdbde7b 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -11,6 +11,8 @@ mod bundle; mod dynamic_texture_atlas_builder; mod mesh2d; +#[cfg(feature = "bevy_picking")] +mod picking_backend; mod render; mod sprite; mod texture_atlas; @@ -133,6 +135,9 @@ impl Plugin for SpritePlugin { ), ); + #[cfg(feature = "bevy_picking")] + app.add_plugins(picking_backend::SpritePickingBackend); + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app .init_resource::() diff --git a/crates/bevy_sprite/src/picking_backend.rs b/crates/bevy_sprite/src/picking_backend.rs new file mode 100644 index 00000000000000..cc1728b151ac22 --- /dev/null +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -0,0 +1,171 @@ +//! A [`bevy_picking`] backend for sprites. Works for simple sprites and sprite atlases. Works for +//! sprites with arbitrary transforms. Picking is done based on sprite bounds, not visible pixels. +//! This means a partially transparent sprite is pickable even in its transparent areas. + +use std::cmp::Ordering; + +use crate::{Sprite, TextureAtlas, TextureAtlasLayout}; +use bevy_app::prelude::*; +use bevy_asset::prelude::*; +use bevy_ecs::prelude::*; +use bevy_math::{prelude::*, FloatExt}; +use bevy_picking::backend::prelude::*; +use bevy_render::prelude::*; +use bevy_transform::prelude::*; +use bevy_window::PrimaryWindow; + +#[derive(Clone)] +pub struct SpritePickingBackend; + +impl Plugin for SpritePickingBackend { + fn build(&self, app: &mut App) { + app.add_systems(PreUpdate, sprite_picking.in_set(PickSet::Backend)); + } +} + +pub fn sprite_picking( + pointers: Query<(&PointerId, &PointerLocation)>, + cameras: Query<(Entity, &Camera, &GlobalTransform, &OrthographicProjection)>, + primary_window: Query>, + images: Res>, + texture_atlas_layout: Res>, + sprite_query: Query< + ( + Entity, + Option<&Sprite>, + Option<&TextureAtlas>, + Option<&Handle>, + &GlobalTransform, + Option<&Pickable>, + &ViewVisibility, + ), + Or<(With, With)>, + >, + mut output: EventWriter, +) { + let mut sorted_sprites: Vec<_> = sprite_query.iter().collect(); + sorted_sprites.sort_by(|a, b| { + (b.4.translation().z) + .partial_cmp(&a.4.translation().z) + .unwrap_or(Ordering::Equal) + }); + + let primary_window = primary_window.get_single().ok(); + + for (pointer, location) in pointers.iter().filter_map(|(pointer, pointer_location)| { + pointer_location.location().map(|loc| (pointer, loc)) + }) { + let mut blocked = false; + let Some((cam_entity, camera, cam_transform, cam_ortho)) = cameras + .iter() + .filter(|(_, camera, _, _)| camera.is_active) + .find(|(_, camera, _, _)| { + camera + .target + .normalize(primary_window) + .map(|x| x == location.target) + .unwrap_or(false) + }) + else { + continue; + }; + + let Some(cursor_ray_world) = camera.viewport_to_world(cam_transform, location.position) + else { + continue; + }; + let cursor_ray_len = cam_ortho.far - cam_ortho.near; + let cursor_ray_end = cursor_ray_world.origin + cursor_ray_world.direction * cursor_ray_len; + + let picks: Vec<(Entity, HitData)> = sorted_sprites + .iter() + .copied() + .filter(|(.., visibility)| visibility.get()) + .filter_map( + |(entity, sprite, atlas, image, sprite_transform, pickable, ..)| { + if blocked { + return None; + } + + // Hit box in sprite coordinate system + let (extents, anchor) = if let Some((sprite, atlas)) = sprite.zip(atlas) { + let extents = sprite.custom_size.or_else(|| { + texture_atlas_layout + .get(&atlas.layout) + .map(|f| f.textures[atlas.index].size().as_vec2()) + })?; + let anchor = sprite.anchor.as_vec(); + (extents, anchor) + } else if let Some((sprite, image)) = sprite.zip(image) { + let extents = sprite + .custom_size + .or_else(|| images.get(image).map(|f| f.size().as_vec2()))?; + let anchor = sprite.anchor.as_vec(); + (extents, anchor) + } else { + return None; + }; + + let center = -anchor * extents; + let rect = Rect::from_center_half_size(center, extents / 2.0); + + // Transform cursor line segment to sprite coordinate system + let world_to_sprite = sprite_transform.affine().inverse(); + let cursor_start_sprite = + world_to_sprite.transform_point3(cursor_ray_world.origin); + let cursor_end_sprite = world_to_sprite.transform_point3(cursor_ray_end); + + // Find where the cursor segment intersects the plane Z=0 (which is the sprite's + // plane in sprite-local space). It may not intersect if, for example, we're + // viewing the sprite side-on + if cursor_start_sprite.z == cursor_end_sprite.z { + // Cursor ray is parallel to the sprite and misses it + return None; + } + let lerp_factor = + f32::inverse_lerp(cursor_start_sprite.z, cursor_end_sprite.z, 0.0); + if !(0.0..=1.0).contains(&lerp_factor) { + // Lerp factor is out of range, meaning that while an infinite line cast by + // the cursor would intersect the sprite, the sprite is not between the + // camera's near and far planes + return None; + } + // Otherwise we can interpolate the xy of the start and end positions by the + // lerp factor to get the cursor position in sprite space! + let cursor_pos_sprite = cursor_start_sprite + .lerp(cursor_end_sprite, lerp_factor) + .xy(); + + let is_cursor_in_sprite = rect.contains(cursor_pos_sprite); + + blocked = is_cursor_in_sprite + && pickable.map(|p| p.should_block_lower) != Some(false); + + is_cursor_in_sprite.then(|| { + let hit_pos_world = + sprite_transform.transform_point(cursor_pos_sprite.extend(0.0)); + // Transform point from world to camera space to get the Z distance + let hit_pos_cam = cam_transform + .affine() + .inverse() + .transform_point3(hit_pos_world); + // HitData requires a depth as calculated from the camera's near clipping plane + let depth = -cam_ortho.near - hit_pos_cam.z; + ( + entity, + HitData::new( + cam_entity, + depth, + Some(hit_pos_world), + Some(*sprite_transform.back()), + ), + ) + }) + }, + ) + .collect(); + + let order = camera.order as f32; + output.send(PointerHits::new(*pointer, picks, order)); + } +} diff --git a/examples/README.md b/examples/README.md index d5ff03c45a9ab6..e592f427dcdc54 100644 --- a/examples/README.md +++ b/examples/README.md @@ -358,6 +358,7 @@ Example | Description Example | Description --- | --- [Showcases simple picking events and usage](../examples/picking/simple_picking.rs) | Demonstrates how to use picking events to spawn simple objects +[Sprite Picking](../examples/picking/sprite_picking.rs) | Demonstrates picking sprites and sprite atlases ## Reflection diff --git a/examples/picking/sprite_picking.rs b/examples/picking/sprite_picking.rs new file mode 100644 index 00000000000000..ba1a21715d52b8 --- /dev/null +++ b/examples/picking/sprite_picking.rs @@ -0,0 +1,160 @@ +//! Demonstrates picking for sprites and sprite atlases. The picking backend only tests against the +//! sprite bounds, so the sprite atlas can be picked by clicking on its trnasparent areas. + +use bevy::{prelude::*, sprite::Anchor}; +use std::fmt::Debug; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, (setup, setup_atlas)) + .add_systems(Update, (move_sprite, animate_sprite)) + .run(); +} + +fn move_sprite( + time: Res

Release notes

Sourced from crate-ci/typos's releases.

v1.24.1

[1.24.1] - 2024-08-23

Fixes

  • Remove unverified varcon (locale data) entries

v1.24.0

[1.24.0] - 2024-08-23

Features

  • Update varcon (locale data) to version 2020.12.07

v1.23.7

[1.23.7] - 2024-08-22

Fixes

  • (config) Respect --locale / default.locale again after it was broken in 1.16.24
Changelog

Sourced from crate-ci/typos's changelog.

[1.24.1] - 2024-08-23

Fixes

  • Remove unverified varcon (locale data) entries

[1.24.0] - 2024-08-23

Features

  • Update varcon (locale data) to version 2020.12.07

[1.23.7] - 2024-08-22

Fixes

  • (config) Respect --locale / default.locale again after it was broken in 1.16.24
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=crate-ci/typos&package-manager=github_actions&previous-version=1.23.6&new-version=1.24.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a73d77cc838c85..f4988610d5b71c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -219,7 +219,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Check for typos - uses: crate-ci/typos@v1.23.6 + uses: crate-ci/typos@v1.24.1 - name: Typos info if: failure() run: | From 20c5270a0cf0a301a7ed2d12ba6b56771662288f Mon Sep 17 00:00:00 2001 From: Robert Walter <26892280+RobWalt@users.noreply.github.com> Date: Mon, 26 Aug 2024 18:37:16 +0000 Subject: [PATCH 27/53] add `Interval::UNIT` constant (#14923) # Objective This is a value that is and will be used as a domain of curves pretty often. By adding it as a dedicated constant we can get rid of some `unwraps` and function calls. ## Solution added `Interval::UNIT` ## Testing I replaced all occurrences of `interval(0.0, 1.0).unwrap()` with the new `Interval::UNIT` constant in tests and doc tests. --- crates/bevy_math/src/curve/interval.rs | 24 +++++++++++---------- crates/bevy_math/src/curve/mod.rs | 30 ++++++++++++-------------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/crates/bevy_math/src/curve/interval.rs b/crates/bevy_math/src/curve/interval.rs index 3ac30de4a6aaca..e3852ad06ef604 100644 --- a/crates/bevy_math/src/curve/interval.rs +++ b/crates/bevy_math/src/curve/interval.rs @@ -65,6 +65,12 @@ impl Interval { } } + /// The unit interval covering the range between `0.0` and `1.0`. + pub const UNIT: Self = Self { + start: 0.0, + end: 1.0, + }; + /// An interval which stretches across the entire real line from negative infinity to infinity. pub const EVERYWHERE: Self = Self { start: f32::NEG_INFINITY, @@ -255,16 +261,12 @@ mod tests { let ivl5 = interval(f32::NEG_INFINITY, 0.0).unwrap(); let ivl6 = Interval::EVERYWHERE; - assert!(ivl1 - .intersect(ivl2) - .is_ok_and(|ivl| ivl == interval(0.0, 1.0).unwrap())); + assert!(ivl1.intersect(ivl2).is_ok_and(|ivl| ivl == Interval::UNIT)); assert!(ivl1 .intersect(ivl3) .is_ok_and(|ivl| ivl == interval(-1.0, 0.0).unwrap())); assert!(ivl2.intersect(ivl3).is_err()); - assert!(ivl1 - .intersect(ivl4) - .is_ok_and(|ivl| ivl == interval(0.0, 1.0).unwrap())); + assert!(ivl1.intersect(ivl4).is_ok_and(|ivl| ivl == Interval::UNIT)); assert!(ivl1 .intersect(ivl5) .is_ok_and(|ivl| ivl == interval(-1.0, 0.0).unwrap())); @@ -276,7 +278,7 @@ mod tests { #[test] fn containment() { - let ivl = interval(0.0, 1.0).unwrap(); + let ivl = Interval::UNIT; assert!(ivl.contains(0.0)); assert!(ivl.contains(1.0)); assert!(ivl.contains(0.5)); @@ -295,7 +297,7 @@ mod tests { #[test] fn interval_containment() { - let ivl = interval(0.0, 1.0).unwrap(); + let ivl = Interval::UNIT; assert!(ivl.contains_interval(interval(-0.0, 0.5).unwrap())); assert!(ivl.contains_interval(interval(0.5, 1.0).unwrap())); assert!(ivl.contains_interval(interval(0.25, 0.75).unwrap())); @@ -323,18 +325,18 @@ mod tests { #[test] fn linear_maps() { let ivl1 = interval(-3.0, 5.0).unwrap(); - let ivl2 = interval(0.0, 1.0).unwrap(); + let ivl2 = Interval::UNIT; let map = ivl1.linear_map_to(ivl2); assert!(map.is_ok_and(|f| f(-3.0).abs_diff_eq(&0.0, f32::EPSILON) && f(5.0).abs_diff_eq(&1.0, f32::EPSILON) && f(1.0).abs_diff_eq(&0.5, f32::EPSILON))); - let ivl1 = interval(0.0, 1.0).unwrap(); + let ivl1 = Interval::UNIT; let ivl2 = Interval::EVERYWHERE; assert!(ivl1.linear_map_to(ivl2).is_err()); let ivl1 = interval(f32::NEG_INFINITY, -4.0).unwrap(); - let ivl2 = interval(0.0, 1.0).unwrap(); + let ivl2 = Interval::UNIT; assert!(ivl1.linear_map_to(ivl2).is_err()); } diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index 4440fb72ed9059..fc54223ed51972 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -116,7 +116,7 @@ pub trait Curve { /// factor rather than multiplying: /// ``` /// # use bevy_math::curve::*; - /// let my_curve = constant_curve(interval(0.0, 1.0).unwrap(), 1.0); + /// let my_curve = constant_curve(Interval::UNIT, 1.0); /// let scaled_curve = my_curve.reparametrize(interval(0.0, 2.0).unwrap(), |t| t / 2.0); /// ``` /// This kind of linear remapping is provided by the convenience method @@ -127,17 +127,17 @@ pub trait Curve { /// // Reverse a curve: /// # use bevy_math::curve::*; /// # use bevy_math::vec2; - /// let my_curve = constant_curve(interval(0.0, 1.0).unwrap(), 1.0); + /// let my_curve = constant_curve(Interval::UNIT, 1.0); /// let domain = my_curve.domain(); /// let reversed_curve = my_curve.reparametrize(domain, |t| domain.end() - t); /// /// // Take a segment of a curve: - /// # let my_curve = constant_curve(interval(0.0, 1.0).unwrap(), 1.0); + /// # let my_curve = constant_curve(Interval::UNIT, 1.0); /// let curve_segment = my_curve.reparametrize(interval(0.0, 0.5).unwrap(), |t| 0.5 + t); /// /// // Reparametrize by an easing curve: - /// # let my_curve = constant_curve(interval(0.0, 1.0).unwrap(), 1.0); - /// # let easing_curve = constant_curve(interval(0.0, 1.0).unwrap(), vec2(1.0, 1.0)); + /// # let my_curve = constant_curve(Interval::UNIT, 1.0); + /// # let easing_curve = constant_curve(Interval::UNIT, vec2(1.0, 1.0)); /// let domain = my_curve.domain(); /// let eased_curve = my_curve.reparametrize(domain, |t| easing_curve.sample_unchecked(t).y); /// ``` @@ -424,7 +424,7 @@ pub trait Curve { /// # Example /// ``` /// # use bevy_math::curve::*; - /// let my_curve = function_curve(interval(0.0, 1.0).unwrap(), |t| t * t + 1.0); + /// let my_curve = function_curve(Interval::UNIT, |t| t * t + 1.0); /// // Borrow `my_curve` long enough to resample a mapped version. Note that `map` takes /// // ownership of its input. /// let samples = my_curve.by_ref().map(|x| x * 2.0).resample_auto(100).unwrap(); @@ -1018,7 +1018,7 @@ mod tests { let curve = constant_curve(Interval::EVERYWHERE, 5.0); assert!(curve.sample_unchecked(-35.0) == 5.0); - let curve = constant_curve(interval(0.0, 1.0).unwrap(), true); + let curve = constant_curve(Interval::UNIT, true); assert!(curve.sample_unchecked(2.0)); assert!(curve.sample(2.0).is_none()); } @@ -1046,11 +1046,11 @@ mod tests { ); assert_eq!(mapped_curve.domain(), Interval::EVERYWHERE); - let curve = function_curve(interval(0.0, 1.0).unwrap(), |t| t * TAU); + let curve = function_curve(Interval::UNIT, |t| t * TAU); let mapped_curve = curve.map(Quat::from_rotation_z); assert_eq!(mapped_curve.sample_unchecked(0.0), Quat::IDENTITY); assert!(mapped_curve.sample_unchecked(1.0).is_near_identity()); - assert_eq!(mapped_curve.domain(), interval(0.0, 1.0).unwrap()); + assert_eq!(mapped_curve.domain(), Interval::UNIT); } #[test] @@ -1066,18 +1066,16 @@ mod tests { interval(0.0, f32::INFINITY).unwrap() ); - let reparametrized_curve = curve - .by_ref() - .reparametrize(interval(0.0, 1.0).unwrap(), |t| t + 1.0); + let reparametrized_curve = curve.by_ref().reparametrize(Interval::UNIT, |t| t + 1.0); assert_abs_diff_eq!(reparametrized_curve.sample_unchecked(0.0), 0.0); assert_abs_diff_eq!(reparametrized_curve.sample_unchecked(1.0), 1.0); - assert_eq!(reparametrized_curve.domain(), interval(0.0, 1.0).unwrap()); + assert_eq!(reparametrized_curve.domain(), Interval::UNIT); } #[test] fn multiple_maps() { // Make sure these actually happen in the right order. - let curve = function_curve(interval(0.0, 1.0).unwrap(), ops::exp2); + let curve = function_curve(Interval::UNIT, ops::exp2); let first_mapped = curve.map(ops::log2); let second_mapped = first_mapped.map(|x| x * -2.0); assert_abs_diff_eq!(second_mapped.sample_unchecked(0.0), 0.0); @@ -1088,9 +1086,9 @@ mod tests { #[test] fn multiple_reparams() { // Make sure these happen in the right order too. - let curve = function_curve(interval(0.0, 1.0).unwrap(), ops::exp2); + let curve = function_curve(Interval::UNIT, ops::exp2); let first_reparam = curve.reparametrize(interval(1.0, 2.0).unwrap(), ops::log2); - let second_reparam = first_reparam.reparametrize(interval(0.0, 1.0).unwrap(), |t| t + 1.0); + let second_reparam = first_reparam.reparametrize(Interval::UNIT, |t| t + 1.0); assert_abs_diff_eq!(second_reparam.sample_unchecked(0.0), 1.0); assert_abs_diff_eq!(second_reparam.sample_unchecked(0.5), 1.5); assert_abs_diff_eq!(second_reparam.sample_unchecked(1.0), 2.0); From 95ef8f6975bc85d4031e9026bbcc6c960f4cdd81 Mon Sep 17 00:00:00 2001 From: kivi Date: Mon, 26 Aug 2024 20:38:56 +0200 Subject: [PATCH 28/53] rename Drop to bevy::picking::events::DragDrop to unclash std::ops:Drop (#14926) # Objective - Fixes #14902 - > #14686 Introduced a name clash when using use bevy::prelude::*; ## Solution - renamed `bevy::picking::events::Drop` `bevy::picking::events::DragDrop` ## Testing - Not being used in tests or examples, so I just compiled. --- ## Migration Guide - Rename `Drop` to `DragDrop` - `bevy::picking::events::Drop` is now `bevy::picking::events::DragDrop` --- crates/bevy_picking/src/events.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/bevy_picking/src/events.rs b/crates/bevy_picking/src/events.rs index 092fa6ac264b70..a1d5aee04bace9 100644 --- a/crates/bevy_picking/src/events.rs +++ b/crates/bevy_picking/src/events.rs @@ -26,7 +26,7 @@ pub struct Pointer { pub pointer_id: PointerId, /// The location of the pointer during this event pub pointer_location: Location, - /// Additional event-specific data. [`Drop`] for example, has an additional field to describe + /// Additional event-specific data. [`DragDrop`] for example, has an additional field to describe /// the `Entity` that is being dropped on the target. pub event: E, } @@ -193,7 +193,7 @@ pub struct DragLeave { /// Fires when a pointer drops the `dropped` entity onto the `target` entity. #[derive(Clone, PartialEq, Debug, Reflect)] -pub struct Drop { +pub struct DragDrop { /// Pointer button lifted to drop. pub button: PointerButton, /// The entity that was dropped onto the `target` entity. @@ -517,7 +517,7 @@ pub fn send_drag_over_events( // - Pointer, // - Pointer, // - Pointer, - // - Pointer, + // - Pointer, mut commands: Commands, // Input drag_map: Res, @@ -626,7 +626,7 @@ pub fn send_drag_over_events( pointer_id, pointer_location.clone(), target, - Drop { + DragDrop { button, dropped: target, hit: hit.clone(), From 6ddbf9771a0070cc906ec948d5e1251083a993b4 Mon Sep 17 00:00:00 2001 From: Chris Russell <8494645+chescock@users.noreply.github.com> Date: Mon, 26 Aug 2024 20:16:29 -0400 Subject: [PATCH 29/53] SystemParamBuilder - Support buildable Vec parameters (#14821) # Objective Allow dynamic systems to take lists of system parameters whose length is not known at compile time. This can be used for building a system that runs a script defined at runtime, where the script needs a variable number of query parameters. It can also be used for building a system that collects a list of plugins at runtime, and provides a parameter to each one. This is most useful today with `Vec>`. It will be even more useful with `Vec` if #14817 is merged, since the parameters in the list can then be of different types. ## Solution Implement `SystemParam` and `SystemParamBuilder` for `Vec` and `ParamSet`. ## Example ```rust let system = (vec![ QueryParamBuilder::new_box(|builder| { builder.with::().without::(); }), QueryParamBuilder::new_box(|builder| { builder.with::().without::(); }), ],) .build_state(&mut world) .build_system(|params: Vec>| { let mut count: usize = 0; params .into_iter() .for_each(|mut query| count += query.iter_mut().count()); count }); ``` --- crates/bevy_ecs/src/system/builder.rs | 101 ++++++++++++++++ crates/bevy_ecs/src/system/system_param.rs | 133 +++++++++++++++++++++ 2 files changed, 234 insertions(+) diff --git a/crates/bevy_ecs/src/system/builder.rs b/crates/bevy_ecs/src/system/builder.rs index 861f915a70145c..ab440ff3042dde 100644 --- a/crates/bevy_ecs/src/system/builder.rs +++ b/crates/bevy_ecs/src/system/builder.rs @@ -209,6 +209,15 @@ macro_rules! impl_system_param_builder_tuple { all_tuples!(impl_system_param_builder_tuple, 0, 16, P, B); +// SAFETY: implementors of each `SystemParamBuilder` in the vec have validated their impls +unsafe impl> SystemParamBuilder> for Vec { + fn build(self, world: &mut World, meta: &mut SystemMeta) -> as SystemParam>::State { + self.into_iter() + .map(|builder| builder.build(world, meta)) + .collect() + } +} + /// A [`SystemParamBuilder`] for a [`ParamSet`]. /// To build a [`ParamSet`] with a tuple of system parameters, pass a tuple of matching [`SystemParamBuilder`]s. /// To build a [`ParamSet`] with a `Vec` of system parameters, pass a `Vec` of matching [`SystemParamBuilder`]s. @@ -251,6 +260,38 @@ macro_rules! impl_param_set_builder_tuple { all_tuples!(impl_param_set_builder_tuple, 1, 8, P, B, meta); +// SAFETY: Relevant parameter ComponentId and ArchetypeComponentId access is applied to SystemMeta. If any ParamState conflicts +// with any prior access, a panic will occur. +unsafe impl<'w, 's, P: SystemParam, B: SystemParamBuilder

> + SystemParamBuilder>> for ParamSetBuilder> +{ + fn build( + self, + world: &mut World, + system_meta: &mut SystemMeta, + ) -> as SystemParam>::State { + let mut states = Vec::with_capacity(self.0.len()); + let mut metas = Vec::with_capacity(self.0.len()); + for builder in self.0 { + let mut meta = system_meta.clone(); + states.push(builder.build(world, &mut meta)); + metas.push(meta); + } + if metas.iter().any(|m| !m.is_send()) { + system_meta.set_non_send(); + } + for meta in metas { + system_meta + .component_access_set + .extend(meta.component_access_set); + system_meta + .archetype_component_access + .extend(&meta.archetype_component_access); + } + states + } +} + /// A [`SystemParamBuilder`] for a [`DynSystemParam`]. pub struct DynParamBuilder<'a>( Box DynSystemParamState + 'a>, @@ -385,6 +426,37 @@ mod tests { assert_eq!(result, 1); } + #[test] + fn vec_builder() { + let mut world = World::new(); + + world.spawn((A, B, C)); + world.spawn((A, B)); + world.spawn((A, C)); + world.spawn((A, C)); + world.spawn_empty(); + + let system = (vec![ + QueryParamBuilder::new_box(|builder| { + builder.with::().without::(); + }), + QueryParamBuilder::new_box(|builder| { + builder.with::().without::(); + }), + ],) + .build_state(&mut world) + .build_system(|params: Vec>| { + let mut count: usize = 0; + params + .into_iter() + .for_each(|mut query| count += query.iter_mut().count()); + count + }); + + let result = world.run_system_once(system); + assert_eq!(result, 3); + } + #[test] fn param_set_builder() { let mut world = World::new(); @@ -412,6 +484,35 @@ mod tests { assert_eq!(result, 5); } + #[test] + fn param_set_vec_builder() { + let mut world = World::new(); + + world.spawn((A, B, C)); + world.spawn((A, B)); + world.spawn((A, C)); + world.spawn((A, C)); + world.spawn_empty(); + + let system = (ParamSetBuilder(vec![ + QueryParamBuilder::new_box(|builder| { + builder.with::(); + }), + QueryParamBuilder::new_box(|builder| { + builder.with::(); + }), + ]),) + .build_state(&mut world) + .build_system(|mut params: ParamSet>>| { + let mut count = 0; + params.for_each(|mut query| count += query.iter_mut().count()); + count + }); + + let result = world.run_system_once(system); + assert_eq!(result, 5); + } + #[test] fn dyn_builder() { let mut world = World::new(); diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index 7968338764b146..10e7fd4ec35837 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -1450,6 +1450,139 @@ unsafe impl SystemParam for SystemChangeTick { } } +// SAFETY: When initialized with `init_state`, `get_param` returns an empty `Vec` and does no access. +// Therefore, `init_state` trivially registers all access, and no accesses can conflict. +// Note that the safety requirements for non-empty `Vec`s are handled by the `SystemParamBuilder` impl that builds them. +unsafe impl SystemParam for Vec { + type State = Vec; + + type Item<'world, 'state> = Vec>; + + fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State { + Vec::new() + } + + unsafe fn get_param<'world, 'state>( + state: &'state mut Self::State, + system_meta: &SystemMeta, + world: UnsafeWorldCell<'world>, + change_tick: Tick, + ) -> Self::Item<'world, 'state> { + state + .iter_mut() + // SAFETY: + // - We initialized the state for each parameter in the builder, so the caller ensures we have access to any world data needed by each param. + // - The caller ensures this was the world used to initialize our state, and we used that world to initialize parameter states + .map(|state| unsafe { T::get_param(state, system_meta, world, change_tick) }) + .collect() + } + + unsafe fn new_archetype( + state: &mut Self::State, + archetype: &Archetype, + system_meta: &mut SystemMeta, + ) { + for state in state { + // SAFETY: The caller ensures that `archetype` is from the World the state was initialized from in `init_state`. + unsafe { T::new_archetype(state, archetype, system_meta) }; + } + } + + fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { + for state in state { + T::apply(state, system_meta, world); + } + } + + fn queue(state: &mut Self::State, system_meta: &SystemMeta, mut world: DeferredWorld) { + for state in state { + T::queue(state, system_meta, world.reborrow()); + } + } +} + +// SAFETY: When initialized with `init_state`, `get_param` returns an empty `Vec` and does no access. +// Therefore, `init_state` trivially registers all access, and no accesses can conflict. +// Note that the safety requirements for non-empty `Vec`s are handled by the `SystemParamBuilder` impl that builds them. +unsafe impl SystemParam for ParamSet<'_, '_, Vec> { + type State = Vec; + + type Item<'world, 'state> = ParamSet<'world, 'state, Vec>; + + fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State { + Vec::new() + } + + unsafe fn get_param<'world, 'state>( + state: &'state mut Self::State, + system_meta: &SystemMeta, + world: UnsafeWorldCell<'world>, + change_tick: Tick, + ) -> Self::Item<'world, 'state> { + ParamSet { + param_states: state, + system_meta: system_meta.clone(), + world, + change_tick, + } + } + + unsafe fn new_archetype( + state: &mut Self::State, + archetype: &Archetype, + system_meta: &mut SystemMeta, + ) { + for state in state { + // SAFETY: The caller ensures that `archetype` is from the World the state was initialized from in `init_state`. + unsafe { T::new_archetype(state, archetype, system_meta) } + } + } + + fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { + for state in state { + T::apply(state, system_meta, world); + } + } + + fn queue(state: &mut Self::State, system_meta: &SystemMeta, mut world: DeferredWorld) { + for state in state { + T::queue(state, system_meta, world.reborrow()); + } + } +} + +impl ParamSet<'_, '_, Vec> { + /// Accesses the parameter at the given index. + /// No other parameters may be accessed while this one is active. + pub fn get_mut(&mut self, index: usize) -> T::Item<'_, '_> { + // SAFETY: + // - We initialized the state for each parameter in the builder, so the caller ensures we have access to any world data needed by any param. + // We have mutable access to the ParamSet, so no other params in the set are active. + // - The caller of `get_param` ensured that this was the world used to initialize our state, and we used that world to initialize parameter states + unsafe { + T::get_param( + &mut self.param_states[index], + &self.system_meta, + self.world, + self.change_tick, + ) + } + } + + /// Calls a closure for each parameter in the set. + pub fn for_each(&mut self, mut f: impl FnMut(T::Item<'_, '_>)) { + self.param_states.iter_mut().for_each(|state| { + f( + // SAFETY: + // - We initialized the state for each parameter in the builder, so the caller ensures we have access to any world data needed by any param. + // We have mutable access to the ParamSet, so no other params in the set are active. + // - The caller of `get_param` ensured that this was the world used to initialize our state, and we used that world to initialize parameter states + unsafe { T::get_param(state, &self.system_meta, self.world, self.change_tick) }, + ); + }); + } +} + macro_rules! impl_system_param_tuple { ($(#[$meta:meta])* $($param: ident),*) => { $(#[$meta])* From f06cd448db9171bda656ded1e63c68581d6e19aa Mon Sep 17 00:00:00 2001 From: robtfm <50659922+robtfm@users.noreply.github.com> Date: Tue, 27 Aug 2024 01:16:44 +0100 Subject: [PATCH 30/53] drop pending asset loads (#14808) # Objective when handles for loading assets are dropped, we currently wait until load is completed before dropping the handle. drop asset-load tasks immediately ## Solution - track tasks for loading assets and drop them immediately when all handles are dropped. ~~- use `join_all` in `gltf_loader.rs` to allow it to yield and be dropped.~~ doesn't cover all the load apis - for those it doesn't cover the task will still be detached and will still complete before the result is discarded. separated out from #13170 --- crates/bevy_asset/src/server/info.rs | 12 ++ crates/bevy_asset/src/server/mod.rs | 164 +++++++++++++++------------ 2 files changed, 103 insertions(+), 73 deletions(-) diff --git a/crates/bevy_asset/src/server/info.rs b/crates/bevy_asset/src/server/info.rs index 98d92702077c79..b63c139f326a26 100644 --- a/crates/bevy_asset/src/server/info.rs +++ b/crates/bevy_asset/src/server/info.rs @@ -5,6 +5,7 @@ use crate::{ UntypedAssetId, UntypedHandle, }; use bevy_ecs::world::World; +use bevy_tasks::Task; use bevy_utils::tracing::warn; use bevy_utils::{Entry, HashMap, HashSet, TypeIdMap}; use crossbeam_channel::Sender; @@ -76,6 +77,7 @@ pub(crate) struct AssetInfos { pub(crate) dependency_loaded_event_sender: TypeIdMap, pub(crate) dependency_failed_event_sender: TypeIdMap, AssetLoadError)>, + pub(crate) pending_tasks: HashMap>, } impl std::fmt::Debug for AssetInfos { @@ -364,6 +366,7 @@ impl AssetInfos { &mut self.path_to_id, &mut self.loader_dependants, &mut self.living_labeled_assets, + &mut self.pending_tasks, self.watching_for_changes, id, ) @@ -587,6 +590,11 @@ impl AssetInfos { } pub(crate) fn process_asset_fail(&mut self, failed_id: UntypedAssetId, error: AssetLoadError) { + // Check whether the handle has been dropped since the asset was loaded. + if !self.infos.contains_key(&failed_id) { + return; + } + let (dependants_waiting_on_load, dependants_waiting_on_rec_load) = { let Some(info) = self.get_mut(failed_id) else { // The asset was already dropped. @@ -648,6 +656,7 @@ impl AssetInfos { path_to_id: &mut HashMap, TypeIdMap>, loader_dependants: &mut HashMap, HashSet>>, living_labeled_assets: &mut HashMap, HashSet>>, + pending_tasks: &mut HashMap>, watching_for_changes: bool, id: UntypedAssetId, ) -> bool { @@ -662,6 +671,8 @@ impl AssetInfos { return false; } + pending_tasks.remove(&id); + let type_id = entry.key().type_id(); let info = entry.remove(); @@ -704,6 +715,7 @@ impl AssetInfos { &mut self.path_to_id, &mut self.loader_dependants, &mut self.living_labeled_assets, + &mut self.pending_tasks, self.watching_for_changes, id.untyped(provider.type_id), ); diff --git a/crates/bevy_asset/src/server/mod.rs b/crates/bevy_asset/src/server/mod.rs index b2ea0466f68f02..ef72d2b4042951 100644 --- a/crates/bevy_asset/src/server/mod.rs +++ b/crates/bevy_asset/src/server/mod.rs @@ -368,7 +368,8 @@ impl AssetServer { guard: G, ) -> Handle { let path = path.into().into_owned(); - let (handle, should_load) = self.data.infos.write().get_or_create_path_handle::( + let mut infos = self.data.infos.write(); + let (handle, should_load) = infos.get_or_create_path_handle::( path.clone(), HandleLoadingMode::Request, meta_transform, @@ -377,14 +378,18 @@ impl AssetServer { if should_load { let owned_handle = Some(handle.clone().untyped()); let server = self.clone(); - IoTaskPool::get() - .spawn(async move { - if let Err(err) = server.load_internal(owned_handle, path, false, None).await { - error!("{}", err); - } - drop(guard); - }) - .detach(); + let task = IoTaskPool::get().spawn(async move { + if let Err(err) = server.load_internal(owned_handle, path, false, None).await { + error!("{}", err); + } + drop(guard); + }); + + #[cfg(not(any(target_arch = "wasm32", not(feature = "multi_threaded"))))] + infos.pending_tasks.insert(handle.id().untyped(), task); + + #[cfg(any(target_arch = "wasm32", not(feature = "multi_threaded")))] + task.detach(); } handle @@ -414,44 +419,47 @@ impl AssetServer { CowArc::Owned(format!("{source}--{UNTYPED_SOURCE_SUFFIX}").into()) } }); - let (handle, should_load) = self - .data - .infos - .write() - .get_or_create_path_handle::( - path.clone().with_source(untyped_source), - HandleLoadingMode::Request, - meta_transform, - ); + let mut infos = self.data.infos.write(); + let (handle, should_load) = infos.get_or_create_path_handle::( + path.clone().with_source(untyped_source), + HandleLoadingMode::Request, + meta_transform, + ); if !should_load { return handle; } let id = handle.id().untyped(); + let owned_handle = Some(handle.clone().untyped()); let server = self.clone(); - IoTaskPool::get() - .spawn(async move { - let path_clone = path.clone(); - match server.load_untyped_async(path).await { - Ok(handle) => server.send_asset_event(InternalAssetEvent::Loaded { + let task = IoTaskPool::get().spawn(async move { + let path_clone = path.clone(); + match server.load_internal(owned_handle, path, false, None).await { + Ok(handle) => server.send_asset_event(InternalAssetEvent::Loaded { + id, + loaded_asset: LoadedAsset::new_with_dependencies( + LoadedUntypedAsset { handle }, + None, + ) + .into(), + }), + Err(err) => { + error!("{err}"); + server.send_asset_event(InternalAssetEvent::Failed { id, - loaded_asset: LoadedAsset::new_with_dependencies( - LoadedUntypedAsset { handle }, - None, - ) - .into(), - }), - Err(err) => { - error!("{err}"); - server.send_asset_event(InternalAssetEvent::Failed { - id, - path: path_clone, - error: err, - }); - } + path: path_clone, + error: err, + }); } - }) - .detach(); + } + }); + + #[cfg(not(any(target_arch = "wasm32", not(feature = "multi_threaded"))))] + infos.pending_tasks.insert(handle.id().untyped(), task); + + #[cfg(any(target_arch = "wasm32", not(feature = "multi_threaded")))] + task.detach(); + handle } @@ -488,7 +496,7 @@ impl AssetServer { /// avoid looking up `should_load` twice, but it means you _must_ be sure a load is necessary when calling this function with [`Some`]. async fn load_internal<'a>( &self, - input_handle: Option, + mut input_handle: Option, path: AssetPath<'a>, force: bool, meta_transform: Option, @@ -512,6 +520,13 @@ 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 @@ -580,10 +595,6 @@ impl AssetServer { (handle.clone().unwrap(), path.clone()) }; - if let Some(meta_transform) = base_handle.meta_transform() { - (*meta_transform)(&mut *meta); - } - match self .load_with_meta_loader_and_reader(&base_path, meta, &*loader, &mut *reader, true, false) .await @@ -721,40 +732,42 @@ impl AssetServer { &self, future: impl Future> + Send + 'static, ) -> Handle { - let handle = self - .data - .infos - .write() - .create_loading_handle_untyped(TypeId::of::(), std::any::type_name::()); + let mut infos = self.data.infos.write(); + let handle = + infos.create_loading_handle_untyped(TypeId::of::(), std::any::type_name::()); let id = handle.id(); let event_sender = self.data.asset_event_sender.clone(); - IoTaskPool::get() - .spawn(async move { - match future.await { - Ok(asset) => { - let loaded_asset = LoadedAsset::new_with_dependencies(asset, None).into(); - event_sender - .send(InternalAssetEvent::Loaded { id, loaded_asset }) - .unwrap(); - } - Err(error) => { - let error = AddAsyncError { - error: Arc::new(error), - }; - error!("{error}"); - event_sender - .send(InternalAssetEvent::Failed { - id, - path: Default::default(), - error: AssetLoadError::AddAsyncError(error), - }) - .unwrap(); - } + let task = IoTaskPool::get().spawn(async move { + match future.await { + Ok(asset) => { + let loaded_asset = LoadedAsset::new_with_dependencies(asset, None).into(); + event_sender + .send(InternalAssetEvent::Loaded { id, loaded_asset }) + .unwrap(); } - }) - .detach(); + Err(error) => { + let error = AddAsyncError { + error: Arc::new(error), + }; + error!("{error}"); + event_sender + .send(InternalAssetEvent::Failed { + id, + path: Default::default(), + error: AssetLoadError::AddAsyncError(error), + }) + .unwrap(); + } + } + }); + + #[cfg(not(any(target_arch = "wasm32", not(feature = "multi_threaded"))))] + infos.pending_tasks.insert(id, task); + + #[cfg(any(target_arch = "wasm32", not(feature = "multi_threaded")))] + task.detach(); handle.typed_debug_checked() } @@ -1312,6 +1325,11 @@ pub fn handle_internal_asset_events(world: &mut World) { info!("Reloading {path} because it has changed"); server.reload(path); } + + #[cfg(not(any(target_arch = "wasm32", not(feature = "multi_threaded"))))] + infos + .pending_tasks + .retain(|_, load_task| !load_task.is_finished()); }); } From e320fa0738e3f4582b0d61db85a00d7a70b676eb Mon Sep 17 00:00:00 2001 From: Giacomo Stevanato Date: Tue, 27 Aug 2024 02:58:40 +0200 Subject: [PATCH 31/53] Fix query transmute from table to archetype iteration unsoundness (#14615) # Objective - Fixes #14348 - Fixes #14528 - Less complex (but also likely less performant) alternative to #14611 ## Solution - Add a `is_dense` field flag to `QueryIter` indicating whether it is dense or not, that is whether it can perform dense iteration or not; - Check this flag any time iteration over a query is performed. --- It would be nice if someone could try benching this change to see if it actually matters. ~Note that this not 100% ready for mergin, since there are a bunch of safety comments on the use of the various `IS_DENSE` for checks that still need to be updated.~ This is ready modulo benchmarks --------- Co-authored-by: Alice Cecile --- crates/bevy_ecs/src/query/builder.rs | 44 +++++++++++++++ crates/bevy_ecs/src/query/iter.rs | 64 +++++++++++++--------- crates/bevy_ecs/src/query/par_iter.rs | 2 +- crates/bevy_ecs/src/query/state.rs | 79 ++++++++++++++++++++++++--- 4 files changed, 154 insertions(+), 35 deletions(-) diff --git a/crates/bevy_ecs/src/query/builder.rs b/crates/bevy_ecs/src/query/builder.rs index 3076b5d54e0905..ed4e2cd4ba71d4 100644 --- a/crates/bevy_ecs/src/query/builder.rs +++ b/crates/bevy_ecs/src/query/builder.rs @@ -1,5 +1,6 @@ use std::marker::PhantomData; +use crate::component::StorageType; use crate::{component::ComponentId, prelude::*}; use super::{FilteredAccess, QueryData, QueryFilter}; @@ -68,6 +69,26 @@ impl<'w, D: QueryData, F: QueryFilter> QueryBuilder<'w, D, F> { } } + pub(super) fn is_dense(&self) -> bool { + // Note: `component_id` comes from the user in safe code, so we cannot trust it to + // exist. If it doesn't exist we pessimistically assume it's sparse. + let is_dense = |component_id| { + self.world() + .components() + .get_info(component_id) + .map_or(false, |info| info.storage_type() == StorageType::Table) + }; + + self.access + .access() + .component_reads_and_writes() + .all(is_dense) + && self.access.access().archetypal().all(is_dense) + && !self.access.access().has_read_all_components() + && self.access.with_filters().all(is_dense) + && self.access.without_filters().all(is_dense) + } + /// Returns a reference to the world passed to [`Self::new`]. pub fn world(&self) -> &World { self.world @@ -396,4 +417,27 @@ mod tests { assert_eq!(1, b.deref::().0); } } + + /// Regression test for issue #14348 + #[test] + fn builder_static_dense_dynamic_sparse() { + #[derive(Component)] + struct Dense; + + #[derive(Component)] + #[component(storage = "SparseSet")] + struct Sparse; + + let mut world = World::new(); + + world.spawn(Dense); + world.spawn((Dense, Sparse)); + + let mut query = QueryBuilder::<&Dense>::new(&mut world) + .with::() + .build(); + + let matched = query.iter(&world).count(); + assert_eq!(matched, 1); + } } diff --git a/crates/bevy_ecs/src/query/iter.rs b/crates/bevy_ecs/src/query/iter.rs index f8564025aa15c8..5c1912b7bcc18f 100644 --- a/crates/bevy_ecs/src/query/iter.rs +++ b/crates/bevy_ecs/src/query/iter.rs @@ -128,7 +128,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { /// # Safety /// - all `rows` must be in `[0, table.entity_count)`. /// - `table` must match D and F - /// - Both `D::IS_DENSE` and `F::IS_DENSE` must be true. + /// - The query iteration must be dense (i.e. `self.query_state.is_dense` must be true). #[inline] pub(super) unsafe fn fold_over_table_range( &mut self, @@ -183,7 +183,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { /// # Safety /// - all `indices` must be in `[0, archetype.len())`. /// - `archetype` must match D and F - /// - Either `D::IS_DENSE` or `F::IS_DENSE` must be false. + /// - The query iteration must not be dense (i.e. `self.query_state.is_dense` must be false). #[inline] pub(super) unsafe fn fold_over_archetype_range( &mut self, @@ -252,7 +252,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { /// - all `indices` must be in `[0, archetype.len())`. /// - `archetype` must match D and F /// - `archetype` must have the same length with it's table. - /// - Either `D::IS_DENSE` or `F::IS_DENSE` must be false. + /// - The query iteration must not be dense (i.e. `self.query_state.is_dense` must be false). #[inline] pub(super) unsafe fn fold_over_dense_archetype_range( &mut self, @@ -1031,20 +1031,27 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Iterator for QueryIter<'w, 's, D, F> let Some(item) = self.next() else { break }; accum = func(accum, item); } - for id in self.cursor.storage_id_iter.clone() { - if D::IS_DENSE && F::IS_DENSE { + + if self.cursor.is_dense { + for id in self.cursor.storage_id_iter.clone() { + // SAFETY: `self.cursor.is_dense` is true, so storage ids are guaranteed to be table ids. + let table_id = unsafe { id.table_id }; // SAFETY: Matched table IDs are guaranteed to still exist. - let table = unsafe { self.tables.get(id.table_id).debug_checked_unwrap() }; + let table = unsafe { self.tables.get(table_id).debug_checked_unwrap() }; + accum = // SAFETY: // - The fetched table matches both D and F // - The provided range is equivalent to [0, table.entity_count) - // - The if block ensures that D::IS_DENSE and F::IS_DENSE are both true + // - The if block ensures that the query iteration is dense unsafe { self.fold_over_table_range(accum, &mut func, table, 0..table.entity_count()) }; - } else { - let archetype = - // SAFETY: Matched archetype IDs are guaranteed to still exist. - unsafe { self.archetypes.get(id.archetype_id).debug_checked_unwrap() }; + } + } else { + for id in self.cursor.storage_id_iter.clone() { + // SAFETY: `self.cursor.is_dense` is false, so storage ids are guaranteed to be archetype ids. + let archetype_id = unsafe { id.archetype_id }; + // SAFETY: Matched archetype IDs are guaranteed to still exist. + let archetype = unsafe { self.archetypes.get(archetype_id).debug_checked_unwrap() }; // SAFETY: Matched table IDs are guaranteed to still exist. let table = unsafe { self.tables.get(archetype.table_id()).debug_checked_unwrap() }; @@ -1052,19 +1059,19 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Iterator for QueryIter<'w, 's, D, F> // this leverages cache locality to optimize performance. if table.entity_count() == archetype.len() { accum = - // SAFETY: - // - The fetched archetype matches both D and F - // - The provided archetype and its' table have the same length. - // - The provided range is equivalent to [0, archetype.len) - // - The if block ensures that ether D::IS_DENSE or F::IS_DENSE are false - unsafe { self.fold_over_dense_archetype_range(accum, &mut func, archetype,0..archetype.len()) }; + // SAFETY: + // - The fetched archetype matches both D and F + // - The provided archetype and its' table have the same length. + // - The provided range is equivalent to [0, archetype.len) + // - The if block ensures that the query iteration is not dense. + unsafe { self.fold_over_dense_archetype_range(accum, &mut func, archetype, 0..archetype.len()) }; } else { accum = - // SAFETY: - // - The fetched archetype matches both D and F - // - The provided range is equivalent to [0, archetype.len) - // - The if block ensures that ether D::IS_DENSE or F::IS_DENSE are false - unsafe { self.fold_over_archetype_range(accum, &mut func, archetype,0..archetype.len()) }; + // SAFETY: + // - The fetched archetype matches both D and F + // - The provided range is equivalent to [0, archetype.len) + // - The if block ensures that the query iteration is not dense. + unsafe { self.fold_over_archetype_range(accum, &mut func, archetype, 0..archetype.len()) }; } } } @@ -1675,6 +1682,8 @@ impl<'w, 's, D: QueryData, F: QueryFilter, const K: usize> Debug } struct QueryIterationCursor<'w, 's, D: QueryData, F: QueryFilter> { + // whether the query iteration is dense or not. Mirrors QueryState's `is_dense` field. + is_dense: bool, storage_id_iter: std::slice::Iter<'s, StorageId>, table_entities: &'w [Entity], archetype_entities: &'w [ArchetypeEntity], @@ -1689,6 +1698,7 @@ struct QueryIterationCursor<'w, 's, D: QueryData, F: QueryFilter> { impl Clone for QueryIterationCursor<'_, '_, D, F> { fn clone(&self) -> Self { Self { + is_dense: self.is_dense, storage_id_iter: self.storage_id_iter.clone(), table_entities: self.table_entities, archetype_entities: self.archetype_entities, @@ -1701,8 +1711,6 @@ impl Clone for QueryIterationCursor<'_, '_, D, F> } impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { - const IS_DENSE: bool = D::IS_DENSE && F::IS_DENSE; - unsafe fn init_empty( world: UnsafeWorldCell<'w>, query_state: &'s QueryState, @@ -1732,6 +1740,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { table_entities: &[], archetype_entities: &[], storage_id_iter: query_state.matched_storage_ids.iter(), + is_dense: query_state.is_dense, current_len: 0, current_row: 0, } @@ -1739,6 +1748,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { fn reborrow(&mut self) -> QueryIterationCursor<'_, 's, D, F> { QueryIterationCursor { + is_dense: self.is_dense, fetch: D::shrink_fetch(self.fetch.clone()), filter: F::shrink_fetch(self.filter.clone()), table_entities: self.table_entities, @@ -1754,7 +1764,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { unsafe fn peek_last(&mut self) -> Option> { if self.current_row > 0 { let index = self.current_row - 1; - if Self::IS_DENSE { + if self.is_dense { let entity = self.table_entities.get_unchecked(index); Some(D::fetch( &mut self.fetch, @@ -1780,7 +1790,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { /// will be **the exact count of remaining values**. fn max_remaining(&self, tables: &'w Tables, archetypes: &'w Archetypes) -> usize { let ids = self.storage_id_iter.clone(); - let remaining_matched: usize = if Self::IS_DENSE { + let remaining_matched: usize = if self.is_dense { // SAFETY: The if check ensures that storage_id_iter stores TableIds unsafe { ids.map(|id| tables[id.table_id].entity_count()).sum() } } else { @@ -1803,7 +1813,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { archetypes: &'w Archetypes, query_state: &'s QueryState, ) -> Option> { - if Self::IS_DENSE { + if self.is_dense { loop { // we are on the beginning of the query, or finished processing a table, so skip to the next if self.current_row == self.current_len { diff --git a/crates/bevy_ecs/src/query/par_iter.rs b/crates/bevy_ecs/src/query/par_iter.rs index 7889228ba7af65..b3ea93fbbc5144 100644 --- a/crates/bevy_ecs/src/query/par_iter.rs +++ b/crates/bevy_ecs/src/query/par_iter.rs @@ -126,7 +126,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryParIter<'w, 's, D, F> { fn get_batch_size(&self, thread_count: usize) -> usize { let max_items = || { let id_iter = self.state.matched_storage_ids.iter(); - if D::IS_DENSE && F::IS_DENSE { + if self.state.is_dense { // SAFETY: We only access table metadata. let tables = unsafe { &self.world.world_metadata().storages().tables }; id_iter diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index 70cf722daff3e4..b94cb4aa904549 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -24,7 +24,10 @@ use super::{ /// An ID for either a table or an archetype. Used for Query iteration. /// /// Query iteration is exclusively dense (over tables) or archetypal (over archetypes) based on whether -/// both `D::IS_DENSE` and `F::IS_DENSE` are true or not. +/// the query filters are dense or not. This is represented by the [`QueryState::is_dense`] field. +/// +/// Note that `D::IS_DENSE` and `F::IS_DENSE` have no relationship with `QueryState::is_dense` and +/// any combination of their values can happen. /// /// This is a union instead of an enum as the usage is determined at compile time, as all [`StorageId`]s for /// a [`QueryState`] will be all [`TableId`]s or all [`ArchetypeId`]s, and not a mixture of both. This @@ -68,6 +71,9 @@ pub struct QueryState { pub(crate) component_access: FilteredAccess, // NOTE: we maintain both a bitset and a vec because iterating the vec is faster pub(super) matched_storage_ids: Vec, + // Represents whether this query iteration is dense or not. When this is true + // `matched_storage_ids` stores `TableId`s, otherwise it stores `ArchetypeId`s. + pub(super) is_dense: bool, pub(crate) fetch_state: D::State, pub(crate) filter_state: F::State, #[cfg(feature = "trace")] @@ -194,10 +200,15 @@ impl QueryState { // properly considered in a global "cross-query" context (both within systems and across systems). component_access.extend(&filter_component_access); + // For queries without dynamic filters the dense-ness of the query is equal to the dense-ness + // of its static type parameters. + let is_dense = D::IS_DENSE && F::IS_DENSE; + Self { world_id: world.id(), archetype_generation: ArchetypeGeneration::initial(), matched_storage_ids: Vec::new(), + is_dense, fetch_state, filter_state, component_access, @@ -222,6 +233,8 @@ impl QueryState { world_id: builder.world().id(), archetype_generation: ArchetypeGeneration::initial(), matched_storage_ids: Vec::new(), + // For dynamic queries the dense-ness is given by the query builder. + is_dense: builder.is_dense(), fetch_state, filter_state, component_access: builder.access().clone(), @@ -450,7 +463,7 @@ impl QueryState { let archetype_index = archetype.id().index(); if !self.matched_archetypes.contains(archetype_index) { self.matched_archetypes.grow_and_insert(archetype_index); - if !D::IS_DENSE || !F::IS_DENSE { + if !self.is_dense { self.matched_storage_ids.push(StorageId { archetype_id: archetype.id(), }); @@ -459,7 +472,7 @@ impl QueryState { let table_index = archetype.table_id().as_usize(); if !self.matched_tables.contains(table_index) { self.matched_tables.grow_and_insert(table_index); - if D::IS_DENSE && F::IS_DENSE { + if self.is_dense { self.matched_storage_ids.push(StorageId { table_id: archetype.table_id(), }); @@ -560,6 +573,7 @@ impl QueryState { world_id: self.world_id, archetype_generation: self.archetype_generation, matched_storage_ids: self.matched_storage_ids.clone(), + is_dense: self.is_dense, fetch_state, filter_state, component_access: self.component_access.clone(), @@ -653,12 +667,15 @@ impl QueryState { warn!("You have tried to join queries with different archetype_generations. This could lead to unpredictable results."); } + // the join is dense of both the queries were dense. + let is_dense = self.is_dense && other.is_dense; + // take the intersection of the matched ids let mut matched_tables = self.matched_tables.clone(); let mut matched_archetypes = self.matched_archetypes.clone(); matched_tables.intersect_with(&other.matched_tables); matched_archetypes.intersect_with(&other.matched_archetypes); - let matched_storage_ids = if NewD::IS_DENSE && NewF::IS_DENSE { + let matched_storage_ids = if is_dense { matched_tables .ones() .map(|id| StorageId { @@ -678,6 +695,7 @@ impl QueryState { world_id: self.world_id, archetype_generation: self.archetype_generation, matched_storage_ids, + is_dense, fetch_state: new_fetch_state, filter_state: new_filter_state, component_access: joined_component_access, @@ -1487,7 +1505,7 @@ impl QueryState { let mut iter = self.iter_unchecked_manual(world, last_run, this_run); let mut accum = init_accum(); for storage_id in queue { - if D::IS_DENSE && F::IS_DENSE { + if self.is_dense { let id = storage_id.table_id; let table = &world.storages().tables.get(id).debug_checked_unwrap(); accum = iter.fold_over_table_range( @@ -1521,7 +1539,7 @@ impl QueryState { #[cfg(feature = "trace")] let _span = self.par_iter_span.enter(); let accum = init_accum(); - if D::IS_DENSE && F::IS_DENSE { + if self.is_dense { let id = storage_id.table_id; let table = world.storages().tables.get(id).debug_checked_unwrap(); self.iter_unchecked_manual(world, last_run, this_run) @@ -1537,7 +1555,7 @@ impl QueryState { }; let storage_entity_count = |storage_id: StorageId| -> usize { - if D::IS_DENSE && F::IS_DENSE { + if self.is_dense { tables[storage_id.table_id].entity_count() } else { archetypes[storage_id.archetype_id].len() @@ -2042,6 +2060,53 @@ mod tests { world.query::<(&A, &B)>().transmute::<&B>(&world2); } + /// Regression test for issue #14528 + #[test] + fn transmute_from_sparse_to_dense() { + #[derive(Component)] + struct Dense; + + #[derive(Component)] + #[component(storage = "SparseSet")] + struct Sparse; + + let mut world = World::new(); + + world.spawn(Dense); + world.spawn((Dense, Sparse)); + + let mut query = world + .query_filtered::<&Dense, With>() + .transmute::<&Dense>(&world); + + let matched = query.iter(&world).count(); + assert_eq!(matched, 1); + } + #[test] + fn transmute_from_dense_to_sparse() { + #[derive(Component)] + struct Dense; + + #[derive(Component)] + #[component(storage = "SparseSet")] + struct Sparse; + + let mut world = World::new(); + + world.spawn(Dense); + world.spawn((Dense, Sparse)); + + let mut query = world + .query::<&Dense>() + .transmute_filtered::<&Dense, With>(&world); + + // Note: `transmute_filtered` is supposed to keep the same matched tables/archetypes, + // so it doesn't actually filter out those entities without `Sparse` and the iteration + // remains dense. + let matched = query.iter(&world).count(); + assert_eq!(matched, 2); + } + #[test] fn join() { let mut world = World::new(); From 1690b28e9f5f5c42027000e24fdae0300f7b3192 Mon Sep 17 00:00:00 2001 From: Erick Z <567737+eckz@users.noreply.github.com> Date: Tue, 27 Aug 2024 15:29:02 +0200 Subject: [PATCH 32/53] Fixing Curve trait not being object safe. (#14939) # Objective - `Curve` was meant to be object safe, but one of the latest commits made it not object safe. - When trying to use `Curve` as `&dyn Curve` this compile error is raised: ``` error[E0038]: the trait `curve::Curve` cannot be made into an object --> crates/bevy_math/src/curve/mod.rs:1025:20 note: for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit --> crates/bevy_math/src/curve/mod.rs:60:8 | 23 | pub trait Curve { | ----- this trait cannot be made into an object... ... 60 | fn sample_iter(&self, iter: impl IntoIterator) -> impl Iterator> { | ^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ...because method `sample_iter` references an `impl Trait` type in its return type | | | ...because method `sample_iter` has generic type parameters ... ``` ## Solution - Making `Curve` object safe again by adding `Self: Sized` to newly added methods. ## Testing - Added new test that ensures the `Curve` trait can be made into an objet. --- crates/bevy_math/src/curve/mod.rs | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index fc54223ed51972..dfadf3db4b6202 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -57,7 +57,10 @@ pub trait Curve { /// The samples are returned in the same order as the parameter values `t_n` were provided and /// will include all results. This leaves the responsibility for things like filtering and /// sorting to the user for maximum flexibility. - fn sample_iter(&self, iter: impl IntoIterator) -> impl Iterator> { + fn sample_iter(&self, iter: impl IntoIterator) -> impl Iterator> + where + Self: Sized, + { iter.into_iter().map(|t| self.sample(t)) } @@ -72,10 +75,10 @@ pub trait Curve { /// The samples are returned in the same order as the parameter values `t_n` were provided and /// will include all results. This leaves the responsibility for things like filtering and /// sorting to the user for maximum flexibility. - fn sample_iter_unchecked( - &self, - iter: impl IntoIterator, - ) -> impl Iterator { + fn sample_iter_unchecked(&self, iter: impl IntoIterator) -> impl Iterator + where + Self: Sized, + { iter.into_iter().map(|t| self.sample_unchecked(t)) } @@ -85,7 +88,10 @@ pub trait Curve { /// The samples are returned in the same order as the parameter values `t_n` were provided and /// will include all results. This leaves the responsibility for things like filtering and /// sorting to the user for maximum flexibility. - fn sample_iter_clamped(&self, iter: impl IntoIterator) -> impl Iterator { + fn sample_iter_clamped(&self, iter: impl IntoIterator) -> impl Iterator + where + Self: Sized, + { iter.into_iter().map(|t| self.sample_clamped(t)) } @@ -760,6 +766,9 @@ where /// must be left-finite. /// /// Curves of this type are produced by [`Curve::chain`]. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] pub struct ChainCurve { first: C, second: D, @@ -1013,6 +1022,15 @@ mod tests { use approx::{assert_abs_diff_eq, AbsDiffEq}; use std::f32::consts::TAU; + #[test] + fn curve_can_be_made_into_an_object() { + let curve = constant_curve(Interval::UNIT, 42.0); + let curve: &dyn Curve = &curve; + + assert_eq!(curve.sample(1.0), Some(42.0)); + assert_eq!(curve.sample(2.0), None); + } + #[test] fn constant_curves() { let curve = constant_curve(Interval::EVERYWHERE, 5.0); From 5f061ea0086c170853e8a9eb8f1c2e6ece414ef3 Mon Sep 17 00:00:00 2001 From: Sam Pettersson Date: Tue, 27 Aug 2024 17:35:01 +0000 Subject: [PATCH 33/53] Fix Adreno 642L crash (#14937) # Objective The Android example on Adreno 642L currently crashes on startup. Previous PRs #14176 and #13323 have adressed this specific crash occurring on some Adreno GPUs, that fix works as it should but isn't applied when to the GPU name contains a suffix like in the case of `642L`. ## Solution - Amending the logic to filter out any parts of the GPU name not containing digits thus enabling the fix on `642L`. ## Testing - Ran the Android example on a Nothing Phone 1. Before this change it crashed, after it works as intended. --------- Co-authored-by: Sam Pettersson --- .../src/batching/gpu_preprocessing.rs | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/crates/bevy_render/src/batching/gpu_preprocessing.rs b/crates/bevy_render/src/batching/gpu_preprocessing.rs index b03f4ba1843adb..7d5ce067f5fbce 100644 --- a/crates/bevy_render/src/batching/gpu_preprocessing.rs +++ b/crates/bevy_render/src/batching/gpu_preprocessing.rs @@ -226,15 +226,31 @@ impl FromWorld for GpuPreprocessingSupport { let adapter = world.resource::(); let device = world.resource::(); - if device.limits().max_compute_workgroup_size_x == 0 || - // filter some Qualcomm devices on Android as they crash when using GPU preprocessing - (cfg!(target_os = "android") && { - let name = adapter.get_info().name; - // filter out Adreno 730 and earlier GPUs (except 720, it's newer than 730) - name.strip_prefix("Adreno (TM) ").is_some_and(|version| - version != "720" && version.parse::().is_ok_and(|version| version <= 730) - ) - }) + // filter some Qualcomm devices on Android as they crash when using GPU preprocessing. + fn is_non_supported_android_device(adapter: &RenderAdapter) -> bool { + if cfg!(target_os = "android") { + let adapter_name = adapter.get_info().name; + + // Filter out Adreno 730 and earlier GPUs (except 720, as it's newer than 730) + // while also taking suffixes into account like Adreno 642L. + let non_supported_adreno_model = |model: &str| -> bool { + let model = model + .chars() + .map_while(|c| c.to_digit(10)) + .fold(0, |acc, digit| acc * 10 + digit); + + model != 720 && model <= 730 + }; + + adapter_name + .strip_prefix("Adreno (TM) ") + .is_some_and(non_supported_adreno_model) + } else { + false + } + } + + if device.limits().max_compute_workgroup_size_x == 0 || is_non_supported_android_device(adapter) { GpuPreprocessingSupport::None } else if !device From 9cdb915809254bf93209ac79e340ed361c0936b8 Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Tue, 27 Aug 2024 13:22:23 -0700 Subject: [PATCH 34/53] Required Components (#14791) ## Introduction This is the first step in my [Next Generation Scene / UI Proposal](https://github.com/bevyengine/bevy/discussions/14437). Fixes https://github.com/bevyengine/bevy/issues/7272 #14800. Bevy's current Bundles as the "unit of construction" hamstring the UI user experience and have been a pain point in the Bevy ecosystem generally when composing scenes: * They are an additional _object defining_ concept, which must be learned separately from components. Notably, Bundles _are not present at runtime_, which is confusing and limiting. * They can completely erase the _defining component_ during Bundle init. For example, `ButtonBundle { style: Style::default(), ..default() }` _makes no mention_ of the `Button` component symbol, which is what makes the Entity a "button"! * They are not capable of representing "dependency inheritance" without completely non-viable / ergonomically crushing nested bundles. This limitation is especially painful in UI scenarios, but it applies to everything across the board. * They introduce a bunch of additional nesting when defining scenes, making them ugly to look at * They introduce component name "stutter": `SomeBundle { component_name: ComponentName::new() }` * They require copious sprinklings of `..default()` when spawning them in Rust code, due to the additional layer of nesting **Required Components** solve this by allowing you to define which components a given component needs, and how to construct those components when they aren't explicitly provided. This is what a `ButtonBundle` looks like with Bundles (the current approach): ```rust #[derive(Component, Default)] struct Button; #[derive(Bundle, Default)] struct ButtonBundle { pub button: Button, pub node: Node, pub style: Style, pub interaction: Interaction, pub focus_policy: FocusPolicy, pub border_color: BorderColor, pub border_radius: BorderRadius, pub image: UiImage, pub transform: Transform, pub global_transform: GlobalTransform, pub visibility: Visibility, pub inherited_visibility: InheritedVisibility, pub view_visibility: ViewVisibility, pub z_index: ZIndex, } commands.spawn(ButtonBundle { style: Style { width: Val::Px(100.0), height: Val::Px(50.0), ..default() }, focus_policy: FocusPolicy::Block, ..default() }) ``` And this is what it looks like with Required Components: ```rust #[derive(Component)] #[require(Node, UiImage)] struct Button; commands.spawn(( Button, Style { width: Val::Px(100.0), height: Val::Px(50.0), ..default() }, FocusPolicy::Block, )); ``` With Required Components, we mention only the most relevant components. Every component required by `Node` (ex: `Style`, `FocusPolicy`, etc) is automatically brought in! ### Efficiency 1. At insertion/spawn time, Required Components (including recursive required components) are initialized and inserted _as if they were manually inserted alongside the given components_. This means that this is maximally efficient: there are no archetype or table moves. 2. Required components are only initialized and inserted if they were not manually provided by the developer. For the code example in the previous section, because `Style` and `FocusPolicy` are inserted manually, they _will not_ be initialized and inserted as part of the required components system. Efficient! 3. The "missing required components _and_ constructors needed for an insertion" are cached in the "archetype graph edge", meaning they aren't computed per-insertion. When a component is inserted, the "missing required components" list is iterated (and that graph edge (AddBundle) is actually already looked up for us during insertion, because we need that for "normal" insert logic too). ### IDE Integration The `#[require(SomeComponent)]` macro has been written in such a way that Rust Analyzer can provide type-inspection-on-hover and `F12` / go-to-definition for required components. ### Custom Constructors The `require` syntax expects a `Default` constructor by default, but it can be overridden with a custom constructor: ```rust #[derive(Component)] #[require( Node, Style(button_style), UiImage )] struct Button; fn button_style() -> Style { Style { width: Val::Px(100.0), ..default() } } ``` ### Multiple Inheritance You may have noticed by now that this behaves a bit like "multiple inheritance". One of the problems that this presents is that it is possible to have duplicate requires for a given type at different levels of the inheritance tree: ```rust #[derive(Component) struct X(usize); #[derive(Component)] #[require(X(x1)) struct Y; fn x1() -> X { X(1) } #[derive(Component)] #[require( Y, X(x2), )] struct Z; fn x2() -> X { X(2) } // What version of X is inserted for Z? commands.spawn(Z); ``` This is allowed (and encouraged), although this doesn't appear to occur much in practice. First: only one version of `X` is initialized and inserted for `Z`. In the case above, I think we can all probably agree that it makes the most sense to use the `x2` constructor for `X`, because `Y`'s `x1` constructor exists "beneath" `Z` in the inheritance hierarchy; `Z`'s constructor is "more specific". The algorithm is simple and predictable: 1. Use all of the constructors (including default constructors) directly defined in the spawned component's require list 2. In the order the requires are defined in `#[require()]`, recursively visit the require list of each of the components in the list (this is a depth Depth First Search). When a constructor is found, it will only be used if one has not already been found. From a user perspective, just think about this as the following: 1. Specifying a required component constructor for `Foo` directly on a spawned component `Bar` will result in that constructor being used (and overriding existing constructors lower in the inheritance tree). This is the classic "inheritance override" behavior people expect. 2. For cases where "multiple inheritance" results in constructor clashes, Components should be listed in "importance order". List a component earlier in the requirement list to initialize its inheritance tree earlier. Required Components _does_ generally result in a model where component values are decoupled from each other at construction time. Notably, some existing Bundle patterns use bundle constructors to initialize multiple components with shared state. I think (in general) moving away from this is necessary: 1. It allows Required Components (and the Scene system more generally) to operate according to simple rules 2. The "do arbitrary init value sharing in Bundle constructors" approach _already_ causes data consistency problems, and those problems would be exacerbated in the context of a Scene/UI system. For cases where shared state is truly necessary, I think we are better served by observers / hooks. 3. If a situation _truly_ needs shared state constructors (which should be rare / generally discouraged), Bundles are still there if they are needed. ## Next Steps * **Require Construct-ed Components**: I have already implemented this (as defined in the [Next Generation Scene / UI Proposal](https://github.com/bevyengine/bevy/discussions/14437). However I've removed `Construct` support from this PR, as that has not landed yet. Adding this back in requires relatively minimal changes to the current impl, and can be done as part of a future Construct pr. * **Port Built-in Bundles to Required Components**: This isn't something we should do right away. It will require rethinking our public interfaces, which IMO should be done holistically after the rest of Next Generation Scene / UI lands. I think we should merge this PR first and let people experiment _inside their own code with their own Components_ while we wait for the rest of the new scene system to land. * **_Consider_ Automatic Required Component Removal**: We should evaluate _if_ automatic Required Component removal should be done. Ex: if all components that explicitly require a component are removed, automatically remove that component. This issue has been explicitly deferred in this PR, as I consider the insertion behavior to be desirable on its own (and viable on its own). I am also doubtful that we can find a design that has behavior we actually want. Aka: can we _really_ distinguish between a component that is "only there because it was automatically inserted" and "a component that was necessary / should be kept". See my [discussion response here](https://github.com/bevyengine/bevy/discussions/14437#discussioncomment-10268668) for more details. --------- Co-authored-by: Alice Cecile Co-authored-by: BD103 <59022059+BD103@users.noreply.github.com> Co-authored-by: Pascal Hertleif --- crates/bevy_ecs/macros/src/component.rs | 153 +++++++-- crates/bevy_ecs/macros/src/lib.rs | 14 +- crates/bevy_ecs/src/archetype.rs | 25 +- crates/bevy_ecs/src/bundle.rs | 284 +++++++++++++--- crates/bevy_ecs/src/component.rs | 352 ++++++++++++++++++-- crates/bevy_ecs/src/entity/mod.rs | 2 +- crates/bevy_ecs/src/lib.rs | 229 +++++++++++++ crates/bevy_ecs/src/system/mod.rs | 2 +- crates/bevy_ecs/src/world/deferred_world.rs | 4 +- crates/bevy_ecs/src/world/entity_ref.rs | 35 +- 10 files changed, 987 insertions(+), 113 deletions(-) diff --git a/crates/bevy_ecs/macros/src/component.rs b/crates/bevy_ecs/macros/src/component.rs index f9f1ad1fc67b15..8bd444304ecc7c 100644 --- a/crates/bevy_ecs/macros/src/component.rs +++ b/crates/bevy_ecs/macros/src/component.rs @@ -1,7 +1,16 @@ use proc_macro::TokenStream; use proc_macro2::{Span, TokenStream as TokenStream2}; -use quote::quote; -use syn::{parse_macro_input, parse_quote, DeriveInput, ExprPath, Ident, LitStr, Path, Result}; +use quote::{quote, ToTokens}; +use std::collections::HashSet; +use syn::{ + parenthesized, + parse::Parse, + parse_macro_input, parse_quote, + punctuated::Punctuated, + spanned::Spanned, + token::{Comma, Paren}, + DeriveInput, ExprPath, Ident, LitStr, Path, Result, +}; pub fn derive_event(input: TokenStream) -> TokenStream { let mut ast = parse_macro_input!(input as DeriveInput); @@ -66,12 +75,55 @@ pub fn derive_component(input: TokenStream) -> TokenStream { .predicates .push(parse_quote! { Self: Send + Sync + 'static }); + let requires = &attrs.requires; + let mut register_required = Vec::with_capacity(attrs.requires.iter().len()); + let mut register_recursive_requires = Vec::with_capacity(attrs.requires.iter().len()); + if let Some(requires) = requires { + for require in requires { + let ident = &require.path; + register_recursive_requires.push(quote! { + <#ident as Component>::register_required_components(components, storages, required_components); + }); + if let Some(func) = &require.func { + register_required.push(quote! { + required_components.register(components, storages, || { let x: #ident = #func().into(); x }); + }); + } else { + register_required.push(quote! { + required_components.register(components, storages, <#ident as Default>::default); + }); + } + } + } let struct_name = &ast.ident; let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl(); + let required_component_docs = attrs.requires.map(|r| { + let paths = r + .iter() + .map(|r| format!("[`{}`]", r.path.to_token_stream())) + .collect::>() + .join(", "); + let doc = format!("Required Components: {paths}. \n\n A component's Required Components are inserted whenever it is inserted. Note that this will also insert the required components _of_ the required components, recursively, in depth-first order."); + quote! { + #[doc = #doc] + } + }); + + // This puts `register_required` before `register_recursive_requires` to ensure that the constructors of _all_ top + // level components are initialized first, giving them precedence over recursively defined constructors for the same component type TokenStream::from(quote! { + #required_component_docs impl #impl_generics #bevy_ecs_path::component::Component for #struct_name #type_generics #where_clause { const STORAGE_TYPE: #bevy_ecs_path::component::StorageType = #storage; + fn register_required_components( + components: &mut #bevy_ecs_path::component::Components, + storages: &mut #bevy_ecs_path::storage::Storages, + required_components: &mut #bevy_ecs_path::component::RequiredComponents + ) { + #(#register_required)* + #(#register_recursive_requires)* + } #[allow(unused_variables)] fn register_component_hooks(hooks: &mut #bevy_ecs_path::component::ComponentHooks) { @@ -86,6 +138,8 @@ pub fn derive_component(input: TokenStream) -> TokenStream { pub const COMPONENT: &str = "component"; pub const STORAGE: &str = "storage"; +pub const REQUIRE: &str = "require"; + pub const ON_ADD: &str = "on_add"; pub const ON_INSERT: &str = "on_insert"; pub const ON_REPLACE: &str = "on_replace"; @@ -93,6 +147,7 @@ pub const ON_REMOVE: &str = "on_remove"; struct Attrs { storage: StorageTy, + requires: Option>, on_add: Option, on_insert: Option, on_replace: Option, @@ -105,6 +160,11 @@ enum StorageTy { SparseSet, } +struct Require { + path: Path, + func: Option, +} + // values for `storage` attribute const TABLE: &str = "Table"; const SPARSE_SET: &str = "SparseSet"; @@ -116,42 +176,77 @@ fn parse_component_attr(ast: &DeriveInput) -> Result { on_insert: None, on_replace: None, on_remove: None, + requires: None, }; - for meta in ast.attrs.iter().filter(|a| a.path().is_ident(COMPONENT)) { - meta.parse_nested_meta(|nested| { - if nested.path.is_ident(STORAGE) { - attrs.storage = match nested.value()?.parse::()?.value() { - s if s == TABLE => StorageTy::Table, - s if s == SPARSE_SET => StorageTy::SparseSet, - s => { - return Err(nested.error(format!( - "Invalid storage type `{s}`, expected '{TABLE}' or '{SPARSE_SET}'.", - ))); - } - }; - Ok(()) - } else if nested.path.is_ident(ON_ADD) { - attrs.on_add = Some(nested.value()?.parse::()?); - Ok(()) - } else if nested.path.is_ident(ON_INSERT) { - attrs.on_insert = Some(nested.value()?.parse::()?); - Ok(()) - } else if nested.path.is_ident(ON_REPLACE) { - attrs.on_replace = Some(nested.value()?.parse::()?); - Ok(()) - } else if nested.path.is_ident(ON_REMOVE) { - attrs.on_remove = Some(nested.value()?.parse::()?); - Ok(()) + let mut require_paths = HashSet::new(); + for attr in ast.attrs.iter() { + if attr.path().is_ident(COMPONENT) { + attr.parse_nested_meta(|nested| { + if nested.path.is_ident(STORAGE) { + attrs.storage = match nested.value()?.parse::()?.value() { + s if s == TABLE => StorageTy::Table, + s if s == SPARSE_SET => StorageTy::SparseSet, + s => { + return Err(nested.error(format!( + "Invalid storage type `{s}`, expected '{TABLE}' or '{SPARSE_SET}'.", + ))); + } + }; + Ok(()) + } else if nested.path.is_ident(ON_ADD) { + attrs.on_add = Some(nested.value()?.parse::()?); + Ok(()) + } else if nested.path.is_ident(ON_INSERT) { + attrs.on_insert = Some(nested.value()?.parse::()?); + Ok(()) + } else if nested.path.is_ident(ON_REPLACE) { + attrs.on_replace = Some(nested.value()?.parse::()?); + Ok(()) + } else if nested.path.is_ident(ON_REMOVE) { + attrs.on_remove = Some(nested.value()?.parse::()?); + Ok(()) + } else { + Err(nested.error("Unsupported attribute")) + } + })?; + } else if attr.path().is_ident(REQUIRE) { + let punctuated = + attr.parse_args_with(Punctuated::::parse_terminated)?; + for require in punctuated.iter() { + if !require_paths.insert(require.path.to_token_stream().to_string()) { + return Err(syn::Error::new( + require.path.span(), + "Duplicate required components are not allowed.", + )); + } + } + if let Some(current) = &mut attrs.requires { + current.extend(punctuated); } else { - Err(nested.error("Unsupported attribute")) + attrs.requires = Some(punctuated); } - })?; + } } Ok(attrs) } +impl Parse for Require { + fn parse(input: syn::parse::ParseStream) -> Result { + let path = input.parse::()?; + let func = if input.peek(Paren) { + let content; + parenthesized!(content in input); + let func = content.parse::()?; + Some(func) + } else { + None + }; + Ok(Require { path, func }) + } +} + fn storage_path(bevy_ecs_path: &Path, ty: StorageTy) -> TokenStream2 { let storage_type = match ty { StorageTy::Table => Ident::new("Table", Span::call_site()), diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index 49a0d6bb013d28..ed5cc22c247a76 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -77,6 +77,7 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { let mut field_get_component_ids = Vec::new(); let mut field_get_components = Vec::new(); let mut field_from_components = Vec::new(); + let mut field_required_components = Vec::new(); for (((i, field_type), field_kind), field) in field_type .iter() .enumerate() @@ -88,6 +89,9 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { field_component_ids.push(quote! { <#field_type as #ecs_path::bundle::Bundle>::component_ids(components, storages, &mut *ids); }); + field_required_components.push(quote! { + <#field_type as #ecs_path::bundle::Bundle>::register_required_components(components, storages, required_components); + }); field_get_component_ids.push(quote! { <#field_type as #ecs_path::bundle::Bundle>::get_component_ids(components, &mut *ids); }); @@ -153,6 +157,14 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { #(#field_from_components)* } } + + fn register_required_components( + components: &mut #ecs_path::component::Components, + storages: &mut #ecs_path::storage::Storages, + required_components: &mut #ecs_path::component::RequiredComponents + ){ + #(#field_required_components)* + } } impl #impl_generics #ecs_path::bundle::DynamicBundle for #struct_name #ty_generics #where_clause { @@ -527,7 +539,7 @@ pub fn derive_resource(input: TokenStream) -> TokenStream { component::derive_resource(input) } -#[proc_macro_derive(Component, attributes(component))] +#[proc_macro_derive(Component, attributes(component, require))] pub fn derive_component(input: TokenStream) -> TokenStream { component::derive_component(input) } diff --git a/crates/bevy_ecs/src/archetype.rs b/crates/bevy_ecs/src/archetype.rs index baafd514db30cd..b7cad4e3895125 100644 --- a/crates/bevy_ecs/src/archetype.rs +++ b/crates/bevy_ecs/src/archetype.rs @@ -21,7 +21,7 @@ use crate::{ bundle::BundleId, - component::{ComponentId, Components, StorageType}, + component::{ComponentId, Components, RequiredComponentConstructor, StorageType}, entity::{Entity, EntityLocation}, observer::Observers, storage::{ImmutableSparseSet, SparseArray, SparseSet, SparseSetIndex, TableId, TableRow}, @@ -123,10 +123,31 @@ pub(crate) struct AddBundle { /// For each component iterated in the same order as the source [`Bundle`](crate::bundle::Bundle), /// indicate if the component is newly added to the target archetype or if it already existed pub bundle_status: Vec, + /// The set of additional required components that must be initialized immediately when adding this Bundle. + /// + /// The initial values are determined based on the provided constructor, falling back to the `Default` trait if none is given. + pub required_components: Vec, + /// The components added by this bundle. This includes any Required Components that are inserted when adding this bundle. pub added: Vec, + /// The components that were explicitly contributed by this bundle, but already existed in the archetype. This _does not_ include any + /// Required Components. pub existing: Vec, } +impl AddBundle { + pub(crate) fn iter_inserted(&self) -> impl Iterator + '_ { + self.added.iter().chain(self.existing.iter()).copied() + } + + pub(crate) fn iter_added(&self) -> impl Iterator + '_ { + self.added.iter().copied() + } + + pub(crate) fn iter_existing(&self) -> impl Iterator + '_ { + self.existing.iter().copied() + } +} + /// This trait is used to report the status of [`Bundle`](crate::bundle::Bundle) components /// being added to a given entity, relative to that entity's original archetype. /// See [`crate::bundle::BundleInfo::write_components`] for more info. @@ -208,6 +229,7 @@ impl Edges { bundle_id: BundleId, archetype_id: ArchetypeId, bundle_status: Vec, + required_components: Vec, added: Vec, existing: Vec, ) { @@ -216,6 +238,7 @@ impl Edges { AddBundle { archetype_id, bundle_status, + required_components, added, existing, }, diff --git a/crates/bevy_ecs/src/bundle.rs b/crates/bevy_ecs/src/bundle.rs index 770926599f1c63..8311040bb32759 100644 --- a/crates/bevy_ecs/src/bundle.rs +++ b/crates/bevy_ecs/src/bundle.rs @@ -2,8 +2,6 @@ //! //! This module contains the [`Bundle`] trait and some other helper types. -use std::any::TypeId; - pub use bevy_ecs_macros::Bundle; use crate::{ @@ -11,7 +9,10 @@ use crate::{ AddBundle, Archetype, ArchetypeId, Archetypes, BundleComponentStatus, ComponentStatus, SpawnBundleStatus, }, - component::{Component, ComponentId, Components, StorageType, Tick}, + component::{ + Component, ComponentId, Components, RequiredComponentConstructor, RequiredComponents, + StorageType, Tick, + }, entity::{Entities, Entity, EntityLocation}, observer::Observers, prelude::World, @@ -19,12 +20,11 @@ use crate::{ storage::{SparseSetIndex, SparseSets, Storages, Table, TableRow}, world::{unsafe_world_cell::UnsafeWorldCell, ON_ADD, ON_INSERT, ON_REPLACE}, }; - use bevy_ptr::{ConstNonNull, OwningPtr}; use bevy_utils::{all_tuples, HashMap, HashSet, TypeIdMap}; #[cfg(feature = "track_change_detection")] use std::panic::Location; -use std::ptr::NonNull; +use std::{any::TypeId, ptr::NonNull}; /// The `Bundle` trait enables insertion and removal of [`Component`]s from an entity. /// @@ -174,6 +174,13 @@ pub unsafe trait Bundle: DynamicBundle + Send + Sync + 'static { // Ensure that the `OwningPtr` is used correctly F: for<'a> FnMut(&'a mut T) -> OwningPtr<'a>, Self: Sized; + + /// Registers components that are required by the components in this [`Bundle`]. + fn register_required_components( + _components: &mut Components, + _storages: &mut Storages, + _required_components: &mut RequiredComponents, + ); } /// The parts from [`Bundle`] that don't require statically knowing the components of the bundle. @@ -212,6 +219,14 @@ unsafe impl Bundle for C { unsafe { ptr.read() } } + fn register_required_components( + components: &mut Components, + storages: &mut Storages, + required_components: &mut RequiredComponents, + ) { + ::register_required_components(components, storages, required_components); + } + fn get_component_ids(components: &Components, ids: &mut impl FnMut(Option)) { ids(components.get_id(TypeId::of::())); } @@ -255,6 +270,14 @@ macro_rules! tuple_impl { // https://doc.rust-lang.org/reference/expressions.html#evaluation-order-of-operands unsafe { ($(<$name as Bundle>::from_components(ctx, func),)*) } } + + fn register_required_components( + _components: &mut Components, + _storages: &mut Storages, + _required_components: &mut RequiredComponents, + ) { + $(<$name as Bundle>::register_required_components(_components, _storages, _required_components);)* + } } impl<$($name: Bundle),*> DynamicBundle for ($($name,)*) { @@ -321,10 +344,17 @@ pub(crate) enum InsertMode { /// [`World`]: crate::world::World pub struct BundleInfo { id: BundleId, - // SAFETY: Every ID in this list must be valid within the World that owns the BundleInfo, - // must have its storage initialized (i.e. columns created in tables, sparse set created), - // and must be in the same order as the source bundle type writes its components in. + /// The list of all components contributed by the bundle (including Required Components). This is in + /// the order `[EXPLICIT_COMPONENTS][REQUIRED_COMPONENTS]` + /// + /// # Safety + /// Every ID in this list must be valid within the World that owns the [`BundleInfo`], + /// must have its storage initialized (i.e. columns created in tables, sparse set created), + /// and the range (0..`explicit_components_len`) must be in the same order as the source bundle + /// type writes its components in. component_ids: Vec, + required_components: Vec, + explicit_components_len: usize, } impl BundleInfo { @@ -338,7 +368,7 @@ impl BundleInfo { unsafe fn new( bundle_type_name: &'static str, components: &Components, - component_ids: Vec, + mut component_ids: Vec, id: BundleId, ) -> BundleInfo { let mut deduped = component_ids.clone(); @@ -367,11 +397,35 @@ impl BundleInfo { panic!("Bundle {bundle_type_name} has duplicate components: {names}"); } + let explicit_components_len = component_ids.len(); + let mut required_components = RequiredComponents::default(); + for component_id in component_ids.iter().copied() { + // SAFETY: caller has verified that all ids are valid + let info = unsafe { components.get_info_unchecked(component_id) }; + required_components.merge(info.required_components()); + } + required_components.remove_explicit_components(&component_ids); + let required_components = required_components + .0 + .into_iter() + .map(|(component_id, v)| { + // This adds required components to the component_ids list _after_ using that list to remove explicitly provided + // components. This ordering is important! + component_ids.push(component_id); + v + }) + .collect(); + // SAFETY: The caller ensures that component_ids: // - is valid for the associated world // - has had its storage initialized // - is in the same order as the source bundle type - BundleInfo { id, component_ids } + BundleInfo { + id, + component_ids, + required_components, + explicit_components_len, + } } /// Returns a value identifying the associated [`Bundle`] type. @@ -380,16 +434,49 @@ impl BundleInfo { self.id } - /// Returns the [ID](ComponentId) of each component stored in this bundle. + /// Returns the [ID](ComponentId) of each component explicitly defined in this bundle (ex: Required Components are excluded). + /// + /// For all components contributed by this bundle (including Required Components), see [`BundleInfo::contributed_components`] + #[inline] + pub fn explicit_components(&self) -> &[ComponentId] { + &self.component_ids[0..self.explicit_components_len] + } + + /// Returns the [ID](ComponentId) of each Required Component needed by this bundle. This _does not include_ Required Components that are + /// explicitly provided by the bundle. #[inline] - pub fn components(&self) -> &[ComponentId] { + pub fn required_components(&self) -> &[ComponentId] { + &self.component_ids[self.explicit_components_len..] + } + + /// Returns the [ID](ComponentId) of each component contributed by this bundle. This includes Required Components. + /// + /// For only components explicitly defined in this bundle, see [`BundleInfo::explicit_components`] + #[inline] + pub fn contributed_components(&self) -> &[ComponentId] { &self.component_ids } - /// Returns an iterator over the [ID](ComponentId) of each component stored in this bundle. + /// Returns an iterator over the [ID](ComponentId) of each component explicitly defined in this bundle (ex: this excludes Required Components). + + /// To iterate all components contributed by this bundle (including Required Components), see [`BundleInfo::iter_contributed_components`] #[inline] - pub fn iter_components(&self) -> impl Iterator + '_ { - self.component_ids.iter().cloned() + pub fn iter_explicit_components(&self) -> impl Iterator + '_ { + self.explicit_components().iter().copied() + } + + /// Returns an iterator over the [ID](ComponentId) of each component contributed by this bundle. This includes Required Components. + /// + /// To iterate only components explicitly defined in this bundle, see [`BundleInfo::iter_explicit_components`] + #[inline] + pub fn iter_contributed_components(&self) -> impl Iterator + '_ { + self.component_ids.iter().copied() + } + + /// Returns an iterator over the [ID](ComponentId) of each Required Component needed by this bundle. This _does not include_ Required Components that are + /// explicitly provided by the bundle. + pub fn iter_required_components(&self) -> impl Iterator + '_ { + self.required_components().iter().copied() } /// This writes components from a given [`Bundle`] to the given entity. @@ -410,11 +497,12 @@ impl BundleInfo { /// `entity`, `bundle` must match this [`BundleInfo`]'s type #[inline] #[allow(clippy::too_many_arguments)] - unsafe fn write_components( + unsafe fn write_components<'a, T: DynamicBundle, S: BundleComponentStatus>( &self, table: &mut Table, sparse_sets: &mut SparseSets, bundle_component_status: &S, + required_components: impl Iterator, entity: Entity, table_row: TableRow, change_tick: Tick, @@ -475,6 +563,74 @@ impl BundleInfo { } bundle_component += 1; }); + + for required_component in required_components { + required_component.initialize( + table, + sparse_sets, + change_tick, + table_row, + entity, + #[cfg(feature = "track_change_detection")] + caller, + ); + } + } + + /// Internal method to initialize a required component from an [`OwningPtr`]. This should ultimately be called + /// in the context of [`BundleInfo::write_components`], via [`RequiredComponentConstructor::initialize`]. + /// + /// # Safety + /// + /// `component_ptr` must point to a required component value that matches the given `component_id`. The `storage_type` must match + /// the type associated with `component_id`. The `entity` and `table_row` must correspond to an entity with an uninitialized + /// component matching `component_id`. + /// + /// This method _should not_ be called outside of [`BundleInfo::write_components`]. + /// For more information, read the [`BundleInfo::write_components`] safety docs. + /// This function inherits the safety requirements defined there. + #[allow(clippy::too_many_arguments)] + pub(crate) unsafe fn initialize_required_component( + table: &mut Table, + sparse_sets: &mut SparseSets, + change_tick: Tick, + table_row: TableRow, + entity: Entity, + component_id: ComponentId, + storage_type: StorageType, + component_ptr: OwningPtr, + #[cfg(feature = "track_change_detection")] caller: &'static Location<'static>, + ) { + { + match storage_type { + StorageType::Table => { + let column = + // SAFETY: If component_id is in required_components, BundleInfo::new requires that + // the target table contains the component. + unsafe { table.get_column_mut(component_id).debug_checked_unwrap() }; + column.initialize( + table_row, + component_ptr, + change_tick, + #[cfg(feature = "track_change_detection")] + caller, + ); + } + StorageType::SparseSet => { + let sparse_set = + // SAFETY: If component_id is in required_components, BundleInfo::new requires that + // a sparse set exists for the component. + unsafe { sparse_sets.get_mut(component_id).debug_checked_unwrap() }; + sparse_set.insert( + entity, + component_ptr, + change_tick, + #[cfg(feature = "track_change_detection")] + caller, + ); + } + } + } } /// Adds a bundle to the given archetype and returns the resulting archetype. This could be the @@ -495,15 +651,16 @@ impl BundleInfo { } let mut new_table_components = Vec::new(); let mut new_sparse_set_components = Vec::new(); - let mut bundle_status = Vec::with_capacity(self.component_ids.len()); + let mut bundle_status = Vec::with_capacity(self.explicit_components_len); + let mut added_required_components = Vec::new(); let mut added = Vec::new(); - let mut mutated = Vec::new(); + let mut existing = Vec::new(); let current_archetype = &mut archetypes[archetype_id]; - for component_id in self.component_ids.iter().cloned() { + for component_id in self.iter_explicit_components() { if current_archetype.contains(component_id) { bundle_status.push(ComponentStatus::Existing); - mutated.push(component_id); + existing.push(component_id); } else { bundle_status.push(ComponentStatus::Added); added.push(component_id); @@ -516,10 +673,34 @@ impl BundleInfo { } } + for (index, component_id) in self.iter_required_components().enumerate() { + if !current_archetype.contains(component_id) { + added_required_components.push(self.required_components[index].clone()); + added.push(component_id); + // SAFETY: component_id exists + let component_info = unsafe { components.get_info_unchecked(component_id) }; + match component_info.storage_type() { + StorageType::Table => { + new_table_components.push(component_id); + } + StorageType::SparseSet => { + new_sparse_set_components.push(component_id); + } + } + } + } + if new_table_components.is_empty() && new_sparse_set_components.is_empty() { let edges = current_archetype.edges_mut(); // the archetype does not change when we add this bundle - edges.insert_add_bundle(self.id, archetype_id, bundle_status, added, mutated); + edges.insert_add_bundle( + self.id, + archetype_id, + bundle_status, + added_required_components, + added, + existing, + ); archetype_id } else { let table_id; @@ -568,8 +749,9 @@ impl BundleInfo { self.id, new_archetype_id, bundle_status, + added_required_components, added, - mutated, + existing, ); new_archetype_id } @@ -720,13 +902,13 @@ impl<'w> BundleInserter<'w> { let mut deferred_world = self.world.into_deferred(); if insert_mode == InsertMode::Replace { - deferred_world.trigger_on_replace( - archetype, - entity, - add_bundle.existing.iter().copied(), - ); + deferred_world.trigger_on_replace(archetype, entity, add_bundle.iter_existing()); if archetype.has_replace_observer() { - deferred_world.trigger_observers(ON_REPLACE, entity, &add_bundle.existing); + deferred_world.trigger_observers( + ON_REPLACE, + entity, + add_bundle.iter_existing(), + ); } } } @@ -747,6 +929,7 @@ impl<'w> BundleInserter<'w> { table, sparse_sets, add_bundle, + add_bundle.required_components.iter(), entity, location.table_row, self.change_tick, @@ -788,6 +971,7 @@ impl<'w> BundleInserter<'w> { table, sparse_sets, add_bundle, + add_bundle.required_components.iter(), entity, result.table_row, self.change_tick, @@ -870,6 +1054,7 @@ impl<'w> BundleInserter<'w> { new_table, sparse_sets, add_bundle, + add_bundle.required_components.iter(), entity, move_result.new_row, self.change_tick, @@ -890,9 +1075,9 @@ impl<'w> BundleInserter<'w> { // SAFETY: All components in the bundle are guaranteed to exist in the World // as they must be initialized before creating the BundleInfo. unsafe { - deferred_world.trigger_on_add(new_archetype, entity, add_bundle.added.iter().cloned()); + deferred_world.trigger_on_add(new_archetype, entity, add_bundle.iter_added()); if new_archetype.has_add_observer() { - deferred_world.trigger_observers(ON_ADD, entity, &add_bundle.added); + deferred_world.trigger_observers(ON_ADD, entity, add_bundle.iter_added()); } match insert_mode { InsertMode::Replace => { @@ -900,13 +1085,13 @@ impl<'w> BundleInserter<'w> { deferred_world.trigger_on_insert( new_archetype, entity, - bundle_info.iter_components(), + add_bundle.iter_inserted(), ); if new_archetype.has_insert_observer() { deferred_world.trigger_observers( ON_INSERT, entity, - bundle_info.components(), + add_bundle.iter_inserted(), ); } } @@ -916,10 +1101,14 @@ impl<'w> BundleInserter<'w> { deferred_world.trigger_on_insert( new_archetype, entity, - add_bundle.added.iter().cloned(), + add_bundle.iter_added(), ); if new_archetype.has_insert_observer() { - deferred_world.trigger_observers(ON_INSERT, entity, &add_bundle.added); + deferred_world.trigger_observers( + ON_INSERT, + entity, + add_bundle.iter_added(), + ); } } } @@ -1017,6 +1206,7 @@ impl<'w> BundleSpawner<'w> { table, sparse_sets, &SpawnBundleStatus, + bundle_info.required_components.iter(), entity, table_row, self.change_tick, @@ -1036,13 +1226,29 @@ impl<'w> BundleSpawner<'w> { // SAFETY: All components in the bundle are guaranteed to exist in the World // as they must be initialized before creating the BundleInfo. unsafe { - deferred_world.trigger_on_add(archetype, entity, bundle_info.iter_components()); + deferred_world.trigger_on_add( + archetype, + entity, + bundle_info.iter_contributed_components(), + ); if archetype.has_add_observer() { - deferred_world.trigger_observers(ON_ADD, entity, bundle_info.components()); + deferred_world.trigger_observers( + ON_ADD, + entity, + bundle_info.iter_contributed_components(), + ); } - deferred_world.trigger_on_insert(archetype, entity, bundle_info.iter_components()); + deferred_world.trigger_on_insert( + archetype, + entity, + bundle_info.iter_contributed_components(), + ); if archetype.has_insert_observer() { - deferred_world.trigger_observers(ON_INSERT, entity, bundle_info.components()); + deferred_world.trigger_observers( + ON_INSERT, + entity, + bundle_info.iter_contributed_components(), + ); } }; @@ -1125,7 +1331,7 @@ impl Bundles { ) -> BundleId { let bundle_infos = &mut self.bundle_infos; let id = *self.bundle_ids.entry(TypeId::of::()).or_insert_with(|| { - let mut component_ids = Vec::new(); + let mut component_ids= Vec::new(); T::component_ids(components, storages, &mut |id| component_ids.push(id)); let id = BundleId(bundle_infos.len()); let bundle_info = diff --git a/crates/bevy_ecs/src/component.rs b/crates/bevy_ecs/src/component.rs index 161db19b9a0e97..53e64e7e2c1dcc 100644 --- a/crates/bevy_ecs/src/component.rs +++ b/crates/bevy_ecs/src/component.rs @@ -3,9 +3,10 @@ use crate::{ self as bevy_ecs, archetype::ArchetypeFlags, + bundle::BundleInfo, change_detection::MAX_CHANGE_AGE, entity::Entity, - storage::{SparseSetIndex, Storages}, + storage::{SparseSetIndex, SparseSets, Storages, Table, TableRow}, system::{Local, Resource, SystemParam}, world::{DeferredWorld, FromWorld, World}, }; @@ -13,15 +14,18 @@ pub use bevy_ecs_macros::Component; use bevy_ptr::{OwningPtr, UnsafeCellDeref}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; -use bevy_utils::TypeIdMap; -use std::cell::UnsafeCell; +use bevy_utils::{HashMap, TypeIdMap}; +#[cfg(feature = "track_change_detection")] +use std::panic::Location; use std::{ alloc::Layout, any::{Any, TypeId}, borrow::Cow, marker::PhantomData, mem::needs_drop, + sync::Arc, }; +use std::{cell::UnsafeCell, fmt::Debug}; /// A data type that can be used to store data for an [entity]. /// @@ -93,6 +97,141 @@ use std::{ /// [`Table`]: crate::storage::Table /// [`SparseSet`]: crate::storage::SparseSet /// +/// # Required Components +/// +/// Components can specify Required Components. If some [`Component`] `A` requires [`Component`] `B`, then when `A` is inserted, +/// `B` will _also_ be initialized and inserted (if it was not manually specified). +/// +/// The [`Default`] constructor will be used to initialize the component, by default: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// #[derive(Component)] +/// #[require(B)] +/// struct A; +/// +/// #[derive(Component, Default, PartialEq, Eq, Debug)] +/// struct B(usize); +/// +/// # let mut world = World::default(); +/// // This will implicitly also insert B with the Default constructor +/// let id = world.spawn(A).id(); +/// assert_eq!(&B(0), world.entity(id).get::().unwrap()); +/// +/// // This will _not_ implicitly insert B, because it was already provided +/// world.spawn((A, B(11))); +/// ``` +/// +/// Components can have more than one required component: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// #[derive(Component)] +/// #[require(B, C)] +/// struct A; +/// +/// #[derive(Component, Default, PartialEq, Eq, Debug)] +/// #[require(C)] +/// struct B(usize); +/// +/// #[derive(Component, Default, PartialEq, Eq, Debug)] +/// struct C(u32); +/// +/// # let mut world = World::default(); +/// // This will implicitly also insert B and C with their Default constructors +/// let id = world.spawn(A).id(); +/// assert_eq!(&B(0), world.entity(id).get::().unwrap()); +/// assert_eq!(&C(0), world.entity(id).get::().unwrap()); +/// ``` +/// +/// You can also define a custom constructor: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// #[derive(Component)] +/// #[require(B(init_b))] +/// struct A; +/// +/// #[derive(Component, PartialEq, Eq, Debug)] +/// struct B(usize); +/// +/// fn init_b() -> B { +/// B(10) +/// } +/// +/// # let mut world = World::default(); +/// // This will implicitly also insert B with the init_b() constructor +/// let id = world.spawn(A).id(); +/// assert_eq!(&B(10), world.entity(id).get::().unwrap()); +/// ``` +/// +/// Required components are _recursive_. This means, if a Required Component has required components, +/// those components will _also_ be inserted if they are missing: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// #[derive(Component)] +/// #[require(B)] +/// struct A; +/// +/// #[derive(Component, Default, PartialEq, Eq, Debug)] +/// #[require(C)] +/// struct B(usize); +/// +/// #[derive(Component, Default, PartialEq, Eq, Debug)] +/// struct C(u32); +/// +/// # let mut world = World::default(); +/// // This will implicitly also insert B and C with their Default constructors +/// let id = world.spawn(A).id(); +/// assert_eq!(&B(0), world.entity(id).get::().unwrap()); +/// assert_eq!(&C(0), world.entity(id).get::().unwrap()); +/// ``` +/// +/// Note that cycles in the "component require tree" will result in stack overflows when attempting to +/// insert a component. +/// +/// This "multiple inheritance" pattern does mean that it is possible to have duplicate requires for a given type +/// at different levels of the inheritance tree: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// #[derive(Component)] +/// struct X(usize); +/// +/// #[derive(Component, Default)] +/// #[require(X(x1))] +/// struct Y; +/// +/// fn x1() -> X { +/// X(1) +/// } +/// +/// #[derive(Component)] +/// #[require( +/// Y, +/// X(x2), +/// )] +/// struct Z; +/// +/// fn x2() -> X { +/// X(2) +/// } +/// +/// # let mut world = World::default(); +/// // In this case, the x2 constructor is used for X +/// let id = world.spawn(Z).id(); +/// assert_eq!(2, world.entity(id).get::().unwrap().0); +/// ``` +/// +/// In general, this shouldn't happen often, but when it does the algorithm is simple and predictable: +/// 1. Use all of the constructors (including default constructors) directly defined in the spawned component's require list +/// 2. In the order the requires are defined in `#[require()]`, recursively visit the require list of each of the components in the list (this is a depth Depth First Search). When a constructor is found, it will only be used if one has not already been found. +/// +/// From a user perspective, just think about this as the following: +/// 1. Specifying a required component constructor for Foo directly on a spawned component Bar will result in that constructor being used (and overriding existing constructors lower in the inheritance tree). This is the classic "inheritance override" behavior people expect. +/// 2. For cases where "multiple inheritance" results in constructor clashes, Components should be listed in "importance order". List a component earlier in the requirement list to initialize its inheritance tree earlier. +/// /// # Adding component's hooks /// /// See [`ComponentHooks`] for a detailed explanation of component's hooks. @@ -198,6 +337,14 @@ pub trait Component: Send + Sync + 'static { /// Called when registering this component, allowing mutable access to its [`ComponentHooks`]. fn register_component_hooks(_hooks: &mut ComponentHooks) {} + + /// Registers required components. + fn register_required_components( + _components: &mut Components, + _storages: &mut Storages, + _required_components: &mut RequiredComponents, + ) { + } } /// The storage used for a specific component type. @@ -408,6 +555,7 @@ pub struct ComponentInfo { id: ComponentId, descriptor: ComponentDescriptor, hooks: ComponentHooks, + required_components: RequiredComponents, } impl ComponentInfo { @@ -466,7 +614,8 @@ impl ComponentInfo { ComponentInfo { id, descriptor, - hooks: ComponentHooks::default(), + hooks: Default::default(), + required_components: Default::default(), } } @@ -491,6 +640,12 @@ impl ComponentInfo { pub fn hooks(&self) -> &ComponentHooks { &self.hooks } + + /// Retrieves the [`RequiredComponents`] collection, which contains all required components (and their constructors) + /// needed by this component. This includes _recursive_ required components. + pub fn required_components(&self) -> &RequiredComponents { + &self.required_components + } } /// A value which uniquely identifies the type of a [`Component`] or [`Resource`] within a @@ -570,7 +725,7 @@ pub struct ComponentDescriptor { } // We need to ignore the `drop` field in our `Debug` impl -impl std::fmt::Debug for ComponentDescriptor { +impl Debug for ComponentDescriptor { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("ComponentDescriptor") .field("name", &self.name) @@ -692,22 +847,32 @@ impl Components { /// * [`Components::init_component_with_descriptor()`] #[inline] pub fn init_component(&mut self, storages: &mut Storages) -> ComponentId { - let type_id = TypeId::of::(); - - let Components { - indices, - components, - .. - } = self; - *indices.entry(type_id).or_insert_with(|| { - let index = Components::init_component_inner( + let mut registered = false; + let id = { + let Components { + indices, components, - storages, - ComponentDescriptor::new::(), - ); - T::register_component_hooks(&mut components[index.index()].hooks); - index - }) + .. + } = self; + let type_id = TypeId::of::(); + *indices.entry(type_id).or_insert_with(|| { + let id = Components::init_component_inner( + components, + storages, + ComponentDescriptor::new::(), + ); + registered = true; + id + }) + }; + if registered { + let mut required_components = RequiredComponents::default(); + T::register_required_components(self, storages, &mut required_components); + let info = &mut self.components[id.index()]; + T::register_component_hooks(&mut info.hooks); + info.required_components = required_components; + } + id } /// Initializes a component described by `descriptor`. @@ -1133,3 +1298,150 @@ impl FromWorld for InitComponentId { } } } + +/// A Required Component constructor. See [`Component`] for details. +#[cfg(feature = "track_change_detection")] +#[derive(Clone)] +pub struct RequiredComponentConstructor( + pub Arc)>, +); + +/// A Required Component constructor. See [`Component`] for details. +#[cfg(not(feature = "track_change_detection"))] +#[derive(Clone)] +pub struct RequiredComponentConstructor( + pub Arc, +); + +impl RequiredComponentConstructor { + /// # Safety + /// This is intended to only be called in the context of [`BundleInfo::write_components`] to initialized required components. + /// Calling it _anywhere else_ should be considered unsafe. + /// + /// `table_row` and `entity` must correspond to a valid entity that currently needs a component initialized via the constructor stored + /// on this [`RequiredComponentConstructor`]. The stored constructor must correspond to a component on `entity` that needs initialization. + /// `table` and `sparse_sets` must correspond to storages on a world where `entity` needs this required component initialized. + /// + /// Again, don't call this anywhere but [`BundleInfo::write_components`]. + pub(crate) unsafe fn initialize( + &self, + table: &mut Table, + sparse_sets: &mut SparseSets, + change_tick: Tick, + table_row: TableRow, + entity: Entity, + #[cfg(feature = "track_change_detection")] caller: &'static Location<'static>, + ) { + (self.0)( + table, + sparse_sets, + change_tick, + table_row, + entity, + #[cfg(feature = "track_change_detection")] + caller, + ); + } +} + +/// The collection of metadata for components that are required for a given component. +/// +/// For more information, see the "Required Components" section of [`Component`]. +#[derive(Default, Clone)] +pub struct RequiredComponents(pub(crate) HashMap); + +impl Debug for RequiredComponents { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("RequiredComponents") + .field(&self.0.keys()) + .finish() + } +} + +impl RequiredComponents { + /// Registers a required component. If the component is already registered, the new registration + /// passed in the arguments will be ignored. + /// + /// # Safety + /// + /// `component_id` must match the type initialized by `constructor`. + /// `constructor` _must_ initialize a component for `component_id` in such a way that + /// matches the storage type of the component. It must only use the given `table_row` or `Entity` to + /// initialize the storage for `component_id` corresponding to the given entity. + pub unsafe fn register_dynamic( + &mut self, + component_id: ComponentId, + constructor: RequiredComponentConstructor, + ) { + self.0.entry(component_id).or_insert(constructor); + } + + /// Registers a required component. If the component is already registered, the new registration + /// passed in the arguments will be ignored. + pub fn register( + &mut self, + components: &mut Components, + storages: &mut Storages, + constructor: fn() -> C, + ) { + let component_id = components.init_component::(storages); + let erased: RequiredComponentConstructor = RequiredComponentConstructor(Arc::new( + move |table, + sparse_sets, + change_tick, + table_row, + entity, + #[cfg(feature = "track_change_detection")] caller| { + OwningPtr::make(constructor(), |ptr| { + // SAFETY: This will only be called in the context of `BundleInfo::write_components`, which will + // pass in a valid table_row and entity requiring a C constructor + // C::STORAGE_TYPE is the storage type associated with `component_id` / `C` + // `ptr` points to valid `C` data, which matches the type associated with `component_id` + unsafe { + BundleInfo::initialize_required_component( + table, + sparse_sets, + change_tick, + table_row, + entity, + component_id, + C::STORAGE_TYPE, + ptr, + #[cfg(feature = "track_change_detection")] + caller, + ); + } + }); + }, + )); + // SAFETY: + // `component_id` matches the type initialized by the `erased` constructor above. + // `erased` initializes a component for `component_id` in such a way that + // matches the storage type of the component. It only uses the given `table_row` or `Entity` to + // initialize the storage corresponding to the given entity. + unsafe { self.register_dynamic(component_id, erased) }; + } + + /// Iterates the ids of all required components. This includes recursive required components. + pub fn iter_ids(&self) -> impl Iterator + '_ { + self.0.keys().copied() + } + + /// Removes components that are explicitly provided in a given [`Bundle`]. These components should + /// be logically treated as normal components, not "required components". + /// + /// [`Bundle`]: crate::bundle::Bundle + pub(crate) fn remove_explicit_components(&mut self, components: &[ComponentId]) { + for component in components { + self.0.remove(component); + } + } + + // Merges `required_components` into this collection. This only inserts a required component + // if it _did not already exist_. + pub(crate) fn merge(&mut self, required_components: &RequiredComponents) { + for (id, constructor) in &required_components.0 { + self.0.entry(*id).or_insert_with(|| constructor.clone()); + } + } +} diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs index cca781d06b652c..deee320fcd0b22 100644 --- a/crates/bevy_ecs/src/entity/mod.rs +++ b/crates/bevy_ecs/src/entity/mod.rs @@ -144,7 +144,7 @@ type IdCursor = isize; /// [SemVer]: https://semver.org/ #[derive(Clone, Copy)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -#[cfg_attr(feature = "bevy_reflect", reflect_value(Hash, PartialEq))] +#[cfg_attr(feature = "bevy_reflect", reflect_value(Hash, PartialEq, Debug))] #[cfg_attr( all(feature = "bevy_reflect", feature = "serialize"), reflect_value(Serialize, Deserialize) diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index a08a62053e1078..09e86f7711ccae 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -1793,6 +1793,235 @@ mod tests { ); } + #[test] + fn required_components() { + #[derive(Component)] + #[require(Y)] + struct X; + + #[derive(Component)] + #[require(Z(new_z))] + struct Y { + value: String, + } + + #[derive(Component)] + struct Z(u32); + + impl Default for Y { + fn default() -> Self { + Self { + value: "hello".to_string(), + } + } + } + + fn new_z() -> Z { + Z(7) + } + + let mut world = World::new(); + let id = world.spawn(X).id(); + assert_eq!( + "hello", + world.entity(id).get::().unwrap().value, + "Y should have the default value" + ); + assert_eq!( + 7, + world.entity(id).get::().unwrap().0, + "Z should have the value provided by the constructor defined in Y" + ); + + let id = world + .spawn(( + X, + Y { + value: "foo".to_string(), + }, + )) + .id(); + assert_eq!( + "foo", + world.entity(id).get::().unwrap().value, + "Y should have the manually provided value" + ); + assert_eq!( + 7, + world.entity(id).get::().unwrap().0, + "Z should have the value provided by the constructor defined in Y" + ); + + let id = world.spawn((X, Z(8))).id(); + assert_eq!( + "hello", + world.entity(id).get::().unwrap().value, + "Y should have the default value" + ); + assert_eq!( + 8, + world.entity(id).get::().unwrap().0, + "Z should have the manually provided value" + ); + } + + #[test] + fn generic_required_components() { + #[derive(Component)] + #[require(Y)] + struct X; + + #[derive(Component, Default)] + struct Y { + value: T, + } + + let mut world = World::new(); + let id = world.spawn(X).id(); + assert_eq!( + 0, + world.entity(id).get::>().unwrap().value, + "Y should have the default value" + ); + } + + #[test] + fn required_components_spawn_nonexistent_hooks() { + #[derive(Component)] + #[require(Y)] + struct X; + + #[derive(Component, Default)] + struct Y; + + #[derive(Resource)] + struct A(usize); + + #[derive(Resource)] + struct I(usize); + + let mut world = World::new(); + world.insert_resource(A(0)); + world.insert_resource(I(0)); + world + .register_component_hooks::() + .on_add(|mut world, _, _| world.resource_mut::().0 += 1) + .on_insert(|mut world, _, _| world.resource_mut::().0 += 1); + + // Spawn entity and ensure Y was added + assert!(world.spawn(X).contains::()); + + assert_eq!(world.resource::().0, 1); + assert_eq!(world.resource::().0, 1); + } + + #[test] + fn required_components_insert_existing_hooks() { + #[derive(Component)] + #[require(Y)] + struct X; + + #[derive(Component, Default)] + struct Y; + + #[derive(Resource)] + struct A(usize); + + #[derive(Resource)] + struct I(usize); + + let mut world = World::new(); + world.insert_resource(A(0)); + world.insert_resource(I(0)); + world + .register_component_hooks::() + .on_add(|mut world, _, _| world.resource_mut::().0 += 1) + .on_insert(|mut world, _, _| world.resource_mut::().0 += 1); + + // Spawn entity and ensure Y was added + assert!(world.spawn_empty().insert(X).contains::()); + + assert_eq!(world.resource::().0, 1); + assert_eq!(world.resource::().0, 1); + } + + #[test] + fn required_components_take_leaves_required() { + #[derive(Component)] + #[require(Y)] + struct X; + + #[derive(Component, Default)] + struct Y; + + let mut world = World::new(); + let e = world.spawn(X).id(); + let _ = world.entity_mut(e).take::().unwrap(); + assert!(world.entity_mut(e).contains::()); + } + + #[test] + fn required_components_retain_keeps_required() { + #[derive(Component)] + #[require(Y)] + struct X; + + #[derive(Component, Default)] + struct Y; + + #[derive(Component, Default)] + struct Z; + + let mut world = World::new(); + let e = world.spawn((X, Z)).id(); + world.entity_mut(e).retain::(); + assert!(world.entity_mut(e).contains::()); + assert!(world.entity_mut(e).contains::()); + assert!(!world.entity_mut(e).contains::()); + } + + #[test] + fn required_components_spawn_then_insert_no_overwrite() { + #[derive(Component)] + #[require(Y)] + struct X; + + #[derive(Component, Default)] + struct Y(usize); + + let mut world = World::new(); + let id = world.spawn((X, Y(10))).id(); + world.entity_mut(id).insert(X); + + assert_eq!( + 10, + world.entity(id).get::().unwrap().0, + "Y should still have the manually provided value" + ); + } + + #[test] + fn dynamic_required_components() { + #[derive(Component)] + #[require(Y)] + struct X; + + #[derive(Component, Default)] + struct Y; + + let mut world = World::new(); + let x_id = world.init_component::(); + + let mut e = world.spawn_empty(); + + // SAFETY: x_id is a valid component id + bevy_ptr::OwningPtr::make(X, |ptr| unsafe { + e.insert_by_id(x_id, ptr); + }); + + assert!(e.contains::()); + } + // These structs are primarily compilation tests to test the derive macros. Because they are // never constructed, we have to manually silence the `dead_code` lint. #[allow(dead_code)] diff --git a/crates/bevy_ecs/src/system/mod.rs b/crates/bevy_ecs/src/system/mod.rs index bfb2dcd396cceb..4eb8301d9b84a5 100644 --- a/crates/bevy_ecs/src/system/mod.rs +++ b/crates/bevy_ecs/src/system/mod.rs @@ -1063,7 +1063,7 @@ mod tests { .get_id(TypeId::of::<(W, W)>()) .expect("Bundle used to spawn entity should exist"); let bundle_info = bundles.get(bundle_id).unwrap(); - let mut bundle_components = bundle_info.components().to_vec(); + let mut bundle_components = bundle_info.contributed_components().to_vec(); bundle_components.sort(); for component_id in &bundle_components { assert!( diff --git a/crates/bevy_ecs/src/world/deferred_world.rs b/crates/bevy_ecs/src/world/deferred_world.rs index 90205b2ed43608..f84b22a4073103 100644 --- a/crates/bevy_ecs/src/world/deferred_world.rs +++ b/crates/bevy_ecs/src/world/deferred_world.rs @@ -370,13 +370,13 @@ impl<'w> DeferredWorld<'w> { &mut self, event: ComponentId, entity: Entity, - components: &[ComponentId], + components: impl Iterator, ) { Observers::invoke::<_>( self.reborrow(), event, entity, - components.iter().copied(), + components, &mut (), &mut false, ); diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 0aa28241557e7b..5adfb2d3020218 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -956,7 +956,7 @@ impl<'w> EntityWorldMut<'w> { let removed_components = &mut world.removed_components; let entity = self.entity; - let mut bundle_components = bundle_info.iter_components(); + let mut bundle_components = bundle_info.iter_explicit_components(); // SAFETY: bundle components are iterated in order, which guarantees that the component type // matches let result = unsafe { @@ -1131,7 +1131,7 @@ impl<'w> EntityWorldMut<'w> { } let old_archetype = &world.archetypes[location.archetype_id]; - for component_id in bundle_info.iter_components() { + for component_id in bundle_info.iter_explicit_components() { if old_archetype.contains(component_id) { world.removed_components.send(component_id, entity); @@ -1180,7 +1180,7 @@ impl<'w> EntityWorldMut<'w> { self } - /// Removes any components except those in the [`Bundle`] from the entity. + /// Removes any components except those in the [`Bundle`] (and its Required Components) from the entity. /// /// See [`EntityCommands::retain`](crate::system::EntityCommands::retain) for more details. pub fn retain(&mut self) -> &mut Self { @@ -1194,9 +1194,10 @@ impl<'w> EntityWorldMut<'w> { let old_location = self.location; let old_archetype = &mut archetypes[old_location.archetype_id]; + // PERF: this could be stored in an Archetype Edge let to_remove = &old_archetype .components() - .filter(|c| !retained_bundle_info.components().contains(c)) + .filter(|c| !retained_bundle_info.contributed_components().contains(c)) .collect::>(); let remove_bundle = self.world.bundles.init_dynamic_info(components, to_remove); @@ -1261,19 +1262,11 @@ impl<'w> EntityWorldMut<'w> { unsafe { deferred_world.trigger_on_replace(archetype, self.entity, archetype.components()); if archetype.has_replace_observer() { - deferred_world.trigger_observers( - ON_REPLACE, - self.entity, - &archetype.components().collect::>(), - ); + deferred_world.trigger_observers(ON_REPLACE, self.entity, archetype.components()); } deferred_world.trigger_on_remove(archetype, self.entity, archetype.components()); if archetype.has_remove_observer() { - deferred_world.trigger_observers( - ON_REMOVE, - self.entity, - &archetype.components().collect::>(), - ); + deferred_world.trigger_observers(ON_REMOVE, self.entity, archetype.components()); } } @@ -1484,13 +1477,17 @@ unsafe fn trigger_on_replace_and_on_remove_hooks_and_observers( entity: Entity, bundle_info: &BundleInfo, ) { - deferred_world.trigger_on_replace(archetype, entity, bundle_info.iter_components()); + deferred_world.trigger_on_replace(archetype, entity, bundle_info.iter_explicit_components()); if archetype.has_replace_observer() { - deferred_world.trigger_observers(ON_REPLACE, entity, bundle_info.components()); + deferred_world.trigger_observers( + ON_REPLACE, + entity, + bundle_info.iter_explicit_components(), + ); } - deferred_world.trigger_on_remove(archetype, entity, bundle_info.iter_components()); + deferred_world.trigger_on_remove(archetype, entity, bundle_info.iter_explicit_components()); if archetype.has_remove_observer() { - deferred_world.trigger_observers(ON_REMOVE, entity, bundle_info.components()); + deferred_world.trigger_observers(ON_REMOVE, entity, bundle_info.iter_explicit_components()); } } @@ -2423,7 +2420,7 @@ unsafe fn remove_bundle_from_archetype( let current_archetype = &mut archetypes[archetype_id]; let mut removed_table_components = Vec::new(); let mut removed_sparse_set_components = Vec::new(); - for component_id in bundle_info.components().iter().cloned() { + for component_id in bundle_info.iter_explicit_components() { if current_archetype.contains(component_id) { // SAFETY: bundle components were already initialized by bundles.get_info let component_info = unsafe { components.get_info_unchecked(component_id) }; From e63d7c340fa9d335877299b934c899838d43b7a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Mockers?= Date: Wed, 28 Aug 2024 00:41:23 +0200 Subject: [PATCH 35/53] don't use padding for layout (#14944) # Objective - Fixes #14792 - Padding is already handled by taffy, don't handle it also on Bevy side ## Solution - Remove extra computation added in https://github.com/bevyengine/bevy/pull/14777 --- crates/bevy_ui/src/layout/mod.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index 803221af3a5bc4..f74ef601319cb7 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -264,16 +264,10 @@ pub fn ui_layout_system( let Ok(layout) = ui_surface.get_layout(entity) else { return; }; - let layout_size = inverse_target_scale_factor - * Vec2::new( - layout.size.width - layout.padding.left - layout.padding.right, - layout.size.height - layout.padding.top - layout.padding.bottom, - ); - let layout_location = inverse_target_scale_factor - * Vec2::new( - layout.location.x + layout.padding.left, - layout.location.y + layout.padding.top, - ); + let layout_size = + inverse_target_scale_factor * Vec2::new(layout.size.width, layout.size.height); + let layout_location = + inverse_target_scale_factor * Vec2::new(layout.location.x, layout.location.y); absolute_location += layout_location; From 45281e62d7ec40b148840c5e3a6758d52d6efa13 Mon Sep 17 00:00:00 2001 From: robtfm <50659922+robtfm@users.noreply.github.com> Date: Wed, 28 Aug 2024 00:43:40 +0100 Subject: [PATCH 36/53] Commands::send_event (#14933) # Objective sending events tends to be low-frequency so ergonomics can be prioritized over efficiency. add `Commands::send_event` to send any type of event without needing a writer in hand. i don't know how we feel about these kind of ergonomic things, i add this to all my projects and find it useful. adding `mut this_particular_event_writer: EventWriter` every time i want to send something is unnecessarily cumbersome. it also simplifies the "send and receive in the same system" pattern significantly. basic example before: ```rs fn my_func( q: Query<(Entity, &State)>, mut damage_event_writer: EventWriter, mut heal_event_writer: EventWriter, ) { for (entity, state) in q.iter() { if let Some(damage) = state.get_damage() { damage_event_writer.send(DamageEvent { entity, damage }); } if let Some(heal) = state.get_heal() { heal_event_writer.send(HealEvent { entity, heal }); } } } ``` basic example after: ```rs import bevy::ecs::event::SendEventEx; fn my_func( mut commands: Commands, q: Query<(Entity, &State)>, ) { for (entity, state) in q.iter() { if let Some(damage) = state.get_damage() { commands.send_event(DamageEvent { entity, damage }); } if let Some(heal) = state.get_heal() { commands.send_event(HealEvent { entity, heal }); } } } ``` send/receive in the same system before: ```rs fn send_and_receive_param_set( mut param_set: ParamSet<(EventReader, EventWriter)>, ) { // We must collect the events to resend, because we can't access the writer while we're iterating over the reader. let mut events_to_resend = Vec::new(); // This is p0, as the first parameter in the `ParamSet` is the reader. for event in param_set.p0().read() { if event.resend_from_param_set { events_to_resend.push(event.clone()); } } // This is p1, as the second parameter in the `ParamSet` is the writer. for mut event in events_to_resend { event.times_sent += 1; param_set.p1().send(event); } } ``` after: ```rs use bevy::ecs::event::SendEventEx; fn send_via_commands_and_receive( mut reader: EventReader, mut commands: Commands, ) { for event in reader.read() { if event.resend_via_commands { commands.send_event(DebugEvent { times_sent: event.times_sent + 1, ..event.clone() }); } } } ``` --------- Co-authored-by: Jan Hohenheim --- crates/bevy_ecs/src/event/mod.rs | 2 ++ crates/bevy_ecs/src/event/send_event.rs | 15 +++++++++++++++ crates/bevy_ecs/src/system/commands/mod.rs | 17 ++++++++++++++++- 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 crates/bevy_ecs/src/event/send_event.rs diff --git a/crates/bevy_ecs/src/event/mod.rs b/crates/bevy_ecs/src/event/mod.rs index 195d66c5394a43..a570be5cab16e1 100644 --- a/crates/bevy_ecs/src/event/mod.rs +++ b/crates/bevy_ecs/src/event/mod.rs @@ -7,6 +7,7 @@ mod mut_iterators; mod mutator; mod reader; mod registry; +mod send_event; mod update; mod writer; @@ -24,6 +25,7 @@ pub use mut_iterators::{EventMutIterator, EventMutIteratorWithId}; pub use mutator::EventMutator; pub use reader::EventReader; pub use registry::{EventRegistry, ShouldUpdateEvents}; +pub use send_event::SendEvent; pub use update::{ event_update_condition, event_update_system, signal_event_update_system, EventUpdates, }; diff --git a/crates/bevy_ecs/src/event/send_event.rs b/crates/bevy_ecs/src/event/send_event.rs new file mode 100644 index 00000000000000..2640357bf024c4 --- /dev/null +++ b/crates/bevy_ecs/src/event/send_event.rs @@ -0,0 +1,15 @@ +use super::{Event, Events}; +use crate::world::{Command, World}; + +/// A command to send an arbitrary [`Event`], used by [`Commands::send_event`](crate::system::Commands::send_event). +pub struct SendEvent { + /// The event to send. + pub event: E, +} + +impl Command for SendEvent { + fn apply(self, world: &mut World) { + let mut events = world.resource_mut::>(); + events.send(self.event); + } +} diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index c0846a8c885942..55468cbcad094d 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -8,7 +8,7 @@ use crate::{ bundle::{Bundle, InsertMode}, component::{ComponentId, ComponentInfo}, entity::{Entities, Entity}, - event::Event, + event::{Event, SendEvent}, observer::{Observer, TriggerEvent, TriggerTargets}, system::{RunSystemWithInput, SystemId}, world::{ @@ -788,6 +788,21 @@ impl<'w, 's> Commands<'w, 's> { ) -> EntityCommands { self.spawn(Observer::new(observer)) } + + /// Sends an arbitrary [`Event`]. + /// + /// This is a convenience method for sending events without requiring an [`EventWriter`]. + /// ## Performance + /// Since this is a command, exclusive world access is used, which means that it will not profit from + /// system-level parallelism on supported platforms. + /// If these events are performance-critical or very frequently + /// sent, consider using a typed [`EventWriter`] instead. + /// + /// [`EventWriter`]: crate::event::EventWriter + pub fn send_event(&mut self, event: E) -> &mut Self { + self.add(SendEvent { event }); + self + } } /// A [`Command`] which gets executed for a given [`Entity`]. From 8895113784e58d4f9c220732aba8571eab8b5db3 Mon Sep 17 00:00:00 2001 From: Robert Walter <26892280+RobWalt@users.noreply.github.com> Date: Wed, 28 Aug 2024 01:37:19 +0000 Subject: [PATCH 37/53] Use `Isometry` in `bevy_gizmos` wherever we can (#14676) # Objective - Solves the last bullet in and closes #14319 - Make better use of the `Isometry` types - Prevent issues like #14655 - Probably simplify and clean up a lot of code through the use of Gizmos as well (i.e. the 3D gizmos for cylinders circles & lines don't connect well, probably due to wrong rotations) ## Solution - go through the `bevy_gizmos` crate and give all methods a slight workover ## Testing - For all the changed examples I run `git switch main && cargo rr --example && git switch && cargo rr --example ` and compare the visual results - Check if all doc tests are still compiling - Check the docs in general and update them !!! --- ## Migration Guide The gizmos methods function signature changes as follows: - 2D - if it took `position` & `rotation_angle` before -> `Isometry2d::new(position, Rot2::radians(rotation_angle))` - if it just took `position` before -> `Isometry2d::from_translation(position)` - 3D - if it took `position` & `rotation` before -> `Isometry3d::new(position, rotation)` - if it just took `position` before -> `Isometry3d::from_translation(position)` --- crates/bevy_gizmos/src/arcs.rs | 32 +- crates/bevy_gizmos/src/circles.rs | 123 +++-- crates/bevy_gizmos/src/cross.rs | 62 +-- crates/bevy_gizmos/src/gizmos.rs | 35 +- crates/bevy_gizmos/src/grid.rs | 69 ++- crates/bevy_gizmos/src/light.rs | 20 +- crates/bevy_gizmos/src/primitives/dim2.rs | 186 +++---- crates/bevy_gizmos/src/primitives/dim3.rs | 524 +++++++------------ crates/bevy_gizmos/src/primitives/helpers.rs | 55 +- crates/bevy_gizmos/src/rounded_box.rs | 76 ++- examples/2d/2d_viewport_to_world.rs | 4 +- examples/2d/bounding_2d.rs | 44 +- examples/2d/mesh2d_arcs.rs | 12 +- examples/3d/3d_viewport_to_world.rs | 9 +- examples/ecs/observers.rs | 3 +- examples/gizmos/2d_gizmos.rs | 20 +- examples/gizmos/3d_gizmos.rs | 49 +- examples/math/cubic_splines.rs | 20 +- examples/math/custom_primitives.rs | 18 +- examples/math/render_primitives.rs | 61 ++- 20 files changed, 632 insertions(+), 790 deletions(-) diff --git a/crates/bevy_gizmos/src/arcs.rs b/crates/bevy_gizmos/src/arcs.rs index 1aa6b5c78a0b6e..819a87d5ca5514 100644 --- a/crates/bevy_gizmos/src/arcs.rs +++ b/crates/bevy_gizmos/src/arcs.rs @@ -6,7 +6,7 @@ use crate::circles::DEFAULT_CIRCLE_RESOLUTION; use crate::prelude::{GizmoConfigGroup, Gizmos}; use bevy_color::Color; -use bevy_math::{Isometry2d, Quat, Vec2, Vec3}; +use bevy_math::{Isometry2d, Isometry3d, Quat, Vec2, Vec3}; use std::f32::consts::{FRAC_PI_2, TAU}; // === 2D === @@ -140,9 +140,9 @@ where /// - `angle`: sets how much of a circle circumference is passed, e.g. PI is half a circle. This /// value should be in the range (-2 * PI..=2 * PI) /// - `radius`: distance between the arc and its center point - /// - `position`: position of the arcs center point - /// - `rotation`: defines orientation of the arc, by default we assume the arc is contained in a - /// plane parallel to the XZ plane and the default starting point is (`position + Vec3::X`) + /// - `isometry` defines the translation and rotation of the arc. + /// - the translation specifies the center of the arc + /// - the rotation is counter-clockwise starting from `Vec3::Y` /// - `color`: color of the arc /// /// # Builder methods @@ -163,8 +163,7 @@ where /// .arc_3d( /// 270.0_f32.to_radians(), /// 0.25, - /// Vec3::ONE, - /// rotation, + /// Isometry3d::new(Vec3::ONE, rotation), /// ORANGE /// ) /// .resolution(100); @@ -176,15 +175,13 @@ where &mut self, angle: f32, radius: f32, - position: Vec3, - rotation: Quat, + isometry: Isometry3d, color: impl Into, ) -> Arc3dBuilder<'_, 'w, 's, Config, Clear> { Arc3dBuilder { gizmos: self, start_vertex: Vec3::X, - center: position, - rotation, + isometry, angle, radius, color: color.into(), @@ -317,8 +314,7 @@ where Arc3dBuilder { gizmos: self, start_vertex, - center, - rotation, + isometry: Isometry3d::new(center, rotation), angle, radius, color: color.into(), @@ -344,8 +340,7 @@ where // // DO NOT expose this field to users as it is easy to mess this up start_vertex: Vec3, - center: Vec3, - rotation: Quat, + isometry: Isometry3d, angle: f32, radius: f32, color: Color, @@ -380,8 +375,7 @@ where let positions = arc_3d_inner( self.start_vertex, - self.center, - self.rotation, + self.isometry, self.angle, self.radius, resolution, @@ -392,8 +386,7 @@ where fn arc_3d_inner( start_vertex: Vec3, - center: Vec3, - rotation: Quat, + isometry: Isometry3d, angle: f32, radius: f32, resolution: u32, @@ -406,7 +399,8 @@ fn arc_3d_inner( .map(move |frac| frac as f32 / resolution as f32) .map(move |percentage| angle * percentage) .map(move |frac_angle| Quat::from_axis_angle(Vec3::Y, frac_angle) * start_vertex) - .map(move |p| rotation * (p * radius) + center) + .map(move |vec3| vec3 * radius) + .map(move |vec3| isometry * vec3) } // helper function for getting a default value for the resolution parameter diff --git a/crates/bevy_gizmos/src/circles.rs b/crates/bevy_gizmos/src/circles.rs index 1f1d2bac217605..d819311e4d16dc 100644 --- a/crates/bevy_gizmos/src/circles.rs +++ b/crates/bevy_gizmos/src/circles.rs @@ -5,8 +5,8 @@ use crate::prelude::{GizmoConfigGroup, Gizmos}; use bevy_color::Color; -use bevy_math::Mat2; -use bevy_math::{Dir3, Quat, Vec2, Vec3}; +use bevy_math::{Isometry2d, Isometry3d}; +use bevy_math::{Quat, Vec2, Vec3}; use std::f32::consts::TAU; pub(crate) const DEFAULT_CIRCLE_RESOLUTION: u32 = 32; @@ -24,7 +24,12 @@ where Config: GizmoConfigGroup, Clear: 'static + Send + Sync, { - /// Draw an ellipse in 3D at `position` with the flat side facing `normal`. + /// Draw an ellipse in 3D with the given `isometry` applied. + /// + /// If `isometry == Isometry3d::IDENTITY` then + /// + /// - the center is at `Vec3::ZERO` + /// - the `half_sizes` are aligned with the `Vec3::X` and `Vec3::Y` axes. /// /// This should be called for each frame the ellipse needs to be rendered. /// @@ -34,12 +39,12 @@ where /// # use bevy_math::prelude::*; /// # use bevy_color::palettes::basic::{RED, GREEN}; /// fn system(mut gizmos: Gizmos) { - /// gizmos.ellipse(Vec3::ZERO, Quat::IDENTITY, Vec2::new(1., 2.), GREEN); + /// gizmos.ellipse(Isometry3d::IDENTITY, Vec2::new(1., 2.), GREEN); /// /// // Ellipses have 32 line-segments by default. /// // You may want to increase this for larger ellipses. /// gizmos - /// .ellipse(Vec3::ZERO, Quat::IDENTITY, Vec2::new(5., 1.), RED) + /// .ellipse(Isometry3d::IDENTITY, Vec2::new(5., 1.), RED) /// .resolution(64); /// } /// # bevy_ecs::system::assert_is_system(system); @@ -47,22 +52,25 @@ where #[inline] pub fn ellipse( &mut self, - position: Vec3, - rotation: Quat, + isometry: Isometry3d, half_size: Vec2, color: impl Into, ) -> EllipseBuilder<'_, 'w, 's, Config, Clear> { EllipseBuilder { gizmos: self, - position, - rotation, + isometry, half_size, color: color.into(), resolution: DEFAULT_CIRCLE_RESOLUTION, } } - /// Draw an ellipse in 2D. + /// Draw an ellipse in 2D with the given `isometry` applied. + /// + /// If `isometry == Isometry2d::IDENTITY` then + /// + /// - the center is at `Vec2::ZERO` + /// - the `half_sizes` are aligned with the `Vec2::X` and `Vec2::Y` axes. /// /// This should be called for each frame the ellipse needs to be rendered. /// @@ -72,12 +80,12 @@ where /// # use bevy_math::prelude::*; /// # use bevy_color::palettes::basic::{RED, GREEN}; /// fn system(mut gizmos: Gizmos) { - /// gizmos.ellipse_2d(Vec2::ZERO, 180.0_f32.to_radians(), Vec2::new(2., 1.), GREEN); + /// gizmos.ellipse_2d(Isometry2d::from_rotation(Rot2::degrees(180.0)), Vec2::new(2., 1.), GREEN); /// /// // Ellipses have 32 line-segments by default. /// // You may want to increase this for larger ellipses. /// gizmos - /// .ellipse_2d(Vec2::ZERO, 180.0_f32.to_radians(), Vec2::new(5., 1.), RED) + /// .ellipse_2d(Isometry2d::from_rotation(Rot2::degrees(180.0)), Vec2::new(5., 1.), RED) /// .resolution(64); /// } /// # bevy_ecs::system::assert_is_system(system); @@ -85,24 +93,25 @@ where #[inline] pub fn ellipse_2d( &mut self, - position: Vec2, - angle: f32, + isometry: Isometry2d, half_size: Vec2, color: impl Into, ) -> Ellipse2dBuilder<'_, 'w, 's, Config, Clear> { Ellipse2dBuilder { gizmos: self, - position, - rotation: Mat2::from_angle(angle), + isometry, half_size, color: color.into(), resolution: DEFAULT_CIRCLE_RESOLUTION, } } - /// Draw a circle in 3D at `position` with the flat side facing `normal`. + /// Draw a circle in 3D with the given `isometry` applied. /// - /// This should be called for each frame the circle needs to be rendered. + /// If `isometry == Isometry3d::IDENTITY` then + /// + /// - the center is at `Vec3::ZERO` + /// - the radius is aligned with the `Vec3::X` and `Vec3::Y` axes. /// /// # Example /// ``` @@ -110,12 +119,12 @@ where /// # use bevy_math::prelude::*; /// # use bevy_color::palettes::basic::{RED, GREEN}; /// fn system(mut gizmos: Gizmos) { - /// gizmos.circle(Vec3::ZERO, Dir3::Z, 1., GREEN); + /// gizmos.circle(Isometry3d::IDENTITY, 1., GREEN); /// /// // Circles have 32 line-segments by default. /// // You may want to increase this for larger circles. /// gizmos - /// .circle(Vec3::ZERO, Dir3::Z, 5., RED) + /// .circle(Isometry3d::IDENTITY, 5., RED) /// .resolution(64); /// } /// # bevy_ecs::system::assert_is_system(system); @@ -123,22 +132,25 @@ where #[inline] pub fn circle( &mut self, - position: Vec3, - normal: Dir3, + isometry: Isometry3d, radius: f32, color: impl Into, ) -> EllipseBuilder<'_, 'w, 's, Config, Clear> { EllipseBuilder { gizmos: self, - position, - rotation: Quat::from_rotation_arc(Vec3::Z, *normal), + isometry, half_size: Vec2::splat(radius), color: color.into(), resolution: DEFAULT_CIRCLE_RESOLUTION, } } - /// Draw a circle in 2D. + /// Draw a circle in 2D with the given `isometry` applied. + /// + /// If `isometry == Isometry2d::IDENTITY` then + /// + /// - the center is at `Vec2::ZERO` + /// - the radius is aligned with the `Vec2::X` and `Vec2::Y` axes. /// /// This should be called for each frame the circle needs to be rendered. /// @@ -148,12 +160,12 @@ where /// # use bevy_math::prelude::*; /// # use bevy_color::palettes::basic::{RED, GREEN}; /// fn system(mut gizmos: Gizmos) { - /// gizmos.circle_2d(Vec2::ZERO, 1., GREEN); + /// gizmos.circle_2d(Isometry2d::IDENTITY, 1., GREEN); /// /// // Circles have 32 line-segments by default. /// // You may want to increase this for larger circles. /// gizmos - /// .circle_2d(Vec2::ZERO, 5., RED) + /// .circle_2d(Isometry2d::IDENTITY, 5., RED) /// .resolution(64); /// } /// # bevy_ecs::system::assert_is_system(system); @@ -161,21 +173,26 @@ where #[inline] pub fn circle_2d( &mut self, - position: Vec2, + isometry: Isometry2d, radius: f32, color: impl Into, ) -> Ellipse2dBuilder<'_, 'w, 's, Config, Clear> { Ellipse2dBuilder { gizmos: self, - position, - rotation: Mat2::IDENTITY, + isometry, half_size: Vec2::splat(radius), color: color.into(), resolution: DEFAULT_CIRCLE_RESOLUTION, } } - /// Draw a wireframe sphere in 3D made out of 3 circles around the axes. + /// Draw a wireframe sphere in 3D made out of 3 circles around the axes with the given + /// `isometry` applied. + /// + /// If `isometry == Isometry3d::IDENTITY` then + /// + /// - the center is at `Vec3::ZERO` + /// - the 3 circles are in the XY, YZ and XZ planes. /// /// This should be called for each frame the sphere needs to be rendered. /// @@ -185,12 +202,12 @@ where /// # use bevy_math::prelude::*; /// # use bevy_color::Color; /// fn system(mut gizmos: Gizmos) { - /// gizmos.sphere(Vec3::ZERO, Quat::IDENTITY, 1., Color::BLACK); + /// gizmos.sphere(Isometry3d::IDENTITY, 1., Color::BLACK); /// /// // Each circle has 32 line-segments by default. /// // You may want to increase this for larger spheres. /// gizmos - /// .sphere(Vec3::ZERO, Quat::IDENTITY, 5., Color::BLACK) + /// .sphere(Isometry3d::IDENTITY, 5., Color::BLACK) /// .resolution(64); /// } /// # bevy_ecs::system::assert_is_system(system); @@ -198,16 +215,14 @@ where #[inline] pub fn sphere( &mut self, - position: Vec3, - rotation: Quat, + isometry: Isometry3d, radius: f32, color: impl Into, ) -> SphereBuilder<'_, 'w, 's, Config, Clear> { SphereBuilder { gizmos: self, radius, - position, - rotation: rotation.normalize(), + isometry, color: color.into(), resolution: DEFAULT_CIRCLE_RESOLUTION, } @@ -221,8 +236,7 @@ where Clear: 'static + Send + Sync, { gizmos: &'a mut Gizmos<'w, 's, Config, Clear>, - position: Vec3, - rotation: Quat, + isometry: Isometry3d, half_size: Vec2, color: Color, resolution: u32, @@ -251,8 +265,7 @@ where } let positions = ellipse_inner(self.half_size, self.resolution) - .map(|vec2| self.rotation * vec2.extend(0.)) - .map(|vec3| vec3 + self.position); + .map(|vec2| self.isometry * vec2.extend(0.)); self.gizmos.linestrip(positions, self.color); } } @@ -264,8 +277,7 @@ where Clear: 'static + Send + Sync, { gizmos: &'a mut Gizmos<'w, 's, Config, Clear>, - position: Vec2, - rotation: Mat2, + isometry: Isometry2d, half_size: Vec2, color: Color, resolution: u32, @@ -294,9 +306,8 @@ where return; }; - let positions = ellipse_inner(self.half_size, self.resolution) - .map(|vec2| self.rotation * vec2) - .map(|vec2| vec2 + self.position); + let positions = + ellipse_inner(self.half_size, self.resolution).map(|vec2| self.isometry * vec2); self.gizmos.linestrip_2d(positions, self.color); } } @@ -312,10 +323,7 @@ where // Radius of the sphere radius: f32, - // Rotation of the sphere around the origin in 3D space - rotation: Quat, - // Center position of the sphere in 3D space - position: Vec3, + isometry: Isometry3d, // Color of the sphere color: Color, @@ -345,21 +353,12 @@ where return; } - let SphereBuilder { - radius, - position: center, - rotation, - color, - resolution, - .. - } = self; - // draws one great circle around each of the local axes Vec3::AXES.into_iter().for_each(|axis| { - let normal = *rotation * axis; + let axis_rotation = Isometry3d::from_rotation(Quat::from_rotation_arc(Vec3::Z, axis)); self.gizmos - .circle(*center, Dir3::new_unchecked(normal), *radius, *color) - .resolution(*resolution); + .circle(self.isometry * axis_rotation, self.radius, self.color) + .resolution(self.resolution); }); } } diff --git a/crates/bevy_gizmos/src/cross.rs b/crates/bevy_gizmos/src/cross.rs index 282f6c3dfde5e6..710aa71e9a982a 100644 --- a/crates/bevy_gizmos/src/cross.rs +++ b/crates/bevy_gizmos/src/cross.rs @@ -5,13 +5,18 @@ use crate::prelude::{GizmoConfigGroup, Gizmos}; use bevy_color::Color; -use bevy_math::{Mat2, Mat3, Quat, Vec2, Vec3}; +use bevy_math::{Isometry2d, Isometry3d, Vec2, Vec3}; impl Gizmos<'_, '_, Config> where Config: GizmoConfigGroup, { - /// Draw a cross in 3D at `position`. + /// Draw a cross in 3D with the given `isometry` applied. + /// + /// If `isometry == Isometry3d::IDENTITY` then + /// + /// - the center is at `Vec3::ZERO` + /// - the `half_size`s are aligned with the `Vec3::X`, `Vec3::Y` and `Vec3::Z` axes. /// /// This should be called for each frame the cross needs to be rendered. /// @@ -21,29 +26,26 @@ where /// # use bevy_math::prelude::*; /// # use bevy_color::palettes::basic::WHITE; /// fn system(mut gizmos: Gizmos) { - /// gizmos.cross(Vec3::ZERO, Quat::IDENTITY, 0.5, WHITE); + /// gizmos.cross(Isometry3d::IDENTITY, 0.5, WHITE); /// } /// # bevy_ecs::system::assert_is_system(system); /// ``` - pub fn cross( - &mut self, - position: Vec3, - rotation: Quat, - half_size: f32, - color: impl Into, - ) { - let axes = half_size * Mat3::from_quat(rotation); - let local_x = axes.col(0); - let local_y = axes.col(1); - let local_z = axes.col(2); - + pub fn cross(&mut self, isometry: Isometry3d, half_size: f32, color: impl Into) { let color: Color = color.into(); - self.line(position + local_x, position - local_x, color); - self.line(position + local_y, position - local_y, color); - self.line(position + local_z, position - local_z, color); + [Vec3::X, Vec3::Y, Vec3::Z] + .map(|axis| axis * half_size) + .into_iter() + .for_each(|axis| { + self.line(isometry * axis, isometry * (-axis), color); + }); } - /// Draw a cross in 2D (on the xy plane) at `position`. + /// Draw a cross in 2D with the given `isometry` applied. + /// + /// If `isometry == Isometry2d::IDENTITY` then + /// + /// - the center is at `Vec3::ZERO` + /// - the `half_size`s are aligned with the `Vec3::X` and `Vec3::Y` axes. /// /// This should be called for each frame the cross needs to be rendered. /// @@ -53,23 +55,17 @@ where /// # use bevy_math::prelude::*; /// # use bevy_color::palettes::basic::WHITE; /// fn system(mut gizmos: Gizmos) { - /// gizmos.cross_2d(Vec2::ZERO, 0.0, 0.5, WHITE); + /// gizmos.cross_2d(Isometry2d::IDENTITY, 0.5, WHITE); /// } /// # bevy_ecs::system::assert_is_system(system); /// ``` - pub fn cross_2d( - &mut self, - position: Vec2, - angle: f32, - half_size: f32, - color: impl Into, - ) { - let axes = half_size * Mat2::from_angle(angle); - let local_x = axes.col(0); - let local_y = axes.col(1); - + pub fn cross_2d(&mut self, isometry: Isometry2d, half_size: f32, color: impl Into) { let color: Color = color.into(); - self.line_2d(position + local_x, position - local_x, color); - self.line_2d(position + local_y, position - local_y, color); + [Vec2::X, Vec2::Y] + .map(|axis| axis * half_size) + .into_iter() + .for_each(|axis| { + self.line_2d(isometry * axis, isometry * (-axis), color); + }); } } diff --git a/crates/bevy_gizmos/src/gizmos.rs b/crates/bevy_gizmos/src/gizmos.rs index bd32ba8f1fb040..50998cc6d8a089 100644 --- a/crates/bevy_gizmos/src/gizmos.rs +++ b/crates/bevy_gizmos/src/gizmos.rs @@ -8,7 +8,7 @@ use bevy_ecs::{ system::{Deferred, ReadOnlySystemParam, Res, Resource, SystemBuffer, SystemMeta, SystemParam}, world::{unsafe_world_cell::UnsafeWorldCell, World}, }; -use bevy_math::{Quat, Rot2, Vec2, Vec3}; +use bevy_math::{Isometry2d, Isometry3d, Vec2, Vec3}; use bevy_transform::TransformPoint; use bevy_utils::default; @@ -452,7 +452,12 @@ where strip_colors.push(LinearRgba::NAN); } - /// Draw a wireframe rectangle in 3D. + /// Draw a wireframe rectangle in 3D with the given `isometry` applied. + /// + /// If `isometry == Isometry3d::IDENTITY` then + /// + /// - the center is at `Vec3::ZERO` + /// - the sizes are aligned with the `Vec3::X` and `Vec3::Y` axes. /// /// This should be called for each frame the rectangle needs to be rendered. /// @@ -462,16 +467,16 @@ where /// # use bevy_math::prelude::*; /// # use bevy_color::palettes::basic::GREEN; /// fn system(mut gizmos: Gizmos) { - /// gizmos.rect(Vec3::ZERO, Quat::IDENTITY, Vec2::ONE, GREEN); + /// gizmos.rect(Isometry3d::IDENTITY, Vec2::ONE, GREEN); /// } /// # bevy_ecs::system::assert_is_system(system); /// ``` #[inline] - pub fn rect(&mut self, position: Vec3, rotation: Quat, size: Vec2, color: impl Into) { + pub fn rect(&mut self, isometry: Isometry3d, size: Vec2, color: impl Into) { if !self.enabled { return; } - let [tl, tr, br, bl] = rect_inner(size).map(|vec2| position + rotation * vec2.extend(0.)); + let [tl, tr, br, bl] = rect_inner(size).map(|vec2| isometry * vec2.extend(0.)); self.linestrip([tl, tr, br, bl, tl], color); } @@ -674,7 +679,12 @@ where self.line_gradient_2d(start, start + vector, start_color, end_color); } - /// Draw a wireframe rectangle in 2D. + /// Draw a wireframe rectangle in 2D with the given `isometry` applied. + /// + /// If `isometry == Isometry2d::IDENTITY` then + /// + /// - the center is at `Vec2::ZERO` + /// - the sizes are aligned with the `Vec2::X` and `Vec2::Y` axes. /// /// This should be called for each frame the rectangle needs to be rendered. /// @@ -684,23 +694,16 @@ where /// # use bevy_math::prelude::*; /// # use bevy_color::palettes::basic::GREEN; /// fn system(mut gizmos: Gizmos) { - /// gizmos.rect_2d(Vec2::ZERO, 0., Vec2::ONE, GREEN); + /// gizmos.rect_2d(Isometry2d::IDENTITY, Vec2::ONE, GREEN); /// } /// # bevy_ecs::system::assert_is_system(system); /// ``` #[inline] - pub fn rect_2d( - &mut self, - position: Vec2, - rotation: impl Into, - size: Vec2, - color: impl Into, - ) { + pub fn rect_2d(&mut self, isometry: Isometry2d, size: Vec2, color: impl Into) { if !self.enabled { return; } - let rotation: Rot2 = rotation.into(); - let [tl, tr, br, bl] = rect_inner(size).map(|vec2| position + rotation * vec2); + let [tl, tr, br, bl] = rect_inner(size).map(|vec2| isometry * vec2); self.linestrip_2d([tl, tr, br, bl, tl], color); } diff --git a/crates/bevy_gizmos/src/grid.rs b/crates/bevy_gizmos/src/grid.rs index b65137205befb5..05b04c03767355 100644 --- a/crates/bevy_gizmos/src/grid.rs +++ b/crates/bevy_gizmos/src/grid.rs @@ -5,7 +5,8 @@ use crate::prelude::{GizmoConfigGroup, Gizmos}; use bevy_color::Color; -use bevy_math::{Quat, UVec2, UVec3, Vec2, Vec3, Vec3Swizzles}; +use bevy_math::Vec3Swizzles; +use bevy_math::{Isometry2d, Isometry3d, Quat, UVec2, UVec3, Vec2, Vec3}; /// A builder returned by [`Gizmos::grid_3d`] pub struct GridBuilder3d<'a, 'w, 's, Config, Clear> @@ -14,8 +15,7 @@ where Clear: 'static + Send + Sync, { gizmos: &'a mut Gizmos<'w, 's, Config, Clear>, - position: Vec3, - rotation: Quat, + isometry: Isometry3d, spacing: Vec3, cell_count: UVec3, skew: Vec3, @@ -29,8 +29,7 @@ where Clear: 'static + Send + Sync, { gizmos: &'a mut Gizmos<'w, 's, Config, Clear>, - position: Vec3, - rotation: Quat, + isometry: Isometry3d, spacing: Vec2, cell_count: UVec2, skew: Vec2, @@ -147,8 +146,7 @@ where fn drop(&mut self) { draw_grid( self.gizmos, - self.position, - self.rotation, + self.isometry, self.spacing, self.cell_count, self.skew, @@ -166,8 +164,7 @@ where fn drop(&mut self) { draw_grid( self.gizmos, - self.position, - self.rotation, + self.isometry, self.spacing.extend(0.), self.cell_count.extend(0), self.skew.extend(0.), @@ -187,8 +184,11 @@ where /// /// # Arguments /// - /// - `position`: The center point of the grid. - /// - `rotation`: defines the orientation of the grid, by default we assume the grid is contained in a plane parallel to the XY plane. + /// - `isometry` defines the translation and rotation of the grid. + /// - the translation specifies the center of the grid + /// - defines the orientation of the grid, by default + /// we assume the grid is contained in a plane parallel + /// to the XY plane /// - `cell_count`: defines the amount of cells in the x and y axes /// - `spacing`: defines the distance between cells along the x and y axes /// - `color`: color of the grid @@ -205,8 +205,7 @@ where /// # use bevy_color::palettes::basic::GREEN; /// fn system(mut gizmos: Gizmos) { /// gizmos.grid( - /// Vec3::ZERO, - /// Quat::IDENTITY, + /// Isometry3d::IDENTITY, /// UVec2::new(10, 10), /// Vec2::splat(2.), /// GREEN @@ -218,16 +217,14 @@ where /// ``` pub fn grid( &mut self, - position: Vec3, - rotation: Quat, + isometry: Isometry3d, cell_count: UVec2, spacing: Vec2, color: impl Into, ) -> GridBuilder2d<'_, 'w, 's, Config, Clear> { GridBuilder2d { gizmos: self, - position, - rotation, + isometry, spacing, cell_count, skew: Vec2::ZERO, @@ -242,8 +239,10 @@ where /// /// # Arguments /// - /// - `position`: The center point of the grid. - /// - `rotation`: defines the orientation of the grid, by default we assume the grid is contained in a plane parallel to the XY plane. + /// - `isometry` defines the translation and rotation of the grid. + /// - the translation specifies the center of the grid + /// - defines the orientation of the grid, by default + /// we assume the grid is aligned with all axes /// - `cell_count`: defines the amount of cells in the x, y and z axes /// - `spacing`: defines the distance between cells along the x, y and z axes /// - `color`: color of the grid @@ -260,8 +259,7 @@ where /// # use bevy_color::palettes::basic::GREEN; /// fn system(mut gizmos: Gizmos) { /// gizmos.grid_3d( - /// Vec3::ZERO, - /// Quat::IDENTITY, + /// Isometry3d::IDENTITY, /// UVec3::new(10, 2, 10), /// Vec3::splat(2.), /// GREEN @@ -273,16 +271,14 @@ where /// ``` pub fn grid_3d( &mut self, - position: Vec3, - rotation: Quat, + isometry: Isometry3d, cell_count: UVec3, spacing: Vec3, color: impl Into, ) -> GridBuilder3d<'_, 'w, 's, Config, Clear> { GridBuilder3d { gizmos: self, - position, - rotation, + isometry, spacing, cell_count, skew: Vec3::ZERO, @@ -297,8 +293,10 @@ where /// /// # Arguments /// - /// - `position`: The center point of the grid. - /// - `rotation`: defines the orientation of the grid. + /// - `isometry` defines the translation and rotation of the grid. + /// - the translation specifies the center of the grid + /// - defines the orientation of the grid, by default + /// we assume the grid is aligned with all axes /// - `cell_count`: defines the amount of cells in the x and y axes /// - `spacing`: defines the distance between cells along the x and y axes /// - `color`: color of the grid @@ -315,8 +313,7 @@ where /// # use bevy_color::palettes::basic::GREEN; /// fn system(mut gizmos: Gizmos) { /// gizmos.grid_2d( - /// Vec2::ZERO, - /// 0.0, + /// Isometry2d::IDENTITY, /// UVec2::new(10, 10), /// Vec2::splat(1.), /// GREEN @@ -328,16 +325,17 @@ where /// ``` pub fn grid_2d( &mut self, - position: Vec2, - rotation: f32, + isometry: Isometry2d, cell_count: UVec2, spacing: Vec2, color: impl Into, ) -> GridBuilder2d<'_, 'w, 's, Config, Clear> { GridBuilder2d { gizmos: self, - position: position.extend(0.), - rotation: Quat::from_rotation_z(rotation), + isometry: Isometry3d::new( + isometry.translation.extend(0.0), + Quat::from_rotation_z(isometry.rotation.as_radians()), + ), spacing, cell_count, skew: Vec2::ZERO, @@ -350,8 +348,7 @@ where #[allow(clippy::too_many_arguments)] fn draw_grid( gizmos: &mut Gizmos<'_, '_, Config, Clear>, - position: Vec3, - rotation: Quat, + isometry: Isometry3d, spacing: Vec3, cell_count: UVec3, skew: Vec3, @@ -428,7 +425,7 @@ fn draw_grid( x_lines .chain(y_lines) .chain(z_lines) - .map(|ps| ps.map(|p| position + rotation * p)) + .map(|vec3s| vec3s.map(|vec3| isometry * vec3)) .for_each(|[start, end]| { gizmos.line(start, end, color); }); diff --git a/crates/bevy_gizmos/src/light.rs b/crates/bevy_gizmos/src/light.rs index 343f0bf33e3cef..6155b9e6a0042a 100644 --- a/crates/bevy_gizmos/src/light.rs +++ b/crates/bevy_gizmos/src/light.rs @@ -19,7 +19,7 @@ use bevy_ecs::{ }; use bevy_math::{ primitives::{Cone, Sphere}, - Quat, Vec3, + Isometry3d, Quat, Vec3, }; use bevy_pbr::{DirectionalLight, PointLight, SpotLight}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; @@ -44,13 +44,16 @@ fn point_light_gizmo( &Sphere { radius: point_light.radius, }, - position, - Quat::IDENTITY, + Isometry3d::from_translation(position), color, ) .resolution(16); gizmos - .sphere(position, Quat::IDENTITY, point_light.range, color) + .sphere( + Isometry3d::from_translation(position), + point_light.range, + color, + ) .resolution(32); } @@ -68,8 +71,7 @@ fn spot_light_gizmo( &Sphere { radius: spot_light.radius, }, - translation, - Quat::IDENTITY, + Isometry3d::from_translation(translation), color, ) .resolution(16); @@ -84,8 +86,7 @@ fn spot_light_gizmo( radius: spot_light.range * angle.sin(), height, }, - position, - rotation * Quat::from_rotation_x(PI / 2.0), + Isometry3d::new(position, rotation * Quat::from_rotation_x(PI / 2.0)), color, ) .height_resolution(4) @@ -105,8 +106,7 @@ fn spot_light_gizmo( .arc_3d( 2.0 * spot_light.outer_angle, spot_light.range, - translation, - rotation * arc_rotation, + Isometry3d::new(translation, rotation * arc_rotation), color, ) .resolution(16); diff --git a/crates/bevy_gizmos/src/primitives/dim2.rs b/crates/bevy_gizmos/src/primitives/dim2.rs index f57904bb806ccf..7d0d3850a11c6e 100644 --- a/crates/bevy_gizmos/src/primitives/dim2.rs +++ b/crates/bevy_gizmos/src/primitives/dim2.rs @@ -10,7 +10,7 @@ use bevy_math::primitives::{ CircularSegment, Ellipse, Line2d, Plane2d, Polygon, Polyline2d, Primitive2d, Rectangle, RegularPolygon, Rhombus, Segment2d, Triangle2d, }; -use bevy_math::{Dir2, Isometry2d, Mat2, Rot2, Vec2}; +use bevy_math::{Dir2, Isometry2d, Rot2, Vec2}; use crate::prelude::{GizmoConfigGroup, Gizmos}; @@ -31,8 +31,7 @@ pub trait GizmoPrimitive2d { fn primitive_2d( &mut self, primitive: &P, - position: Vec2, - angle: f32, + isometry: Isometry2d, color: impl Into, ) -> Self::Output<'_>; } @@ -49,19 +48,15 @@ where fn primitive_2d( &mut self, primitive: &Dir2, - position: Vec2, - angle: f32, + isometry: Isometry2d, color: impl Into, ) -> Self::Output<'_> { if !self.enabled { return; } - - let direction = Mat2::from_angle(angle) * **primitive; - - let start = position; - let end = position + MIN_LINE_LEN * direction; - self.arrow_2d(start, end, color); + let start = Vec2::ZERO; + let end = *primitive * MIN_LINE_LEN; + self.arrow_2d(isometry * start, isometry * end, color); } } @@ -77,16 +72,17 @@ where fn primitive_2d( &mut self, primitive: &Arc2d, - position: Vec2, - angle: f32, + isometry: Isometry2d, color: impl Into, ) -> Self::Output<'_> { if !self.enabled { return; } + let start_iso = isometry * Isometry2d::from_rotation(Rot2::radians(-primitive.half_angle)); + self.arc_2d( - Isometry2d::new(position, Rot2::radians(angle - primitive.half_angle)), + start_iso, primitive.half_angle * 2.0, primitive.radius, color, @@ -106,11 +102,10 @@ where fn primitive_2d( &mut self, primitive: &Circle, - position: Vec2, - _angle: f32, + isometry: Isometry2d, color: impl Into, ) -> Self::Output<'_> { - self.circle_2d(position, primitive.radius, color) + self.circle_2d(isometry, primitive.radius, color) } } @@ -126,8 +121,7 @@ where fn primitive_2d( &mut self, primitive: &CircularSector, - position: Vec2, - angle: f32, + isometry: Isometry2d, color: impl Into, ) -> Self::Output<'_> { if !self.enabled { @@ -136,20 +130,21 @@ where let color = color.into(); + let start_iso = + isometry * Isometry2d::from_rotation(Rot2::radians(-primitive.arc.half_angle)); + let end_iso = isometry * Isometry2d::from_rotation(Rot2::radians(primitive.arc.half_angle)); + // we need to draw the arc part of the sector, and the two lines connecting the arc and the center self.arc_2d( - Isometry2d::new(position, Rot2::radians(angle - primitive.arc.half_angle)), + start_iso, primitive.arc.half_angle * 2.0, primitive.arc.radius, color, ); - let start = position - + primitive.arc.radius * Mat2::from_angle(angle - primitive.arc.half_angle) * Vec2::Y; - let end = position - + primitive.arc.radius * Mat2::from_angle(angle + primitive.arc.half_angle) * Vec2::Y; - self.line_2d(position, start, color); - self.line_2d(position, end, color); + let end_position = primitive.arc.radius * Vec2::Y; + self.line_2d(isometry * Vec2::ZERO, start_iso * end_position, color); + self.line_2d(isometry * Vec2::ZERO, end_iso * end_position, color); } } @@ -165,8 +160,7 @@ where fn primitive_2d( &mut self, primitive: &CircularSegment, - position: Vec2, - angle: f32, + isometry: Isometry2d, color: impl Into, ) -> Self::Output<'_> { if !self.enabled { @@ -175,19 +169,20 @@ where let color = color.into(); + let start_iso = + isometry * Isometry2d::from_rotation(Rot2::radians(-primitive.arc.half_angle)); + let end_iso = isometry * Isometry2d::from_rotation(Rot2::radians(primitive.arc.half_angle)); + // we need to draw the arc part of the segment, and the line connecting the two ends self.arc_2d( - Isometry2d::new(position, Rot2::radians(angle - primitive.arc.half_angle)), + start_iso, primitive.arc.half_angle * 2.0, primitive.arc.radius, color, ); - let start = position - + primitive.arc.radius * Mat2::from_angle(angle - primitive.arc.half_angle) * Vec2::Y; - let end = position - + primitive.arc.radius * Mat2::from_angle(angle + primitive.arc.half_angle) * Vec2::Y; - self.line_2d(end, start, color); + let position = primitive.arc.radius * Vec2::Y; + self.line_2d(start_iso * position, end_iso * position, color); } } @@ -203,11 +198,10 @@ where fn primitive_2d<'a>( &mut self, primitive: &Ellipse, - position: Vec2, - angle: f32, + isometry: Isometry2d, color: impl Into, ) -> Self::Output<'_> { - self.ellipse_2d(position, angle, primitive.half_size, color) + self.ellipse_2d(isometry, primitive.half_size, color) } } @@ -220,7 +214,7 @@ where Clear: 'static + Send + Sync, { gizmos: &'a mut Gizmos<'w, 's, Config, Clear>, - position: Vec2, + isometry: Isometry2d, inner_radius: f32, outer_radius: f32, color: Color, @@ -263,13 +257,12 @@ where fn primitive_2d( &mut self, primitive: &Annulus, - position: Vec2, - _angle: f32, + isometry: Isometry2d, color: impl Into, ) -> Self::Output<'_> { Annulus2dBuilder { gizmos: self, - position, + isometry, inner_radius: primitive.inner_circle.radius, outer_radius: primitive.outer_circle.radius, color: color.into(), @@ -291,7 +284,7 @@ where let Annulus2dBuilder { gizmos, - position, + isometry, inner_radius, outer_radius, inner_resolution, @@ -301,10 +294,10 @@ where } = self; gizmos - .circle_2d(*position, *outer_radius, *color) + .circle_2d(*isometry, *outer_radius, *color) .resolution(*outer_resolution); gizmos - .circle_2d(*position, *inner_radius, *color) + .circle_2d(*isometry, *inner_radius, *color) .resolution(*inner_resolution); } } @@ -321,8 +314,7 @@ where fn primitive_2d( &mut self, primitive: &Rhombus, - position: Vec2, - angle: f32, + isometry: Isometry2d, color: impl Into, ) -> Self::Output<'_> { if !self.enabled { @@ -335,7 +327,7 @@ where primitive.half_diagonals.y * sign_y, ) }); - let positions = [a, b, c, d, a].map(rotate_then_translate_2d(angle, position)); + let positions = [a, b, c, d, a].map(|vec2| isometry * vec2); self.linestrip_2d(positions, color); } } @@ -352,8 +344,7 @@ where fn primitive_2d( &mut self, primitive: &Capsule2d, - position: Vec2, - angle: f32, + isometry: Isometry2d, color: impl Into, ) -> Self::Output<'_> { let polymorphic_color: Color = color.into(); @@ -377,14 +368,14 @@ where let scaling = Vec2::X * primitive.radius + Vec2::Y * primitive.half_length; reference_point * scaling }) - .map(rotate_then_translate_2d(angle, position)); + .map(|vec2| isometry * vec2); // draw left and right side of capsule "rectangle" self.line_2d(bottom_left, top_left, polymorphic_color); self.line_2d(bottom_right, top_right, polymorphic_color); - let start_angle_top = angle - FRAC_PI_2; - let start_angle_bottom = angle + FRAC_PI_2; + let start_angle_top = isometry.rotation.as_radians() - FRAC_PI_2; + let start_angle_bottom = isometry.rotation.as_radians() + FRAC_PI_2; // draw arcs self.arc_2d( @@ -414,9 +405,8 @@ where direction: Dir2, // Direction of the line - position: Vec2, // position of the center of the line - rotation: Mat2, // rotation of the line - color: Color, // color of the line + isometry: Isometry2d, + color: Color, // color of the line draw_arrow: bool, // decides whether to indicate the direction of the line with an arrow } @@ -443,15 +433,13 @@ where fn primitive_2d( &mut self, primitive: &Line2d, - position: Vec2, - angle: f32, + isometry: Isometry2d, color: impl Into, ) -> Self::Output<'_> { Line2dBuilder { gizmos: self, direction: primitive.direction, - position, - rotation: Mat2::from_angle(angle), + isometry, color: color.into(), draw_arrow: false, } @@ -468,22 +456,20 @@ where return; } - let direction = self.rotation * *self.direction; - let [start, end] = [1.0, -1.0] .map(|sign| sign * INFINITE_LEN) // offset the line from the origin infinitely into the given direction - .map(|length| direction * length) - // translate the line to the given position - .map(|offset| self.position + offset); + .map(|length| self.direction * length) + // transform the line with the given isometry + .map(|offset| self.isometry * offset); self.gizmos.line_2d(start, end, self.color); // optionally draw an arrow head at the center of the line if self.draw_arrow { self.gizmos.arrow_2d( - self.position - direction * MIN_LINE_LEN, - self.position, + self.isometry * (-self.direction * MIN_LINE_LEN), + self.isometry * Vec2::ZERO, self.color, ); } @@ -502,8 +488,7 @@ where fn primitive_2d( &mut self, primitive: &Plane2d, - position: Vec2, - angle: f32, + isometry: Isometry2d, color: impl Into, ) -> Self::Output<'_> { let polymorphic_color: Color = color.into(); @@ -511,8 +496,6 @@ where if !self.enabled { return; } - let rotation = Mat2::from_angle(angle); - // draw normal of the plane (orthogonal to the plane itself) let normal = primitive.normal; let normal_segment = Segment2d { @@ -522,22 +505,21 @@ where self.primitive_2d( &normal_segment, // offset the normal so it starts on the plane line - position + HALF_MIN_LINE_LEN * rotation * *normal, - angle, + Isometry2d::new(isometry * (HALF_MIN_LINE_LEN * normal), isometry.rotation), polymorphic_color, ) .draw_arrow(true); // draw the plane line let direction = Dir2::new_unchecked(-normal.perp()); - self.primitive_2d(&Line2d { direction }, position, angle, polymorphic_color) + self.primitive_2d(&Line2d { direction }, isometry, polymorphic_color) .draw_arrow(false); // draw an arrow such that the normal is always left side of the plane with respect to the // planes direction. This is to follow the "counter-clockwise" convention self.arrow_2d( - position, - position + MIN_LINE_LEN * (rotation * *direction), + isometry * Vec2::ZERO, + isometry * (MIN_LINE_LEN * direction), polymorphic_color, ); } @@ -556,9 +538,8 @@ where direction: Dir2, // Direction of the line segment half_length: f32, // Half-length of the line segment - position: Vec2, // position of the center of the line segment - rotation: Mat2, // rotation of the line segment - color: Color, // color of the line segment + isometry: Isometry2d, // isometric transformation of the line segment + color: Color, // color of the line segment draw_arrow: bool, // decides whether to draw just a line or an arrow } @@ -585,8 +566,7 @@ where fn primitive_2d( &mut self, primitive: &Segment2d, - position: Vec2, - angle: f32, + isometry: Isometry2d, color: impl Into, ) -> Self::Output<'_> { Segment2dBuilder { @@ -594,8 +574,7 @@ where direction: primitive.direction, half_length: primitive.half_length, - position, - rotation: Mat2::from_angle(angle), + isometry, color: color.into(), draw_arrow: Default::default(), @@ -613,9 +592,9 @@ where return; } - let direction = self.rotation * *self.direction; - let start = self.position - direction * self.half_length; - let end = self.position + direction * self.half_length; + let direction = self.direction * self.half_length; + let start = self.isometry * (-direction); + let end = self.isometry * direction; if self.draw_arrow { self.gizmos.arrow_2d(start, end, self.color); @@ -638,8 +617,7 @@ where fn primitive_2d( &mut self, primitive: &Polyline2d, - position: Vec2, - angle: f32, + isometry: Isometry2d, color: impl Into, ) -> Self::Output<'_> { if !self.enabled { @@ -651,7 +629,7 @@ where .vertices .iter() .copied() - .map(rotate_then_translate_2d(angle, position)), + .map(|vec2| isometry * vec2), color, ); } @@ -669,8 +647,7 @@ where fn primitive_2d( &mut self, primitive: &BoxedPolyline2d, - position: Vec2, - angle: f32, + isometry: Isometry2d, color: impl Into, ) -> Self::Output<'_> { if !self.enabled { @@ -682,7 +659,7 @@ where .vertices .iter() .copied() - .map(rotate_then_translate_2d(angle, position)), + .map(|vec2| isometry * vec2), color, ); } @@ -700,15 +677,14 @@ where fn primitive_2d( &mut self, primitive: &Triangle2d, - position: Vec2, - angle: f32, + isometry: Isometry2d, color: impl Into, ) -> Self::Output<'_> { if !self.enabled { return; } let [a, b, c] = primitive.vertices; - let positions = [a, b, c, a].map(rotate_then_translate_2d(angle, position)); + let positions = [a, b, c, a].map(|vec2| isometry * vec2); self.linestrip_2d(positions, color); } } @@ -725,8 +701,7 @@ where fn primitive_2d( &mut self, primitive: &Rectangle, - position: Vec2, - angle: f32, + isometry: Isometry2d, color: impl Into, ) -> Self::Output<'_> { if !self.enabled { @@ -740,7 +715,7 @@ where primitive.half_size.y * sign_y, ) }); - let positions = [a, b, c, d, a].map(rotate_then_translate_2d(angle, position)); + let positions = [a, b, c, d, a].map(|vec2| isometry * vec2); self.linestrip_2d(positions, color); } } @@ -758,8 +733,7 @@ where fn primitive_2d( &mut self, primitive: &Polygon, - position: Vec2, - angle: f32, + isometry: Isometry2d, color: impl Into, ) -> Self::Output<'_> { if !self.enabled { @@ -781,7 +755,7 @@ where .iter() .copied() .chain(closing_point) - .map(rotate_then_translate_2d(angle, position)), + .map(|vec2| isometry * vec2), color, ); } @@ -799,8 +773,7 @@ where fn primitive_2d( &mut self, primitive: &BoxedPolygon, - position: Vec2, - angle: f32, + isometry: Isometry2d, color: impl Into, ) -> Self::Output<'_> { if !self.enabled { @@ -820,7 +793,7 @@ where .iter() .copied() .chain(closing_point) - .map(rotate_then_translate_2d(angle, position)), + .map(|vec2| isometry * vec2), color, ); } @@ -838,8 +811,7 @@ where fn primitive_2d( &mut self, primitive: &RegularPolygon, - position: Vec2, - angle: f32, + isometry: Isometry2d, color: impl Into, ) -> Self::Output<'_> { if !self.enabled { @@ -847,8 +819,8 @@ where } let points = (0..=primitive.sides) - .map(|p| single_circle_coordinate(primitive.circumcircle.radius, primitive.sides, p)) - .map(rotate_then_translate_2d(angle, position)); + .map(|n| single_circle_coordinate(primitive.circumcircle.radius, primitive.sides, n)) + .map(|vec2| isometry * vec2); self.linestrip_2d(points, color); } } diff --git a/crates/bevy_gizmos/src/primitives/dim3.rs b/crates/bevy_gizmos/src/primitives/dim3.rs index 0839da864a25cc..e2da1a115894e6 100644 --- a/crates/bevy_gizmos/src/primitives/dim3.rs +++ b/crates/bevy_gizmos/src/primitives/dim3.rs @@ -1,14 +1,14 @@ //! A module for rendering each of the 3D [`bevy_math::primitives`] with [`Gizmos`]. use super::helpers::*; -use std::f32::consts::{FRAC_PI_2, PI, TAU}; +use std::f32::consts::TAU; use bevy_color::Color; use bevy_math::primitives::{ BoxedPolyline3d, Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, Line3d, Plane3d, Polyline3d, Primitive3d, Segment3d, Sphere, Tetrahedron, Torus, Triangle3d, }; -use bevy_math::{Dir3, Quat, Vec3}; +use bevy_math::{Dir3, Isometry3d, Quat, Vec3}; use crate::circles::SphereBuilder; use crate::prelude::{GizmoConfigGroup, Gizmos}; @@ -28,8 +28,7 @@ pub trait GizmoPrimitive3d { fn primitive_3d( &mut self, primitive: &P, - position: Vec3, - rotation: Quat, + isometry: Isometry3d, color: impl Into, ) -> Self::Output<'_>; } @@ -46,11 +45,12 @@ where fn primitive_3d( &mut self, primitive: &Dir3, - position: Vec3, - rotation: Quat, + isometry: Isometry3d, color: impl Into, ) -> Self::Output<'_> { - self.arrow(position, position + (rotation * **primitive), color); + let start = Vec3::ZERO; + let end = primitive.as_vec3(); + self.arrow(isometry * start, isometry * end, color); } } @@ -66,11 +66,10 @@ where fn primitive_3d( &mut self, primitive: &Sphere, - position: Vec3, - rotation: Quat, + isometry: Isometry3d, color: impl Into, ) -> Self::Output<'_> { - self.sphere(position, rotation, primitive.radius, color) + self.sphere(isometry, primitive.radius, color) } } @@ -87,10 +86,7 @@ where // direction of the normal orthogonal to the plane normal: Dir3, - // Rotation of the plane around the origin in 3D space - rotation: Quat, - // Center position of the plane in 3D space - position: Vec3, + isometry: Isometry3d, // Color of the plane color: Color, @@ -136,15 +132,13 @@ where fn primitive_3d( &mut self, primitive: &Plane3d, - position: Vec3, - rotation: Quat, + isometry: Isometry3d, color: impl Into, ) -> Self::Output<'_> { Plane3dBuilder { gizmos: self, normal: primitive.normal, - rotation, - position, + isometry, color: color.into(), axis_count: 4, segment_count: 3, @@ -164,37 +158,33 @@ where } // draws the normal - let normal = self.rotation * *self.normal; self.gizmos - .primitive_3d(&self.normal, self.position, self.rotation, self.color); - let normals_normal = self.rotation * self.normal.any_orthonormal_vector(); + .primitive_3d(&self.normal, self.isometry, self.color); // draws the axes // get rotation for each direction + let normals_normal = self.normal.any_orthonormal_vector(); (0..self.axis_count) .map(|i| i as f32 * (1.0 / self.axis_count as f32) * TAU) - .map(|angle| Quat::from_axis_angle(normal, angle)) - .for_each(|quat| { - let axis_direction = quat * normals_normal; - let direction = Dir3::new_unchecked(axis_direction); - + .map(|angle| Quat::from_axis_angle(self.normal.as_vec3(), angle)) + .flat_map(|quat| { + let segment_length = self.segment_length; + let isometry = self.isometry; // for each axis draw dotted line (0..) .filter(|i| i % 2 != 0) - .map(|percent| (percent as f32 + 0.5) * self.segment_length * axis_direction) - .map(|position| position + self.position) .take(self.segment_count as usize) - .for_each(|position| { - self.gizmos.primitive_3d( - &Segment3d { - direction, - half_length: self.segment_length * 0.5, - }, - position, - Quat::IDENTITY, - self.color, - ); - }); + .map(|i| [i, i + 1]) + .map(move |percents| { + percents + .map(|percent| percent as f32 + 0.5) + .map(|percent| percent * segment_length * normals_normal) + .map(|vec3| quat * vec3) + .map(|vec3| isometry * vec3) + }) + }) + .for_each(|[start, end]| { + self.gizmos.line(start, end, self.color); }); } } @@ -211,8 +201,7 @@ where fn primitive_3d( &mut self, primitive: &Line3d, - position: Vec3, - rotation: Quat, + isometry: Isometry3d, color: impl Into, ) -> Self::Output<'_> { if !self.enabled { @@ -220,13 +209,13 @@ where } let color = color.into(); - let direction = rotation * *primitive.direction; - self.arrow(position, position + direction, color); + let direction = primitive.direction.as_vec3(); + self.arrow(isometry * Vec3::ZERO, isometry * direction, color); let [start, end] = [1.0, -1.0] .map(|sign| sign * INFINITE_LEN) - .map(|length| direction * length) - .map(|offset| position + offset); + .map(|length| primitive.direction * length) + .map(|offset| isometry * offset); self.line(start, end, color); } } @@ -243,18 +232,15 @@ where fn primitive_3d( &mut self, primitive: &Segment3d, - position: Vec3, - rotation: Quat, + isometry: Isometry3d, color: impl Into, ) -> Self::Output<'_> { if !self.enabled { return; } - let direction = rotation * *primitive.direction; - let start = position - direction * primitive.half_length; - let end = position + direction * primitive.half_length; - self.line(start, end, color); + let direction = primitive.direction.as_vec3(); + self.line(isometry * direction, isometry * (-direction), color); } } @@ -271,20 +257,14 @@ where fn primitive_3d( &mut self, primitive: &Polyline3d, - position: Vec3, - rotation: Quat, + isometry: Isometry3d, color: impl Into, ) -> Self::Output<'_> { if !self.enabled { return; } - self.linestrip( - primitive - .vertices - .map(rotate_then_translate_3d(rotation, position)), - color, - ); + self.linestrip(primitive.vertices.map(|vec3| isometry * vec3), color); } } @@ -300,8 +280,7 @@ where fn primitive_3d( &mut self, primitive: &BoxedPolyline3d, - position: Vec3, - rotation: Quat, + isometry: Isometry3d, color: impl Into, ) -> Self::Output<'_> { if !self.enabled { @@ -313,7 +292,7 @@ where .vertices .iter() .copied() - .map(rotate_then_translate_3d(rotation, position)), + .map(|vec3| isometry * vec3), color, ); } @@ -331,8 +310,7 @@ where fn primitive_3d( &mut self, primitive: &Triangle3d, - position: Vec3, - rotation: Quat, + isometry: Isometry3d, color: impl Into, ) -> Self::Output<'_> { if !self.enabled { @@ -340,10 +318,7 @@ where } let [a, b, c] = primitive.vertices; - self.linestrip( - [a, b, c, a].map(rotate_then_translate_3d(rotation, position)), - color, - ); + self.linestrip([a, b, c, a].map(|vec3| isometry * vec3), color); } } @@ -359,16 +334,13 @@ where fn primitive_3d( &mut self, primitive: &Cuboid, - position: Vec3, - rotation: Quat, + isometry: Isometry3d, color: impl Into, ) -> Self::Output<'_> { if !self.enabled { return; } - let [half_extend_x, half_extend_y, half_extend_z] = primitive.half_size.to_array(); - // transform the points from the reference unit cube to the cuboid coords let vertices @ [a, b, c, d, e, f, g, h] = [ [1.0, 1.0, 1.0], @@ -380,8 +352,9 @@ where [-1.0, -1.0, -1.0], [1.0, -1.0, -1.0], ] - .map(|[sx, sy, sz]| Vec3::new(sx * half_extend_x, sy * half_extend_y, sz * half_extend_z)) - .map(rotate_then_translate_3d(rotation, position)); + .map(Vec3::from) + .map(|vec3| vec3 * primitive.half_size) + .map(|vec3| isometry * vec3); // lines for the upper rectangle of the cuboid let upper = [a, b, c, d] @@ -421,12 +394,7 @@ where // Half height of the cylinder half_height: f32, - // Center position of the cylinder - position: Vec3, - // Rotation of the cylinder - // - // default orientation is: the cylinder is aligned with `Vec3::Y` axis - rotation: Quat, + isometry: Isometry3d, // Color of the cylinder color: Color, @@ -456,16 +424,14 @@ where fn primitive_3d( &mut self, primitive: &Cylinder, - position: Vec3, - rotation: Quat, + isometry: Isometry3d, color: impl Into, ) -> Self::Output<'_> { Cylinder3dBuilder { gizmos: self, radius: primitive.radius, half_height: primitive.half_height, - position, - rotation, + isometry, color: color.into(), resolution: DEFAULT_RESOLUTION, } @@ -482,37 +448,17 @@ where return; } - let Cylinder3dBuilder { - gizmos, - radius, - half_height, - position, - rotation, - color, - resolution, - } = self; - - let normal = Dir3::new_unchecked(*rotation * Vec3::Y); - let up = normal.as_vec3() * *half_height; - - // draw upper and lower circle of the cylinder - [-1.0, 1.0].into_iter().for_each(|sign| { - gizmos - .circle(*position + sign * up, normal, *radius, *color) - .resolution(*resolution); - }); - - // draw lines connecting the two cylinder circles - [Vec3::NEG_X, Vec3::NEG_Z, Vec3::X, Vec3::Z] - .into_iter() - .for_each(|axis| { - let axis = *rotation * axis; - gizmos.line( - *position + up + axis * *radius, - *position - up + axis * *radius, - *color, - ); - }); + self.gizmos + .primitive_3d( + &ConicalFrustum { + radius_top: self.radius, + radius_bottom: self.radius, + height: self.half_height * 2.0, + }, + self.isometry, + self.color, + ) + .resolution(self.resolution); } } @@ -531,12 +477,7 @@ where // Half length of the capsule half_length: f32, - // Center position of the capsule - position: Vec3, - // Rotation of the capsule - // - // default orientation is: the capsule is aligned with `Vec3::Y` axis - rotation: Quat, + isometry: Isometry3d, // Color of the capsule color: Color, @@ -566,16 +507,14 @@ where fn primitive_3d( &mut self, primitive: &Capsule3d, - position: Vec3, - rotation: Quat, + isometry: Isometry3d, color: impl Into, ) -> Self::Output<'_> { Capsule3dBuilder { gizmos: self, radius: primitive.radius, half_length: primitive.half_length, - position, - rotation, + isometry, color: color.into(), resolution: DEFAULT_RESOLUTION, } @@ -592,66 +531,39 @@ where return; } - let Capsule3dBuilder { - gizmos, - radius, - half_length, - position, - rotation, - color, - resolution, - } = self; - - // Draw the circles at the top and bottom of the cylinder - let y_offset = *rotation * Vec3::Y; - gizmos - .circle( - *position + y_offset * *half_length, - Dir3::new_unchecked(y_offset), - *radius, - *color, - ) - .resolution(*resolution); - gizmos - .circle( - *position - y_offset * *half_length, - Dir3::new_unchecked(y_offset), - *radius, - *color, - ) - .resolution(*resolution); - let y_offset = y_offset * *half_length; - - // Draw the vertical lines and the cap semicircles - [Vec3::X, Vec3::Z].into_iter().for_each(|axis| { - let normal = *rotation * axis; - - gizmos.line( - *position + normal * *radius + y_offset, - *position + normal * *radius - y_offset, - *color, - ); - gizmos.line( - *position - normal * *radius + y_offset, - *position - normal * *radius - y_offset, - *color, - ); - - let rotation = *rotation - * Quat::from_euler(bevy_math::EulerRot::ZYX, 0., axis.z * FRAC_PI_2, FRAC_PI_2); - - gizmos - .arc_3d(PI, *radius, *position + y_offset, rotation, *color) - .resolution(*resolution / 2); - gizmos - .arc_3d( - PI, - *radius, - *position - y_offset, - rotation * Quat::from_rotation_y(PI), - *color, - ) - .resolution(*resolution / 2); + let [upper_apex, lower_apex] = [-1.0, 1.0] + .map(|sign| Vec3::Y * sign * (self.half_length + self.radius)) + .map(|vec3| self.isometry * vec3); + let [upper_center, lower_center] = [-1.0, 1.0] + .map(|sign| Vec3::Y * sign * self.half_length) + .map(|vec3| self.isometry * vec3); + let [upper_points, lower_points] = [-1.0, 1.0] + .map(|sign| Vec3::Y * sign * self.half_length) + .map(|vec3| { + circle_coordinates_closed(self.radius, self.resolution) + .map(|vec2| Vec3::new(vec2.x, 0.0, vec2.y) + vec3) + .map(|vec3| self.isometry * vec3) + .collect::>() + }); + + upper_points.iter().skip(1).copied().for_each(|start| { + self.gizmos + .short_arc_3d_between(upper_center, start, upper_apex, self.color); + }); + lower_points.iter().skip(1).copied().for_each(|start| { + self.gizmos + .short_arc_3d_between(lower_center, start, lower_apex, self.color); + }); + + let upper_lines = upper_points.windows(2).map(|win| (win[0], win[1])); + let lower_lines = lower_points.windows(2).map(|win| (win[0], win[1])); + upper_lines.chain(lower_lines).for_each(|(start, end)| { + self.gizmos.line(start, end, self.color); + }); + + let connection_lines = upper_points.into_iter().zip(lower_points).skip(1); + connection_lines.for_each(|(start, end)| { + self.gizmos.line(start, end, self.color); }); } } @@ -671,12 +583,7 @@ where // Height of the cone height: f32, - // Center of the cone, half-way between the tip and the base - position: Vec3, - // Rotation of the cone - // - // default orientation is: cone base normal is aligned with the `Vec3::Y` axis - rotation: Quat, + isometry: Isometry3d, // Color of the cone color: Color, @@ -728,16 +635,14 @@ where fn primitive_3d( &mut self, primitive: &Cone, - position: Vec3, - rotation: Quat, + isometry: Isometry3d, color: impl Into, ) -> Self::Output<'_> { Cone3dBuilder { gizmos: self, radius: primitive.radius, height: primitive.height, - position, - rotation, + isometry, color: color.into(), base_resolution: DEFAULT_RESOLUTION, height_resolution: DEFAULT_RESOLUTION, @@ -755,37 +660,29 @@ where return; } - let Cone3dBuilder { - gizmos, - radius, - height, - position, - rotation, - color, - base_resolution, - height_resolution, - } = self; - - let half_height = *height * 0.5; - - // draw the base circle of the cone - draw_circle_3d( - gizmos, - *radius, - *base_resolution, - *rotation, - *position - *rotation * Vec3::Y * half_height, - *color, - ); + let half_height = self.height * 0.5; + let apex = self.isometry * (Vec3::Y * half_height); + let circle_center = half_height * Vec3::NEG_Y; + let circle_coords = circle_coordinates_closed(self.radius, self.height_resolution) + .map(|vec2| Vec3::new(vec2.x, 0.0, vec2.y) + circle_center) + .map(|vec3| self.isometry * vec3) + .collect::>(); + + // connections to apex + circle_coords + .iter() + .skip(1) + .map(|vec3| (*vec3, apex)) + .for_each(|(start, end)| { + self.gizmos.line(start, end, self.color); + }); - // connect the base circle with the tip of the cone - let end = Vec3::Y * half_height; - circle_coordinates(*radius, *height_resolution) - .map(|p| Vec3::new(p.x, -half_height, p.y)) - .map(move |p| [p, end]) - .map(|ps| ps.map(rotate_then_translate_3d(*rotation, *position))) - .for_each(|[start, end]| { - gizmos.line(start, end, *color); + // base circle + circle_coords + .windows(2) + .map(|win| (win[0], win[1])) + .for_each(|(start, end)| { + self.gizmos.line(start, end, self.color); }); } } @@ -807,12 +704,7 @@ where // Height of the conical frustum height: f32, - // Center of conical frustum, half-way between the top and the bottom - position: Vec3, - // Rotation of the conical frustum - // - // default orientation is: conical frustum base shape normals are aligned with `Vec3::Y` axis - rotation: Quat, + isometry: Isometry3d, // Color of the conical frustum color: Color, @@ -842,8 +734,7 @@ where fn primitive_3d( &mut self, primitive: &ConicalFrustum, - position: Vec3, - rotation: Quat, + isometry: Isometry3d, color: impl Into, ) -> Self::Output<'_> { ConicalFrustum3dBuilder { @@ -851,8 +742,7 @@ where radius_top: primitive.radius_top, radius_bottom: primitive.radius_bottom, height: primitive.height, - position, - rotation, + isometry, color: color.into(), resolution: DEFAULT_RESOLUTION, } @@ -869,46 +759,26 @@ where return; } - let ConicalFrustum3dBuilder { - gizmos, - radius_top, - radius_bottom, - height, - position, - rotation, - color, - resolution, - } = self; - - let half_height = *height * 0.5; - let normal = *rotation * Vec3::Y; - - // draw the two circles of the conical frustum - [(*radius_top, half_height), (*radius_bottom, -half_height)] - .into_iter() - .for_each(|(radius, height)| { - draw_circle_3d( - gizmos, - radius, - *resolution, - *rotation, - *position + height * normal, - *color, - ); + let half_height = self.height * 0.5; + let [upper_points, lower_points] = [(-1.0, self.radius_bottom), (1.0, self.radius_top)] + .map(|(sign, radius)| { + let translation = Vec3::Y * sign * half_height; + circle_coordinates_closed(radius, self.resolution) + .map(|vec2| Vec3::new(vec2.x, 0.0, vec2.y) + translation) + .map(|vec3| self.isometry * vec3) + .collect::>() }); - // connect the two circles of the conical frustum - circle_coordinates(*radius_top, *resolution) - .map(move |p| Vec3::new(p.x, half_height, p.y)) - .zip( - circle_coordinates(*radius_bottom, *resolution) - .map(|p| Vec3::new(p.x, -half_height, p.y)), - ) - .map(|(start, end)| [start, end]) - .map(|ps| ps.map(rotate_then_translate_3d(*rotation, *position))) - .for_each(|[start, end]| { - gizmos.line(start, end, *color); - }); + let upper_lines = upper_points.windows(2).map(|win| (win[0], win[1])); + let lower_lines = lower_points.windows(2).map(|win| (win[0], win[1])); + upper_lines.chain(lower_lines).for_each(|(start, end)| { + self.gizmos.line(start, end, self.color); + }); + + let connection_lines = upper_points.into_iter().zip(lower_points).skip(1); + connection_lines.for_each(|(start, end)| { + self.gizmos.line(start, end, self.color); + }); } } @@ -927,12 +797,7 @@ where // Radius of the major circle (ring) major_radius: f32, - // Center of the torus - position: Vec3, - // Rotation of the conical frustum - // - // default orientation is: major circle normal is aligned with `Vec3::Y` axis - rotation: Quat, + isometry: Isometry3d, // Color of the torus color: Color, @@ -970,16 +835,14 @@ where fn primitive_3d( &mut self, primitive: &Torus, - position: Vec3, - rotation: Quat, + isometry: Isometry3d, color: impl Into, ) -> Self::Output<'_> { Torus3dBuilder { gizmos: self, minor_radius: primitive.minor_radius, major_radius: primitive.major_radius, - position, - rotation, + isometry, color: color.into(), minor_resolution: DEFAULT_RESOLUTION, major_resolution: DEFAULT_RESOLUTION, @@ -997,62 +860,42 @@ where return; } - let Torus3dBuilder { - gizmos, - minor_radius, - major_radius, - position, - rotation, - color, - minor_resolution, - major_resolution, - } = self; - - let normal = *rotation * Vec3::Y; - // draw 4 circles with major_radius - [ - (*major_radius - *minor_radius, 0.0), - (*major_radius + *minor_radius, 0.0), - (*major_radius, *minor_radius), - (*major_radius, -*minor_radius), + let [inner, outer, top, bottom] = [ + (self.major_radius - self.minor_radius, 0.0), + (self.major_radius + self.minor_radius, 0.0), + (self.major_radius, self.minor_radius), + (self.major_radius, -self.minor_radius), ] - .into_iter() - .for_each(|(radius, height)| { - draw_circle_3d( - gizmos, - radius, - *major_resolution, - *rotation, - *position + height * normal, - *color, - ); + .map(|(radius, height)| { + let translation = height * Vec3::Y; + circle_coordinates_closed(radius, self.major_resolution) + .map(|vec2| Vec3::new(vec2.x, 0.0, vec2.y) + translation) + .map(|vec3| self.isometry * vec3) + .collect::>() }); - // along the major circle draw orthogonal minor circles - let affine = rotate_then_translate_3d(*rotation, *position); - circle_coordinates(*major_radius, *major_resolution) - .map(|p| Vec3::new(p.x, 0.0, p.y)) - .flat_map(|major_circle_point| { - let minor_center = affine(major_circle_point); - - // direction facing from the center of the torus towards the minor circles center - let dir_to_translation = (minor_center - *position).normalize(); - - // the minor circle is draw with 4 arcs this is done to make the minor circle - // connect properly with each of the major circles - let circle_points = [dir_to_translation, normal, -dir_to_translation, -normal] - .map(|offset| minor_center + offset.normalize() * *minor_radius); - circle_points - .into_iter() - .zip(circle_points.into_iter().cycle().skip(1)) - .map(move |(from, to)| (minor_center, from, to)) - .collect::>() + [&inner, &outer, &top, &bottom] + .iter() + .flat_map(|points| points.windows(2).map(|win| (win[0], win[1]))) + .for_each(|(start, end)| { + self.gizmos.line(start, end, self.color); + }); + + inner + .into_iter() + .zip(top) + .zip(outer) + .zip(bottom) + .flat_map(|(((inner, top), outer), bottom)| { + let center = (inner + top + outer + bottom) * 0.25; + [(inner, top), (top, outer), (outer, bottom), (bottom, inner)] + .map(|(start, end)| (start, end, center)) }) - .for_each(|(center, from, to)| { - gizmos - .short_arc_3d_between(center, from, to, *color) - .resolution(*minor_resolution); + .for_each(|(from, to, center)| { + self.gizmos + .short_arc_3d_between(center, from, to, self.color) + .resolution(self.minor_resolution); }); } } @@ -1065,23 +908,20 @@ impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d for Gizmos<'w, ' fn primitive_3d( &mut self, primitive: &Tetrahedron, - position: Vec3, - rotation: Quat, + isometry: Isometry3d, color: impl Into, ) -> Self::Output<'_> { if !self.enabled { return; } - let [a, b, c, d] = primitive - .vertices - .map(rotate_then_translate_3d(rotation, position)); + let [a, b, c, d] = primitive.vertices.map(|vec3| isometry * vec3); let lines = [(a, b), (a, c), (a, d), (b, c), (b, d), (c, d)]; let color = color.into(); - for (a, b) in lines.into_iter() { - self.line(a, b, color); - } + lines.into_iter().for_each(|(start, end)| { + self.line(start, end, color); + }); } } diff --git a/crates/bevy_gizmos/src/primitives/helpers.rs b/crates/bevy_gizmos/src/primitives/helpers.rs index b76fb34f6b24f4..864c4bd0dc9bab 100644 --- a/crates/bevy_gizmos/src/primitives/helpers.rs +++ b/crates/bevy_gizmos/src/primitives/helpers.rs @@ -1,28 +1,6 @@ use std::f32::consts::TAU; -use bevy_color::Color; -use bevy_math::{Mat2, Quat, Vec2, Vec3}; - -use crate::prelude::{GizmoConfigGroup, Gizmos}; - -/// Performs an isometric transformation on 2D vectors. -/// -/// This function takes angle and a position vector, and returns a closure that applies -/// the isometric transformation to any given 2D vector. The transformation involves rotating -/// the vector by the specified angle and then translating it by the given position. -pub(crate) fn rotate_then_translate_2d(angle: f32, position: Vec2) -> impl Fn(Vec2) -> Vec2 { - move |v| Mat2::from_angle(angle) * v + position -} - -/// Performs an isometric transformation on 3D vectors. -/// -/// This function takes a quaternion representing rotation and a 3D vector representing -/// translation, and returns a closure that applies the isometric transformation to any -/// given 3D vector. The transformation involves rotating the vector by the specified -/// quaternion and then translating it by the given translation vector. -pub(crate) fn rotate_then_translate_3d(rotation: Quat, translation: Vec3) -> impl Fn(Vec3) -> Vec3 { - move |v| rotation * v + translation -} +use bevy_math::Vec2; /// Calculates the `nth` coordinate of a circle. /// @@ -37,6 +15,8 @@ pub(crate) fn single_circle_coordinate(radius: f32, resolution: u32, nth_point: /// Generates an iterator over the coordinates of a circle. /// +/// The coordinates form a open circle, meaning the first and last points aren't the same. +/// /// This function creates an iterator that yields the positions of points approximating a /// circle with the given radius, divided into linear segments. The iterator produces `resolution` /// number of points. @@ -46,27 +26,18 @@ pub(crate) fn circle_coordinates(radius: f32, resolution: u32) -> impl Iterator< .take(resolution as usize) } -/// Draws a circle in 3D space. +/// Generates an iterator over the coordinates of a circle. /// -/// # Note +/// The coordinates form a closed circle, meaning the first and last points are the same. /// -/// This function is necessary to use instead of `gizmos.circle` for certain primitives to ensure that points align correctly. For example, the major circles of a torus are drawn with this method, and using `gizmos.circle` would result in the minor circles not being positioned precisely on the major circles' segment points. -pub(crate) fn draw_circle_3d( - gizmos: &mut Gizmos<'_, '_, Config, Clear>, +/// This function creates an iterator that yields the positions of points approximating a +/// circle with the given radius, divided into linear segments. The iterator produces `resolution` +/// number of points. +pub(crate) fn circle_coordinates_closed( radius: f32, resolution: u32, - rotation: Quat, - translation: Vec3, - color: Color, -) where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - let positions = (0..=resolution) - .map(|frac| frac as f32 / resolution as f32) - .map(|percentage| percentage * TAU) - .map(|angle| Vec2::from(angle.sin_cos()) * radius) - .map(|p| Vec3::new(p.x, 0.0, p.y)) - .map(rotate_then_translate_3d(rotation, translation)); - gizmos.linestrip(positions, color); +) -> impl Iterator { + circle_coordinates(radius, resolution).chain(std::iter::once(single_circle_coordinate( + radius, resolution, resolution, + ))) } diff --git a/crates/bevy_gizmos/src/rounded_box.rs b/crates/bevy_gizmos/src/rounded_box.rs index 232bcee6dfb05b..ac459c917866fb 100644 --- a/crates/bevy_gizmos/src/rounded_box.rs +++ b/crates/bevy_gizmos/src/rounded_box.rs @@ -7,7 +7,7 @@ use std::f32::consts::FRAC_PI_2; use crate::prelude::{GizmoConfigGroup, Gizmos}; use bevy_color::Color; -use bevy_math::{Quat, Vec2, Vec3}; +use bevy_math::{Isometry2d, Isometry3d, Quat, Vec2, Vec3}; use bevy_transform::components::Transform; /// A builder returned by [`Gizmos::rounded_rect`] and [`Gizmos::rounded_rect_2d`] @@ -23,8 +23,7 @@ pub struct RoundedCuboidBuilder<'a, 'w, 's, T: GizmoConfigGroup> { config: RoundedBoxConfig, } struct RoundedBoxConfig { - position: Vec3, - rotation: Quat, + isometry: Isometry3d, color: Color, corner_radius: f32, arc_resolution: u32, @@ -82,15 +81,14 @@ impl Drop for RoundedRectBuilder<'_, '_, '_, T> { // Handle cases where the rectangle collapses into simpler shapes if outer_half_size.x * outer_half_size.y == 0. { self.gizmos.line( - config.position + config.rotation * -outer_half_size.extend(0.), - config.position + config.rotation * outer_half_size.extend(0.), + config.isometry * -outer_half_size.extend(0.), + config.isometry * outer_half_size.extend(0.), config.color, ); return; } if corner_radius == 0. { - self.gizmos - .rect(config.position, config.rotation, self.size, config.color); + self.gizmos.rect(config.isometry, self.size, config.color); return; } @@ -112,7 +110,7 @@ impl Drop for RoundedRectBuilder<'_, '_, '_, T> { Vec3::new(-inner_half_size.x, inner_half_size.y, 0.), Vec3::new(-inner_half_size.x, outer_half_size.y, 0.), ] - .map(|v| config.position + config.rotation * v); + .map(|vec3| config.isometry * vec3); for chunk in vertices.chunks_exact(3) { self.gizmos @@ -159,8 +157,8 @@ impl Drop for RoundedCuboidBuilder<'_, '_, '_, T> { // Handle cases where the rounded cuboid collapses into simpler shapes if edge_radius == 0.0 { - let transform = Transform::from_translation(config.position) - .with_rotation(config.rotation) + let transform = Transform::from_translation(config.isometry.translation.into()) + .with_rotation(config.isometry.rotation) .with_scale(self.size); self.gizmos.cuboid(transform, config.color); return; @@ -181,12 +179,10 @@ impl Drop for RoundedCuboidBuilder<'_, '_, '_, T> { ]; for (position, size, rotation) in rects { - let world_rotation = config.rotation * rotation; - let local_position = config.rotation * (position * inner_half_size); + let local_position = position * inner_half_size; self.gizmos .rounded_rect( - config.position + local_position, - world_rotation, + config.isometry * Isometry3d::new(local_position, rotation), size, config.color, ) @@ -195,8 +191,7 @@ impl Drop for RoundedCuboidBuilder<'_, '_, '_, T> { self.gizmos .rounded_rect( - config.position - local_position, - world_rotation, + config.isometry * Isometry3d::new(-local_position, rotation), size, config.color, ) @@ -213,8 +208,11 @@ impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { /// /// # Arguments /// - /// - `position`: The center point of the rectangle. - /// - `rotation`: defines orientation of the rectangle, by default we assume the rectangle is contained in a plane parallel to the XY plane. + /// - `isometry` defines the translation and rotation of the rectangle. + /// - the translation specifies the center of the rectangle + /// - defines orientation of the rectangle, by default we + /// assume the rectangle is contained in a plane parallel + /// to the XY plane. /// - `size`: defines the size of the rectangle. This refers to the 'outer size', similar to a bounding box. /// - `color`: color of the rectangle /// @@ -231,8 +229,7 @@ impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { /// # use bevy_color::palettes::css::GREEN; /// fn system(mut gizmos: Gizmos) { /// gizmos.rounded_rect( - /// Vec3::ZERO, - /// Quat::IDENTITY, + /// Isometry3d::IDENTITY, /// Vec2::ONE, /// GREEN /// ) @@ -243,8 +240,7 @@ impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { /// ``` pub fn rounded_rect( &mut self, - position: Vec3, - rotation: Quat, + isometry: Isometry3d, size: Vec2, color: impl Into, ) -> RoundedRectBuilder<'_, 'w, 's, T> { @@ -252,8 +248,7 @@ impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { RoundedRectBuilder { gizmos: self, config: RoundedBoxConfig { - position, - rotation, + isometry, color: color.into(), corner_radius, arc_resolution: DEFAULT_ARC_RESOLUTION, @@ -268,8 +263,10 @@ impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { /// /// # Arguments /// - /// - `position`: The center point of the rectangle. - /// - `rotation`: defines orientation of the rectangle. + /// - `isometry` defines the translation and rotation of the rectangle. + /// - the translation specifies the center of the rectangle + /// - defines orientation of the rectangle, by default we + /// assume the rectangle aligned with all axes. /// - `size`: defines the size of the rectangle. This refers to the 'outer size', similar to a bounding box. /// - `color`: color of the rectangle /// @@ -286,8 +283,7 @@ impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { /// # use bevy_color::palettes::css::GREEN; /// fn system(mut gizmos: Gizmos) { /// gizmos.rounded_rect_2d( - /// Vec2::ZERO, - /// 0., + /// Isometry2d::IDENTITY, /// Vec2::ONE, /// GREEN /// ) @@ -298,8 +294,7 @@ impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { /// ``` pub fn rounded_rect_2d( &mut self, - position: Vec2, - rotation: f32, + isometry: Isometry2d, size: Vec2, color: impl Into, ) -> RoundedRectBuilder<'_, 'w, 's, T> { @@ -307,8 +302,10 @@ impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { RoundedRectBuilder { gizmos: self, config: RoundedBoxConfig { - position: position.extend(0.), - rotation: Quat::from_rotation_z(rotation), + isometry: Isometry3d::new( + isometry.translation.extend(0.0), + Quat::from_rotation_z(isometry.rotation.as_radians()), + ), color: color.into(), corner_radius, arc_resolution: DEFAULT_ARC_RESOLUTION, @@ -323,8 +320,10 @@ impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { /// /// # Arguments /// - /// - `position`: The center point of the cuboid. - /// - `rotation`: defines orientation of the cuboid. + /// - `isometry` defines the translation and rotation of the cuboid. + /// - the translation specifies the center of the cuboid + /// - defines orientation of the cuboid, by default we + /// assume the cuboid aligned with all axes. /// - `size`: defines the size of the cuboid. This refers to the 'outer size', similar to a bounding box. /// - `color`: color of the cuboid /// @@ -341,8 +340,7 @@ impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { /// # use bevy_color::palettes::css::GREEN; /// fn system(mut gizmos: Gizmos) { /// gizmos.rounded_cuboid( - /// Vec3::ZERO, - /// Quat::IDENTITY, + /// Isometry3d::IDENTITY, /// Vec3::ONE, /// GREEN /// ) @@ -353,8 +351,7 @@ impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { /// ``` pub fn rounded_cuboid( &mut self, - position: Vec3, - rotation: Quat, + isometry: Isometry3d, size: Vec3, color: impl Into, ) -> RoundedCuboidBuilder<'_, 'w, 's, T> { @@ -362,8 +359,7 @@ impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { RoundedCuboidBuilder { gizmos: self, config: RoundedBoxConfig { - position, - rotation, + isometry, color: color.into(), corner_radius, arc_resolution: DEFAULT_ARC_RESOLUTION, diff --git a/examples/2d/2d_viewport_to_world.rs b/examples/2d/2d_viewport_to_world.rs index 45f759a5e4ede1..c065d1c119b5fc 100644 --- a/examples/2d/2d_viewport_to_world.rs +++ b/examples/2d/2d_viewport_to_world.rs @@ -1,6 +1,6 @@ //! This example demonstrates how to use the `Camera::viewport_to_world_2d` method. -use bevy::{color::palettes::basic::WHITE, prelude::*}; +use bevy::{color::palettes::basic::WHITE, math::Isometry2d, prelude::*}; fn main() { App::new() @@ -30,7 +30,7 @@ fn draw_cursor( return; }; - gizmos.circle_2d(point, 10., WHITE); + gizmos.circle_2d(Isometry2d::from_translation(point), 10., WHITE); } fn setup(mut commands: Commands) { diff --git a/examples/2d/bounding_2d.rs b/examples/2d/bounding_2d.rs index 2025b00741f1e1..625a11aec608cf 100644 --- a/examples/2d/bounding_2d.rs +++ b/examples/2d/bounding_2d.rs @@ -105,24 +105,25 @@ fn render_shapes(mut gizmos: Gizmos, query: Query<(&Shape, &Transform)>) { for (shape, transform) in query.iter() { let translation = transform.translation.xy(); let rotation = transform.rotation.to_euler(EulerRot::YXZ).2; + let isometry = Isometry2d::new(translation, Rot2::radians(rotation)); match shape { Shape::Rectangle(r) => { - gizmos.primitive_2d(r, translation, rotation, color); + gizmos.primitive_2d(r, isometry, color); } Shape::Circle(c) => { - gizmos.primitive_2d(c, translation, rotation, color); + gizmos.primitive_2d(c, isometry, color); } Shape::Triangle(t) => { - gizmos.primitive_2d(t, translation, rotation, color); + gizmos.primitive_2d(t, isometry, color); } Shape::Line(l) => { - gizmos.primitive_2d(l, translation, rotation, color); + gizmos.primitive_2d(l, isometry, color); } Shape::Capsule(c) => { - gizmos.primitive_2d(c, translation, rotation, color); + gizmos.primitive_2d(c, isometry, color); } Shape::Polygon(p) => { - gizmos.primitive_2d(p, translation, rotation, color); + gizmos.primitive_2d(p, isometry, color); } } } @@ -185,10 +186,14 @@ fn render_volumes(mut gizmos: Gizmos, query: Query<(&CurrentVolume, &Intersects) let color = if **intersects { AQUA } else { ORANGE_RED }; match volume { CurrentVolume::Aabb(a) => { - gizmos.rect_2d(a.center(), 0., a.half_size() * 2., color); + gizmos.rect_2d( + Isometry2d::from_translation(a.center()), + a.half_size() * 2., + color, + ); } CurrentVolume::Circle(c) => { - gizmos.circle_2d(c.center(), c.radius(), color); + gizmos.circle_2d(Isometry2d::from_translation(c.center()), c.radius(), color); } } } @@ -283,7 +288,7 @@ fn setup(mut commands: Commands) { fn draw_filled_circle(gizmos: &mut Gizmos, position: Vec2, color: Srgba) { for r in [1., 2., 3.] { - gizmos.circle_2d(position, r, color); + gizmos.circle_2d(Isometry2d::from_translation(position), r, color); } } @@ -353,8 +358,9 @@ fn aabb_cast_system( **intersects = toi.is_some(); if let Some(toi) = toi { gizmos.rect_2d( - aabb_cast.ray.ray.origin + *aabb_cast.ray.ray.direction * toi, - 0., + Isometry2d::from_translation( + aabb_cast.ray.ray.origin + *aabb_cast.ray.ray.direction * toi, + ), aabb_cast.aabb.half_size() * 2., LIME, ); @@ -382,7 +388,9 @@ fn bounding_circle_cast_system( **intersects = toi.is_some(); if let Some(toi) = toi { gizmos.circle_2d( - circle_cast.ray.ray.origin + *circle_cast.ray.ray.direction * toi, + Isometry2d::from_translation( + circle_cast.ray.ray.origin + *circle_cast.ray.ray.direction * toi, + ), circle_cast.circle.radius(), LIME, ); @@ -403,7 +411,11 @@ fn aabb_intersection_system( ) { let center = get_intersection_position(&time); let aabb = Aabb2d::new(center, Vec2::splat(50.)); - gizmos.rect_2d(center, 0., aabb.half_size() * 2., YELLOW); + gizmos.rect_2d( + Isometry2d::from_translation(center), + aabb.half_size() * 2., + YELLOW, + ); for (volume, mut intersects) in volumes.iter_mut() { let hit = match volume { @@ -422,7 +434,11 @@ fn circle_intersection_system( ) { let center = get_intersection_position(&time); let circle = BoundingCircle::new(center, 50.); - gizmos.circle_2d(center, circle.radius(), YELLOW); + gizmos.circle_2d( + Isometry2d::from_translation(center), + circle.radius(), + YELLOW, + ); for (volume, mut intersects) in volumes.iter_mut() { let hit = match volume { diff --git a/examples/2d/mesh2d_arcs.rs b/examples/2d/mesh2d_arcs.rs index 4b22417deb5aa1..a600c986b6bb24 100644 --- a/examples/2d/mesh2d_arcs.rs +++ b/examples/2d/mesh2d_arcs.rs @@ -120,9 +120,17 @@ fn draw_bounds( let isometry = Isometry2d::new(translation, Rot2::radians(rotation)); let aabb = shape.0.aabb_2d(isometry); - gizmos.rect_2d(aabb.center(), 0.0, aabb.half_size() * 2.0, RED); + gizmos.rect_2d( + Isometry2d::from_translation(aabb.center()), + aabb.half_size() * 2.0, + RED, + ); let bounding_circle = shape.0.bounding_circle(isometry); - gizmos.circle_2d(bounding_circle.center, bounding_circle.radius(), BLUE); + gizmos.circle_2d( + Isometry2d::from_translation(bounding_circle.center), + bounding_circle.radius(), + BLUE, + ); } } diff --git a/examples/3d/3d_viewport_to_world.rs b/examples/3d/3d_viewport_to_world.rs index 9e4e4f73da3ed9..98c143c5537aac 100644 --- a/examples/3d/3d_viewport_to_world.rs +++ b/examples/3d/3d_viewport_to_world.rs @@ -37,7 +37,14 @@ fn draw_cursor( let point = ray.get_point(distance); // Draw a circle just above the ground plane at that position. - gizmos.circle(point + ground.up() * 0.01, ground.up(), 0.2, Color::WHITE); + gizmos.circle( + Isometry3d::new( + point + ground.up() * 0.01, + Quat::from_rotation_arc(Vec3::Z, ground.up().as_vec3()), + ), + 0.2, + Color::WHITE, + ); } #[derive(Component)] diff --git a/examples/ecs/observers.rs b/examples/ecs/observers.rs index cd06fef3ab6720..8820ce4919a9a2 100644 --- a/examples/ecs/observers.rs +++ b/examples/ecs/observers.rs @@ -1,6 +1,7 @@ //! Demonstrates how to observe life-cycle triggers as well as define custom ones. use bevy::{ + math::Isometry2d, prelude::*, utils::{HashMap, HashSet}, }; @@ -165,7 +166,7 @@ fn explode_mine(trigger: Trigger, query: Query<&Mine>, mut commands: Co fn draw_shapes(mut gizmos: Gizmos, mines: Query<&Mine>) { for mine in &mines { gizmos.circle_2d( - mine.pos, + Isometry2d::from_translation(mine.pos), mine.size, Color::hsl((mine.size - 4.0) / 16.0 * 360.0, 1.0, 0.8), ); diff --git a/examples/gizmos/2d_gizmos.rs b/examples/gizmos/2d_gizmos.rs index b935b2fb7a2d41..45cee663720232 100644 --- a/examples/gizmos/2d_gizmos.rs +++ b/examples/gizmos/2d_gizmos.rs @@ -49,8 +49,7 @@ fn draw_example_collection( gizmos .grid_2d( - Vec2::ZERO, - 0.0, + Isometry2d::IDENTITY, UVec2::new(16, 9), Vec2::new(80., 80.), // Dark gray @@ -66,21 +65,26 @@ fn draw_example_collection( (Vec2::Y * 300., BLUE), ]); - gizmos.rect_2d(Vec2::ZERO, 0., Vec2::splat(650.), BLACK); + gizmos.rect_2d(Isometry2d::IDENTITY, Vec2::splat(650.), BLACK); - gizmos.cross_2d(Vec2::new(-160., 120.), 0., 12., FUCHSIA); + gizmos.cross_2d( + Isometry2d::from_translation(Vec2::new(-160., 120.)), + 12., + FUCHSIA, + ); my_gizmos - .rounded_rect_2d(Vec2::ZERO, 0., Vec2::splat(630.), BLACK) + .rounded_rect_2d(Isometry2d::IDENTITY, Vec2::splat(630.), BLACK) .corner_radius((time.elapsed_seconds() / 3.).cos() * 100.); // Circles have 32 line-segments by default. // You may want to increase this for larger circles. - my_gizmos.circle_2d(Vec2::ZERO, 300., NAVY).resolution(64); + my_gizmos + .circle_2d(Isometry2d::from_translation(Vec2::ZERO), 300., NAVY) + .resolution(64); my_gizmos.ellipse_2d( - Vec2::ZERO, - time.elapsed_seconds() % TAU, + Isometry2d::new(Vec2::ZERO, Rot2::radians(time.elapsed_seconds() % TAU)), Vec2::new(100., 200.), YELLOW_GREEN, ); diff --git a/examples/gizmos/3d_gizmos.rs b/examples/gizmos/3d_gizmos.rs index d74d3dd307fcd2..e66682180c3fd0 100644 --- a/examples/gizmos/3d_gizmos.rs +++ b/examples/gizmos/3d_gizmos.rs @@ -83,41 +83,48 @@ fn draw_example_collection( time: Res

,...)`. The explicit `FnMut` constraint will allow the compiler to infer the necessary higher-ranked lifetimes along with the parameter types. I wanted to show that this was possible, but I can't tell whether it's worth the complexity. It requires a separate method for each arity, which pollutes the docs a bit: ![SystemState build_system docs](https://github.com/user-attachments/assets/5069b749-7ec7-47e3-a5e4-1a4c78129f78) ## Example ```rust let system = (LocalBuilder(0u64), ParamBuilder::local::()) .build_state(&mut world) .build_system(|a, b| *a + *b + 1); ``` --- crates/bevy_ecs/src/system/builder.rs | 15 ++++++ crates/bevy_ecs/src/system/function_system.rs | 50 ++++++++++++++++++- 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/system/builder.rs b/crates/bevy_ecs/src/system/builder.rs index ab440ff3042dde..cd532776e5b881 100644 --- a/crates/bevy_ecs/src/system/builder.rs +++ b/crates/bevy_ecs/src/system/builder.rs @@ -457,6 +457,21 @@ mod tests { assert_eq!(result, 3); } + #[test] + fn multi_param_builder_inference() { + let mut world = World::new(); + + world.spawn(A); + world.spawn_empty(); + + let system = (LocalBuilder(0u64), ParamBuilder::local::()) + .build_state(&mut world) + .build_system(|a, b| *a + *b + 1); + + let result = world.run_system_once(system); + assert_eq!(result, 1); + } + #[test] fn param_set_builder() { let mut world = World::new(); diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index fe2420a663b85a..aff5da6eb522c4 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -208,6 +208,52 @@ pub struct SystemState { archetype_generation: ArchetypeGeneration, } +// Allow closure arguments to be inferred. +// For a closure to be used as a `SystemParamFunction`, it needs to be generic in any `'w` or `'s` lifetimes. +// Rust will only infer a closure to be generic over lifetimes if it's passed to a function with a Fn constraint. +// So, generate a function for each arity with an explicit `FnMut` constraint to enable higher-order lifetimes, +// along with a regular `SystemParamFunction` constraint to allow the system to be built. +macro_rules! impl_build_system { + ($($param: ident),*) => { + impl<$($param: SystemParam),*> SystemState<($($param,)*)> { + /// Create a [`FunctionSystem`] from a [`SystemState`]. + /// This method signature allows type inference of closure parameters for a system with no input. + /// You can use [`SystemState::build_system_with_input()`] if you have input, or [`SystemState::build_any_system()`] if you don't need type inference. + pub fn build_system< + Out: 'static, + Marker, + F: FnMut($(SystemParamItem<$param>),*) -> Out + + SystemParamFunction + > + ( + self, + func: F, + ) -> FunctionSystem + { + self.build_any_system(func) + } + + /// Create a [`FunctionSystem`] from a [`SystemState`]. + /// This method signature allows type inference of closure parameters for a system with input. + /// You can use [`SystemState::build_system()`] if you have no input, or [`SystemState::build_any_system()`] if you don't need type inference. + pub fn build_system_with_input< + Input, + Out: 'static, + Marker, + F: FnMut(In, $(SystemParamItem<$param>),*) -> Out + + SystemParamFunction, + >( + self, + func: F, + ) -> FunctionSystem { + self.build_any_system(func) + } + } + } +} + +all_tuples!(impl_build_system, 0, 16, P); + impl SystemState { /// Creates a new [`SystemState`] with default state. /// @@ -242,7 +288,9 @@ impl SystemState { } /// Create a [`FunctionSystem`] from a [`SystemState`]. - pub fn build_system>( + /// This method signature allows any system function, but the compiler will not perform type inference on closure parameters. + /// You can use [`SystemState::build_system()`] or [`SystemState::build_system_with_input()`] to get type inference on parameters. + pub fn build_any_system>( self, func: F, ) -> FunctionSystem { From 210c79c9f9f64613f708dd548e10eb85c04fc285 Mon Sep 17 00:00:00 2001 From: Robert Walter <26892280+RobWalt@users.noreply.github.com> Date: Wed, 28 Aug 2024 11:33:11 +0000 Subject: [PATCH 39/53] Gizmos: `arc_2d` utility helpers (#14932) # Objective Since https://github.com/bevyengine/bevy/pull/14731 is merged, it unblocked a few utility methods for 2D arcs. In 2D the pendant to `long_arc_3d_between` and `short_arc_3d_between` are missing. Since `arc_2d` can be a bit hard to use, this PR is trying to plug some holes in the `arcs` API. ## Solution Implement - `long_arc_2d_between(center, from, tp, color)` - `short_arc_2d_between(center, from, tp, color)` ## Testing - There are new doc tests - The `2d_gizmos` example has been extended a bit to include a few more arcs which can easily be checked with respect to the grid --- ## Showcase ![image](https://github.com/user-attachments/assets/b90ad8b1-86c2-4304-a481-4f9a5246c457) Code related to the screenshot (from outer = first line to inner = last line) ```rust my_gizmos.arc_2d(Isometry2d::IDENTITY, FRAC_PI_2, 80.0, ORANGE_RED); my_gizmos.short_arc_2d_between(Vec2::ZERO, Vec2::X * 40.0, Vec2::Y * 40.0, ORANGE_RED); my_gizmos.long_arc_2d_between(Vec2::ZERO, Vec2::X * 20.0, Vec2::Y * 20.0, ORANGE_RED); ``` --- crates/bevy_gizmos/src/arcs.rs | 124 ++++++++++++++++++++++++++++++++- examples/gizmos/2d_gizmos.rs | 4 +- 2 files changed, 126 insertions(+), 2 deletions(-) diff --git a/crates/bevy_gizmos/src/arcs.rs b/crates/bevy_gizmos/src/arcs.rs index 819a87d5ca5514..7164a6e79f7353 100644 --- a/crates/bevy_gizmos/src/arcs.rs +++ b/crates/bevy_gizmos/src/arcs.rs @@ -6,7 +6,7 @@ use crate::circles::DEFAULT_CIRCLE_RESOLUTION; use crate::prelude::{GizmoConfigGroup, Gizmos}; use bevy_color::Color; -use bevy_math::{Isometry2d, Isometry3d, Quat, Vec2, Vec3}; +use bevy_math::{Isometry2d, Isometry3d, Quat, Rot2, Vec2, Vec3}; use std::f32::consts::{FRAC_PI_2, TAU}; // === 2D === @@ -321,6 +321,128 @@ where resolution: None, } } + + /// Draws the shortest arc between two points (`from` and `to`) relative to a specified `center` point. + /// + /// # Arguments + /// + /// - `center`: The center point around which the arc is drawn. + /// - `from`: The starting point of the arc. + /// - `to`: The ending point of the arc. + /// - `color`: color of the arc + /// + /// # Builder methods + /// The resolution of the arc (i.e. the level of detail) can be adjusted with the + /// `.resolution(...)` method. + /// + /// # Examples + /// ``` + /// # use bevy_gizmos::prelude::*; + /// # use bevy_math::prelude::*; + /// # use bevy_color::palettes::css::ORANGE; + /// fn system(mut gizmos: Gizmos) { + /// gizmos.short_arc_2d_between( + /// Vec2::ZERO, + /// Vec2::X, + /// Vec2::Y, + /// ORANGE + /// ) + /// .resolution(100); + /// } + /// # bevy_ecs::system::assert_is_system(system); + /// ``` + /// + /// # Notes + /// - This method assumes that the points `from` and `to` are distinct from `center`. If one of + /// the points is coincident with `center`, nothing is rendered. + /// - The arc is drawn as a portion of a circle with a radius equal to the distance from the + /// `center` to `from`. If the distance from `center` to `to` is not equal to the radius, then + /// the results will behave as if this were the case + #[inline] + pub fn short_arc_2d_between( + &mut self, + center: Vec2, + from: Vec2, + to: Vec2, + color: impl Into, + ) -> Arc2dBuilder<'_, 'w, 's, Config, Clear> { + self.arc_2d_from_to(center, from, to, color, std::convert::identity) + } + + /// Draws the longest arc between two points (`from` and `to`) relative to a specified `center` point. + /// + /// # Arguments + /// - `center`: The center point around which the arc is drawn. + /// - `from`: The starting point of the arc. + /// - `to`: The ending point of the arc. + /// - `color`: color of the arc + /// + /// # Builder methods + /// The resolution of the arc (i.e. the level of detail) can be adjusted with the + /// `.resolution(...)` method. + /// + /// # Examples + /// ``` + /// # use bevy_gizmos::prelude::*; + /// # use bevy_math::prelude::*; + /// # use bevy_color::palettes::css::ORANGE; + /// fn system(mut gizmos: Gizmos) { + /// gizmos.long_arc_2d_between( + /// Vec2::ZERO, + /// Vec2::X, + /// Vec2::Y, + /// ORANGE + /// ) + /// .resolution(100); + /// } + /// # bevy_ecs::system::assert_is_system(system); + /// ``` + /// + /// # Notes + /// - This method assumes that the points `from` and `to` are distinct from `center`. If one of + /// the points is coincident with `center`, nothing is rendered. + /// - The arc is drawn as a portion of a circle with a radius equal to the distance from the + /// `center` to `from`. If the distance from `center` to `to` is not equal to the radius, then + /// the results will behave as if this were the case. + #[inline] + pub fn long_arc_2d_between( + &mut self, + center: Vec2, + from: Vec2, + to: Vec2, + color: impl Into, + ) -> Arc2dBuilder<'_, 'w, 's, Config, Clear> { + self.arc_2d_from_to(center, from, to, color, |angle| angle - TAU) + } + + #[inline] + fn arc_2d_from_to( + &mut self, + center: Vec2, + from: Vec2, + to: Vec2, + color: impl Into, + angle_fn: impl Fn(f32) -> f32, + ) -> Arc2dBuilder<'_, 'w, 's, Config, Clear> { + // `from` and `to` can be the same here since in either case nothing gets rendered and the + // orientation ambiguity of `up` doesn't matter + let from_axis = (from - center).normalize_or_zero(); + let to_axis = (to - center).normalize_or_zero(); + let rotation = Vec2::Y.angle_to(from_axis); + let arc_angle_raw = from_axis.angle_to(to_axis); + + let arc_angle = angle_fn(arc_angle_raw); + let radius = center.distance(from); + + Arc2dBuilder { + gizmos: self, + isometry: Isometry2d::new(center, Rot2::radians(rotation)), + arc_angle, + radius, + color: color.into(), + resolution: None, + } + } } /// A builder returned by [`Gizmos::arc_2d`]. diff --git a/examples/gizmos/2d_gizmos.rs b/examples/gizmos/2d_gizmos.rs index 45cee663720232..23d07a8b1a06f1 100644 --- a/examples/gizmos/2d_gizmos.rs +++ b/examples/gizmos/2d_gizmos.rs @@ -97,7 +97,9 @@ fn draw_example_collection( 310., ORANGE_RED, ); - my_gizmos.arc_2d(Isometry2d::IDENTITY, FRAC_PI_2, 75.0, ORANGE_RED); + my_gizmos.arc_2d(Isometry2d::IDENTITY, FRAC_PI_2, 80.0, ORANGE_RED); + my_gizmos.long_arc_2d_between(Vec2::ZERO, Vec2::X * 20.0, Vec2::Y * 20.0, ORANGE_RED); + my_gizmos.short_arc_2d_between(Vec2::ZERO, Vec2::X * 40.0, Vec2::Y * 40.0, ORANGE_RED); gizmos.arrow_2d( Vec2::ZERO, From 371e07e77d826463f75f29aae6993f15689cdfb5 Mon Sep 17 00:00:00 2001 From: Zachary Harrold Date: Wed, 28 Aug 2024 21:37:31 +1000 Subject: [PATCH 40/53] Updated `FromWorld` Documentation to mention `Default` (#14954) # Objective - Fixes #14860 ## Solution - Added a line of documentation to `FromWorld`'s trait definition mention the `Default` blanket implementation. - Added custom documentation to the `from_world` method for the `Default` blanket implementation. This ensures when inspecting the `from_world` function within an IDE, the tooltip will explicitly state the `default()` method will be used for any `Default` types. ## Testing - CI passes. --- crates/bevy_ecs/src/world/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 6f1f1a12141780..16486885b2eb11 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -2949,12 +2949,15 @@ unsafe impl Sync for World {} /// using data from the supplied [`World`]. /// /// This can be helpful for complex initialization or context-aware defaults. +/// +/// [`FromWorld`] is automatically implemented for any type implementing [`Default`]. pub trait FromWorld { /// Creates `Self` using data from the given [`World`]. fn from_world(world: &mut World) -> Self; } impl FromWorld for T { + /// Creates `Self` using [`default()`](`Default::default`). fn from_world(_world: &mut World) -> Self { T::default() } From d93b78a66edb81eab08b7aad9c8c5f7115a0799e Mon Sep 17 00:00:00 2001 From: Allen Pocket Date: Wed, 28 Aug 2024 19:38:38 +0800 Subject: [PATCH 41/53] Remove unnecessary muts in `RenderSet::QueueMeshes` (#14953) # Objective Fixes #14952 --- crates/bevy_pbr/src/material.rs | 4 ++-- crates/bevy_pbr/src/prepass/mod.rs | 4 ++-- crates/bevy_pbr/src/render/light.rs | 4 ++-- crates/bevy_sprite/src/mesh2d/material.rs | 4 ++-- examples/2d/mesh2d_manual.rs | 4 ++-- examples/shader/shader_instancing.rs | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index 05bb1d7acc5e29..cee8129febe752 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -545,7 +545,7 @@ pub fn queue_material_meshes( mut alpha_mask_render_phases: ResMut>, mut transmissive_render_phases: ResMut>, mut transparent_render_phases: ResMut>, - mut views: Query<( + views: Query<( Entity, &ExtractedView, &VisibleEntities, @@ -585,7 +585,7 @@ pub fn queue_material_meshes( temporal_jitter, projection, (has_environment_maps, has_irradiance_volumes), - ) in &mut views + ) in &views { let ( Some(opaque_phase), diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index a639813d0d97d7..859a8624065c4a 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -688,7 +688,7 @@ pub fn queue_prepass_material_meshes( mut alpha_mask_prepass_render_phases: ResMut>, mut opaque_deferred_render_phases: ResMut>, mut alpha_mask_deferred_render_phases: ResMut>, - mut views: Query< + views: Query< ( Entity, &VisibleEntities, @@ -727,7 +727,7 @@ pub fn queue_prepass_material_meshes( normal_prepass, motion_vector_prepass, deferred_prepass, - ) in &mut views + ) in &views { let ( mut opaque_phase, diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index ae509c8739c3cc..6bf10132956be2 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -1174,7 +1174,7 @@ pub fn queue_shadows( pipeline_cache: Res, render_lightmaps: Res, view_lights: Query<(Entity, &ViewLightEntities)>, - mut view_light_entities: Query<&LightEntity>, + view_light_entities: Query<&LightEntity>, point_light_entities: Query<&CubemapVisibleEntities, With>, directional_light_entities: Query<&CascadesVisibleEntities, With>, spot_light_entities: Query<&VisibleMeshEntities, With>, @@ -1184,7 +1184,7 @@ pub fn queue_shadows( for (entity, view_lights) in &view_lights { let draw_shadow_mesh = shadow_draw_functions.read().id::>(); for view_light_entity in view_lights.lights.iter().copied() { - let Ok(light_entity) = view_light_entities.get_mut(view_light_entity) else { + let Ok(light_entity) = view_light_entities.get(view_light_entity) else { continue; }; let Some(shadow_phase) = shadow_render_phases.get_mut(&view_light_entity) else { diff --git a/crates/bevy_sprite/src/mesh2d/material.rs b/crates/bevy_sprite/src/mesh2d/material.rs index 17743f5aab693f..6c91e27ee849ff 100644 --- a/crates/bevy_sprite/src/mesh2d/material.rs +++ b/crates/bevy_sprite/src/mesh2d/material.rs @@ -418,7 +418,7 @@ pub fn queue_material2d_meshes( mut transparent_render_phases: ResMut>, mut opaque_render_phases: ResMut>, mut alpha_mask_render_phases: ResMut>, - mut views: Query<( + views: Query<( Entity, &ExtractedView, &VisibleEntities, @@ -433,7 +433,7 @@ pub fn queue_material2d_meshes( return; } - for (view_entity, view, visible_entities, msaa, tonemapping, dither) in &mut views { + for (view_entity, view, visible_entities, msaa, tonemapping, dither) in &views { let Some(transparent_phase) = transparent_render_phases.get_mut(&view_entity) else { continue; }; diff --git a/examples/2d/mesh2d_manual.rs b/examples/2d/mesh2d_manual.rs index 6a7e97f6fd071b..b298a12fec24c9 100644 --- a/examples/2d/mesh2d_manual.rs +++ b/examples/2d/mesh2d_manual.rs @@ -370,13 +370,13 @@ pub fn queue_colored_mesh2d( render_meshes: Res>, render_mesh_instances: Res, mut transparent_render_phases: ResMut>, - mut views: Query<(Entity, &VisibleEntities, &ExtractedView, &Msaa)>, + views: Query<(Entity, &VisibleEntities, &ExtractedView, &Msaa)>, ) { if render_mesh_instances.is_empty() { return; } // Iterate each view (a camera is a view) - for (view_entity, visible_entities, view, msaa) in &mut views { + for (view_entity, visible_entities, view, msaa) in &views { let Some(transparent_phase) = transparent_render_phases.get_mut(&view_entity) else { continue; }; diff --git a/examples/shader/shader_instancing.rs b/examples/shader/shader_instancing.rs index e93eeac61cefc3..734408f15d311a 100644 --- a/examples/shader/shader_instancing.rs +++ b/examples/shader/shader_instancing.rs @@ -123,11 +123,11 @@ fn queue_custom( render_mesh_instances: Res, material_meshes: Query>, mut transparent_render_phases: ResMut>, - mut views: Query<(Entity, &ExtractedView, &Msaa)>, + views: Query<(Entity, &ExtractedView, &Msaa)>, ) { let draw_custom = transparent_3d_draw_functions.read().id::(); - for (view_entity, view, msaa) in &mut views { + for (view_entity, view, msaa) in &views { let Some(transparent_phase) = transparent_render_phases.get_mut(&view_entity) else { continue; }; From 4648f7bf7228dd26bcd04b9fc5df8053196e1c42 Mon Sep 17 00:00:00 2001 From: akimakinai <105044389+akimakinai@users.noreply.github.com> Date: Wed, 28 Aug 2024 20:41:42 +0900 Subject: [PATCH 42/53] Make TrackedRenderPass::set_vertex_buffer aware of slice size (#14916) # Objective - Fixes #14841 ## Solution - Compute BufferSlice size manually and use it for comparison in `TrackedRenderPass` ## Testing - Gizmo example does not crash with #14721 (without system ordering), and `slice` computes correct size there --- ## Migration Guide - `TrackedRenderPass::set_vertex_buffer` function has been modified to update vertex buffers when the same buffer with the same offset is provided, but its size has changed. Some existing code may rely on the previous behavior, which did not update the vertex buffer in this scenario. --------- Co-authored-by: Zachary Harrold --- crates/bevy_gizmos/src/pipeline_2d.rs | 2 - crates/bevy_gizmos/src/pipeline_3d.rs | 2 - .../src/render_phase/draw_state.rs | 56 ++++++++++--------- .../bevy_render/src/render_resource/buffer.rs | 27 +++++++-- 4 files changed, 50 insertions(+), 37 deletions(-) diff --git a/crates/bevy_gizmos/src/pipeline_2d.rs b/crates/bevy_gizmos/src/pipeline_2d.rs index 6154d8edc9e063..0f6552f7874065 100644 --- a/crates/bevy_gizmos/src/pipeline_2d.rs +++ b/crates/bevy_gizmos/src/pipeline_2d.rs @@ -54,9 +54,7 @@ impl Plugin for LineGizmo2dPlugin { ) .add_systems( Render, - // FIXME: added `chain()` to workaround vertex buffer being not updated when sliced size changed (queue_line_gizmos_2d, queue_line_joint_gizmos_2d) - .chain() .in_set(GizmoRenderSystem::QueueLineGizmos2d) .after(prepare_assets::), ); diff --git a/crates/bevy_gizmos/src/pipeline_3d.rs b/crates/bevy_gizmos/src/pipeline_3d.rs index 37cf9b01db6629..8197623b3618cf 100644 --- a/crates/bevy_gizmos/src/pipeline_3d.rs +++ b/crates/bevy_gizmos/src/pipeline_3d.rs @@ -53,9 +53,7 @@ impl Plugin for LineGizmo3dPlugin { ) .add_systems( Render, - // FIXME: added `chain()` to workaround vertex buffer being not updated when sliced size changed (queue_line_gizmos_3d, queue_line_joint_gizmos_3d) - .chain() .in_set(GizmoRenderSystem::QueueLineGizmos3d) .after(prepare_assets::), ); diff --git a/crates/bevy_render/src/render_phase/draw_state.rs b/crates/bevy_render/src/render_phase/draw_state.rs index d7758452f701c8..0f682655c3057f 100644 --- a/crates/bevy_render/src/render_phase/draw_state.rs +++ b/crates/bevy_render/src/render_phase/draw_state.rs @@ -21,13 +21,14 @@ use wgpu::{IndexFormat, QuerySet, RenderPass}; struct DrawState { pipeline: Option, bind_groups: Vec<(Option, Vec)>, - vertex_buffers: Vec>, + /// List of vertex buffers by [`BufferId`], offset, and size. See [`DrawState::buffer_slice_key`] + vertex_buffers: Vec>, index_buffer: Option<(BufferId, u64, IndexFormat)>, } impl DrawState { /// Marks the `pipeline` as bound. - pub fn set_pipeline(&mut self, pipeline: RenderPipelineId) { + fn set_pipeline(&mut self, pipeline: RenderPipelineId) { // TODO: do these need to be cleared? // self.bind_groups.clear(); // self.vertex_buffers.clear(); @@ -36,17 +37,12 @@ impl DrawState { } /// Checks, whether the `pipeline` is already bound. - pub fn is_pipeline_set(&self, pipeline: RenderPipelineId) -> bool { + fn is_pipeline_set(&self, pipeline: RenderPipelineId) -> bool { self.pipeline == Some(pipeline) } /// Marks the `bind_group` as bound to the `index`. - pub fn set_bind_group( - &mut self, - index: usize, - bind_group: BindGroupId, - dynamic_indices: &[u32], - ) { + fn set_bind_group(&mut self, index: usize, bind_group: BindGroupId, dynamic_indices: &[u32]) { let group = &mut self.bind_groups[index]; group.0 = Some(bind_group); group.1.clear(); @@ -54,7 +50,7 @@ impl DrawState { } /// Checks, whether the `bind_group` is already bound to the `index`. - pub fn is_bind_group_set( + fn is_bind_group_set( &self, index: usize, bind_group: BindGroupId, @@ -68,26 +64,35 @@ impl DrawState { } /// Marks the vertex `buffer` as bound to the `index`. - pub fn set_vertex_buffer(&mut self, index: usize, buffer: BufferId, offset: u64) { - self.vertex_buffers[index] = Some((buffer, offset)); + fn set_vertex_buffer(&mut self, index: usize, buffer_slice: BufferSlice) { + self.vertex_buffers[index] = Some(self.buffer_slice_key(&buffer_slice)); } /// Checks, whether the vertex `buffer` is already bound to the `index`. - pub fn is_vertex_buffer_set(&self, index: usize, buffer: BufferId, offset: u64) -> bool { + fn is_vertex_buffer_set(&self, index: usize, buffer_slice: &BufferSlice) -> bool { if let Some(current) = self.vertex_buffers.get(index) { - *current == Some((buffer, offset)) + *current == Some(self.buffer_slice_key(buffer_slice)) } else { false } } + /// Returns the value used for checking whether `BufferSlice`s are equivalent. + fn buffer_slice_key(&self, buffer_slice: &BufferSlice) -> (BufferId, u64, u64) { + ( + buffer_slice.id(), + buffer_slice.offset(), + buffer_slice.size(), + ) + } + /// Marks the index `buffer` as bound. - pub fn set_index_buffer(&mut self, buffer: BufferId, offset: u64, index_format: IndexFormat) { + fn set_index_buffer(&mut self, buffer: BufferId, offset: u64, index_format: IndexFormat) { self.index_buffer = Some((buffer, offset, index_format)); } /// Checks, whether the index `buffer` is already bound. - pub fn is_index_buffer_set( + fn is_index_buffer_set( &self, buffer: BufferId, offset: u64, @@ -188,30 +193,27 @@ impl<'a> TrackedRenderPass<'a> { /// [`draw`]: TrackedRenderPass::draw /// [`draw_indexed`]: TrackedRenderPass::draw_indexed pub fn set_vertex_buffer(&mut self, slot_index: usize, buffer_slice: BufferSlice<'a>) { - let offset = buffer_slice.offset(); - if self - .state - .is_vertex_buffer_set(slot_index, buffer_slice.id(), offset) - { + if self.state.is_vertex_buffer_set(slot_index, &buffer_slice) { detailed_trace!( - "set vertex buffer {} (already set): {:?} ({})", + "set vertex buffer {} (already set): {:?} (offset = {}, size = {})", slot_index, buffer_slice.id(), - offset + buffer_slice.offset(), + buffer_slice.size(), ); return; } detailed_trace!( - "set vertex buffer {}: {:?} ({})", + "set vertex buffer {}: {:?} (offset = {}, size = {})", slot_index, buffer_slice.id(), - offset + buffer_slice.offset(), + buffer_slice.size(), ); self.pass .set_vertex_buffer(slot_index as u32, *buffer_slice); - self.state - .set_vertex_buffer(slot_index, buffer_slice.id(), offset); + self.state.set_vertex_buffer(slot_index, buffer_slice); } /// Sets the active index buffer. diff --git a/crates/bevy_render/src/render_resource/buffer.rs b/crates/bevy_render/src/render_resource/buffer.rs index 9867945673efba..cf98fdafb86923 100644 --- a/crates/bevy_render/src/render_resource/buffer.rs +++ b/crates/bevy_render/src/render_resource/buffer.rs @@ -8,6 +8,7 @@ render_resource_wrapper!(ErasedBuffer, wgpu::Buffer); pub struct Buffer { id: BufferId, value: ErasedBuffer, + size: wgpu::BufferAddress, } impl Buffer { @@ -17,14 +18,21 @@ impl Buffer { } pub fn slice(&self, bounds: impl RangeBounds) -> BufferSlice { + // need to compute and store this manually because wgpu doesn't export offset and size on wgpu::BufferSlice + let offset = match bounds.start_bound() { + Bound::Included(&bound) => bound, + Bound::Excluded(&bound) => bound + 1, + Bound::Unbounded => 0, + }; + let size = match bounds.end_bound() { + Bound::Included(&bound) => bound + 1, + Bound::Excluded(&bound) => bound, + Bound::Unbounded => self.size, + } - offset; BufferSlice { id: self.id, - // need to compute and store this manually because wgpu doesn't export offset on wgpu::BufferSlice - offset: match bounds.start_bound() { - Bound::Included(&bound) => bound, - Bound::Excluded(&bound) => bound + 1, - Bound::Unbounded => 0, - }, + offset, + size, value: self.value.slice(bounds), } } @@ -39,6 +47,7 @@ impl From for Buffer { fn from(value: wgpu::Buffer) -> Self { Buffer { id: BufferId::new(), + size: value.size(), value: ErasedBuffer::new(value), } } @@ -58,6 +67,7 @@ pub struct BufferSlice<'a> { id: BufferId, offset: wgpu::BufferAddress, value: wgpu::BufferSlice<'a>, + size: wgpu::BufferAddress, } impl<'a> BufferSlice<'a> { @@ -70,6 +80,11 @@ impl<'a> BufferSlice<'a> { pub fn offset(&self) -> wgpu::BufferAddress { self.offset } + + #[inline] + pub fn size(&self) -> wgpu::BufferAddress { + self.size + } } impl<'a> Deref for BufferSlice<'a> { From 4be8e497ca2130561faa0a8e70644e8e36b7adbf Mon Sep 17 00:00:00 2001 From: Chris Russell <8494645+chescock@users.noreply.github.com> Date: Wed, 28 Aug 2024 14:24:52 -0400 Subject: [PATCH 43/53] SystemParamBuilder - Allow deriving a SystemParamBuilder struct when deriving SystemParam. (#14818) # Objective Allow `SystemParamBuilder` implementations for custom system parameters created using `#[derive(SystemParam)]`. ## Solution Extend the derive macro to accept a `#[system_param(builder)]` attribute. When present, emit a builder type with a field corresponding to each field of the param. ## Example ```rust #[derive(SystemParam)] #[system_param(builder)] struct CustomParam<'w, 's> { query: Query<'w, 's, ()>, local: Local<'s, usize>, } let system = (CustomParamBuilder { local: LocalBuilder(100), query: QueryParamBuilder::new(|builder| { builder.with::(); }), },) .build_state(&mut world) .build_system(|param: CustomParam| *param.local + param.query.iter().count()); ``` --- crates/bevy_ecs/macros/src/lib.rs | 54 ++++++++++++++++++++++ crates/bevy_ecs/src/system/builder.rs | 27 +++++++++++ crates/bevy_ecs/src/system/system_param.rs | 49 ++++++++++++++++++++ 3 files changed, 130 insertions(+) diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index ed5cc22c247a76..f33659ca148b17 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -423,6 +423,56 @@ pub fn derive_system_param(input: TokenStream) -> TokenStream { let state_struct_visibility = &ast.vis; let state_struct_name = ensure_no_collision(format_ident!("FetchState"), token_stream); + let mut builder_name = None; + for meta in ast + .attrs + .iter() + .filter(|a| a.path().is_ident("system_param")) + { + if let Err(e) = meta.parse_nested_meta(|nested| { + if nested.path.is_ident("builder") { + builder_name = Some(format_ident!("{struct_name}Builder")); + Ok(()) + } else { + Err(nested.error("Unsupported attribute")) + } + }) { + return e.into_compile_error().into(); + } + } + + let builder = builder_name.map(|builder_name| { + let builder_type_parameters: Vec<_> = (0..fields.len()).map(|i| format_ident!("B{i}")).collect(); + let builder_doc_comment = format!("A [`SystemParamBuilder`] for a [`{struct_name}`]."); + let builder_struct = quote! { + #[doc = #builder_doc_comment] + struct #builder_name<#(#builder_type_parameters,)*> { + #(#fields: #builder_type_parameters,)* + } + }; + let lifetimes: Vec<_> = generics.lifetimes().collect(); + let generic_struct = quote!{ #struct_name <#(#lifetimes,)* #punctuated_generic_idents> }; + let builder_impl = quote!{ + // SAFETY: This delegates to the `SystemParamBuilder` for tuples. + unsafe impl< + #(#lifetimes,)* + #(#builder_type_parameters: #path::system::SystemParamBuilder<#field_types>,)* + #punctuated_generics + > #path::system::SystemParamBuilder<#generic_struct> for #builder_name<#(#builder_type_parameters,)*> + #where_clause + { + fn build(self, world: &mut #path::world::World, meta: &mut #path::system::SystemMeta) -> <#generic_struct as #path::system::SystemParam>::State { + let #builder_name { #(#fields: #field_locals,)* } = self; + #state_struct_name { + state: #path::system::SystemParamBuilder::build((#(#tuple_patterns,)*), world, meta) + } + } + } + }; + (builder_struct, builder_impl) + }); + let (builder_struct, builder_impl) = builder.unzip(); + TokenStream::from(quote! { // We define the FetchState struct in an anonymous scope to avoid polluting the user namespace. // The struct can still be accessed via SystemParam::State, e.g. EventReaderState can be accessed via @@ -479,7 +529,11 @@ pub fn derive_system_param(input: TokenStream) -> TokenStream { // Safety: Each field is `ReadOnlySystemParam`, so this can only read from the `World` unsafe impl<'w, 's, #punctuated_generics> #path::system::ReadOnlySystemParam for #struct_name #ty_generics #read_only_where_clause {} + + #builder_impl }; + + #builder_struct }) } diff --git a/crates/bevy_ecs/src/system/builder.rs b/crates/bevy_ecs/src/system/builder.rs index cd532776e5b881..ea5b070010b2d9 100644 --- a/crates/bevy_ecs/src/system/builder.rs +++ b/crates/bevy_ecs/src/system/builder.rs @@ -556,4 +556,31 @@ mod tests { let result = world.run_system_once(system); assert_eq!(result, 4); } + + #[derive(SystemParam)] + #[system_param(builder)] + struct CustomParam<'w, 's> { + query: Query<'w, 's, ()>, + local: Local<'s, usize>, + } + + #[test] + fn custom_param_builder() { + let mut world = World::new(); + + world.spawn(A); + world.spawn_empty(); + + let system = (CustomParamBuilder { + local: LocalBuilder(100), + query: QueryParamBuilder::new(|builder| { + builder.with::(); + }), + },) + .build_state(&mut world) + .build_system(|param: CustomParam| *param.local + param.query.iter().count()); + + let result = world.run_system_once(system); + assert_eq!(result, 101); + } } diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index 10e7fd4ec35837..6b604c54c2a9a5 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -121,6 +121,55 @@ use std::{ /// This will most commonly occur when working with `SystemParam`s generically, as the requirement /// has not been proven to the compiler. /// +/// ## Builders +/// +/// If you want to use a [`SystemParamBuilder`](crate::system::SystemParamBuilder) with a derived [`SystemParam`] implementation, +/// add a `#[system_param(builder)]` attribute to the struct. +/// This will generate a builder struct whose name is the param struct suffixed with `Builder`. +/// The builder will not be `pub`, so you may want to expose a method that returns an `impl SystemParamBuilder`. +/// +/// ``` +/// mod custom_param { +/// # use bevy_ecs::{ +/// # prelude::*, +/// # system::{LocalBuilder, QueryParamBuilder, SystemParam}, +/// # }; +/// # +/// #[derive(SystemParam)] +/// #[system_param(builder)] +/// pub struct CustomParam<'w, 's> { +/// query: Query<'w, 's, ()>, +/// local: Local<'s, usize>, +/// } +/// +/// impl<'w, 's> CustomParam<'w, 's> { +/// pub fn builder( +/// local: usize, +/// query: impl FnOnce(&mut QueryBuilder<()>), +/// ) -> impl SystemParamBuilder { +/// CustomParamBuilder { +/// local: LocalBuilder(local), +/// query: QueryParamBuilder::new(query), +/// } +/// } +/// } +/// } +/// +/// use custom_param::CustomParam; +/// +/// # use bevy_ecs::prelude::*; +/// # #[derive(Component)] +/// # struct A; +/// # +/// # let mut world = World::new(); +/// # +/// let system = (CustomParam::builder(100, |builder| { +/// builder.with::(); +/// }),) +/// .build_state(&mut world) +/// .build_system(|param: CustomParam| {}); +/// ``` +/// /// # Safety /// /// The implementor must ensure the following is true. From e600e2c1b14b520af65b6c32d5897724ba9aa8fb Mon Sep 17 00:00:00 2001 From: Shane Date: Thu, 29 Aug 2024 05:15:49 -0700 Subject: [PATCH 44/53] Move the default LogPlugin filter to a public constant (#14958) # Objective This moves the default `LogPlugin` filter to be a public constant so that it can be updated and referenced from outside code without changes across releases: ``` fn main() { App::new().add_plugins( DefaultPlugins .set(bevy::log::LogPlugin { filter: format!("{},mylogs=error", bevy::log::LogPlugin::DEFAULT_FILTER), ..default() })).run(); } ``` ## Testing Tested with `cargo run -p ci` --- crates/bevy_log/src/lib.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/bevy_log/src/lib.rs b/crates/bevy_log/src/lib.rs index c2fdd1639b83df..53f1dbd6d10151 100644 --- a/crates/bevy_log/src/lib.rs +++ b/crates/bevy_log/src/lib.rs @@ -157,10 +157,13 @@ pub struct LogPlugin { /// A boxed [`Layer`] that can be used with [`LogPlugin`]. pub type BoxedLayer = Box + Send + Sync + 'static>; +/// The default [`LogPlugin`] [`EnvFilter`]. +pub const DEFAULT_FILTER: &str = "wgpu=error,naga=warn"; + impl Default for LogPlugin { fn default() -> Self { Self { - filter: "wgpu=error,naga=warn".to_string(), + filter: DEFAULT_FILTER.to_string(), level: Level::INFO, custom_layer: |_| None, } From 565324daa32fc4d84da14f3c68b994048f6a4d2e Mon Sep 17 00:00:00 2001 From: Robert Walter <26892280+RobWalt@users.noreply.github.com> Date: Thu, 29 Aug 2024 15:51:36 +0000 Subject: [PATCH 45/53] Improve the gizmo for `Plane3d`, reusing grid (#14650) # Objective With the current implementation of `Plane3d` gizmos, it's really hard to get a good feeling for big planes. Usually I tend to add more axes as a user but that doesn't scale well and is pretty wasteful. It's hard to recognize the plane in the distance here. Especially if there would've been other rendered objects in the scene ![image](https://github.com/user-attachments/assets/b65b7015-c08c-46d7-aa27-c7c0d49b2021) ## Solution - Since we got grid gizmos in the mean time, I went ahead and just reused them here. ## Testing I added an instance of the new `Plane3D` to the `3d_gizmos.rs` example. If you want to look at it you need to look around a bit. I didn't position it in the center since that was too crowded already. --- ## Showcase ![image](https://github.com/user-attachments/assets/e4982afe-7296-416c-9801-7dd85cd975c1) ## Migration Guide The optional builder methods on ```rust gizmos.primitive_3d(&Plane3d { }, ...); ``` changed from - `segment_length` - `segment_count` - `axis_count` to - `cell_count` - `spacing` --- crates/bevy_gizmos/src/grid.rs | 2 + crates/bevy_gizmos/src/primitives/dim3.rs | 73 +++++++---------------- examples/gizmos/3d_gizmos.rs | 15 +++++ 3 files changed, 39 insertions(+), 51 deletions(-) diff --git a/crates/bevy_gizmos/src/grid.rs b/crates/bevy_gizmos/src/grid.rs index 05b04c03767355..8c387fa349e41d 100644 --- a/crates/bevy_gizmos/src/grid.rs +++ b/crates/bevy_gizmos/src/grid.rs @@ -182,6 +182,8 @@ where /// /// This should be called for each frame the grid needs to be rendered. /// + /// The grid's default orientation aligns with the XY-plane. + /// /// # Arguments /// /// - `isometry` defines the translation and rotation of the grid. diff --git a/crates/bevy_gizmos/src/primitives/dim3.rs b/crates/bevy_gizmos/src/primitives/dim3.rs index e2da1a115894e6..f2ee075c16c75b 100644 --- a/crates/bevy_gizmos/src/primitives/dim3.rs +++ b/crates/bevy_gizmos/src/primitives/dim3.rs @@ -1,14 +1,13 @@ //! A module for rendering each of the 3D [`bevy_math::primitives`] with [`Gizmos`]. use super::helpers::*; -use std::f32::consts::TAU; use bevy_color::Color; use bevy_math::primitives::{ BoxedPolyline3d, Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, Line3d, Plane3d, Polyline3d, Primitive3d, Segment3d, Sphere, Tetrahedron, Torus, Triangle3d, }; -use bevy_math::{Dir3, Isometry3d, Quat, Vec3}; +use bevy_math::{Dir3, Isometry3d, Quat, UVec2, Vec2, Vec3}; use crate::circles::SphereBuilder; use crate::prelude::{GizmoConfigGroup, Gizmos}; @@ -83,19 +82,17 @@ where { gizmos: &'a mut Gizmos<'w, 's, Config, Clear>, - // direction of the normal orthogonal to the plane + // Direction of the normal orthogonal to the plane normal: Dir3, isometry: Isometry3d, // Color of the plane color: Color, - // Number of axis to hint the plane - axis_count: u32, - // Number of segments used to hint the plane - segment_count: u32, - // Length of segments used to hint the plane - segment_length: f32, + // Defines the amount of cells in the x and y axes + cell_count: UVec2, + // Defines the distance between cells along the x and y axes + spacing: Vec2, } impl Plane3dBuilder<'_, '_, '_, Config, Clear> @@ -103,21 +100,15 @@ where Config: GizmoConfigGroup, Clear: 'static + Send + Sync, { - /// Set the number of segments used to hint the plane. - pub fn segment_count(mut self, count: u32) -> Self { - self.segment_count = count; + /// Set the number of cells in the x and y axes direction. + pub fn cell_count(mut self, cell_count: UVec2) -> Self { + self.cell_count = cell_count; self } - /// Set the length of segments used to hint the plane. - pub fn segment_length(mut self, length: f32) -> Self { - self.segment_length = length; - self - } - - /// Set the number of axis used to hint the plane. - pub fn axis_count(mut self, count: u32) -> Self { - self.axis_count = count; + /// Set the distance between cells along the x and y axes. + pub fn spacing(mut self, spacing: Vec2) -> Self { + self.spacing = spacing; self } } @@ -140,9 +131,8 @@ where normal: primitive.normal, isometry, color: color.into(), - axis_count: 4, - segment_count: 3, - segment_length: 0.25, + cell_count: UVec2::splat(3), + spacing: Vec2::splat(1.0), } } } @@ -157,35 +147,16 @@ where return; } - // draws the normal self.gizmos .primitive_3d(&self.normal, self.isometry, self.color); - - // draws the axes - // get rotation for each direction - let normals_normal = self.normal.any_orthonormal_vector(); - (0..self.axis_count) - .map(|i| i as f32 * (1.0 / self.axis_count as f32) * TAU) - .map(|angle| Quat::from_axis_angle(self.normal.as_vec3(), angle)) - .flat_map(|quat| { - let segment_length = self.segment_length; - let isometry = self.isometry; - // for each axis draw dotted line - (0..) - .filter(|i| i % 2 != 0) - .take(self.segment_count as usize) - .map(|i| [i, i + 1]) - .map(move |percents| { - percents - .map(|percent| percent as f32 + 0.5) - .map(|percent| percent * segment_length * normals_normal) - .map(|vec3| quat * vec3) - .map(|vec3| isometry * vec3) - }) - }) - .for_each(|[start, end]| { - self.gizmos.line(start, end, self.color); - }); + // the default orientation of the grid is Z-up + let rot = Quat::from_rotation_arc(Vec3::Z, self.normal.as_vec3()); + self.gizmos.grid( + Isometry3d::new(self.isometry.translation, self.isometry.rotation * rot), + self.cell_count, + self.spacing, + self.color, + ); } } diff --git a/examples/gizmos/3d_gizmos.rs b/examples/gizmos/3d_gizmos.rs index e66682180c3fd0..9e506442c1a82d 100644 --- a/examples/gizmos/3d_gizmos.rs +++ b/examples/gizmos/3d_gizmos.rs @@ -97,6 +97,21 @@ fn draw_example_collection( ); gizmos.sphere(Isometry3d::from_translation(Vec3::ONE * 10.0), 1.0, PURPLE); + gizmos + .primitive_3d( + &Plane3d { + normal: Dir3::Y, + half_size: Vec2::splat(1.0), + }, + Isometry3d::new( + Vec3::ONE * 4.0 + Vec2::from(time.elapsed_seconds().sin_cos()).extend(0.0), + Quat::from_rotation_x(PI / 2. + time.elapsed_seconds()), + ), + GREEN, + ) + .cell_count(UVec2::new(5, 10)) + .spacing(Vec2::new(0.2, 0.1)); + gizmos.cuboid( Transform::from_translation(Vec3::Y * 0.5).with_scale(Vec3::splat(1.25)), BLACK, From 1cca4f2968f0600e0f3a06b87bab777d1c822936 Mon Sep 17 00:00:00 2001 From: akimakinai <105044389+akimakinai@users.noreply.github.com> Date: Fri, 30 Aug 2024 01:47:58 +0900 Subject: [PATCH 46/53] Remove some asset examples from web showcase (#14973) # Objective - `custom_asset_reader` and `extra_asset_source` examples are not working on web. - Fixes #14689 ## Solution - Make these examples `wasm=false` per https://github.com/bevyengine/bevy/issues/14689#issuecomment-2313064396 ## Testing --- Cargo.toml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fde3db1f420d24..6e9daa74a4fba2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1407,7 +1407,8 @@ doc-scrape-examples = true name = "Custom Asset IO" description = "Implements a custom AssetReader" category = "Assets" -wasm = true +# Incompatible with the asset path patching of the example-showcase tool +wasm = false [[example]] name = "embedded_asset" @@ -1429,7 +1430,8 @@ doc-scrape-examples = true name = "Extra asset source" description = "Load an asset from a non-standard asset source" category = "Assets" -wasm = true +# Uses non-standard asset path +wasm = false [[example]] name = "hot_asset_reloading" From 9e784334276669295f0d1c83727868ec0086cdf7 Mon Sep 17 00:00:00 2001 From: Robert Walter <26892280+RobWalt@users.noreply.github.com> Date: Thu, 29 Aug 2024 16:48:22 +0000 Subject: [PATCH 47/53] `Curve` gizmos integration (#14971) # Objective - Add gizmos integration for the new `Curve` things in the math lib ## Solution - Add the following methods - `curve_2d(curve, sample_times, color)` - `curve_3d(curve, sample_times, color)` - `curve_gradient_2d(curve, sample_times_with_colors)` - `curve_gradient_3d(curve, sample_times_with_colors)` ## Testing - I added examples of the 2D and 3D variants of the gradient curve gizmos to the gizmos examples. ## Showcase ### 2D ![image](https://github.com/user-attachments/assets/01a75706-a7b4-4fc5-98d5-18018185c877) ```rust let domain = Interval::EVERYWHERE; let curve = function_curve(domain, |t| Vec2::new(t, (t / 25.0).sin() * 100.0)); let resolution = ((time.elapsed_seconds().sin() + 1.0) * 50.0) as usize; let times_and_colors = (0..=resolution) .map(|n| n as f32 / resolution as f32) .map(|t| (t - 0.5) * 600.0) .map(|t| (t, TEAL.mix(&HOT_PINK, (t + 300.0) / 600.0))); gizmos.curve_gradient_2d(curve, times_and_colors); ``` ### 3D ![image](https://github.com/user-attachments/assets/3fd23983-1ec9-46cd-baed-5b5e2dc935d0) ```rust let domain = Interval::EVERYWHERE; let curve = function_curve(domain, |t| { (Vec2::from((t * 10.0).sin_cos())).extend(t - 6.0) }); let resolution = ((time.elapsed_seconds().sin() + 1.0) * 100.0) as usize; let times_and_colors = (0..=resolution) .map(|n| n as f32 / resolution as f32) .map(|t| t * 5.0) .map(|t| (t, TEAL.mix(&HOT_PINK, t / 5.0))); gizmos.curve_gradient_3d(curve, times_and_colors); ``` --- crates/bevy_gizmos/src/curves.rs | 175 +++++++++++++++++++++++++++++++ crates/bevy_gizmos/src/lib.rs | 1 + crates/bevy_math/src/lib.rs | 1 + examples/gizmos/2d_gizmos.rs | 9 ++ examples/gizmos/3d_gizmos.rs | 11 ++ 5 files changed, 197 insertions(+) create mode 100644 crates/bevy_gizmos/src/curves.rs diff --git a/crates/bevy_gizmos/src/curves.rs b/crates/bevy_gizmos/src/curves.rs new file mode 100644 index 00000000000000..4a7b1aec1e0452 --- /dev/null +++ b/crates/bevy_gizmos/src/curves.rs @@ -0,0 +1,175 @@ +//! Additional [`Gizmos`] Functions -- Curves +//! +//! Includes the implementation of [`Gizmos::curve_2d`], +//! [`Gizmos::curve_3d`] and assorted support items. + +use bevy_color::Color; +use bevy_math::{curve::Curve, Vec2, Vec3}; + +use crate::prelude::{GizmoConfigGroup, Gizmos}; + +impl<'w, 's, Config, Clear> Gizmos<'w, 's, Config, Clear> +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + /// Draw a curve, at the given time points, sampling in 2D. + /// + /// This should be called for each frame the curve needs to be rendered. + /// + /// Samples of time points outside of the curve's domain will be filtered out and won't + /// contribute to the rendering. If you wish to render the curve outside of its domain you need + /// to create a new curve with an extended domain. + /// + /// # Arguments + /// - `curve_2d` some type that implements the [`Curve`] trait and samples `Vec2`s + /// - `times` some iterable type yielding `f32` which will be used for sampling the curve + /// - `color` the color of the curve + /// + /// # Example + /// ``` + /// # use bevy_gizmos::prelude::*; + /// # use bevy_math::prelude::*; + /// # use bevy_color::palettes::basic::{RED}; + /// fn system(mut gizmos: Gizmos) { + /// let domain = Interval::UNIT; + /// let curve = function_curve(domain, |t| Vec2::from(t.sin_cos())); + /// gizmos.curve_2d(curve, (0..=100).map(|n| n as f32 / 100.0), RED); + /// } + /// # bevy_ecs::system::assert_is_system(system); + /// ``` + pub fn curve_2d( + &mut self, + curve_2d: impl Curve, + times: impl IntoIterator, + color: impl Into, + ) { + self.linestrip_2d(curve_2d.sample_iter(times).flatten(), color); + } + + /// Draw a curve, at the given time points, sampling in 3D. + /// + /// This should be called for each frame the curve needs to be rendered. + /// + /// Samples of time points outside of the curve's domain will be filtered out and won't + /// contribute to the rendering. If you wish to render the curve outside of its domain you need + /// to create a new curve with an extended domain. + /// + /// # Arguments + /// - `curve_3d` some type that implements the [`Curve`] trait and samples `Vec3`s + /// - `times` some iterable type yielding `f32` which will be used for sampling the curve + /// - `color` the color of the curve + /// + /// # Example + /// ``` + /// # use bevy_gizmos::prelude::*; + /// # use bevy_math::prelude::*; + /// # use bevy_color::palettes::basic::{RED}; + /// fn system(mut gizmos: Gizmos) { + /// let domain = Interval::UNIT; + /// let curve = function_curve(domain, |t| { + /// let (x,y) = t.sin_cos(); + /// Vec3::new(x, y, t) + /// }); + /// gizmos.curve_3d(curve, (0..=100).map(|n| n as f32 / 100.0), RED); + /// } + /// # bevy_ecs::system::assert_is_system(system); + /// ``` + pub fn curve_3d( + &mut self, + curve_3d: impl Curve, + times: impl IntoIterator, + color: impl Into, + ) { + self.linestrip(curve_3d.sample_iter(times).flatten(), color); + } + + /// Draw a curve, at the given time points, sampling in 2D, with a color gradient. + /// + /// This should be called for each frame the curve needs to be rendered. + /// + /// Samples of time points outside of the curve's domain will be filtered out and won't + /// contribute to the rendering. If you wish to render the curve outside of its domain you need + /// to create a new curve with an extended domain. + /// + /// # Arguments + /// - `curve_2d` some type that implements the [`Curve`] trait and samples `Vec2`s + /// - `times_with_colors` some iterable type yielding `f32` which will be used for sampling + /// the curve together with the color at this position + /// + /// # Example + /// ``` + /// # use bevy_gizmos::prelude::*; + /// # use bevy_math::prelude::*; + /// # use bevy_color::{Mix, palettes::basic::{GREEN, RED}}; + /// fn system(mut gizmos: Gizmos) { + /// let domain = Interval::UNIT; + /// let curve = function_curve(domain, |t| Vec2::from(t.sin_cos())); + /// gizmos.curve_gradient_2d( + /// curve, + /// (0..=100).map(|n| n as f32 / 100.0) + /// .map(|t| (t, GREEN.mix(&RED, t))) + /// ); + /// } + /// # bevy_ecs::system::assert_is_system(system); + /// ``` + pub fn curve_gradient_2d( + &mut self, + curve_2d: impl Curve, + times_with_colors: impl IntoIterator, + ) where + C: Into, + { + self.linestrip_gradient_2d( + times_with_colors + .into_iter() + .filter_map(|(time, color)| curve_2d.sample(time).map(|sample| (sample, color))), + ); + } + + /// Draw a curve, at the given time points, sampling in 3D, with a color gradient. + /// + /// This should be called for each frame the curve needs to be rendered. + /// + /// Samples of time points outside of the curve's domain will be filtered out and won't + /// contribute to the rendering. If you wish to render the curve outside of its domain you need + /// to create a new curve with an extended domain. + /// + /// # Arguments + /// - `curve_3d` some type that implements the [`Curve`] trait and samples `Vec3`s + /// - `times_with_colors` some iterable type yielding `f32` which will be used for sampling + /// the curve together with the color at this position + /// + /// # Example + /// ``` + /// # use bevy_gizmos::prelude::*; + /// # use bevy_math::prelude::*; + /// # use bevy_color::{Mix, palettes::basic::{GREEN, RED}}; + /// fn system(mut gizmos: Gizmos) { + /// let domain = Interval::UNIT; + /// let curve = function_curve(domain, |t| { + /// let (x,y) = t.sin_cos(); + /// Vec3::new(x, y, t) + /// }); + /// gizmos.curve_gradient_3d( + /// curve, + /// (0..=100).map(|n| n as f32 / 100.0) + /// .map(|t| (t, GREEN.mix(&RED, t))) + /// ); + /// } + /// # bevy_ecs::system::assert_is_system(system); + /// ``` + pub fn curve_gradient_3d( + &mut self, + curve_3d: impl Curve, + times_with_colors: impl IntoIterator, + ) where + C: Into, + { + self.linestrip_gradient( + times_with_colors + .into_iter() + .filter_map(|(time, color)| curve_3d.sample(time).map(|sample| (sample, color))), + ); + } +} diff --git a/crates/bevy_gizmos/src/lib.rs b/crates/bevy_gizmos/src/lib.rs index 3e28516b608b75..c30d0b09314ef7 100644 --- a/crates/bevy_gizmos/src/lib.rs +++ b/crates/bevy_gizmos/src/lib.rs @@ -37,6 +37,7 @@ pub mod arrows; pub mod circles; pub mod config; pub mod cross; +pub mod curves; pub mod gizmos; pub mod grid; pub mod primitives; diff --git a/crates/bevy_math/src/lib.rs b/crates/bevy_math/src/lib.rs index 76ad5b06a7b5a8..cbbc25e219fa8c 100644 --- a/crates/bevy_math/src/lib.rs +++ b/crates/bevy_math/src/lib.rs @@ -55,6 +55,7 @@ pub mod prelude { CubicHermite, CubicNurbs, CubicNurbsError, CubicSegment, CyclicCubicGenerator, RationalCurve, RationalGenerator, RationalSegment, }, + curve::*, direction::{Dir2, Dir3, Dir3A}, primitives::*, BVec2, BVec3, BVec4, EulerRot, FloatExt, IRect, IVec2, IVec3, IVec4, Isometry2d, diff --git a/examples/gizmos/2d_gizmos.rs b/examples/gizmos/2d_gizmos.rs index 23d07a8b1a06f1..0fa75863d50f8e 100644 --- a/examples/gizmos/2d_gizmos.rs +++ b/examples/gizmos/2d_gizmos.rs @@ -73,6 +73,15 @@ fn draw_example_collection( FUCHSIA, ); + let domain = Interval::EVERYWHERE; + let curve = function_curve(domain, |t| Vec2::new(t, (t / 25.0).sin() * 100.0)); + let resolution = ((time.elapsed_seconds().sin() + 1.0) * 50.0) as usize; + let times_and_colors = (0..=resolution) + .map(|n| n as f32 / resolution as f32) + .map(|t| (t - 0.5) * 600.0) + .map(|t| (t, TEAL.mix(&HOT_PINK, (t + 300.0) / 600.0))); + gizmos.curve_gradient_2d(curve, times_and_colors); + my_gizmos .rounded_rect_2d(Isometry2d::IDENTITY, Vec2::splat(630.), BLACK) .corner_radius((time.elapsed_seconds() / 3.).cos() * 100.); diff --git a/examples/gizmos/3d_gizmos.rs b/examples/gizmos/3d_gizmos.rs index 9e506442c1a82d..1e4238a58a93c8 100644 --- a/examples/gizmos/3d_gizmos.rs +++ b/examples/gizmos/3d_gizmos.rs @@ -131,6 +131,17 @@ fn draw_example_collection( FUCHSIA, ); + let domain = Interval::EVERYWHERE; + let curve = function_curve(domain, |t| { + (Vec2::from((t * 10.0).sin_cos())).extend(t - 6.0) + }); + let resolution = ((time.elapsed_seconds().sin() + 1.0) * 100.0) as usize; + let times_and_colors = (0..=resolution) + .map(|n| n as f32 / resolution as f32) + .map(|t| t * 5.0) + .map(|t| (t, TEAL.mix(&HOT_PINK, t / 5.0))); + gizmos.curve_gradient_3d(curve, times_and_colors); + my_gizmos.sphere( Isometry3d::from_translation(Vec3::new(1., 0.5, 0.)), 0.5, From e08497dc8f26d52b956621153c03d0d22196ea88 Mon Sep 17 00:00:00 2001 From: Chris Juchem Date: Thu, 29 Aug 2024 20:43:07 -0400 Subject: [PATCH 48/53] Replace `bevy_utils::CowArc` with `atomicow` (#14977) # Objective - Fixes https://github.com/bevyengine/bevy/issues/14975 ## Solution - Replace usages of `bevy_utils::CowArc` with `atomicow::CowArc` - Remove bevy_utils::CowArc ## Testing - `bevy_asset` test suite continues to pass. --- ## Migration Guide `bevy_utils::CowArc` has moved to a new crate called [atomicow](https://crates.io/crates/atomicow). --- crates/bevy_asset/Cargo.toml | 1 + crates/bevy_asset/src/io/source.rs | 3 +- crates/bevy_asset/src/loader.rs | 3 +- crates/bevy_asset/src/path.rs | 2 +- crates/bevy_asset/src/saver.rs | 3 +- crates/bevy_asset/src/server/mod.rs | 3 +- crates/bevy_asset/src/transformer.rs | 3 +- crates/bevy_utils/src/cow_arc.rs | 191 --------------------------- crates/bevy_utils/src/lib.rs | 2 - 9 files changed, 12 insertions(+), 199 deletions(-) delete mode 100644 crates/bevy_utils/src/cow_arc.rs diff --git a/crates/bevy_asset/Cargo.toml b/crates/bevy_asset/Cargo.toml index 0b95ab505bae7e..b2348d65094d3d 100644 --- a/crates/bevy_asset/Cargo.toml +++ b/crates/bevy_asset/Cargo.toml @@ -29,6 +29,7 @@ bevy_tasks = { path = "../bevy_tasks", version = "0.15.0-dev" } bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } stackfuture = "0.3" +atomicow = "1.0" async-broadcast = "0.5" async-fs = "2.0" async-lock = "3.0" diff --git a/crates/bevy_asset/src/io/source.rs b/crates/bevy_asset/src/io/source.rs index a979a3327791e7..ec1947a3fee1e0 100644 --- a/crates/bevy_asset/src/io/source.rs +++ b/crates/bevy_asset/src/io/source.rs @@ -2,9 +2,10 @@ use crate::{ io::{processor_gated::ProcessorGatedReader, AssetSourceEvent, AssetWatcher}, processor::AssetProcessorData, }; +use atomicow::CowArc; use bevy_ecs::system::Resource; use bevy_utils::tracing::{error, warn}; -use bevy_utils::{CowArc, Duration, HashMap}; +use bevy_utils::{Duration, HashMap}; use std::{fmt::Display, hash::Hash, sync::Arc}; use thiserror::Error; diff --git a/crates/bevy_asset/src/loader.rs b/crates/bevy_asset/src/loader.rs index 1b444bba8c19b5..f0dce3593da40a 100644 --- a/crates/bevy_asset/src/loader.rs +++ b/crates/bevy_asset/src/loader.rs @@ -6,8 +6,9 @@ use crate::{ Asset, AssetLoadError, AssetServer, AssetServerMode, Assets, Handle, UntypedAssetId, UntypedHandle, }; +use atomicow::CowArc; use bevy_ecs::world::World; -use bevy_utils::{BoxedFuture, ConditionalSendFuture, CowArc, HashMap, HashSet}; +use bevy_utils::{BoxedFuture, ConditionalSendFuture, HashMap, HashSet}; use downcast_rs::{impl_downcast, Downcast}; use ron::error::SpannedError; use serde::{Deserialize, Serialize}; diff --git a/crates/bevy_asset/src/path.rs b/crates/bevy_asset/src/path.rs index dc7719a25f9ad6..67c7c65286facd 100644 --- a/crates/bevy_asset/src/path.rs +++ b/crates/bevy_asset/src/path.rs @@ -1,6 +1,6 @@ use crate::io::AssetSourceId; +use atomicow::CowArc; use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; -use bevy_utils::CowArc; use serde::{de::Visitor, Deserialize, Serialize}; use std::{ fmt::{Debug, Display}, diff --git a/crates/bevy_asset/src/saver.rs b/crates/bevy_asset/src/saver.rs index 36408dd125f298..4d5925dc5492c8 100644 --- a/crates/bevy_asset/src/saver.rs +++ b/crates/bevy_asset/src/saver.rs @@ -1,7 +1,8 @@ use crate::transformer::TransformedAsset; use crate::{io::Writer, meta::Settings, Asset, ErasedLoadedAsset}; use crate::{AssetLoader, Handle, LabeledAsset, UntypedHandle}; -use bevy_utils::{BoxedFuture, ConditionalSendFuture, CowArc, HashMap}; +use atomicow::CowArc; +use bevy_utils::{BoxedFuture, ConditionalSendFuture, HashMap}; use serde::{Deserialize, Serialize}; use std::{borrow::Borrow, hash::Hash, ops::Deref}; diff --git a/crates/bevy_asset/src/server/mod.rs b/crates/bevy_asset/src/server/mod.rs index ef72d2b4042951..61c802e5057663 100644 --- a/crates/bevy_asset/src/server/mod.rs +++ b/crates/bevy_asset/src/server/mod.rs @@ -17,10 +17,11 @@ use crate::{ DeserializeMetaError, ErasedLoadedAsset, Handle, LoadedUntypedAsset, UntypedAssetId, UntypedAssetLoadFailedEvent, UntypedHandle, }; +use atomicow::CowArc; use bevy_ecs::prelude::*; use bevy_tasks::IoTaskPool; use bevy_utils::tracing::{error, info}; -use bevy_utils::{CowArc, HashSet}; +use bevy_utils::HashSet; use crossbeam_channel::{Receiver, Sender}; use futures_lite::{FutureExt, StreamExt}; use info::*; diff --git a/crates/bevy_asset/src/transformer.rs b/crates/bevy_asset/src/transformer.rs index 0ffddc4658a432..1b2cd92991c15b 100644 --- a/crates/bevy_asset/src/transformer.rs +++ b/crates/bevy_asset/src/transformer.rs @@ -1,5 +1,6 @@ use crate::{meta::Settings, Asset, ErasedLoadedAsset, Handle, LabeledAsset, UntypedHandle}; -use bevy_utils::{ConditionalSendFuture, CowArc, HashMap}; +use atomicow::CowArc; +use bevy_utils::{ConditionalSendFuture, HashMap}; use serde::{Deserialize, Serialize}; use std::{ borrow::Borrow, diff --git a/crates/bevy_utils/src/cow_arc.rs b/crates/bevy_utils/src/cow_arc.rs deleted file mode 100644 index 635d31a583ef6d..00000000000000 --- a/crates/bevy_utils/src/cow_arc.rs +++ /dev/null @@ -1,191 +0,0 @@ -use std::{ - borrow::Borrow, - fmt::{Debug, Display}, - hash::Hash, - ops::Deref, - path::{Path, PathBuf}, - sync::Arc, -}; - -/// Much like a [`Cow`](std::borrow::Cow), but owned values are Arc-ed to make clones cheap. This should be used for values that -/// are cloned for use across threads and change rarely (if ever). -/// -/// This also makes an opinionated tradeoff by adding a [`CowArc::Static`] and implementing [`From<&'static T>`] instead of -/// [`From<'a T>`]. This preserves the static context and prevents conversion to [`CowArc::Owned`] in cases where a reference -/// is known to be static. This is an optimization that prevents allocations and atomic ref-counting. -/// -/// This means that static references should prefer [`From::from`] or [`CowArc::Static`] and non-static references must -/// use [`CowArc::Borrowed`]. -pub enum CowArc<'a, T: ?Sized + 'static> { - /// A borrowed value - Borrowed(&'a T), - /// A static value reference. This exists to avoid conversion to [`CowArc::Owned`] in cases where a reference is - /// known to be static. This is an optimization that prevents allocations and atomic ref-counting. - Static(&'static T), - /// An owned [`Arc`]-ed value - Owned(Arc), -} - -impl CowArc<'static, T> { - /// Indicates this [`CowArc`] should have a static lifetime. - /// This ensures if this was created with a value `Borrowed(&'static T)`, it is replaced with `Static(&'static T)`. - #[inline] - pub fn as_static(self) -> Self { - match self { - Self::Borrowed(value) | Self::Static(value) => Self::Static(value), - Self::Owned(value) => Self::Owned(value), - } - } -} - -impl<'a, T: ?Sized> Deref for CowArc<'a, T> { - type Target = T; - - #[inline] - fn deref(&self) -> &Self::Target { - match self { - CowArc::Borrowed(v) | CowArc::Static(v) => v, - CowArc::Owned(v) => v, - } - } -} - -impl<'a, T: ?Sized> Borrow for CowArc<'a, T> { - #[inline] - fn borrow(&self) -> &T { - self - } -} - -impl<'a, T: ?Sized> AsRef for CowArc<'a, T> { - #[inline] - fn as_ref(&self) -> &T { - self - } -} - -impl<'a, T: ?Sized> CowArc<'a, T> -where - &'a T: Into>, -{ - /// Converts this into an "owned" value. If internally a value is borrowed, it will be cloned into an "owned [`Arc`]". - /// If it is already a [`CowArc::Owned`] or a [`CowArc::Static`], it will remain unchanged. - #[inline] - pub fn into_owned(self) -> CowArc<'static, T> { - match self { - CowArc::Borrowed(value) => CowArc::Owned(value.into()), - CowArc::Static(value) => CowArc::Static(value), - CowArc::Owned(value) => CowArc::Owned(value), - } - } - - /// Clones into an owned [`CowArc<'static>`]. If internally a value is borrowed, it will be cloned into an "owned [`Arc`]". - /// If it is already a [`CowArc::Owned`] or [`CowArc::Static`], the value will be cloned. - /// This is equivalent to `.clone().into_owned()`. - #[inline] - pub fn clone_owned(&self) -> CowArc<'static, T> { - self.clone().into_owned() - } -} - -impl<'a, T: ?Sized> Clone for CowArc<'a, T> { - #[inline] - fn clone(&self) -> Self { - match self { - Self::Borrowed(value) => Self::Borrowed(value), - Self::Static(value) => Self::Static(value), - Self::Owned(value) => Self::Owned(value.clone()), - } - } -} - -impl<'a, T: PartialEq + ?Sized> PartialEq for CowArc<'a, T> { - #[inline] - fn eq(&self, other: &Self) -> bool { - self.deref().eq(other.deref()) - } -} - -impl<'a, T: PartialEq + ?Sized> Eq for CowArc<'a, T> {} - -impl<'a, T: Hash + ?Sized> Hash for CowArc<'a, T> { - #[inline] - fn hash(&self, state: &mut H) { - self.deref().hash(state); - } -} - -impl<'a, T: Debug + ?Sized> Debug for CowArc<'a, T> { - #[inline] - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - Debug::fmt(self.deref(), f) - } -} - -impl<'a, T: Display + ?Sized> Display for CowArc<'a, T> { - #[inline] - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - Display::fmt(self.deref(), f) - } -} - -impl<'a, T: PartialOrd + ?Sized> PartialOrd for CowArc<'a, T> { - #[inline] - fn partial_cmp(&self, other: &Self) -> Option { - self.deref().partial_cmp(other.deref()) - } -} - -impl Default for CowArc<'static, str> { - fn default() -> Self { - CowArc::Static(Default::default()) - } -} - -impl Default for CowArc<'static, Path> { - fn default() -> Self { - CowArc::Static(Path::new("")) - } -} - -impl<'a, T: Ord + ?Sized> Ord for CowArc<'a, T> { - #[inline] - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.deref().cmp(other.deref()) - } -} - -impl From for CowArc<'static, Path> { - #[inline] - fn from(value: PathBuf) -> Self { - CowArc::Owned(value.into()) - } -} - -impl From<&'static str> for CowArc<'static, Path> { - #[inline] - fn from(value: &'static str) -> Self { - CowArc::Static(Path::new(value)) - } -} - -impl From for CowArc<'static, str> { - #[inline] - fn from(value: String) -> Self { - CowArc::Owned(value.into()) - } -} - -impl<'a> From<&'a String> for CowArc<'a, str> { - #[inline] - fn from(value: &'a String) -> Self { - CowArc::Borrowed(value) - } -} - -impl From<&'static T> for CowArc<'static, T> { - #[inline] - fn from(value: &'static T) -> Self { - CowArc::Static(value) - } -} diff --git a/crates/bevy_utils/src/lib.rs b/crates/bevy_utils/src/lib.rs index a37a6e18ba3fce..772d3fda31ff3b 100644 --- a/crates/bevy_utils/src/lib.rs +++ b/crates/bevy_utils/src/lib.rs @@ -21,7 +21,6 @@ pub use short_names::get_short_name; pub mod synccell; pub mod syncunsafecell; -mod cow_arc; mod default; mod object_safe; pub use object_safe::assert_object_safe; @@ -30,7 +29,6 @@ mod parallel_queue; pub use ahash::{AHasher, RandomState}; pub use bevy_utils_proc_macros::*; -pub use cow_arc::*; pub use default::default; pub use hashbrown; pub use parallel_queue::*; From f2cf02408ff8766ef53ad7fa2e42fd30d2f0f8e5 Mon Sep 17 00:00:00 2001 From: Alix Bott Date: Fri, 30 Aug 2024 02:43:56 +0200 Subject: [PATCH 49/53] Fix observer unregistering unsetting archetype flags (#14963) # Objective - Fixes https://github.com/bevyengine/bevy/issues/14961 ## Solution - Check that the archetypes don't contain any other observed components before unsetting their flags ## Testing - I added a regression test: `observer_despawn_archetype_flags` --- crates/bevy_ecs/src/archetype.rs | 2 +- crates/bevy_ecs/src/observer/mod.rs | 32 ++++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ecs/src/archetype.rs b/crates/bevy_ecs/src/archetype.rs index b7cad4e3895125..15e962291cbd3f 100644 --- a/crates/bevy_ecs/src/archetype.rs +++ b/crates/bevy_ecs/src/archetype.rs @@ -367,7 +367,7 @@ pub struct Archetype { edges: Edges, entities: Vec, components: ImmutableSparseSet, - flags: ArchetypeFlags, + pub(crate) flags: ArchetypeFlags, } impl Archetype { diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index 3ef2d6b28d63dc..c6ace80ca5202c 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -429,7 +429,17 @@ impl World { if observers.map.is_empty() && observers.entity_map.is_empty() { cache.component_observers.remove(component); if let Some(flag) = Observers::is_archetype_cached(event_type) { - archetypes.update_flags(*component, flag, false); + for archetype in &mut archetypes.archetypes { + if archetype.contains(*component) { + let no_longer_observed = archetype + .components() + .all(|id| !cache.component_observers.contains_key(&id)); + + if no_longer_observed { + archetype.flags.set(flag, false); + } + } + } } } } @@ -656,6 +666,26 @@ mod tests { world.spawn(A).flush(); } + // Regression test for https://github.com/bevyengine/bevy/issues/14961 + #[test] + fn observer_despawn_archetype_flags() { + let mut world = World::new(); + world.init_resource::(); + + let entity = world.spawn((A, B)).flush(); + + world.observe(|_: Trigger, mut res: ResMut| res.0 += 1); + + let observer = world + .observe(|_: Trigger| panic!("Observer triggered after being despawned.")) + .flush(); + world.despawn(observer); + + world.despawn(entity); + + assert_eq!(1, world.resource::().0); + } + #[test] fn observer_multiple_matches() { let mut world = World::new(); From 147768adf612218051f633ab1498ba8af5ace92f Mon Sep 17 00:00:00 2001 From: akimakinai <105044389+akimakinai@users.noreply.github.com> Date: Fri, 30 Aug 2024 10:22:11 +0900 Subject: [PATCH 50/53] Use CowArc::Static (#14981) # Objective - There's one occurence of `CowArc::Borrow` that wraps '&'static str` ## Solution - Replaces it with `CowArc::Static`. I don't think this change is important but I can't unsee it:) ## Testing - `cargo check` compiles fine --- crates/bevy_asset/src/server/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_asset/src/server/mod.rs b/crates/bevy_asset/src/server/mod.rs index 61c802e5057663..a9e5fcddbf31c4 100644 --- a/crates/bevy_asset/src/server/mod.rs +++ b/crates/bevy_asset/src/server/mod.rs @@ -415,7 +415,7 @@ impl AssetServer { ) -> Handle { let path = path.into().into_owned(); let untyped_source = AssetSourceId::Name(match path.source() { - AssetSourceId::Default => CowArc::Borrowed(UNTYPED_SOURCE_SUFFIX), + AssetSourceId::Default => CowArc::Static(UNTYPED_SOURCE_SUFFIX), AssetSourceId::Name(source) => { CowArc::Owned(format!("{source}--{UNTYPED_SOURCE_SUFFIX}").into()) } From c816cf9072e39413c41faeb943d114aa5f74d6eb Mon Sep 17 00:00:00 2001 From: MichiRecRoom <1008889+LikeLakers2@users.noreply.github.com> Date: Thu, 29 Aug 2024 21:24:31 -0400 Subject: [PATCH 51/53] Reorganize some of `bevy_animation`'s imports into a more consistent style (#14983) # Objective `bevy_animation` imports a lot of items - and it uses a very inconsistent code style to do so. ## Solution Changes the offending `use` statements to be more consistent across the crate. ## Testing - Did you test these changes? If so, how? - No testing is needed beyond lint checks, and those finished successfully. - ~~Are there any parts that need more testing?~~ - ~~How can other people (reviewers) test your changes? Is there anything specific they need to know?~~ - ~~If relevant, what platforms did you test these changes on, and are there any important ones you can't test?~~ --- crates/bevy_animation/src/graph.rs | 3 +-- crates/bevy_animation/src/lib.rs | 16 +++++++--------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/crates/bevy_animation/src/graph.rs b/crates/bevy_animation/src/graph.rs index aeeed9fcdf6314..7154a77e974c04 100644 --- a/crates/bevy_animation/src/graph.rs +++ b/crates/bevy_animation/src/graph.rs @@ -3,8 +3,7 @@ use std::io::{self, Write}; use std::ops::{Index, IndexMut}; -use bevy_asset::io::Reader; -use bevy_asset::{Asset, AssetId, AssetLoader, AssetPath, Handle, LoadContext}; +use bevy_asset::{io::Reader, Asset, AssetId, AssetLoader, AssetPath, Handle, LoadContext}; use bevy_reflect::{Reflect, ReflectSerialize}; use petgraph::graph::{DiGraph, NodeIndex}; use ron::de::SpannedError; diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index bf2904440ce5be..4277f0b1141b03 100755 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -21,24 +21,19 @@ use std::ops::{Add, Mul}; use bevy_app::{App, Plugin, PostUpdate}; use bevy_asset::{Asset, AssetApp, Assets, Handle}; use bevy_core::Name; -use bevy_ecs::entity::MapEntities; -use bevy_ecs::prelude::*; -use bevy_ecs::reflect::ReflectMapEntities; +use bevy_ecs::{entity::MapEntities, prelude::*, reflect::ReflectMapEntities}; use bevy_math::{FloatExt, Quat, Vec3}; use bevy_reflect::Reflect; use bevy_render::mesh::morph::MorphWeights; use bevy_time::Time; use bevy_transform::{prelude::Transform, TransformSystem}; -use bevy_utils::hashbrown::HashMap; use bevy_utils::{ + hashbrown::HashMap, tracing::{error, trace}, NoOpHash, }; use fixedbitset::FixedBitSet; -use graph::{AnimationGraph, AnimationNodeIndex}; -use petgraph::graph::NodeIndex; -use petgraph::Direction; -use prelude::{AnimationGraphAssetLoader, AnimationTransitions}; +use petgraph::{graph::NodeIndex, Direction}; use thread_local::ThreadLocal; use uuid::Uuid; @@ -51,7 +46,10 @@ pub mod prelude { }; } -use crate::transition::{advance_transitions, expire_completed_transitions}; +use crate::{ + graph::{AnimationGraph, AnimationGraphAssetLoader, AnimationNodeIndex}, + transition::{advance_transitions, expire_completed_transitions, AnimationTransitions}, +}; /// The [UUID namespace] of animation targets (e.g. bones). /// From bc131614169a1a8d55c401aa3a5a3a737a83f7f1 Mon Sep 17 00:00:00 2001 From: Zachary Harrold Date: Fri, 30 Aug 2024 12:37:47 +1000 Subject: [PATCH 52/53] Migrated `NonZero*` to `NonZero<*>` (#14978) # Objective - Fixes #14974 ## Solution - Replace all* instances of `NonZero*` with `NonZero<*>` ## Testing - CI passed locally. --- ## Notes Within the `bevy_reflect` implementations for `std` types, `impl_reflect_value!()` will continue to use the type aliases instead, as it inappropriately parses the concrete type parameter as a generic argument. If the `ZeroablePrimitive` trait was stable, or the macro could be modified to accept a finite list of types, then we could fully migrate. --- crates/bevy_app/src/app.rs | 8 +- .../src/auto_exposure/pipeline.rs | 6 +- crates/bevy_ecs/src/entity/mod.rs | 73 ++++++++++--------- crates/bevy_ecs/src/identifier/masks.rs | 66 ++++++++--------- crates/bevy_ecs/src/identifier/mod.rs | 10 +-- crates/bevy_ecs/src/lib.rs | 7 +- crates/bevy_ecs/src/storage/blob_vec.rs | 11 +-- crates/bevy_pbr/src/cluster/mod.rs | 8 +- .../src/light_probe/environment_map.rs | 4 +- .../src/light_probe/irradiance_volume.rs | 4 +- crates/bevy_pbr/src/material.rs | 8 +- .../bevy_pbr/src/meshlet/persistent_buffer.rs | 5 +- crates/bevy_pbr/src/render/gpu_preprocess.rs | 4 +- .../bevy_pbr/src/render/mesh_view_bindings.rs | 4 +- crates/bevy_ptr/src/lib.rs | 6 +- crates/bevy_reflect/src/impls/std.rs | 8 +- .../render_resource/batched_uniform_buffer.rs | 6 +- .../bind_group_layout_entries.rs | 14 ++-- .../src/render_resource/resource_macros.rs | 10 +-- .../src/render_resource/uniform_buffer.rs | 4 +- crates/bevy_render/src/view/window/mod.rs | 6 +- crates/bevy_tasks/src/lib.rs | 4 +- crates/bevy_ui/src/ui_node.rs | 28 +++---- crates/bevy_window/src/window.rs | 4 +- examples/app/headless_renderer.rs | 2 +- examples/shader/texture_binding_array.rs | 6 +- 26 files changed, 161 insertions(+), 155 deletions(-) diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index 07a29800f93df2..da41efbc152c41 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -18,7 +18,7 @@ use std::{ process::{ExitCode, Termination}, }; use std::{ - num::NonZeroU8, + num::NonZero, panic::{catch_unwind, resume_unwind, AssertUnwindSafe}, }; use thiserror::Error; @@ -1061,14 +1061,14 @@ pub enum AppExit { Success, /// The [`App`] experienced an unhandleable error. /// Holds the exit code we expect our app to return. - Error(NonZeroU8), + Error(NonZero), } impl AppExit { /// Creates a [`AppExit::Error`] with a error code of 1. #[must_use] pub const fn error() -> Self { - Self::Error(NonZeroU8::MIN) + Self::Error(NonZero::::MIN) } /// Returns `true` if `self` is a [`AppExit::Success`]. @@ -1089,7 +1089,7 @@ impl AppExit { /// [`AppExit::Error`] is constructed. #[must_use] pub const fn from_code(code: u8) -> Self { - match NonZeroU8::new(code) { + match NonZero::::new(code) { Some(code) => Self::Error(code), None => Self::Success, } diff --git a/crates/bevy_core_pipeline/src/auto_exposure/pipeline.rs b/crates/bevy_core_pipeline/src/auto_exposure/pipeline.rs index eacff931c72119..937e18f410485c 100644 --- a/crates/bevy_core_pipeline/src/auto_exposure/pipeline.rs +++ b/crates/bevy_core_pipeline/src/auto_exposure/pipeline.rs @@ -10,7 +10,7 @@ use bevy_render::{ texture::Image, view::ViewUniform, }; -use std::num::NonZeroU64; +use std::num::NonZero; #[derive(Resource)] pub struct AutoExposurePipeline { @@ -64,8 +64,8 @@ impl FromWorld for AutoExposurePipeline { texture_2d(TextureSampleType::Float { filterable: false }), texture_1d(TextureSampleType::Float { filterable: false }), uniform_buffer::(false), - storage_buffer_sized(false, NonZeroU64::new(HISTOGRAM_BIN_COUNT * 4)), - storage_buffer_sized(false, NonZeroU64::new(4)), + storage_buffer_sized(false, NonZero::::new(HISTOGRAM_BIN_COUNT * 4)), + storage_buffer_sized(false, NonZero::::new(4)), storage_buffer::(true), ), ), diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs index deee320fcd0b22..93b1e78ffabe37 100644 --- a/crates/bevy_ecs/src/entity/mod.rs +++ b/crates/bevy_ecs/src/entity/mod.rs @@ -59,7 +59,7 @@ use crate::{ }; #[cfg(feature = "serialize")] use serde::{Deserialize, Serialize}; -use std::{fmt, hash::Hash, mem, num::NonZeroU32, sync::atomic::Ordering}; +use std::{fmt, hash::Hash, mem, num::NonZero, sync::atomic::Ordering}; #[cfg(target_has_atomic = "64")] use std::sync::atomic::AtomicI64 as AtomicIdCursor; @@ -157,7 +157,7 @@ pub struct Entity { // to make this struct equivalent to a u64. #[cfg(target_endian = "little")] index: u32, - generation: NonZeroU32, + generation: NonZero, #[cfg(target_endian = "big")] index: u32, } @@ -223,7 +223,7 @@ impl Entity { /// Construct an [`Entity`] from a raw `index` value and a non-zero `generation` value. /// Ensure that the generation value is never greater than `0x7FFF_FFFF`. #[inline(always)] - pub(crate) const fn from_raw_and_generation(index: u32, generation: NonZeroU32) -> Entity { + pub(crate) const fn from_raw_and_generation(index: u32, generation: NonZero) -> Entity { debug_assert!(generation.get() <= HIGH_MASK); Self { index, generation } @@ -279,7 +279,7 @@ impl Entity { /// a component. #[inline(always)] pub const fn from_raw(index: u32) -> Entity { - Self::from_raw_and_generation(index, NonZeroU32::MIN) + Self::from_raw_and_generation(index, NonZero::::MIN) } /// Convert to a form convenient for passing outside of rust. @@ -722,7 +722,7 @@ impl Entities { meta.generation = IdentifierMask::inc_masked_high_by(meta.generation, 1); - if meta.generation == NonZeroU32::MIN { + if meta.generation == NonZero::::MIN { warn!( "Entity({}) generation wrapped on Entities::free, aliasing may occur", entity.index @@ -949,7 +949,7 @@ impl Entities { #[repr(C)] struct EntityMeta { /// The current generation of the [`Entity`]. - pub generation: NonZeroU32, + pub generation: NonZero, /// The current location of the [`Entity`] pub location: EntityLocation, } @@ -957,7 +957,7 @@ struct EntityMeta { impl EntityMeta { /// meta for **pending entity** const EMPTY: EntityMeta = EntityMeta { - generation: NonZeroU32::MIN, + generation: NonZero::::MIN, location: EntityLocation::INVALID, }; } @@ -1014,7 +1014,8 @@ mod tests { #[test] fn entity_bits_roundtrip() { // Generation cannot be greater than 0x7FFF_FFFF else it will be an invalid Entity id - let e = Entity::from_raw_and_generation(0xDEADBEEF, NonZeroU32::new(0x5AADF00D).unwrap()); + let e = + Entity::from_raw_and_generation(0xDEADBEEF, NonZero::::new(0x5AADF00D).unwrap()); assert_eq!(Entity::from_bits(e.to_bits()), e); } @@ -1091,65 +1092,65 @@ mod tests { #[allow(clippy::nonminimal_bool)] // This is intentionally testing `lt` and `ge` as separate functions. fn entity_comparison() { assert_eq!( - Entity::from_raw_and_generation(123, NonZeroU32::new(456).unwrap()), - Entity::from_raw_and_generation(123, NonZeroU32::new(456).unwrap()) + Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap()), + Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap()) ); assert_ne!( - Entity::from_raw_and_generation(123, NonZeroU32::new(789).unwrap()), - Entity::from_raw_and_generation(123, NonZeroU32::new(456).unwrap()) + Entity::from_raw_and_generation(123, NonZero::::new(789).unwrap()), + Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap()) ); assert_ne!( - Entity::from_raw_and_generation(123, NonZeroU32::new(456).unwrap()), - Entity::from_raw_and_generation(123, NonZeroU32::new(789).unwrap()) + Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap()), + Entity::from_raw_and_generation(123, NonZero::::new(789).unwrap()) ); assert_ne!( - Entity::from_raw_and_generation(123, NonZeroU32::new(456).unwrap()), - Entity::from_raw_and_generation(456, NonZeroU32::new(123).unwrap()) + Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap()), + Entity::from_raw_and_generation(456, NonZero::::new(123).unwrap()) ); // ordering is by generation then by index assert!( - Entity::from_raw_and_generation(123, NonZeroU32::new(456).unwrap()) - >= Entity::from_raw_and_generation(123, NonZeroU32::new(456).unwrap()) + Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap()) + >= Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap()) ); assert!( - Entity::from_raw_and_generation(123, NonZeroU32::new(456).unwrap()) - <= Entity::from_raw_and_generation(123, NonZeroU32::new(456).unwrap()) + Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap()) + <= Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap()) ); assert!( - !(Entity::from_raw_and_generation(123, NonZeroU32::new(456).unwrap()) - < Entity::from_raw_and_generation(123, NonZeroU32::new(456).unwrap())) + !(Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap()) + < Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap())) ); assert!( - !(Entity::from_raw_and_generation(123, NonZeroU32::new(456).unwrap()) - > Entity::from_raw_and_generation(123, NonZeroU32::new(456).unwrap())) + !(Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap()) + > Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap())) ); assert!( - Entity::from_raw_and_generation(9, NonZeroU32::new(1).unwrap()) - < Entity::from_raw_and_generation(1, NonZeroU32::new(9).unwrap()) + Entity::from_raw_and_generation(9, NonZero::::new(1).unwrap()) + < Entity::from_raw_and_generation(1, NonZero::::new(9).unwrap()) ); assert!( - Entity::from_raw_and_generation(1, NonZeroU32::new(9).unwrap()) - > Entity::from_raw_and_generation(9, NonZeroU32::new(1).unwrap()) + Entity::from_raw_and_generation(1, NonZero::::new(9).unwrap()) + > Entity::from_raw_and_generation(9, NonZero::::new(1).unwrap()) ); assert!( - Entity::from_raw_and_generation(1, NonZeroU32::new(1).unwrap()) - < Entity::from_raw_and_generation(2, NonZeroU32::new(1).unwrap()) + Entity::from_raw_and_generation(1, NonZero::::new(1).unwrap()) + < Entity::from_raw_and_generation(2, NonZero::::new(1).unwrap()) ); assert!( - Entity::from_raw_and_generation(1, NonZeroU32::new(1).unwrap()) - <= Entity::from_raw_and_generation(2, NonZeroU32::new(1).unwrap()) + Entity::from_raw_and_generation(1, NonZero::::new(1).unwrap()) + <= Entity::from_raw_and_generation(2, NonZero::::new(1).unwrap()) ); assert!( - Entity::from_raw_and_generation(2, NonZeroU32::new(2).unwrap()) - > Entity::from_raw_and_generation(1, NonZeroU32::new(2).unwrap()) + Entity::from_raw_and_generation(2, NonZero::::new(2).unwrap()) + > Entity::from_raw_and_generation(1, NonZero::::new(2).unwrap()) ); assert!( - Entity::from_raw_and_generation(2, NonZeroU32::new(2).unwrap()) - >= Entity::from_raw_and_generation(1, NonZeroU32::new(2).unwrap()) + Entity::from_raw_and_generation(2, NonZero::::new(2).unwrap()) + >= Entity::from_raw_and_generation(1, NonZero::::new(2).unwrap()) ); } diff --git a/crates/bevy_ecs/src/identifier/masks.rs b/crates/bevy_ecs/src/identifier/masks.rs index d5adc856ddb881..85fb393cb6f13d 100644 --- a/crates/bevy_ecs/src/identifier/masks.rs +++ b/crates/bevy_ecs/src/identifier/masks.rs @@ -1,4 +1,4 @@ -use std::num::NonZeroU32; +use std::num::NonZero; use super::kinds::IdKind; @@ -61,7 +61,7 @@ impl IdentifierMask { /// Will never be greater than [`HIGH_MASK`] or less than `1`, and increments are masked to /// never be greater than [`HIGH_MASK`]. #[inline(always)] - pub(crate) const fn inc_masked_high_by(lhs: NonZeroU32, rhs: u32) -> NonZeroU32 { + pub(crate) const fn inc_masked_high_by(lhs: NonZero, rhs: u32) -> NonZero { let lo = (lhs.get() & HIGH_MASK).wrapping_add(rhs & HIGH_MASK); // Checks high 32 bit for whether we have overflowed 31 bits. let overflowed = lo >> 31; @@ -70,7 +70,7 @@ impl IdentifierMask { // - Adding the overflow flag will offset overflows to start at 1 instead of 0 // - The sum of `0x7FFF_FFFF` + `u32::MAX` + 1 (overflow) == `0x7FFF_FFFF` // - If the operation doesn't overflow at 31 bits, no offsetting takes place - unsafe { NonZeroU32::new_unchecked(lo.wrapping_add(overflowed) & HIGH_MASK) } + unsafe { NonZero::::new_unchecked(lo.wrapping_add(overflowed) & HIGH_MASK) } } } @@ -166,68 +166,68 @@ mod tests { // Adding from lowest value with lowest to highest increment // No result should ever be greater than 0x7FFF_FFFF or HIGH_MASK assert_eq!( - NonZeroU32::MIN, - IdentifierMask::inc_masked_high_by(NonZeroU32::MIN, 0) + NonZero::::MIN, + IdentifierMask::inc_masked_high_by(NonZero::::MIN, 0) ); assert_eq!( - NonZeroU32::new(2).unwrap(), - IdentifierMask::inc_masked_high_by(NonZeroU32::MIN, 1) + NonZero::::new(2).unwrap(), + IdentifierMask::inc_masked_high_by(NonZero::::MIN, 1) ); assert_eq!( - NonZeroU32::new(3).unwrap(), - IdentifierMask::inc_masked_high_by(NonZeroU32::MIN, 2) + NonZero::::new(3).unwrap(), + IdentifierMask::inc_masked_high_by(NonZero::::MIN, 2) ); assert_eq!( - NonZeroU32::MIN, - IdentifierMask::inc_masked_high_by(NonZeroU32::MIN, HIGH_MASK) + NonZero::::MIN, + IdentifierMask::inc_masked_high_by(NonZero::::MIN, HIGH_MASK) ); assert_eq!( - NonZeroU32::MIN, - IdentifierMask::inc_masked_high_by(NonZeroU32::MIN, u32::MAX) + NonZero::::MIN, + IdentifierMask::inc_masked_high_by(NonZero::::MIN, u32::MAX) ); // Adding from absolute highest value with lowest to highest increment // No result should ever be greater than 0x7FFF_FFFF or HIGH_MASK assert_eq!( - NonZeroU32::new(HIGH_MASK).unwrap(), - IdentifierMask::inc_masked_high_by(NonZeroU32::MAX, 0) + NonZero::::new(HIGH_MASK).unwrap(), + IdentifierMask::inc_masked_high_by(NonZero::::MAX, 0) ); assert_eq!( - NonZeroU32::MIN, - IdentifierMask::inc_masked_high_by(NonZeroU32::MAX, 1) + NonZero::::MIN, + IdentifierMask::inc_masked_high_by(NonZero::::MAX, 1) ); assert_eq!( - NonZeroU32::new(2).unwrap(), - IdentifierMask::inc_masked_high_by(NonZeroU32::MAX, 2) + NonZero::::new(2).unwrap(), + IdentifierMask::inc_masked_high_by(NonZero::::MAX, 2) ); assert_eq!( - NonZeroU32::new(HIGH_MASK).unwrap(), - IdentifierMask::inc_masked_high_by(NonZeroU32::MAX, HIGH_MASK) + NonZero::::new(HIGH_MASK).unwrap(), + IdentifierMask::inc_masked_high_by(NonZero::::MAX, HIGH_MASK) ); assert_eq!( - NonZeroU32::new(HIGH_MASK).unwrap(), - IdentifierMask::inc_masked_high_by(NonZeroU32::MAX, u32::MAX) + NonZero::::new(HIGH_MASK).unwrap(), + IdentifierMask::inc_masked_high_by(NonZero::::MAX, u32::MAX) ); // Adding from actual highest value with lowest to highest increment // No result should ever be greater than 0x7FFF_FFFF or HIGH_MASK assert_eq!( - NonZeroU32::new(HIGH_MASK).unwrap(), - IdentifierMask::inc_masked_high_by(NonZeroU32::new(HIGH_MASK).unwrap(), 0) + NonZero::::new(HIGH_MASK).unwrap(), + IdentifierMask::inc_masked_high_by(NonZero::::new(HIGH_MASK).unwrap(), 0) ); assert_eq!( - NonZeroU32::MIN, - IdentifierMask::inc_masked_high_by(NonZeroU32::new(HIGH_MASK).unwrap(), 1) + NonZero::::MIN, + IdentifierMask::inc_masked_high_by(NonZero::::new(HIGH_MASK).unwrap(), 1) ); assert_eq!( - NonZeroU32::new(2).unwrap(), - IdentifierMask::inc_masked_high_by(NonZeroU32::new(HIGH_MASK).unwrap(), 2) + NonZero::::new(2).unwrap(), + IdentifierMask::inc_masked_high_by(NonZero::::new(HIGH_MASK).unwrap(), 2) ); assert_eq!( - NonZeroU32::new(HIGH_MASK).unwrap(), - IdentifierMask::inc_masked_high_by(NonZeroU32::new(HIGH_MASK).unwrap(), HIGH_MASK) + NonZero::::new(HIGH_MASK).unwrap(), + IdentifierMask::inc_masked_high_by(NonZero::::new(HIGH_MASK).unwrap(), HIGH_MASK) ); assert_eq!( - NonZeroU32::new(HIGH_MASK).unwrap(), - IdentifierMask::inc_masked_high_by(NonZeroU32::new(HIGH_MASK).unwrap(), u32::MAX) + NonZero::::new(HIGH_MASK).unwrap(), + IdentifierMask::inc_masked_high_by(NonZero::::new(HIGH_MASK).unwrap(), u32::MAX) ); } } diff --git a/crates/bevy_ecs/src/identifier/mod.rs b/crates/bevy_ecs/src/identifier/mod.rs index 04a93cde45c0c5..e9c7df8006e387 100644 --- a/crates/bevy_ecs/src/identifier/mod.rs +++ b/crates/bevy_ecs/src/identifier/mod.rs @@ -7,7 +7,7 @@ use bevy_reflect::Reflect; use self::{error::IdentifierError, kinds::IdKind, masks::IdentifierMask}; -use std::{hash::Hash, num::NonZeroU32}; +use std::{hash::Hash, num::NonZero}; pub mod error; pub(crate) mod kinds; @@ -28,7 +28,7 @@ pub struct Identifier { // to make this struct equivalent to a u64. #[cfg(target_endian = "little")] low: u32, - high: NonZeroU32, + high: NonZero, #[cfg(target_endian = "big")] low: u32, } @@ -56,7 +56,7 @@ impl Identifier { unsafe { Ok(Self { low, - high: NonZeroU32::new_unchecked(packed_high), + high: NonZero::::new_unchecked(packed_high), }) } } @@ -71,7 +71,7 @@ impl Identifier { /// Returns the value of the high segment of the [`Identifier`]. This /// does not apply any masking. #[inline(always)] - pub const fn high(self) -> NonZeroU32 { + pub const fn high(self) -> NonZero { self.high } @@ -114,7 +114,7 @@ impl Identifier { /// This method is the fallible counterpart to [`Identifier::from_bits`]. #[inline(always)] pub const fn try_from_bits(value: u64) -> Result { - let high = NonZeroU32::new(IdentifierMask::get_high(value)); + let high = NonZero::::new(IdentifierMask::get_high(value)); match high { Some(high) => Ok(Self { diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 09e86f7711ccae..442c25d9a2662f 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -88,7 +88,7 @@ mod tests { }; use bevy_tasks::{ComputeTaskPool, TaskPool}; use bevy_utils::HashSet; - use std::num::NonZeroU32; + use std::num::NonZero; use std::{ any::TypeId, marker::PhantomData, @@ -1659,7 +1659,7 @@ mod tests { ); let e4_mismatched_generation = - Entity::from_raw_and_generation(3, NonZeroU32::new(2).unwrap()); + Entity::from_raw_and_generation(3, NonZero::::new(2).unwrap()); assert!( world_b.get_or_spawn(e4_mismatched_generation).is_none(), "attempting to spawn on top of an entity with a mismatched entity generation fails" @@ -1754,7 +1754,8 @@ mod tests { let e0 = world.spawn(A(0)).id(); let e1 = Entity::from_raw(1); let e2 = world.spawn_empty().id(); - let invalid_e2 = Entity::from_raw_and_generation(e2.index(), NonZeroU32::new(2).unwrap()); + let invalid_e2 = + Entity::from_raw_and_generation(e2.index(), NonZero::::new(2).unwrap()); let values = vec![(e0, (B(0), C)), (e1, (B(1), C)), (invalid_e2, (B(2), C))]; diff --git a/crates/bevy_ecs/src/storage/blob_vec.rs b/crates/bevy_ecs/src/storage/blob_vec.rs index dca9c1542a5519..d5699f37164cbb 100644 --- a/crates/bevy_ecs/src/storage/blob_vec.rs +++ b/crates/bevy_ecs/src/storage/blob_vec.rs @@ -1,7 +1,7 @@ use std::{ alloc::{handle_alloc_error, Layout}, cell::UnsafeCell, - num::NonZeroUsize, + num::NonZero, ptr::NonNull, }; @@ -56,7 +56,7 @@ impl BlobVec { drop: Option)>, capacity: usize, ) -> BlobVec { - let align = NonZeroUsize::new(item_layout.align()).expect("alignment must be > 0"); + let align = NonZero::::new(item_layout.align()).expect("alignment must be > 0"); let data = bevy_ptr::dangling_with_align(align); if item_layout.size() == 0 { BlobVec { @@ -119,7 +119,8 @@ impl BlobVec { let available_space = self.capacity - self.len; if available_space < additional { // SAFETY: `available_space < additional`, so `additional - available_space > 0` - let increment = unsafe { NonZeroUsize::new_unchecked(additional - available_space) }; + let increment = + unsafe { NonZero::::new_unchecked(additional - available_space) }; self.grow_exact(increment); } } @@ -132,7 +133,7 @@ impl BlobVec { #[cold] fn do_reserve(slf: &mut BlobVec, additional: usize) { let increment = slf.capacity.max(additional - (slf.capacity - slf.len)); - let increment = NonZeroUsize::new(increment).unwrap(); + let increment = NonZero::::new(increment).unwrap(); slf.grow_exact(increment); } @@ -148,7 +149,7 @@ impl BlobVec { /// Panics if the new capacity overflows `usize`. /// For ZST it panics unconditionally because ZST `BlobVec` capacity /// is initialized to `usize::MAX` and always stays that way. - fn grow_exact(&mut self, increment: NonZeroUsize) { + fn grow_exact(&mut self, increment: NonZero) { let new_capacity = self .capacity .checked_add(increment.get()) diff --git a/crates/bevy_pbr/src/cluster/mod.rs b/crates/bevy_pbr/src/cluster/mod.rs index fe913a1d196bb7..7da6c5da026cf2 100644 --- a/crates/bevy_pbr/src/cluster/mod.rs +++ b/crates/bevy_pbr/src/cluster/mod.rs @@ -1,6 +1,6 @@ //! Spatial clustering of objects, currently just point and spot lights. -use std::num::NonZeroU64; +use std::num::NonZero; use bevy_core_pipeline::core_3d::Camera3d; use bevy_ecs::{ @@ -468,7 +468,7 @@ impl GpuClusterableObjects { } } - pub fn min_size(buffer_binding_type: BufferBindingType) -> NonZeroU64 { + pub fn min_size(buffer_binding_type: BufferBindingType) -> NonZero { match buffer_binding_type { BufferBindingType::Storage { .. } => GpuClusterableObjectsStorage::min_size(), BufferBindingType::Uniform => GpuClusterableObjectsUniform::min_size(), @@ -749,7 +749,7 @@ impl ViewClusterBindings { pub fn min_size_clusterable_object_index_lists( buffer_binding_type: BufferBindingType, - ) -> NonZeroU64 { + ) -> NonZero { match buffer_binding_type { BufferBindingType::Storage { .. } => GpuClusterableObjectIndexListsStorage::min_size(), BufferBindingType::Uniform => GpuClusterableObjectIndexListsUniform::min_size(), @@ -758,7 +758,7 @@ impl ViewClusterBindings { pub fn min_size_cluster_offsets_and_counts( buffer_binding_type: BufferBindingType, - ) -> NonZeroU64 { + ) -> NonZero { match buffer_binding_type { BufferBindingType::Storage { .. } => GpuClusterOffsetsAndCountsStorage::min_size(), BufferBindingType::Uniform => GpuClusterOffsetsAndCountsUniform::min_size(), diff --git a/crates/bevy_pbr/src/light_probe/environment_map.rs b/crates/bevy_pbr/src/light_probe/environment_map.rs index 8a78e93083024a..1b1604df4d48d4 100644 --- a/crates/bevy_pbr/src/light_probe/environment_map.rs +++ b/crates/bevy_pbr/src/light_probe/environment_map.rs @@ -65,7 +65,7 @@ use bevy_render::{ texture::{FallbackImage, GpuImage, Image}, }; -use std::num::NonZeroU32; +use std::num::NonZero; use std::ops::Deref; use crate::{ @@ -217,7 +217,7 @@ pub(crate) fn get_bind_group_layout_entries( binding_types::texture_cube(TextureSampleType::Float { filterable: true }); if binding_arrays_are_usable(render_device) { texture_cube_binding = - texture_cube_binding.count(NonZeroU32::new(MAX_VIEW_LIGHT_PROBES as _).unwrap()); + texture_cube_binding.count(NonZero::::new(MAX_VIEW_LIGHT_PROBES as _).unwrap()); } [ diff --git a/crates/bevy_pbr/src/light_probe/irradiance_volume.rs b/crates/bevy_pbr/src/light_probe/irradiance_volume.rs index 618da04fa44902..58110e98afb29f 100644 --- a/crates/bevy_pbr/src/light_probe/irradiance_volume.rs +++ b/crates/bevy_pbr/src/light_probe/irradiance_volume.rs @@ -142,7 +142,7 @@ use bevy_render::{ renderer::RenderDevice, texture::{FallbackImage, GpuImage, Image}, }; -use std::{num::NonZeroU32, ops::Deref}; +use std::{num::NonZero, ops::Deref}; use bevy_asset::{AssetId, Handle}; use bevy_reflect::Reflect; @@ -306,7 +306,7 @@ pub(crate) fn get_bind_group_layout_entries( binding_types::texture_3d(TextureSampleType::Float { filterable: true }); if binding_arrays_are_usable(render_device) { texture_3d_binding = - texture_3d_binding.count(NonZeroU32::new(MAX_VIEW_LIGHT_PROBES as _).unwrap()); + texture_3d_binding.count(NonZero::::new(MAX_VIEW_LIGHT_PROBES as _).unwrap()); } [ diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index cee8129febe752..9da61b2ce9e640 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -35,7 +35,7 @@ use bevy_render::{ use bevy_utils::tracing::error; use std::marker::PhantomData; use std::sync::atomic::{AtomicU32, Ordering}; -use std::{hash::Hash, num::NonZeroU32}; +use std::{hash::Hash, num::NonZero}; use self::{irradiance_volume::IrradianceVolume, prelude::EnvironmentMapLight}; @@ -978,7 +978,7 @@ impl AtomicMaterialBindGroupId { /// See also: [`AtomicU32::store`]. pub fn set(&self, id: MaterialBindGroupId) { let id = if let Some(id) = id.0 { - NonZeroU32::from(id).get() + NonZero::::from(id).get() } else { 0 }; @@ -990,7 +990,9 @@ impl AtomicMaterialBindGroupId { /// /// See also: [`AtomicU32::load`]. pub fn get(&self) -> MaterialBindGroupId { - MaterialBindGroupId(NonZeroU32::new(self.0.load(Ordering::Relaxed)).map(BindGroupId::from)) + MaterialBindGroupId( + NonZero::::new(self.0.load(Ordering::Relaxed)).map(BindGroupId::from), + ) } } diff --git a/crates/bevy_pbr/src/meshlet/persistent_buffer.rs b/crates/bevy_pbr/src/meshlet/persistent_buffer.rs index 60e163a87446a7..e10dad6ef0aed7 100644 --- a/crates/bevy_pbr/src/meshlet/persistent_buffer.rs +++ b/crates/bevy_pbr/src/meshlet/persistent_buffer.rs @@ -6,7 +6,7 @@ use bevy_render::{ renderer::{RenderDevice, RenderQueue}, }; use range_alloc::RangeAllocator; -use std::{num::NonZeroU64, ops::Range}; +use std::{num::NonZero, ops::Range}; /// Wrapper for a GPU buffer holding a large amount of data that persists across frames. pub struct PersistentGpuBuffer { @@ -66,7 +66,8 @@ impl PersistentGpuBuffer { let queue_count = self.write_queue.len(); for (data, metadata, buffer_slice) in self.write_queue.drain(..) { - let buffer_slice_size = NonZeroU64::new(buffer_slice.end - buffer_slice.start).unwrap(); + let buffer_slice_size = + NonZero::::new(buffer_slice.end - buffer_slice.start).unwrap(); let mut buffer_view = render_queue .write_buffer_with(&self.buffer, buffer_slice.start, buffer_slice_size) .unwrap(); diff --git a/crates/bevy_pbr/src/render/gpu_preprocess.rs b/crates/bevy_pbr/src/render/gpu_preprocess.rs index 67eca5df5e13d3..38650d280891bb 100644 --- a/crates/bevy_pbr/src/render/gpu_preprocess.rs +++ b/crates/bevy_pbr/src/render/gpu_preprocess.rs @@ -6,7 +6,7 @@ //! [`MeshInputUniform`]s instead and use the GPU to calculate the remaining //! derived fields in [`MeshUniform`]. -use std::num::NonZeroU64; +use std::num::NonZero; use bevy_app::{App, Plugin}; use bevy_asset::{load_internal_asset, Handle}; @@ -408,7 +408,7 @@ pub fn prepare_preprocess_bind_groups( // Don't use `as_entire_binding()` here; the shader reads the array // length and the underlying buffer may be longer than the actual size // of the vector. - let index_buffer_size = NonZeroU64::try_from( + let index_buffer_size = NonZero::::try_from( index_buffer_vec.buffer.len() as u64 * u64::from(PreprocessWorkItem::min_size()), ) .ok(); diff --git a/crates/bevy_pbr/src/render/mesh_view_bindings.rs b/crates/bevy_pbr/src/render/mesh_view_bindings.rs index 36c5d7bfe044f7..d0a506e9c3e426 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.rs +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.rs @@ -1,4 +1,4 @@ -use std::{array, num::NonZeroU64, sync::Arc}; +use std::{array, num::NonZero, sync::Arc}; use bevy_core_pipeline::{ core_3d::ViewTransmissionTexture, @@ -164,7 +164,7 @@ impl From> for MeshPipelineViewLayoutKey { fn buffer_layout( buffer_binding_type: BufferBindingType, has_dynamic_offset: bool, - min_binding_size: Option, + min_binding_size: Option>, ) -> BindGroupLayoutEntryBuilder { match buffer_binding_type { BufferBindingType::Uniform => uniform_buffer_sized(has_dynamic_offset, min_binding_size), diff --git a/crates/bevy_ptr/src/lib.rs b/crates/bevy_ptr/src/lib.rs index 24ea6022962565..5b2186c44fbc55 100644 --- a/crates/bevy_ptr/src/lib.rs +++ b/crates/bevy_ptr/src/lib.rs @@ -12,7 +12,7 @@ use core::{ fmt::{self, Formatter, Pointer}, marker::PhantomData, mem::{align_of, ManuallyDrop}, - num::NonZeroUsize, + num::NonZero, ptr::NonNull, }; @@ -535,10 +535,10 @@ impl<'a, T> From<&'a [T]> for ThinSlicePtr<'a, T> { /// Creates a dangling pointer with specified alignment. /// See [`NonNull::dangling`]. -pub fn dangling_with_align(align: NonZeroUsize) -> NonNull { +pub fn dangling_with_align(align: NonZero) -> NonNull { debug_assert!(align.is_power_of_two(), "Alignment must be power of two."); // SAFETY: The pointer will not be null, since it was created - // from the address of a `NonZeroUsize`. + // from the address of a `NonZero`. unsafe { NonNull::new_unchecked(align.get() as *mut u8) } } diff --git a/crates/bevy_reflect/src/impls/std.rs b/crates/bevy_reflect/src/impls/std.rs index 6320090e6d4f67..c9f27a4cb34a43 100644 --- a/crates/bevy_reflect/src/impls/std.rs +++ b/crates/bevy_reflect/src/impls/std.rs @@ -2444,11 +2444,11 @@ mod tests { #[test] fn nonzero_usize_impl_reflect_from_reflect() { - let a: &dyn PartialReflect = &std::num::NonZeroUsize::new(42).unwrap(); - let b: &dyn PartialReflect = &std::num::NonZeroUsize::new(42).unwrap(); + let a: &dyn PartialReflect = &std::num::NonZero::::new(42).unwrap(); + let b: &dyn PartialReflect = &std::num::NonZero::::new(42).unwrap(); assert!(a.reflect_partial_eq(b).unwrap_or_default()); - let forty_two: std::num::NonZeroUsize = FromReflect::from_reflect(a).unwrap(); - assert_eq!(forty_two, std::num::NonZeroUsize::new(42).unwrap()); + let forty_two: std::num::NonZero = FromReflect::from_reflect(a).unwrap(); + assert_eq!(forty_two, std::num::NonZero::::new(42).unwrap()); } #[test] diff --git a/crates/bevy_render/src/render_resource/batched_uniform_buffer.rs b/crates/bevy_render/src/render_resource/batched_uniform_buffer.rs index d92c7a3897e065..75a747cf46fe38 100644 --- a/crates/bevy_render/src/render_resource/batched_uniform_buffer.rs +++ b/crates/bevy_render/src/render_resource/batched_uniform_buffer.rs @@ -8,7 +8,7 @@ use encase::{ ShaderType, }; use nonmax::NonMaxU32; -use std::{marker::PhantomData, num::NonZeroU64}; +use std::{marker::PhantomData, num::NonZero}; use wgpu::{BindingResource, Limits}; // 1MB else we will make really large arrays on macOS which reports very large @@ -69,7 +69,7 @@ impl BatchedUniformBuffer { } #[inline] - pub fn size(&self) -> NonZeroU64 { + pub fn size(&self) -> NonZero { self.temp.size() } @@ -141,7 +141,7 @@ where const METADATA: Metadata = T::METADATA; - fn size(&self) -> NonZeroU64 { + fn size(&self) -> NonZero { Self::METADATA.stride().mul(self.1.max(1) as u64).0 } } diff --git a/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs b/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs index 05c5ee9c3e8182..58d1d62a198895 100644 --- a/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs +++ b/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs @@ -1,5 +1,5 @@ use bevy_utils::all_tuples_with_size; -use std::num::NonZeroU32; +use std::num::NonZero; use wgpu::{BindGroupLayoutEntry, BindingType, ShaderStages}; /// Helper for constructing bind group layouts. @@ -130,7 +130,7 @@ use wgpu::{BindGroupLayoutEntry, BindingType, ShaderStages}; pub struct BindGroupLayoutEntryBuilder { ty: BindingType, visibility: Option, - count: Option, + count: Option>, } impl BindGroupLayoutEntryBuilder { @@ -139,7 +139,7 @@ impl BindGroupLayoutEntryBuilder { self } - pub fn count(mut self, count: NonZeroU32) -> Self { + pub fn count(mut self, count: NonZero) -> Self { self.count = Some(count); self } @@ -353,7 +353,7 @@ pub mod binding_types { BufferBindingType, SamplerBindingType, TextureSampleType, TextureViewDimension, }; use encase::ShaderType; - use std::num::NonZeroU64; + use std::num::NonZero; use wgpu::{StorageTextureAccess, TextureFormat}; use super::*; @@ -364,7 +364,7 @@ pub mod binding_types { pub fn storage_buffer_sized( has_dynamic_offset: bool, - min_binding_size: Option, + min_binding_size: Option>, ) -> BindGroupLayoutEntryBuilder { BindingType::Buffer { ty: BufferBindingType::Storage { read_only: false }, @@ -382,7 +382,7 @@ pub mod binding_types { pub fn storage_buffer_read_only_sized( has_dynamic_offset: bool, - min_binding_size: Option, + min_binding_size: Option>, ) -> BindGroupLayoutEntryBuilder { BindingType::Buffer { ty: BufferBindingType::Storage { read_only: true }, @@ -398,7 +398,7 @@ pub mod binding_types { pub fn uniform_buffer_sized( has_dynamic_offset: bool, - min_binding_size: Option, + min_binding_size: Option>, ) -> BindGroupLayoutEntryBuilder { BindingType::Buffer { ty: BufferBindingType::Uniform, diff --git a/crates/bevy_render/src/render_resource/resource_macros.rs b/crates/bevy_render/src/render_resource/resource_macros.rs index 68896092ce016e..e22c59b1c031f6 100644 --- a/crates/bevy_render/src/render_resource/resource_macros.rs +++ b/crates/bevy_render/src/render_resource/resource_macros.rs @@ -149,7 +149,7 @@ macro_rules! render_resource_wrapper { macro_rules! define_atomic_id { ($atomic_id_type:ident) => { #[derive(Copy, Clone, Hash, Eq, PartialEq, PartialOrd, Ord, Debug)] - pub struct $atomic_id_type(core::num::NonZeroU32); + pub struct $atomic_id_type(core::num::NonZero); // We use new instead of default to indicate that each ID created will be unique. #[allow(clippy::new_without_default)] @@ -160,7 +160,7 @@ macro_rules! define_atomic_id { static COUNTER: AtomicU32 = AtomicU32::new(1); let counter = COUNTER.fetch_add(1, Ordering::Relaxed); - Self(core::num::NonZeroU32::new(counter).unwrap_or_else(|| { + Self(core::num::NonZero::::new(counter).unwrap_or_else(|| { panic!( "The system ran out of unique `{}`s.", stringify!($atomic_id_type) @@ -169,14 +169,14 @@ macro_rules! define_atomic_id { } } - impl From<$atomic_id_type> for core::num::NonZeroU32 { + impl From<$atomic_id_type> for core::num::NonZero { fn from(value: $atomic_id_type) -> Self { value.0 } } - impl From for $atomic_id_type { - fn from(value: core::num::NonZeroU32) -> Self { + impl From> for $atomic_id_type { + fn from(value: core::num::NonZero) -> Self { Self(value) } } diff --git a/crates/bevy_render/src/render_resource/uniform_buffer.rs b/crates/bevy_render/src/render_resource/uniform_buffer.rs index de4d84c94a06e3..db51653f146ea3 100644 --- a/crates/bevy_render/src/render_resource/uniform_buffer.rs +++ b/crates/bevy_render/src/render_resource/uniform_buffer.rs @@ -1,4 +1,4 @@ -use std::{marker::PhantomData, num::NonZeroU64}; +use std::{marker::PhantomData, num::NonZero}; use crate::{ render_resource::Buffer, @@ -309,7 +309,7 @@ impl DynamicUniformBuffer { if let Some(buffer) = self.buffer.as_deref() { let buffer_view = queue - .write_buffer_with(buffer, 0, NonZeroU64::new(buffer.size())?) + .write_buffer_with(buffer, 0, NonZero::::new(buffer.size())?) .unwrap(); Some(DynamicUniformBufferWriter { buffer: encase::DynamicUniformBuffer::new_with_alignment( diff --git a/crates/bevy_render/src/view/window/mod.rs b/crates/bevy_render/src/view/window/mod.rs index 816d8c4e8dfd79..d9d30d440dcf54 100644 --- a/crates/bevy_render/src/view/window/mod.rs +++ b/crates/bevy_render/src/view/window/mod.rs @@ -13,7 +13,7 @@ use bevy_window::{ }; use bevy_winit::CustomCursorCache; use std::{ - num::NonZeroU32, + num::NonZero, ops::{Deref, DerefMut}, }; use wgpu::{ @@ -63,7 +63,7 @@ pub struct ExtractedWindow { pub physical_width: u32, pub physical_height: u32, pub present_mode: PresentMode, - pub desired_maximum_frame_latency: Option, + pub desired_maximum_frame_latency: Option>, /// Note: this will not always be the swap chain texture view. When taking a screenshot, /// this will point to an alternative texture instead to allow for copying the render result /// to CPU memory. @@ -395,7 +395,7 @@ pub fn create_surfaces( }, desired_maximum_frame_latency: window .desired_maximum_frame_latency - .map(NonZeroU32::get) + .map(NonZero::::get) .unwrap_or(DEFAULT_DESIRED_MAXIMUM_FRAME_LATENCY), alpha_mode: match window.alpha_mode { CompositeAlphaMode::Auto => wgpu::CompositeAlphaMode::Auto, diff --git a/crates/bevy_tasks/src/lib.rs b/crates/bevy_tasks/src/lib.rs index 3eb33c6603e70b..1fe4f9511d9971 100644 --- a/crates/bevy_tasks/src/lib.rs +++ b/crates/bevy_tasks/src/lib.rs @@ -55,7 +55,7 @@ pub mod prelude { }; } -use std::num::NonZeroUsize; +use std::num::NonZero; /// Gets the logical CPU core count available to the current process. /// @@ -65,6 +65,6 @@ use std::num::NonZeroUsize; /// This will always return at least 1. pub fn available_parallelism() -> usize { std::thread::available_parallelism() - .map(NonZeroUsize::get) + .map(NonZero::::get) .unwrap_or(1) } diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index fb285823b37987..3a46e952b32a77 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -12,7 +12,7 @@ use bevy_transform::prelude::GlobalTransform; use bevy_utils::warn_once; use bevy_window::{PrimaryWindow, WindowRef}; use smallvec::SmallVec; -use std::num::{NonZeroI16, NonZeroU16}; +use std::num::NonZero; use thiserror::Error; /// Base component for a UI node, which also provides the computed size of the node. @@ -1481,15 +1481,15 @@ pub struct GridPlacement { /// Lines are 1-indexed. /// Negative indexes count backwards from the end of the grid. /// Zero is not a valid index. - pub(crate) start: Option, + pub(crate) start: Option>, /// How many grid tracks the item should span. /// Defaults to 1. - pub(crate) span: Option, + pub(crate) span: Option>, /// The grid line at which the item should end. /// Lines are 1-indexed. /// Negative indexes count backwards from the end of the grid. /// Zero is not a valid index. - pub(crate) end: Option, + pub(crate) end: Option>, } impl GridPlacement { @@ -1497,7 +1497,7 @@ impl GridPlacement { pub const DEFAULT: Self = Self { start: None, // SAFETY: This is trivially safe as 1 is non-zero. - span: Some(unsafe { NonZeroU16::new_unchecked(1) }), + span: Some(unsafe { NonZero::::new_unchecked(1) }), end: None, }; @@ -1614,17 +1614,17 @@ impl GridPlacement { /// Returns the grid line at which the item should start, or `None` if not set. pub fn get_start(self) -> Option { - self.start.map(NonZeroI16::get) + self.start.map(NonZero::::get) } /// Returns the grid line at which the item should end, or `None` if not set. pub fn get_end(self) -> Option { - self.end.map(NonZeroI16::get) + self.end.map(NonZero::::get) } /// Returns span for this grid item, or `None` if not set. pub fn get_span(self) -> Option { - self.span.map(NonZeroU16::get) + self.span.map(NonZero::::get) } } @@ -1634,17 +1634,17 @@ impl Default for GridPlacement { } } -/// Convert an `i16` to `NonZeroI16`, fails on `0` and returns the `InvalidZeroIndex` error. -fn try_into_grid_index(index: i16) -> Result, GridPlacementError> { +/// Convert an `i16` to `NonZero`, fails on `0` and returns the `InvalidZeroIndex` error. +fn try_into_grid_index(index: i16) -> Result>, GridPlacementError> { Ok(Some( - NonZeroI16::new(index).ok_or(GridPlacementError::InvalidZeroIndex)?, + NonZero::::new(index).ok_or(GridPlacementError::InvalidZeroIndex)?, )) } -/// Convert a `u16` to `NonZeroU16`, fails on `0` and returns the `InvalidZeroSpan` error. -fn try_into_grid_span(span: u16) -> Result, GridPlacementError> { +/// Convert a `u16` to `NonZero`, fails on `0` and returns the `InvalidZeroSpan` error. +fn try_into_grid_span(span: u16) -> Result>, GridPlacementError> { Ok(Some( - NonZeroU16::new(span).ok_or(GridPlacementError::InvalidZeroSpan)?, + NonZero::::new(span).ok_or(GridPlacementError::InvalidZeroSpan)?, )) } diff --git a/crates/bevy_window/src/window.rs b/crates/bevy_window/src/window.rs index dfaa0ecdae36b5..eb8ca610fc9af1 100644 --- a/crates/bevy_window/src/window.rs +++ b/crates/bevy_window/src/window.rs @@ -1,4 +1,4 @@ -use std::num::NonZeroU32; +use std::num::NonZero; use bevy_ecs::{ entity::{Entity, EntityMapper, MapEntities}, @@ -279,7 +279,7 @@ pub struct Window { /// /// [`wgpu::SurfaceConfiguration::desired_maximum_frame_latency`]: /// https://docs.rs/wgpu/latest/wgpu/type.SurfaceConfiguration.html#structfield.desired_maximum_frame_latency - pub desired_maximum_frame_latency: Option, + pub desired_maximum_frame_latency: Option>, /// Sets whether this window recognizes [`PinchGesture`](https://docs.rs/bevy/latest/bevy/input/gestures/struct.PinchGesture.html) /// /// ## Platform-specific diff --git a/examples/app/headless_renderer.rs b/examples/app/headless_renderer.rs index 2a882063d9abfd..f6fa5c347257dd 100644 --- a/examples/app/headless_renderer.rs +++ b/examples/app/headless_renderer.rs @@ -381,7 +381,7 @@ impl render_graph::Node for ImageCopyDriver { layout: ImageDataLayout { offset: 0, bytes_per_row: Some( - std::num::NonZeroU32::new(padded_bytes_per_row as u32) + std::num::NonZero::::new(padded_bytes_per_row as u32) .unwrap() .into(), ), diff --git a/examples/shader/texture_binding_array.rs b/examples/shader/texture_binding_array.rs index 53e774df0485ea..24a8a847a1c192 100644 --- a/examples/shader/texture_binding_array.rs +++ b/examples/shader/texture_binding_array.rs @@ -17,7 +17,7 @@ use bevy::{ RenderApp, }, }; -use std::{num::NonZeroU32, process::exit}; +use std::{num::NonZero, process::exit}; /// This example uses a shader source file from the assets subdirectory const SHADER_ASSET_PATH: &str = "shaders/texture_binding_array.wgsl"; @@ -166,7 +166,7 @@ impl AsBindGroup for BindlessMaterial { ( 0, texture_2d(TextureSampleType::Float { filterable: true }) - .count(NonZeroU32::new(MAX_TEXTURE_COUNT as u32).unwrap()), + .count(NonZero::::new(MAX_TEXTURE_COUNT as u32).unwrap()), ), // Sampler // @@ -177,7 +177,7 @@ impl AsBindGroup for BindlessMaterial { // // ``` // sampler(SamplerBindingType::Filtering) - // .count(NonZeroU32::new(MAX_TEXTURE_COUNT as u32).unwrap()), + // .count(NonZero::::new(MAX_TEXTURE_COUNT as u32).unwrap()), // ``` // // One may need to pay attention to the limit of sampler binding From ffe0f7f2ba592139f5414c61c82e4a89c2c81344 Mon Sep 17 00:00:00 2001 From: BigWingBeat Date: Fri, 30 Aug 2024 19:57:08 +0100 Subject: [PATCH 53/53] Fix compile error caused by incorrect feature flag in `bevy_state` (#14987) # Objective The `reflect` module in `bevy_state` is gated behind the `bevy_reflect` feature, but the type exports from that module in the crate prelude are erroneously gated behind the `bevy_app` feature, causing a compile error when the `bevy_reflect` feature is disabled, but the `bevy_app` feature is enabled. ## Solution Change the feature gate to `bevy_reflect`. ## Testing - Discovered by depending on `bevy_state` with `default-features = false, features = ["bevy_app"]` - Tested by running `cargo check -p bevy_state --no-default-features --features bevy_app` --- crates/bevy_state/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_state/src/lib.rs b/crates/bevy_state/src/lib.rs index 5506221ce35cf7..b9ded18d4ef0e6 100644 --- a/crates/bevy_state/src/lib.rs +++ b/crates/bevy_state/src/lib.rs @@ -54,7 +54,7 @@ pub mod prelude { pub use crate::app::AppExtStates; #[doc(hidden)] pub use crate::condition::*; - #[cfg(feature = "bevy_app")] + #[cfg(feature = "bevy_reflect")] #[doc(hidden)] pub use crate::reflect::{ReflectFreelyMutableState, ReflectState}; #[doc(hidden)]