diff --git a/.github/workflows/validation-jobs.yml b/.github/workflows/validation-jobs.yml index 7e7d2029ac440..932ff4041f4fb 100644 --- a/.github/workflows/validation-jobs.yml +++ b/.github/workflows/validation-jobs.yml @@ -286,3 +286,31 @@ jobs: run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev - name: Run cargo udeps run: cargo udeps + + check-example-showcase-patches-still-work: + if: ${{ github.event_name == 'merge_group' }} + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-check-showcase-patches-${{ hashFiles('**/Cargo.toml') }} + - uses: dtolnay/rust-toolchain@stable + - name: Installs cargo-udeps + run: cargo install --force cargo-udeps + - name: Install alsa and udev + run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev + - name: Apply patches + run: | + for patch in tools/example-showcase/*.patch; do + git apply --ignore-whitespace $patch + done + - name: Build with patches + run: cargo build diff --git a/Cargo.toml b/Cargo.toml index ab9da009fd4d5..d057f022006a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1013,6 +1013,17 @@ description = "Create and play an animation defined by code that operates on the category = "Animation" wasm = true +[[example]] +name = "color_animation" +path = "examples/animation/color_animation.rs" +doc-scrape-examples = true + +[package.metadata.example.color_animation] +name = "Color animation" +description = "Demonstrates how to animate colors using mixing and splines in different color spaces" +category = "Animation" +wasm = true + [[example]] name = "cubic_curve" path = "examples/animation/cubic_curve.rs" @@ -2284,6 +2295,17 @@ description = "Illustrates how to (constantly) rotate an object around an axis" category = "Transforms" wasm = true +[[example]] +name = "align" +path = "examples/transforms/align.rs" +doc-scrape-examples = true + +[package.metadata.example.align] +name = "Alignment" +description = "A demonstration of Transform's axis-alignment feature" +category = "Transforms" +wasm = true + [[example]] name = "scale" path = "examples/transforms/scale.rs" @@ -2329,6 +2351,17 @@ description = "Demonstrates how to create a node with a border" category = "UI (User Interface)" wasm = true +[[example]] +name = "rounded_borders" +path = "examples/ui/rounded_borders.rs" +doc-scrape-examples = true + +[package.metadata.example.rounded_borders] +name = "Rounded Borders" +description = "Demonstrates how to create a node with a rounded border" +category = "UI (User Interface)" +wasm = true + [[example]] name = "button" path = "examples/ui/button.rs" diff --git a/crates/bevy_animation/Cargo.toml b/crates/bevy_animation/Cargo.toml index c6cc5275f5255..13787f46a12cd 100644 --- a/crates/bevy_animation/Cargo.toml +++ b/crates/bevy_animation/Cargo.toml @@ -12,6 +12,7 @@ keywords = ["bevy"] # bevy bevy_app = { path = "../bevy_app", version = "0.14.0-dev" } bevy_asset = { path = "../bevy_asset", version = "0.14.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.14.0-dev" } bevy_core = { path = "../bevy_core", version = "0.14.0-dev" } bevy_derive = { path = "../bevy_derive", version = "0.14.0-dev" } bevy_log = { path = "../bevy_log", version = "0.14.0-dev" } @@ -28,7 +29,7 @@ bevy_transform = { path = "../bevy_transform", version = "0.14.0-dev" } bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.14.0-dev" } # other -fixedbitset = "0.4" +fixedbitset = "0.5" petgraph = { version = "0.6", features = ["serde-1"] } ron = "0.8" serde = "1" diff --git a/crates/bevy_animation/src/animatable.rs b/crates/bevy_animation/src/animatable.rs index 17e11bfe5fd2c..201b8e69192b7 100644 --- a/crates/bevy_animation/src/animatable.rs +++ b/crates/bevy_animation/src/animatable.rs @@ -1,4 +1,5 @@ use crate::util; +use bevy_color::{ClampColor, Laba, LinearRgba, Oklaba, Xyza}; use bevy_ecs::world::World; use bevy_math::*; use bevy_reflect::Reflect; @@ -57,6 +58,31 @@ macro_rules! impl_float_animatable { }; } +macro_rules! impl_color_animatable { + ($ty: ident) => { + impl Animatable for $ty { + #[inline] + fn interpolate(a: &Self, b: &Self, t: f32) -> Self { + let value = *a * (1. - t) + *b * t; + value.clamped() + } + + #[inline] + fn blend(inputs: impl Iterator>) -> Self { + let mut value = Default::default(); + for input in inputs { + if input.additive { + value += input.weight * input.value; + } else { + value = Self::interpolate(&value, &input.value, input.weight); + } + } + value.clamped() + } + } + }; +} + impl_float_animatable!(f32, f32); impl_float_animatable!(Vec2, f32); impl_float_animatable!(Vec3A, f32); @@ -67,6 +93,11 @@ impl_float_animatable!(DVec2, f64); impl_float_animatable!(DVec3, f64); impl_float_animatable!(DVec4, f64); +impl_color_animatable!(LinearRgba); +impl_color_animatable!(Laba); +impl_color_animatable!(Oklaba); +impl_color_animatable!(Xyza); + // Vec3 is special cased to use Vec3A internally for blending impl Animatable for Vec3 { #[inline] diff --git a/crates/bevy_animation/src/graph.rs b/crates/bevy_animation/src/graph.rs index aba53f1d7adfa..01a270ed5385d 100644 --- a/crates/bevy_animation/src/graph.rs +++ b/crates/bevy_animation/src/graph.rs @@ -6,7 +6,6 @@ use std::ops::{Index, IndexMut}; use bevy_asset::io::Reader; use bevy_asset::{Asset, AssetId, AssetLoader, AssetPath, AsyncReadExt as _, Handle, LoadContext}; use bevy_reflect::{Reflect, ReflectSerialize}; -use bevy_utils::BoxedFuture; use petgraph::graph::{DiGraph, NodeIndex}; use ron::de::SpannedError; use serde::{Deserialize, Serialize}; @@ -336,40 +335,37 @@ impl AssetLoader for AnimationGraphAssetLoader { type Error = AnimationGraphLoadError; - fn load<'a>( + async fn load<'a>( &'a self, - reader: &'a mut Reader, + reader: &'a mut Reader<'_>, _: &'a Self::Settings, - load_context: &'a mut LoadContext, - ) -> BoxedFuture<'a, Result> { - Box::pin(async move { - let mut bytes = Vec::new(); - reader.read_to_end(&mut bytes).await?; - - // Deserialize a `SerializedAnimationGraph` directly, so that we can - // get the list of the animation clips it refers to and load them. - let mut deserializer = ron::de::Deserializer::from_bytes(&bytes)?; - let serialized_animation_graph = - SerializedAnimationGraph::deserialize(&mut deserializer) - .map_err(|err| deserializer.span_error(err))?; - - // Load all `AssetPath`s to convert from a - // `SerializedAnimationGraph` to a real `AnimationGraph`. - Ok(AnimationGraph { - graph: serialized_animation_graph.graph.map( - |_, serialized_node| AnimationGraphNode { - clip: serialized_node.clip.as_ref().map(|clip| match clip { - SerializedAnimationClip::AssetId(asset_id) => Handle::Weak(*asset_id), - SerializedAnimationClip::AssetPath(asset_path) => { - load_context.load(asset_path) - } - }), - weight: serialized_node.weight, - }, - |_, _| (), - ), - root: serialized_animation_graph.root, - }) + load_context: &'a mut LoadContext<'_>, + ) -> Result { + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).await?; + + // Deserialize a `SerializedAnimationGraph` directly, so that we can + // get the list of the animation clips it refers to and load them. + let mut deserializer = ron::de::Deserializer::from_bytes(&bytes)?; + let serialized_animation_graph = SerializedAnimationGraph::deserialize(&mut deserializer) + .map_err(|err| deserializer.span_error(err))?; + + // Load all `AssetPath`s to convert from a + // `SerializedAnimationGraph` to a real `AnimationGraph`. + Ok(AnimationGraph { + graph: serialized_animation_graph.graph.map( + |_, serialized_node| AnimationGraphNode { + clip: serialized_node.clip.as_ref().map(|clip| match clip { + SerializedAnimationClip::AssetId(asset_id) => Handle::Weak(*asset_id), + SerializedAnimationClip::AssetPath(asset_path) => { + load_context.load(asset_path) + } + }), + weight: serialized_node.weight, + }, + |_, _| (), + ), + root: serialized_animation_graph.root, }) } diff --git a/crates/bevy_asset/src/assets.rs b/crates/bevy_asset/src/assets.rs index 9cf1b4e5386b7..b04d8552f50e9 100644 --- a/crates/bevy_asset/src/assets.rs +++ b/crates/bevy_asset/src/assets.rs @@ -549,23 +549,24 @@ impl Assets { while let Ok(drop_event) = assets.handle_provider.drop_receiver.try_recv() { let id = drop_event.id.typed(); - assets.queued_events.push(AssetEvent::Unused { id }); - - let mut remove_asset = true; - if drop_event.asset_server_managed { - let untyped_id = drop_event.id.untyped(TypeId::of::()); + let untyped_id = id.untyped(); if let Some(info) = infos.get(untyped_id) { if let LoadState::Loading | LoadState::NotLoaded = info.load_state { not_ready.push(drop_event); continue; } } - remove_asset = infos.process_handle_drop(untyped_id); - } - if remove_asset { - assets.remove_dropped(id); + + // the process_handle_drop call checks whether new handles have been created since the drop event was fired, before removing the asset + if !infos.process_handle_drop(untyped_id) { + // a new handle has been created, or the asset doesn't exist + continue; + } } + + assets.queued_events.push(AssetEvent::Unused { id }); + assets.remove_dropped(id); } // TODO: this is _extremely_ inefficient find a better fix diff --git a/crates/bevy_asset/src/io/android.rs b/crates/bevy_asset/src/io/android.rs index 38791b35e0071..18daac5949645 100644 --- a/crates/bevy_asset/src/io/android.rs +++ b/crates/bevy_asset/src/io/android.rs @@ -2,7 +2,6 @@ use crate::io::{ get_meta_path, AssetReader, AssetReaderError, EmptyPathStream, PathStream, Reader, VecReader, }; use bevy_utils::tracing::error; -use bevy_utils::BoxedFuture; use std::{ffi::CString, path::Path}; /// [`AssetReader`] implementation for Android devices, built on top of Android's [`AssetManager`]. @@ -17,57 +16,47 @@ use std::{ffi::CString, path::Path}; pub struct AndroidAssetReader; impl AssetReader for AndroidAssetReader { - fn read<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result>, AssetReaderError>> { - Box::pin(async move { - let asset_manager = bevy_winit::ANDROID_APP - .get() - .expect("Bevy must be setup with the #[bevy_main] macro on Android") - .asset_manager(); - let mut opened_asset = asset_manager - .open(&CString::new(path.to_str().unwrap()).unwrap()) - .ok_or(AssetReaderError::NotFound(path.to_path_buf()))?; - let bytes = opened_asset.buffer()?; - let reader: Box = Box::new(VecReader::new(bytes.to_vec())); - Ok(reader) - }) + async fn read<'a>(&'a self, path: &'a Path) -> Result>, AssetReaderError> { + let asset_manager = bevy_winit::ANDROID_APP + .get() + .expect("Bevy must be setup with the #[bevy_main] macro on Android") + .asset_manager(); + let mut opened_asset = asset_manager + .open(&CString::new(path.to_str().unwrap()).unwrap()) + .ok_or(AssetReaderError::NotFound(path.to_path_buf()))?; + let bytes = opened_asset.buffer()?; + let reader: Box = Box::new(VecReader::new(bytes.to_vec())); + Ok(reader) } - fn read_meta<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result>, AssetReaderError>> { - Box::pin(async move { - let meta_path = get_meta_path(path); - let asset_manager = bevy_winit::ANDROID_APP - .get() - .expect("Bevy must be setup with the #[bevy_main] macro on Android") - .asset_manager(); - let mut opened_asset = asset_manager - .open(&CString::new(meta_path.to_str().unwrap()).unwrap()) - .ok_or(AssetReaderError::NotFound(meta_path))?; - let bytes = opened_asset.buffer()?; - let reader: Box = Box::new(VecReader::new(bytes.to_vec())); - Ok(reader) - }) + async fn read_meta<'a>(&'a self, path: &'a Path) -> Result>, AssetReaderError> { + let meta_path = get_meta_path(path); + let asset_manager = bevy_winit::ANDROID_APP + .get() + .expect("Bevy must be setup with the #[bevy_main] macro on Android") + .asset_manager(); + let mut opened_asset = asset_manager + .open(&CString::new(meta_path.to_str().unwrap()).unwrap()) + .ok_or(AssetReaderError::NotFound(meta_path))?; + let bytes = opened_asset.buffer()?; + let reader: Box = Box::new(VecReader::new(bytes.to_vec())); + Ok(reader) } - fn read_directory<'a>( + async fn read_directory<'a>( &'a self, _path: &'a Path, - ) -> BoxedFuture<'a, Result, AssetReaderError>> { + ) -> Result, AssetReaderError> { let stream: Box = Box::new(EmptyPathStream); error!("Reading directories is not supported with the AndroidAssetReader"); - Box::pin(async move { Ok(stream) }) + Ok(stream) } - fn is_directory<'a>( + async fn is_directory<'a>( &'a self, _path: &'a Path, - ) -> BoxedFuture<'a, std::result::Result> { + ) -> std::result::Result { error!("Reading directories is not supported with the AndroidAssetReader"); - Box::pin(async move { Ok(false) }) + Ok(false) } } diff --git a/crates/bevy_asset/src/io/file/file_asset.rs b/crates/bevy_asset/src/io/file/file_asset.rs index aa20913140111..5826fe097ddb3 100644 --- a/crates/bevy_asset/src/io/file/file_asset.rs +++ b/crates/bevy_asset/src/io/file/file_asset.rs @@ -3,7 +3,6 @@ use crate::io::{ Reader, Writer, }; use async_fs::{read_dir, File}; -use bevy_utils::BoxedFuture; use futures_lite::StreamExt; use std::path::Path; @@ -11,215 +10,168 @@ use std::path::Path; use super::{FileAssetReader, FileAssetWriter}; impl AssetReader for FileAssetReader { - fn read<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result>, AssetReaderError>> { - Box::pin(async move { - let full_path = self.root_path.join(path); - match File::open(&full_path).await { - Ok(file) => { - let reader: Box = Box::new(file); - Ok(reader) - } - Err(e) => { - if e.kind() == std::io::ErrorKind::NotFound { - Err(AssetReaderError::NotFound(full_path)) - } else { - Err(e.into()) - } + async fn read<'a>(&'a self, path: &'a Path) -> Result>, AssetReaderError> { + let full_path = self.root_path.join(path); + match File::open(&full_path).await { + Ok(file) => { + let reader: Box = Box::new(file); + Ok(reader) + } + Err(e) => { + if e.kind() == std::io::ErrorKind::NotFound { + Err(AssetReaderError::NotFound(full_path)) + } else { + Err(e.into()) } } - }) + } } - fn read_meta<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result>, AssetReaderError>> { + async fn read_meta<'a>(&'a self, path: &'a Path) -> Result>, AssetReaderError> { let meta_path = get_meta_path(path); - Box::pin(async move { - let full_path = self.root_path.join(meta_path); - match File::open(&full_path).await { - Ok(file) => { - let reader: Box = Box::new(file); - Ok(reader) - } - Err(e) => { - if e.kind() == std::io::ErrorKind::NotFound { - Err(AssetReaderError::NotFound(full_path)) - } else { - Err(e.into()) - } + let full_path = self.root_path.join(meta_path); + match File::open(&full_path).await { + Ok(file) => { + let reader: Box = Box::new(file); + Ok(reader) + } + Err(e) => { + if e.kind() == std::io::ErrorKind::NotFound { + Err(AssetReaderError::NotFound(full_path)) + } else { + Err(e.into()) } } - }) + } } - fn read_directory<'a>( + async fn read_directory<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, Result, AssetReaderError>> { - Box::pin(async move { - let full_path = self.root_path.join(path); - match read_dir(&full_path).await { - Ok(read_dir) => { - let root_path = self.root_path.clone(); - let mapped_stream = read_dir.filter_map(move |f| { - f.ok().and_then(|dir_entry| { - let path = dir_entry.path(); - // filter out meta files as they are not considered assets - if let Some(ext) = path.extension().and_then(|e| e.to_str()) { - if ext.eq_ignore_ascii_case("meta") { - return None; - } + ) -> Result, AssetReaderError> { + let full_path = self.root_path.join(path); + match read_dir(&full_path).await { + Ok(read_dir) => { + let root_path = self.root_path.clone(); + let mapped_stream = read_dir.filter_map(move |f| { + f.ok().and_then(|dir_entry| { + let path = dir_entry.path(); + // filter out meta files as they are not considered assets + if let Some(ext) = path.extension().and_then(|e| e.to_str()) { + if ext.eq_ignore_ascii_case("meta") { + return None; } - let relative_path = path.strip_prefix(&root_path).unwrap(); - Some(relative_path.to_owned()) - }) - }); - let read_dir: Box = Box::new(mapped_stream); - Ok(read_dir) - } - Err(e) => { - if e.kind() == std::io::ErrorKind::NotFound { - Err(AssetReaderError::NotFound(full_path)) - } else { - Err(e.into()) - } + } + let relative_path = path.strip_prefix(&root_path).unwrap(); + Some(relative_path.to_owned()) + }) + }); + let read_dir: Box = Box::new(mapped_stream); + Ok(read_dir) + } + Err(e) => { + if e.kind() == std::io::ErrorKind::NotFound { + Err(AssetReaderError::NotFound(full_path)) + } else { + Err(e.into()) } } - }) + } } - fn is_directory<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result> { - Box::pin(async move { - let full_path = self.root_path.join(path); - let metadata = full_path - .metadata() - .map_err(|_e| AssetReaderError::NotFound(path.to_owned()))?; - Ok(metadata.file_type().is_dir()) - }) + async fn is_directory<'a>(&'a self, path: &'a Path) -> Result { + let full_path = self.root_path.join(path); + let metadata = full_path + .metadata() + .map_err(|_e| AssetReaderError::NotFound(path.to_owned()))?; + Ok(metadata.file_type().is_dir()) } } impl AssetWriter for FileAssetWriter { - fn write<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result, AssetWriterError>> { - Box::pin(async move { - let full_path = self.root_path.join(path); - if let Some(parent) = full_path.parent() { - async_fs::create_dir_all(parent).await?; - } - let file = File::create(&full_path).await?; - let writer: Box = Box::new(file); - Ok(writer) - }) + async fn write<'a>(&'a self, path: &'a Path) -> Result, AssetWriterError> { + let full_path = self.root_path.join(path); + if let Some(parent) = full_path.parent() { + async_fs::create_dir_all(parent).await?; + } + let file = File::create(&full_path).await?; + let writer: Box = Box::new(file); + Ok(writer) } - fn write_meta<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result, AssetWriterError>> { - Box::pin(async move { - let meta_path = get_meta_path(path); - let full_path = self.root_path.join(meta_path); - if let Some(parent) = full_path.parent() { - async_fs::create_dir_all(parent).await?; - } - let file = File::create(&full_path).await?; - let writer: Box = Box::new(file); - Ok(writer) - }) + async fn write_meta<'a>(&'a self, path: &'a Path) -> Result, AssetWriterError> { + let meta_path = get_meta_path(path); + let full_path = self.root_path.join(meta_path); + if let Some(parent) = full_path.parent() { + async_fs::create_dir_all(parent).await?; + } + let file = File::create(&full_path).await?; + let writer: Box = Box::new(file); + Ok(writer) } - fn remove<'a>(&'a self, path: &'a Path) -> BoxedFuture<'a, Result<(), AssetWriterError>> { - Box::pin(async move { - let full_path = self.root_path.join(path); - async_fs::remove_file(full_path).await?; - Ok(()) - }) + async fn remove<'a>(&'a self, path: &'a Path) -> Result<(), AssetWriterError> { + let full_path = self.root_path.join(path); + async_fs::remove_file(full_path).await?; + Ok(()) } - fn remove_meta<'a>(&'a self, path: &'a Path) -> BoxedFuture<'a, Result<(), AssetWriterError>> { - Box::pin(async move { - let meta_path = get_meta_path(path); - let full_path = self.root_path.join(meta_path); - async_fs::remove_file(full_path).await?; - Ok(()) - }) + async fn remove_meta<'a>(&'a self, path: &'a Path) -> Result<(), AssetWriterError> { + let meta_path = get_meta_path(path); + let full_path = self.root_path.join(meta_path); + async_fs::remove_file(full_path).await?; + Ok(()) } - fn rename<'a>( + async fn rename<'a>( &'a self, old_path: &'a Path, new_path: &'a Path, - ) -> BoxedFuture<'a, Result<(), AssetWriterError>> { - Box::pin(async move { - let full_old_path = self.root_path.join(old_path); - let full_new_path = self.root_path.join(new_path); - if let Some(parent) = full_new_path.parent() { - async_fs::create_dir_all(parent).await?; - } - async_fs::rename(full_old_path, full_new_path).await?; - Ok(()) - }) + ) -> Result<(), AssetWriterError> { + let full_old_path = self.root_path.join(old_path); + let full_new_path = self.root_path.join(new_path); + if let Some(parent) = full_new_path.parent() { + async_fs::create_dir_all(parent).await?; + } + async_fs::rename(full_old_path, full_new_path).await?; + Ok(()) } - fn rename_meta<'a>( + async fn rename_meta<'a>( &'a self, old_path: &'a Path, new_path: &'a Path, - ) -> BoxedFuture<'a, Result<(), AssetWriterError>> { - Box::pin(async move { - let old_meta_path = get_meta_path(old_path); - let new_meta_path = get_meta_path(new_path); - let full_old_path = self.root_path.join(old_meta_path); - let full_new_path = self.root_path.join(new_meta_path); - if let Some(parent) = full_new_path.parent() { - async_fs::create_dir_all(parent).await?; - } - async_fs::rename(full_old_path, full_new_path).await?; - Ok(()) - }) + ) -> Result<(), AssetWriterError> { + let old_meta_path = get_meta_path(old_path); + let new_meta_path = get_meta_path(new_path); + let full_old_path = self.root_path.join(old_meta_path); + let full_new_path = self.root_path.join(new_meta_path); + if let Some(parent) = full_new_path.parent() { + async_fs::create_dir_all(parent).await?; + } + async_fs::rename(full_old_path, full_new_path).await?; + Ok(()) } - fn remove_directory<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result<(), AssetWriterError>> { - Box::pin(async move { - let full_path = self.root_path.join(path); - async_fs::remove_dir_all(full_path).await?; - Ok(()) - }) + async fn remove_directory<'a>(&'a self, path: &'a Path) -> Result<(), AssetWriterError> { + let full_path = self.root_path.join(path); + async_fs::remove_dir_all(full_path).await?; + Ok(()) } - fn remove_empty_directory<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result<(), AssetWriterError>> { - Box::pin(async move { - let full_path = self.root_path.join(path); - async_fs::remove_dir(full_path).await?; - Ok(()) - }) + async fn remove_empty_directory<'a>(&'a self, path: &'a Path) -> Result<(), AssetWriterError> { + let full_path = self.root_path.join(path); + async_fs::remove_dir(full_path).await?; + Ok(()) } - fn remove_assets_in_directory<'a>( + async fn remove_assets_in_directory<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, Result<(), AssetWriterError>> { - Box::pin(async move { - let full_path = self.root_path.join(path); - async_fs::remove_dir_all(&full_path).await?; - async_fs::create_dir_all(&full_path).await?; - Ok(()) - }) + ) -> Result<(), AssetWriterError> { + let full_path = self.root_path.join(path); + async_fs::remove_dir_all(&full_path).await?; + async_fs::create_dir_all(&full_path).await?; + Ok(()) } } diff --git a/crates/bevy_asset/src/io/file/sync_file_asset.rs b/crates/bevy_asset/src/io/file/sync_file_asset.rs index a8bf573a7ab07..426472150167e 100644 --- a/crates/bevy_asset/src/io/file/sync_file_asset.rs +++ b/crates/bevy_asset/src/io/file/sync_file_asset.rs @@ -5,7 +5,6 @@ use crate::io::{ get_meta_path, AssetReader, AssetReaderError, AssetWriter, AssetWriterError, PathStream, Reader, Writer, }; -use bevy_utils::BoxedFuture; use std::{ fs::{read_dir, File}, @@ -76,221 +75,180 @@ impl Stream for DirReader { } impl AssetReader for FileAssetReader { - fn read<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result>, AssetReaderError>> { - Box::pin(async move { - let full_path = self.root_path.join(path); - match File::open(&full_path) { - Ok(file) => { - let reader: Box = Box::new(FileReader(file)); - Ok(reader) - } - Err(e) => { - if e.kind() == std::io::ErrorKind::NotFound { - Err(AssetReaderError::NotFound(full_path)) - } else { - Err(e.into()) - } + async fn read<'a>(&'a self, path: &'a Path) -> Result>, AssetReaderError> { + let full_path = self.root_path.join(path); + match File::open(&full_path) { + Ok(file) => { + let reader: Box = Box::new(FileReader(file)); + Ok(reader) + } + Err(e) => { + if e.kind() == std::io::ErrorKind::NotFound { + Err(AssetReaderError::NotFound(full_path)) + } else { + Err(e.into()) } } - }) + } } - fn read_meta<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result>, AssetReaderError>> { + async fn read_meta<'a>(&'a self, path: &'a Path) -> Result>, AssetReaderError> { let meta_path = get_meta_path(path); - Box::pin(async move { - let full_path = self.root_path.join(meta_path); - match File::open(&full_path) { - Ok(file) => { - let reader: Box = Box::new(FileReader(file)); - Ok(reader) - } - Err(e) => { - if e.kind() == std::io::ErrorKind::NotFound { - Err(AssetReaderError::NotFound(full_path)) - } else { - Err(e.into()) - } + let full_path = self.root_path.join(meta_path); + match File::open(&full_path) { + Ok(file) => { + let reader: Box = Box::new(FileReader(file)); + Ok(reader) + } + Err(e) => { + if e.kind() == std::io::ErrorKind::NotFound { + Err(AssetReaderError::NotFound(full_path)) + } else { + Err(e.into()) } } - }) + } } - fn read_directory<'a>( + async fn read_directory<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, Result, AssetReaderError>> { - Box::pin(async move { - let full_path = self.root_path.join(path); - match read_dir(&full_path) { - Ok(read_dir) => { - let root_path = self.root_path.clone(); - let mapped_stream = read_dir.filter_map(move |f| { - f.ok().and_then(|dir_entry| { - let path = dir_entry.path(); - // filter out meta files as they are not considered assets - if let Some(ext) = path.extension().and_then(|e| e.to_str()) { - if ext.eq_ignore_ascii_case("meta") { - return None; - } + ) -> Result, AssetReaderError> { + let full_path = self.root_path.join(path); + match read_dir(&full_path) { + Ok(read_dir) => { + let root_path = self.root_path.clone(); + let mapped_stream = read_dir.filter_map(move |f| { + f.ok().and_then(|dir_entry| { + let path = dir_entry.path(); + // filter out meta files as they are not considered assets + if let Some(ext) = path.extension().and_then(|e| e.to_str()) { + if ext.eq_ignore_ascii_case("meta") { + return None; } - let relative_path = path.strip_prefix(&root_path).unwrap(); - Some(relative_path.to_owned()) - }) - }); - let read_dir: Box = Box::new(DirReader(mapped_stream.collect())); - Ok(read_dir) - } - Err(e) => { - if e.kind() == std::io::ErrorKind::NotFound { - Err(AssetReaderError::NotFound(full_path)) - } else { - Err(e.into()) - } + } + let relative_path = path.strip_prefix(&root_path).unwrap(); + Some(relative_path.to_owned()) + }) + }); + let read_dir: Box = Box::new(DirReader(mapped_stream.collect())); + Ok(read_dir) + } + Err(e) => { + if e.kind() == std::io::ErrorKind::NotFound { + Err(AssetReaderError::NotFound(full_path)) + } else { + Err(e.into()) } } - }) + } } - fn is_directory<'a>( + async fn is_directory<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, std::result::Result> { - Box::pin(async move { - let full_path = self.root_path.join(path); - let metadata = full_path - .metadata() - .map_err(|_e| AssetReaderError::NotFound(path.to_owned()))?; - Ok(metadata.file_type().is_dir()) - }) + ) -> std::result::Result { + let full_path = self.root_path.join(path); + let metadata = full_path + .metadata() + .map_err(|_e| AssetReaderError::NotFound(path.to_owned()))?; + Ok(metadata.file_type().is_dir()) } } impl AssetWriter for FileAssetWriter { - fn write<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result, AssetWriterError>> { - Box::pin(async move { - let full_path = self.root_path.join(path); - if let Some(parent) = full_path.parent() { - std::fs::create_dir_all(parent)?; - } - let file = File::create(&full_path)?; - let writer: Box = Box::new(FileWriter(file)); - Ok(writer) - }) + async fn write<'a>(&'a self, path: &'a Path) -> Result, AssetWriterError> { + let full_path = self.root_path.join(path); + if let Some(parent) = full_path.parent() { + std::fs::create_dir_all(parent)?; + } + let file = File::create(&full_path)?; + let writer: Box = Box::new(FileWriter(file)); + Ok(writer) } - fn write_meta<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result, AssetWriterError>> { - Box::pin(async move { - let meta_path = get_meta_path(path); - let full_path = self.root_path.join(meta_path); - if let Some(parent) = full_path.parent() { - std::fs::create_dir_all(parent)?; - } - let file = File::create(&full_path)?; - let writer: Box = Box::new(FileWriter(file)); - Ok(writer) - }) + async fn write_meta<'a>(&'a self, path: &'a Path) -> Result, AssetWriterError> { + let meta_path = get_meta_path(path); + let full_path = self.root_path.join(meta_path); + if let Some(parent) = full_path.parent() { + std::fs::create_dir_all(parent)?; + } + let file = File::create(&full_path)?; + let writer: Box = Box::new(FileWriter(file)); + Ok(writer) } - fn remove<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> { - Box::pin(async move { - let full_path = self.root_path.join(path); - std::fs::remove_file(full_path)?; - Ok(()) - }) + async fn remove<'a>(&'a self, path: &'a Path) -> std::result::Result<(), AssetWriterError> { + let full_path = self.root_path.join(path); + std::fs::remove_file(full_path)?; + Ok(()) } - fn remove_meta<'a>( + async fn remove_meta<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> { - Box::pin(async move { - let meta_path = get_meta_path(path); - let full_path = self.root_path.join(meta_path); - std::fs::remove_file(full_path)?; - Ok(()) - }) + ) -> std::result::Result<(), AssetWriterError> { + let meta_path = get_meta_path(path); + let full_path = self.root_path.join(meta_path); + std::fs::remove_file(full_path)?; + Ok(()) } - fn remove_directory<'a>( + async fn remove_directory<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> { - Box::pin(async move { - let full_path = self.root_path.join(path); - std::fs::remove_dir_all(full_path)?; - Ok(()) - }) + ) -> std::result::Result<(), AssetWriterError> { + let full_path = self.root_path.join(path); + std::fs::remove_dir_all(full_path)?; + Ok(()) } - fn remove_empty_directory<'a>( + async fn remove_empty_directory<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> { - Box::pin(async move { - let full_path = self.root_path.join(path); - std::fs::remove_dir(full_path)?; - Ok(()) - }) + ) -> std::result::Result<(), AssetWriterError> { + let full_path = self.root_path.join(path); + std::fs::remove_dir(full_path)?; + Ok(()) } - fn remove_assets_in_directory<'a>( + async fn remove_assets_in_directory<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> { - Box::pin(async move { - let full_path = self.root_path.join(path); - std::fs::remove_dir_all(&full_path)?; - std::fs::create_dir_all(&full_path)?; - Ok(()) - }) + ) -> std::result::Result<(), AssetWriterError> { + let full_path = self.root_path.join(path); + std::fs::remove_dir_all(&full_path)?; + std::fs::create_dir_all(&full_path)?; + Ok(()) } - fn rename<'a>( + async fn rename<'a>( &'a self, old_path: &'a Path, new_path: &'a Path, - ) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> { - Box::pin(async move { - let full_old_path = self.root_path.join(old_path); - let full_new_path = self.root_path.join(new_path); - if let Some(parent) = full_new_path.parent() { - std::fs::create_dir_all(parent)?; - } - std::fs::rename(full_old_path, full_new_path)?; - Ok(()) - }) + ) -> std::result::Result<(), AssetWriterError> { + let full_old_path = self.root_path.join(old_path); + let full_new_path = self.root_path.join(new_path); + if let Some(parent) = full_new_path.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::rename(full_old_path, full_new_path)?; + Ok(()) } - fn rename_meta<'a>( + async fn rename_meta<'a>( &'a self, old_path: &'a Path, new_path: &'a Path, - ) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> { - Box::pin(async move { - let old_meta_path = get_meta_path(old_path); - let new_meta_path = get_meta_path(new_path); - let full_old_path = self.root_path.join(old_meta_path); - let full_new_path = self.root_path.join(new_meta_path); - if let Some(parent) = full_new_path.parent() { - std::fs::create_dir_all(parent)?; - } - std::fs::rename(full_old_path, full_new_path)?; - Ok(()) - }) + ) -> std::result::Result<(), AssetWriterError> { + let old_meta_path = get_meta_path(old_path); + let new_meta_path = get_meta_path(new_path); + let full_old_path = self.root_path.join(old_meta_path); + let full_new_path = self.root_path.join(new_meta_path); + if let Some(parent) = full_new_path.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::rename(full_old_path, full_new_path)?; + Ok(()) } } diff --git a/crates/bevy_asset/src/io/gated.rs b/crates/bevy_asset/src/io/gated.rs index 76f531a04c88a..d3d2b35f1f066 100644 --- a/crates/bevy_asset/src/io/gated.rs +++ b/crates/bevy_asset/src/io/gated.rs @@ -1,5 +1,5 @@ use crate::io::{AssetReader, AssetReaderError, PathStream, Reader}; -use bevy_utils::{BoxedFuture, HashMap}; +use bevy_utils::HashMap; use crossbeam_channel::{Receiver, Sender}; use parking_lot::RwLock; use std::{path::Path, sync::Arc}; @@ -55,10 +55,7 @@ impl GatedReader { } impl AssetReader for GatedReader { - fn read<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result>, AssetReaderError>> { + async fn read<'a>(&'a self, path: &'a Path) -> Result>, AssetReaderError> { let receiver = { let mut gates = self.gates.write(); let gates = gates @@ -66,31 +63,23 @@ impl AssetReader for GatedReader { .or_insert_with(crossbeam_channel::unbounded); gates.1.clone() }; - Box::pin(async move { - receiver.recv().unwrap(); - let result = self.reader.read(path).await?; - Ok(result) - }) + receiver.recv().unwrap(); + let result = self.reader.read(path).await?; + Ok(result) } - fn read_meta<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result>, AssetReaderError>> { - self.reader.read_meta(path) + async fn read_meta<'a>(&'a self, path: &'a Path) -> Result>, AssetReaderError> { + self.reader.read_meta(path).await } - fn read_directory<'a>( + async fn read_directory<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, Result, AssetReaderError>> { - self.reader.read_directory(path) + ) -> Result, AssetReaderError> { + self.reader.read_directory(path).await } - fn is_directory<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result> { - self.reader.is_directory(path) + async fn is_directory<'a>(&'a self, path: &'a Path) -> Result { + self.reader.is_directory(path).await } } diff --git a/crates/bevy_asset/src/io/memory.rs b/crates/bevy_asset/src/io/memory.rs index cc13d0482056a..563086f7b0620 100644 --- a/crates/bevy_asset/src/io/memory.rs +++ b/crates/bevy_asset/src/io/memory.rs @@ -1,5 +1,5 @@ use crate::io::{AssetReader, AssetReaderError, PathStream, Reader}; -use bevy_utils::{BoxedFuture, HashMap}; +use bevy_utils::HashMap; use futures_io::AsyncRead; use futures_lite::{ready, Stream}; use parking_lot::RwLock; @@ -237,62 +237,47 @@ impl AsyncRead for DataReader { } impl AssetReader for MemoryAssetReader { - fn read<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result>, AssetReaderError>> { - Box::pin(async move { - self.root - .get_asset(path) - .map(|data| { - let reader: Box = Box::new(DataReader { - data, - bytes_read: 0, - }); - reader - }) - .ok_or_else(|| AssetReaderError::NotFound(path.to_path_buf())) - }) + async fn read<'a>(&'a self, path: &'a Path) -> Result>, AssetReaderError> { + self.root + .get_asset(path) + .map(|data| { + let reader: Box = Box::new(DataReader { + data, + bytes_read: 0, + }); + reader + }) + .ok_or_else(|| AssetReaderError::NotFound(path.to_path_buf())) } - fn read_meta<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result>, AssetReaderError>> { - Box::pin(async move { - self.root - .get_metadata(path) - .map(|data| { - let reader: Box = Box::new(DataReader { - data, - bytes_read: 0, - }); - reader - }) - .ok_or_else(|| AssetReaderError::NotFound(path.to_path_buf())) - }) + async fn read_meta<'a>(&'a self, path: &'a Path) -> Result>, AssetReaderError> { + self.root + .get_metadata(path) + .map(|data| { + let reader: Box = Box::new(DataReader { + data, + bytes_read: 0, + }); + reader + }) + .ok_or_else(|| AssetReaderError::NotFound(path.to_path_buf())) } - fn read_directory<'a>( + async fn read_directory<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, Result, AssetReaderError>> { - Box::pin(async move { - self.root - .get_dir(path) - .map(|dir| { - let stream: Box = Box::new(DirStream::new(dir)); - stream - }) - .ok_or_else(|| AssetReaderError::NotFound(path.to_path_buf())) - }) + ) -> Result, AssetReaderError> { + self.root + .get_dir(path) + .map(|dir| { + let stream: Box = Box::new(DirStream::new(dir)); + stream + }) + .ok_or_else(|| AssetReaderError::NotFound(path.to_path_buf())) } - fn is_directory<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result> { - Box::pin(async move { Ok(self.root.get_dir(path).is_some()) }) + async fn is_directory<'a>(&'a self, path: &'a Path) -> Result { + Ok(self.root.get_dir(path).is_some()) } } diff --git a/crates/bevy_asset/src/io/mod.rs b/crates/bevy_asset/src/io/mod.rs index 9b8f83b0eea7f..766e536455fa7 100644 --- a/crates/bevy_asset/src/io/mod.rs +++ b/crates/bevy_asset/src/io/mod.rs @@ -21,7 +21,7 @@ mod source; pub use futures_lite::{AsyncReadExt, AsyncWriteExt}; pub use source::*; -use bevy_utils::BoxedFuture; +use bevy_utils::{BoxedFuture, ConditionalSendFuture}; use futures_io::{AsyncRead, AsyncWrite}; use futures_lite::{ready, Stream}; use std::{ @@ -59,7 +59,7 @@ pub type Reader<'a> = dyn AsyncRead + Unpin + Send + Sync + 'a; /// Performs read operations on an asset storage. [`AssetReader`] exposes a "virtual filesystem" /// API, where asset bytes and asset metadata bytes are both stored and accessible for a given -/// `path`. +/// `path`. This trait is not object safe, if needed use a dyn [`ErasedAssetReader`] instead. /// /// Also see [`AssetWriter`]. pub trait AssetReader: Send + Sync + 'static { @@ -67,35 +67,90 @@ pub trait AssetReader: Send + Sync + 'static { fn read<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, Result>, AssetReaderError>>; + ) -> impl ConditionalSendFuture>, AssetReaderError>>; /// Returns a future to load the full file data at the provided path. fn read_meta<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, Result>, AssetReaderError>>; + ) -> impl ConditionalSendFuture>, AssetReaderError>>; /// Returns an iterator of directory entry names at the provided path. fn read_directory<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, Result, AssetReaderError>>; - /// Returns true if the provided path points to a directory. + ) -> impl ConditionalSendFuture, AssetReaderError>>; + /// Returns an iterator of directory entry names at the provided path. fn is_directory<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, Result>; - + ) -> impl ConditionalSendFuture>; /// Reads asset metadata bytes at the given `path` into a [`Vec`]. This is a convenience /// function that wraps [`AssetReader::read_meta`] by default. fn read_meta_bytes<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, Result, AssetReaderError>> { - Box::pin(async move { + ) -> impl ConditionalSendFuture, AssetReaderError>> { + async { let mut meta_reader = self.read_meta(path).await?; let mut meta_bytes = Vec::new(); meta_reader.read_to_end(&mut meta_bytes).await?; Ok(meta_bytes) - }) + } + } +} + +/// Equivalent to an [`AssetReader`] but using boxed futures, necessary eg. when using a `dyn AssetReader`, +/// as [`AssetReader`] isn't currently object safe. +pub trait ErasedAssetReader: Send + Sync + 'static { + /// Returns a future to load the full file data at the provided path. + fn read<'a>(&'a self, path: &'a Path) + -> BoxedFuture>, AssetReaderError>>; + /// Returns a future to load the full file data at the provided path. + fn read_meta<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture>, AssetReaderError>>; + /// Returns an iterator of directory entry names at the provided path. + fn read_directory<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture, AssetReaderError>>; + /// Returns true if the provided path points to a directory. + fn is_directory<'a>(&'a self, path: &'a Path) -> BoxedFuture>; + /// Reads asset metadata bytes at the given `path` into a [`Vec`]. This is a convenience + /// function that wraps [`ErasedAssetReader::read_meta`] by default. + fn read_meta_bytes<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture, AssetReaderError>>; +} + +impl ErasedAssetReader for T { + fn read<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture>, AssetReaderError>> { + Box::pin(Self::read(self, path)) + } + fn read_meta<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture>, AssetReaderError>> { + Box::pin(Self::read_meta(self, path)) + } + fn read_directory<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture, AssetReaderError>> { + Box::pin(Self::read_directory(self, path)) + } + fn is_directory<'a>(&'a self, path: &'a Path) -> BoxedFuture> { + Box::pin(Self::is_directory(self, path)) + } + fn read_meta_bytes<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture, AssetReaderError>> { + Box::pin(Self::read_meta_bytes(self, path)) } } @@ -113,7 +168,7 @@ pub enum AssetWriterError { /// Preforms write operations on an asset storage. [`AssetWriter`] exposes a "virtual filesystem" /// API, where asset bytes and asset metadata bytes are both stored and accessible for a given -/// `path`. +/// `path`. This trait is not object safe, if needed use a dyn [`ErasedAssetWriter`] instead. /// /// Also see [`AssetReader`]. pub trait AssetWriter: Send + Sync + 'static { @@ -121,72 +176,195 @@ pub trait AssetWriter: Send + Sync + 'static { fn write<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, Result, AssetWriterError>>; + ) -> impl ConditionalSendFuture, AssetWriterError>>; /// Writes the full asset meta bytes at the provided path. /// This _should not_ include storage specific extensions like `.meta`. fn write_meta<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, Result, AssetWriterError>>; + ) -> impl ConditionalSendFuture, AssetWriterError>>; /// Removes the asset stored at the given path. - fn remove<'a>(&'a self, path: &'a Path) -> BoxedFuture<'a, Result<(), AssetWriterError>>; + fn remove<'a>( + &'a self, + path: &'a Path, + ) -> impl ConditionalSendFuture>; /// Removes the asset meta stored at the given path. /// This _should not_ include storage specific extensions like `.meta`. - fn remove_meta<'a>(&'a self, path: &'a Path) -> BoxedFuture<'a, Result<(), AssetWriterError>>; + fn remove_meta<'a>( + &'a self, + path: &'a Path, + ) -> impl ConditionalSendFuture>; /// Renames the asset at `old_path` to `new_path` fn rename<'a>( &'a self, old_path: &'a Path, new_path: &'a Path, - ) -> BoxedFuture<'a, Result<(), AssetWriterError>>; + ) -> impl ConditionalSendFuture>; /// Renames the asset meta for the asset at `old_path` to `new_path`. /// This _should not_ include storage specific extensions like `.meta`. fn rename_meta<'a>( &'a self, old_path: &'a Path, new_path: &'a Path, - ) -> BoxedFuture<'a, Result<(), AssetWriterError>>; + ) -> impl ConditionalSendFuture>; /// Removes the directory at the given path, including all assets _and_ directories in that directory. fn remove_directory<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, Result<(), AssetWriterError>>; + ) -> impl ConditionalSendFuture>; /// Removes the directory at the given path, but only if it is completely empty. This will return an error if the /// directory is not empty. fn remove_empty_directory<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, Result<(), AssetWriterError>>; + ) -> impl ConditionalSendFuture>; /// Removes all assets (and directories) in this directory, resulting in an empty directory. fn remove_assets_in_directory<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, Result<(), AssetWriterError>>; + ) -> impl ConditionalSendFuture>; /// Writes the asset `bytes` to the given `path`. fn write_bytes<'a>( &'a self, path: &'a Path, bytes: &'a [u8], - ) -> BoxedFuture<'a, Result<(), AssetWriterError>> { - Box::pin(async move { + ) -> impl ConditionalSendFuture> { + async { let mut writer = self.write(path).await?; writer.write_all(bytes).await?; writer.flush().await?; Ok(()) - }) + } } /// Writes the asset meta `bytes` to the given `path`. fn write_meta_bytes<'a>( &'a self, path: &'a Path, bytes: &'a [u8], - ) -> BoxedFuture<'a, Result<(), AssetWriterError>> { - Box::pin(async move { + ) -> impl ConditionalSendFuture> { + async { let mut meta_writer = self.write_meta(path).await?; meta_writer.write_all(bytes).await?; meta_writer.flush().await?; Ok(()) - }) + } + } +} + +/// Equivalent to an [`AssetWriter`] but using boxed futures, necessary eg. when using a `dyn AssetWriter`, +/// as [`AssetWriter`] isn't currently object safe. +pub trait ErasedAssetWriter: Send + Sync + 'static { + /// Writes the full asset bytes at the provided path. + fn write<'a>(&'a self, path: &'a Path) -> BoxedFuture, AssetWriterError>>; + /// Writes the full asset meta bytes at the provided path. + /// This _should not_ include storage specific extensions like `.meta`. + fn write_meta<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture, AssetWriterError>>; + /// Removes the asset stored at the given path. + fn remove<'a>(&'a self, path: &'a Path) -> BoxedFuture>; + /// Removes the asset meta stored at the given path. + /// This _should not_ include storage specific extensions like `.meta`. + fn remove_meta<'a>(&'a self, path: &'a Path) -> BoxedFuture>; + /// Renames the asset at `old_path` to `new_path` + fn rename<'a>( + &'a self, + old_path: &'a Path, + new_path: &'a Path, + ) -> BoxedFuture>; + /// Renames the asset meta for the asset at `old_path` to `new_path`. + /// This _should not_ include storage specific extensions like `.meta`. + fn rename_meta<'a>( + &'a self, + old_path: &'a Path, + new_path: &'a Path, + ) -> BoxedFuture>; + /// Removes the directory at the given path, including all assets _and_ directories in that directory. + fn remove_directory<'a>(&'a self, path: &'a Path) -> BoxedFuture>; + /// Removes the directory at the given path, but only if it is completely empty. This will return an error if the + /// directory is not empty. + fn remove_empty_directory<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture>; + /// Removes all assets (and directories) in this directory, resulting in an empty directory. + fn remove_assets_in_directory<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture>; + /// Writes the asset `bytes` to the given `path`. + fn write_bytes<'a>( + &'a self, + path: &'a Path, + bytes: &'a [u8], + ) -> BoxedFuture>; + /// Writes the asset meta `bytes` to the given `path`. + fn write_meta_bytes<'a>( + &'a self, + path: &'a Path, + bytes: &'a [u8], + ) -> BoxedFuture>; +} + +impl ErasedAssetWriter for T { + fn write<'a>(&'a self, path: &'a Path) -> BoxedFuture, AssetWriterError>> { + Box::pin(Self::write(self, path)) + } + fn write_meta<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture, AssetWriterError>> { + Box::pin(Self::write_meta(self, path)) + } + fn remove<'a>(&'a self, path: &'a Path) -> BoxedFuture> { + Box::pin(Self::remove(self, path)) + } + fn remove_meta<'a>(&'a self, path: &'a Path) -> BoxedFuture> { + Box::pin(Self::remove_meta(self, path)) + } + fn rename<'a>( + &'a self, + old_path: &'a Path, + new_path: &'a Path, + ) -> BoxedFuture> { + Box::pin(Self::rename(self, old_path, new_path)) + } + fn rename_meta<'a>( + &'a self, + old_path: &'a Path, + new_path: &'a Path, + ) -> BoxedFuture> { + Box::pin(Self::rename_meta(self, old_path, new_path)) + } + fn remove_directory<'a>(&'a self, path: &'a Path) -> BoxedFuture> { + Box::pin(Self::remove_directory(self, path)) + } + fn remove_empty_directory<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture> { + Box::pin(Self::remove_empty_directory(self, path)) + } + fn remove_assets_in_directory<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture> { + Box::pin(Self::remove_assets_in_directory(self, path)) + } + fn write_bytes<'a>( + &'a self, + path: &'a Path, + bytes: &'a [u8], + ) -> BoxedFuture> { + Box::pin(Self::write_bytes(self, path, bytes)) + } + fn write_meta_bytes<'a>( + &'a self, + path: &'a Path, + bytes: &'a [u8], + ) -> BoxedFuture> { + Box::pin(Self::write_meta_bytes(self, path, bytes)) } } diff --git a/crates/bevy_asset/src/io/processor_gated.rs b/crates/bevy_asset/src/io/processor_gated.rs index b86460f04b6fd..5f3bfe1467838 100644 --- a/crates/bevy_asset/src/io/processor_gated.rs +++ b/crates/bevy_asset/src/io/processor_gated.rs @@ -5,16 +5,17 @@ use crate::{ }; use async_lock::RwLockReadGuardArc; use bevy_utils::tracing::trace; -use bevy_utils::BoxedFuture; use futures_io::AsyncRead; use std::{path::Path, pin::Pin, sync::Arc}; +use super::ErasedAssetReader; + /// An [`AssetReader`] that will prevent asset (and asset metadata) read futures from returning for a /// given path until that path has been processed by [`AssetProcessor`]. /// /// [`AssetProcessor`]: crate::processor::AssetProcessor pub struct ProcessorGatedReader { - reader: Box, + reader: Box, source: AssetSourceId<'static>, processor_data: Arc, } @@ -23,7 +24,7 @@ impl ProcessorGatedReader { /// Creates a new [`ProcessorGatedReader`]. pub fn new( source: AssetSourceId<'static>, - reader: Box, + reader: Box, processor_data: Arc, ) -> Self { Self { @@ -48,87 +49,69 @@ impl ProcessorGatedReader { } impl AssetReader for ProcessorGatedReader { - fn read<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result>, AssetReaderError>> { - Box::pin(async move { - let asset_path = AssetPath::from(path.to_path_buf()).with_source(self.source.clone()); - trace!("Waiting for processing to finish before reading {asset_path}"); - let process_result = self - .processor_data - .wait_until_processed(asset_path.clone()) - .await; - match process_result { - ProcessStatus::Processed => {} - ProcessStatus::Failed | ProcessStatus::NonExistent => { - return Err(AssetReaderError::NotFound(path.to_owned())); - } + async fn read<'a>(&'a self, path: &'a Path) -> Result>, AssetReaderError> { + let asset_path = AssetPath::from(path.to_path_buf()).with_source(self.source.clone()); + trace!("Waiting for processing to finish before reading {asset_path}"); + let process_result = self + .processor_data + .wait_until_processed(asset_path.clone()) + .await; + match process_result { + ProcessStatus::Processed => {} + ProcessStatus::Failed | ProcessStatus::NonExistent => { + return Err(AssetReaderError::NotFound(path.to_owned())); } - trace!("Processing finished with {asset_path}, reading {process_result:?}",); - let lock = self.get_transaction_lock(&asset_path).await?; - let asset_reader = self.reader.read(path).await?; - let reader: Box> = - Box::new(TransactionLockedReader::new(asset_reader, lock)); - Ok(reader) - }) + } + trace!("Processing finished with {asset_path}, reading {process_result:?}",); + let lock = self.get_transaction_lock(&asset_path).await?; + let asset_reader = self.reader.read(path).await?; + let reader: Box> = Box::new(TransactionLockedReader::new(asset_reader, lock)); + Ok(reader) } - fn read_meta<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result>, AssetReaderError>> { - Box::pin(async move { - let asset_path = AssetPath::from(path.to_path_buf()).with_source(self.source.clone()); - trace!("Waiting for processing to finish before reading meta for {asset_path}",); - let process_result = self - .processor_data - .wait_until_processed(asset_path.clone()) - .await; - match process_result { - ProcessStatus::Processed => {} - ProcessStatus::Failed | ProcessStatus::NonExistent => { - return Err(AssetReaderError::NotFound(path.to_owned())); - } + async fn read_meta<'a>(&'a self, path: &'a Path) -> Result>, AssetReaderError> { + let asset_path = AssetPath::from(path.to_path_buf()).with_source(self.source.clone()); + trace!("Waiting for processing to finish before reading meta for {asset_path}",); + let process_result = self + .processor_data + .wait_until_processed(asset_path.clone()) + .await; + match process_result { + ProcessStatus::Processed => {} + ProcessStatus::Failed | ProcessStatus::NonExistent => { + return Err(AssetReaderError::NotFound(path.to_owned())); } - trace!("Processing finished with {process_result:?}, reading meta for {asset_path}",); - let lock = self.get_transaction_lock(&asset_path).await?; - let meta_reader = self.reader.read_meta(path).await?; - let reader: Box> = Box::new(TransactionLockedReader::new(meta_reader, lock)); - Ok(reader) - }) + } + trace!("Processing finished with {process_result:?}, reading meta for {asset_path}",); + let lock = self.get_transaction_lock(&asset_path).await?; + let meta_reader = self.reader.read_meta(path).await?; + let reader: Box> = Box::new(TransactionLockedReader::new(meta_reader, lock)); + Ok(reader) } - fn read_directory<'a>( + async fn read_directory<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, Result, AssetReaderError>> { - Box::pin(async move { - trace!( - "Waiting for processing to finish before reading directory {:?}", - path - ); - self.processor_data.wait_until_finished().await; - trace!("Processing finished, reading directory {:?}", path); - let result = self.reader.read_directory(path).await?; - Ok(result) - }) + ) -> Result, AssetReaderError> { + trace!( + "Waiting for processing to finish before reading directory {:?}", + path + ); + self.processor_data.wait_until_finished().await; + trace!("Processing finished, reading directory {:?}", path); + let result = self.reader.read_directory(path).await?; + Ok(result) } - fn is_directory<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result> { - Box::pin(async move { - trace!( - "Waiting for processing to finish before reading directory {:?}", - path - ); - self.processor_data.wait_until_finished().await; - trace!("Processing finished, getting directory status {:?}", path); - let result = self.reader.is_directory(path).await?; - Ok(result) - }) + async fn is_directory<'a>(&'a self, path: &'a Path) -> Result { + trace!( + "Waiting for processing to finish before reading directory {:?}", + path + ); + self.processor_data.wait_until_finished().await; + trace!("Processing finished, getting directory status {:?}", path); + let result = self.reader.is_directory(path).await?; + Ok(result) } } diff --git a/crates/bevy_asset/src/io/source.rs b/crates/bevy_asset/src/io/source.rs index 7293a73bea418..21bd29294e31b 100644 --- a/crates/bevy_asset/src/io/source.rs +++ b/crates/bevy_asset/src/io/source.rs @@ -1,8 +1,5 @@ use crate::{ - io::{ - processor_gated::ProcessorGatedReader, AssetReader, AssetSourceEvent, AssetWatcher, - AssetWriter, - }, + io::{processor_gated::ProcessorGatedReader, AssetSourceEvent, AssetWatcher}, processor::AssetProcessorData, }; use bevy_ecs::system::Resource; @@ -11,6 +8,12 @@ use bevy_utils::{CowArc, Duration, HashMap}; use std::{fmt::Display, hash::Hash, sync::Arc}; use thiserror::Error; +use super::{ErasedAssetReader, ErasedAssetWriter}; + +// Needed for doc strings. +#[allow(unused_imports)] +use crate::io::{AssetReader, AssetWriter}; + /// A reference to an "asset source", which maps to an [`AssetReader`] and/or [`AssetWriter`]. /// /// * [`AssetSourceId::Default`] corresponds to "default asset paths" that don't specify a source: `/path/to/asset.png` @@ -110,8 +113,8 @@ impl<'a> PartialEq for AssetSourceId<'a> { /// and whether or not the source is processed. #[derive(Default)] pub struct AssetSourceBuilder { - pub reader: Option Box + Send + Sync>>, - pub writer: Option Option> + Send + Sync>>, + pub reader: Option Box + Send + Sync>>, + pub writer: Option Option> + Send + Sync>>, pub watcher: Option< Box< dyn FnMut(crossbeam_channel::Sender) -> Option> @@ -119,9 +122,9 @@ pub struct AssetSourceBuilder { + Sync, >, >, - pub processed_reader: Option Box + Send + Sync>>, + pub processed_reader: Option Box + Send + Sync>>, pub processed_writer: - Option Option> + Send + Sync>>, + Option Option> + Send + Sync>>, pub processed_watcher: Option< Box< dyn FnMut(crossbeam_channel::Sender) -> Option> @@ -192,7 +195,7 @@ impl AssetSourceBuilder { /// Will use the given `reader` function to construct unprocessed [`AssetReader`] instances. pub fn with_reader( mut self, - reader: impl FnMut() -> Box + Send + Sync + 'static, + reader: impl FnMut() -> Box + Send + Sync + 'static, ) -> Self { self.reader = Some(Box::new(reader)); self @@ -201,7 +204,7 @@ impl AssetSourceBuilder { /// Will use the given `writer` function to construct unprocessed [`AssetWriter`] instances. pub fn with_writer( mut self, - writer: impl FnMut(bool) -> Option> + Send + Sync + 'static, + writer: impl FnMut(bool) -> Option> + Send + Sync + 'static, ) -> Self { self.writer = Some(Box::new(writer)); self @@ -222,7 +225,7 @@ impl AssetSourceBuilder { /// Will use the given `reader` function to construct processed [`AssetReader`] instances. pub fn with_processed_reader( mut self, - reader: impl FnMut() -> Box + Send + Sync + 'static, + reader: impl FnMut() -> Box + Send + Sync + 'static, ) -> Self { self.processed_reader = Some(Box::new(reader)); self @@ -231,7 +234,7 @@ impl AssetSourceBuilder { /// Will use the given `writer` function to construct processed [`AssetWriter`] instances. pub fn with_processed_writer( mut self, - writer: impl FnMut(bool) -> Option> + Send + Sync + 'static, + writer: impl FnMut(bool) -> Option> + Send + Sync + 'static, ) -> Self { self.processed_writer = Some(Box::new(writer)); self @@ -355,10 +358,10 @@ impl AssetSourceBuilders { /// for a specific asset source, identified by an [`AssetSourceId`]. pub struct AssetSource { id: AssetSourceId<'static>, - reader: Box, - writer: Option>, - processed_reader: Option>, - processed_writer: Option>, + reader: Box, + writer: Option>, + processed_reader: Option>, + processed_writer: Option>, watcher: Option>, processed_watcher: Option>, event_receiver: Option>, @@ -379,13 +382,13 @@ impl AssetSource { /// Return's this source's unprocessed [`AssetReader`]. #[inline] - pub fn reader(&self) -> &dyn AssetReader { + pub fn reader(&self) -> &dyn ErasedAssetReader { &*self.reader } /// Return's this source's unprocessed [`AssetWriter`], if it exists. #[inline] - pub fn writer(&self) -> Result<&dyn AssetWriter, MissingAssetWriterError> { + pub fn writer(&self) -> Result<&dyn ErasedAssetWriter, MissingAssetWriterError> { self.writer .as_deref() .ok_or_else(|| MissingAssetWriterError(self.id.clone_owned())) @@ -393,7 +396,9 @@ impl AssetSource { /// Return's this source's processed [`AssetReader`], if it exists. #[inline] - pub fn processed_reader(&self) -> Result<&dyn AssetReader, MissingProcessedAssetReaderError> { + pub fn processed_reader( + &self, + ) -> Result<&dyn ErasedAssetReader, MissingProcessedAssetReaderError> { self.processed_reader .as_deref() .ok_or_else(|| MissingProcessedAssetReaderError(self.id.clone_owned())) @@ -401,7 +406,9 @@ impl AssetSource { /// Return's this source's processed [`AssetWriter`], if it exists. #[inline] - pub fn processed_writer(&self) -> Result<&dyn AssetWriter, MissingProcessedAssetWriterError> { + pub fn processed_writer( + &self, + ) -> Result<&dyn ErasedAssetWriter, MissingProcessedAssetWriterError> { self.processed_writer .as_deref() .ok_or_else(|| MissingProcessedAssetWriterError(self.id.clone_owned())) @@ -429,7 +436,9 @@ impl AssetSource { /// Returns a builder function for this platform's default [`AssetReader`]. `path` is the relative path to /// the asset root. - pub fn get_default_reader(_path: String) -> impl FnMut() -> Box + Send + Sync { + pub fn get_default_reader( + _path: String, + ) -> impl FnMut() -> Box + Send + Sync { move || { #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))] return Box::new(super::file::FileAssetReader::new(&_path)); @@ -444,7 +453,7 @@ impl AssetSource { /// the asset root. This will return [`None`] if this platform does not support writing assets by default. pub fn get_default_writer( _path: String, - ) -> impl FnMut(bool) -> Option> + Send + Sync { + ) -> impl FnMut(bool) -> Option> + Send + Sync { move |_create_root: bool| { #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))] return Some(Box::new(super::file::FileAssetWriter::new( diff --git a/crates/bevy_asset/src/io/wasm.rs b/crates/bevy_asset/src/io/wasm.rs index aab497ddfa7cc..95f490550815c 100644 --- a/crates/bevy_asset/src/io/wasm.rs +++ b/crates/bevy_asset/src/io/wasm.rs @@ -2,7 +2,6 @@ use crate::io::{ get_meta_path, AssetReader, AssetReaderError, EmptyPathStream, PathStream, Reader, VecReader, }; use bevy_utils::tracing::error; -use bevy_utils::BoxedFuture; use js_sys::{Uint8Array, JSON}; use std::path::{Path, PathBuf}; use wasm_bindgen::{JsCast, JsValue}; @@ -59,40 +58,30 @@ impl HttpWasmAssetReader { } impl AssetReader for HttpWasmAssetReader { - fn read<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result>, AssetReaderError>> { - Box::pin(async move { - let path = self.root_path.join(path); - self.fetch_bytes(path).await - }) + async fn read<'a>(&'a self, path: &'a Path) -> Result>, AssetReaderError> { + let path = self.root_path.join(path); + self.fetch_bytes(path).await } - fn read_meta<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result>, AssetReaderError>> { - Box::pin(async move { - let meta_path = get_meta_path(&self.root_path.join(path)); - Ok(self.fetch_bytes(meta_path).await?) - }) + async fn read_meta<'a>(&'a self, path: &'a Path) -> Result>, AssetReaderError> { + let meta_path = get_meta_path(&self.root_path.join(path)); + Ok(self.fetch_bytes(meta_path).await?) } - fn read_directory<'a>( + async fn read_directory<'a>( &'a self, _path: &'a Path, - ) -> BoxedFuture<'a, Result, AssetReaderError>> { + ) -> Result, AssetReaderError> { let stream: Box = Box::new(EmptyPathStream); error!("Reading directories is not supported with the HttpWasmAssetReader"); - Box::pin(async move { Ok(stream) }) + Ok(stream) } - fn is_directory<'a>( + async fn is_directory<'a>( &'a self, _path: &'a Path, - ) -> BoxedFuture<'a, std::result::Result> { + ) -> std::result::Result { error!("Reading directories is not supported with the HttpWasmAssetReader"); - Box::pin(async move { Ok(false) }) + Ok(false) } } diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index 4e579a04f2597..f15797f619402 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -40,8 +40,6 @@ pub use path::*; pub use reflect::*; pub use server::*; -pub use bevy_utils::BoxedFuture; - /// Rusty Object Notation, a crate used to serialize and deserialize bevy assets. pub use ron; @@ -448,7 +446,7 @@ mod tests { }; use bevy_log::LogPlugin; use bevy_reflect::TypePath; - use bevy_utils::{BoxedFuture, Duration, HashMap}; + use bevy_utils::{Duration, HashMap}; use futures_lite::AsyncReadExt; use serde::{Deserialize, Serialize}; use std::{path::Path, sync::Arc}; @@ -497,40 +495,38 @@ mod tests { type Error = CoolTextLoaderError; - fn load<'a>( + async fn load<'a>( &'a self, - reader: &'a mut Reader, + reader: &'a mut Reader<'_>, _settings: &'a Self::Settings, - load_context: &'a mut LoadContext, - ) -> BoxedFuture<'a, Result> { - Box::pin(async move { - let mut bytes = Vec::new(); - reader.read_to_end(&mut bytes).await?; - let mut ron: CoolTextRon = ron::de::from_bytes(&bytes)?; - let mut embedded = String::new(); - for dep in ron.embedded_dependencies { - let loaded = load_context.load_direct(&dep).await.map_err(|_| { - Self::Error::CannotLoadDependency { - dependency: dep.into(), - } - })?; - let cool = loaded.get::().unwrap(); - embedded.push_str(&cool.text); - } - Ok(CoolText { - text: ron.text, - embedded, - dependencies: ron - .dependencies - .iter() - .map(|p| load_context.load(p)) - .collect(), - sub_texts: ron - .sub_texts - .drain(..) - .map(|text| load_context.add_labeled_asset(text.clone(), SubText { text })) - .collect(), - }) + load_context: &'a mut LoadContext<'_>, + ) -> Result { + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).await?; + let mut ron: CoolTextRon = ron::de::from_bytes(&bytes)?; + let mut embedded = String::new(); + for dep in ron.embedded_dependencies { + let loaded = load_context.load_direct(&dep).await.map_err(|_| { + Self::Error::CannotLoadDependency { + dependency: dep.into(), + } + })?; + let cool = loaded.get::().unwrap(); + embedded.push_str(&cool.text); + } + Ok(CoolText { + text: ron.text, + embedded, + dependencies: ron + .dependencies + .iter() + .map(|p| load_context.load(p)) + .collect(), + sub_texts: ron + .sub_texts + .drain(..) + .map(|text| load_context.add_labeled_asset(text.clone(), SubText { text })) + .collect(), }) } @@ -560,31 +556,25 @@ mod tests { } impl AssetReader for UnstableMemoryAssetReader { - fn is_directory<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result> { - self.memory_reader.is_directory(path) + async fn is_directory<'a>(&'a self, path: &'a Path) -> Result { + self.memory_reader.is_directory(path).await } - fn read_directory<'a>( + async fn read_directory<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, Result, AssetReaderError>> { - self.memory_reader.read_directory(path) + ) -> Result, AssetReaderError> { + self.memory_reader.read_directory(path).await } - fn read_meta<'a>( + async fn read_meta<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, Result>, AssetReaderError>> { - self.memory_reader.read_meta(path) + ) -> Result>, AssetReaderError> { + self.memory_reader.read_meta(path).await } - fn read<'a>( + async fn read<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture< - 'a, - Result>, bevy_asset::io::AssetReaderError>, - > { + ) -> Result>, bevy_asset::io::AssetReaderError> { let attempt_number = { let mut attempt_counters = self.attempt_counters.lock().unwrap(); if let Some(existing) = attempt_counters.get_mut(path) { @@ -605,13 +595,14 @@ mod tests { ), ); let wait = self.load_delay; - return Box::pin(async move { + return async move { std::thread::sleep(wait); Err(AssetReaderError::Io(io_error.into())) - }); + } + .await; } - self.memory_reader.read(path) + self.memory_reader.read(path).await } } diff --git a/crates/bevy_asset/src/loader.rs b/crates/bevy_asset/src/loader.rs index 627b286482a5d..ae3f08511adbf 100644 --- a/crates/bevy_asset/src/loader.rs +++ b/crates/bevy_asset/src/loader.rs @@ -9,7 +9,7 @@ use crate::{ UntypedAssetId, UntypedHandle, }; use bevy_ecs::world::World; -use bevy_utils::{BoxedFuture, CowArc, HashMap, HashSet}; +use bevy_utils::{BoxedFuture, ConditionalSendFuture, CowArc, HashMap, HashSet}; use downcast_rs::{impl_downcast, Downcast}; use futures_lite::AsyncReadExt; use ron::error::SpannedError; @@ -35,7 +35,7 @@ pub trait AssetLoader: Send + Sync + 'static { reader: &'a mut Reader, settings: &'a Self::Settings, load_context: &'a mut LoadContext, - ) -> BoxedFuture<'a, Result>; + ) -> impl ConditionalSendFuture>; /// Returns a list of extensions supported by this [`AssetLoader`], without the preceding dot. /// Note that users of this [`AssetLoader`] may choose to load files with a non-matching extension. diff --git a/crates/bevy_asset/src/meta.rs b/crates/bevy_asset/src/meta.rs index 2b082e9550589..ccc1df9b727ca 100644 --- a/crates/bevy_asset/src/meta.rs +++ b/crates/bevy_asset/src/meta.rs @@ -171,12 +171,12 @@ impl Process for () { type Settings = (); type OutputLoader = (); - fn process<'a>( + async fn process<'a>( &'a self, - _context: &'a mut bevy_asset::processor::ProcessContext, + _context: &'a mut bevy_asset::processor::ProcessContext<'_>, _meta: AssetMeta<(), Self>, _writer: &'a mut bevy_asset::io::Writer, - ) -> bevy_utils::BoxedFuture<'a, Result<(), bevy_asset::processor::ProcessError>> { + ) -> Result<(), bevy_asset::processor::ProcessError> { unreachable!() } } @@ -194,12 +194,12 @@ impl AssetLoader for () { type Asset = (); type Settings = (); type Error = std::io::Error; - fn load<'a>( + async fn load<'a>( &'a self, - _reader: &'a mut crate::io::Reader, + _reader: &'a mut crate::io::Reader<'_>, _settings: &'a Self::Settings, - _load_context: &'a mut crate::LoadContext, - ) -> bevy_utils::BoxedFuture<'a, Result> { + _load_context: &'a mut crate::LoadContext<'_>, + ) -> Result { unreachable!(); } diff --git a/crates/bevy_asset/src/processor/mod.rs b/crates/bevy_asset/src/processor/mod.rs index ace12c8f7301b..b0724e4e17358 100644 --- a/crates/bevy_asset/src/processor/mod.rs +++ b/crates/bevy_asset/src/processor/mod.rs @@ -6,8 +6,9 @@ pub use process::*; use crate::{ io::{ - AssetReader, AssetReaderError, AssetSource, AssetSourceBuilders, AssetSourceEvent, - AssetSourceId, AssetSources, AssetWriter, AssetWriterError, MissingAssetSourceError, + AssetReaderError, AssetSource, AssetSourceBuilders, AssetSourceEvent, AssetSourceId, + AssetSources, AssetWriterError, ErasedAssetReader, ErasedAssetWriter, + MissingAssetSourceError, }, meta::{ get_asset_hash, get_full_asset_hash, AssetAction, AssetActionMinimal, AssetHash, AssetMeta, @@ -30,6 +31,10 @@ use std::{ }; use thiserror::Error; +// Needed for doc strings +#[allow(unused_imports)] +use crate::io::{AssetReader, AssetWriter}; + /// A "background" asset processor that reads asset values from a source [`AssetSource`] (which corresponds to an [`AssetReader`] / [`AssetWriter`] pair), /// processes them in some way, and writes them to a destination [`AssetSource`]. /// @@ -510,8 +515,8 @@ impl AssetProcessor { /// Retrieves asset paths recursively. If `clean_empty_folders_writer` is Some, it will be used to clean up empty /// folders when they are discovered. fn get_asset_paths<'a>( - reader: &'a dyn AssetReader, - clean_empty_folders_writer: Option<&'a dyn AssetWriter>, + reader: &'a dyn ErasedAssetReader, + clean_empty_folders_writer: Option<&'a dyn ErasedAssetWriter>, path: PathBuf, paths: &'a mut Vec, ) -> BoxedFuture<'a, Result> { @@ -717,9 +722,7 @@ impl AssetProcessor { (meta, Some(processor)) } AssetActionMinimal::Ignore => { - let meta: Box = - Box::new(AssetMeta::<(), ()>::deserialize(&meta_bytes)?); - (meta, None) + return Ok(ProcessResult::Ignored); } }; (meta, meta_bytes, processor) @@ -1033,6 +1036,7 @@ impl AssetProcessorData { pub enum ProcessResult { Processed(ProcessedInfo), SkippedNotChanged, + Ignored, } /// The final status of processing an asset @@ -1180,6 +1184,9 @@ impl ProcessorAssetInfos { // "block until first pass finished" mode info.update_status(ProcessStatus::Processed).await; } + Ok(ProcessResult::Ignored) => { + debug!("Skipping processing (ignored) \"{:?}\"", asset_path); + } Err(ProcessError::ExtensionRequired) => { // Skip assets without extensions } diff --git a/crates/bevy_asset/src/processor/process.rs b/crates/bevy_asset/src/processor/process.rs index 75b10acfa2640..ce18bb53da2d8 100644 --- a/crates/bevy_asset/src/processor/process.rs +++ b/crates/bevy_asset/src/processor/process.rs @@ -10,7 +10,7 @@ use crate::{ AssetLoadError, AssetLoader, AssetPath, DeserializeMetaError, ErasedLoadedAsset, MissingAssetLoaderForExtensionError, MissingAssetLoaderForTypeNameError, }; -use bevy_utils::BoxedFuture; +use bevy_utils::{BoxedFuture, ConditionalSendFuture}; use serde::{Deserialize, Serialize}; use std::marker::PhantomData; use thiserror::Error; @@ -32,7 +32,9 @@ pub trait Process: Send + Sync + Sized + 'static { context: &'a mut ProcessContext, meta: AssetMeta<(), Self>, writer: &'a mut Writer, - ) -> BoxedFuture<'a, Result<::Settings, ProcessError>>; + ) -> impl ConditionalSendFuture< + Output = Result<::Settings, ProcessError>, + >; } /// A flexible [`Process`] implementation that loads the source [`Asset`] using the `L` [`AssetLoader`], then transforms @@ -173,41 +175,38 @@ impl< type Settings = LoadTransformAndSaveSettings; type OutputLoader = Saver::OutputLoader; - fn process<'a>( + async fn process<'a>( &'a self, - context: &'a mut ProcessContext, + context: &'a mut ProcessContext<'_>, meta: AssetMeta<(), Self>, writer: &'a mut Writer, - ) -> BoxedFuture<'a, Result<::Settings, ProcessError>> { - Box::pin(async move { - let AssetAction::Process { settings, .. } = meta.asset else { - return Err(ProcessError::WrongMetaType); - }; - let loader_meta = AssetMeta::::new(AssetAction::Load { - loader: std::any::type_name::().to_string(), - settings: settings.loader_settings, - }); - let pre_transformed_asset = TransformedAsset::::from_loaded( - context.load_source_asset(loader_meta).await?, - ) - .unwrap(); + ) -> Result<::Settings, ProcessError> { + let AssetAction::Process { settings, .. } = meta.asset else { + return Err(ProcessError::WrongMetaType); + }; + let loader_meta = AssetMeta::::new(AssetAction::Load { + loader: std::any::type_name::().to_string(), + settings: settings.loader_settings, + }); + let pre_transformed_asset = TransformedAsset::::from_loaded( + context.load_source_asset(loader_meta).await?, + ) + .unwrap(); - let post_transformed_asset = self - .transformer - .transform(pre_transformed_asset, &settings.transformer_settings) - .await - .map_err(|err| ProcessError::AssetTransformError(err.into()))?; + let post_transformed_asset = self + .transformer + .transform(pre_transformed_asset, &settings.transformer_settings) + .await + .map_err(|err| ProcessError::AssetTransformError(err.into()))?; - let saved_asset = - SavedAsset::::from_transformed(&post_transformed_asset); + let saved_asset = SavedAsset::::from_transformed(&post_transformed_asset); - let output_settings = self - .saver - .save(writer, saved_asset, &settings.saver_settings) - .await - .map_err(|error| ProcessError::AssetSaveError(error.into()))?; - Ok(output_settings) - }) + let output_settings = self + .saver + .save(writer, saved_asset, &settings.saver_settings) + .await + .map_err(|error| ProcessError::AssetSaveError(error.into()))?; + Ok(output_settings) } } @@ -217,29 +216,27 @@ impl> Process type Settings = LoadAndSaveSettings; type OutputLoader = Saver::OutputLoader; - fn process<'a>( + async fn process<'a>( &'a self, - context: &'a mut ProcessContext, + context: &'a mut ProcessContext<'_>, meta: AssetMeta<(), Self>, writer: &'a mut Writer, - ) -> BoxedFuture<'a, Result<::Settings, ProcessError>> { - Box::pin(async move { - let AssetAction::Process { settings, .. } = meta.asset else { - return Err(ProcessError::WrongMetaType); - }; - let loader_meta = AssetMeta::::new(AssetAction::Load { - loader: std::any::type_name::().to_string(), - settings: settings.loader_settings, - }); - let loaded_asset = context.load_source_asset(loader_meta).await?; - let saved_asset = SavedAsset::::from_loaded(&loaded_asset).unwrap(); - let output_settings = self - .saver - .save(writer, saved_asset, &settings.saver_settings) - .await - .map_err(|error| ProcessError::AssetSaveError(error.into()))?; - Ok(output_settings) - }) + ) -> Result<::Settings, ProcessError> { + let AssetAction::Process { settings, .. } = meta.asset else { + return Err(ProcessError::WrongMetaType); + }; + let loader_meta = AssetMeta::::new(AssetAction::Load { + loader: std::any::type_name::().to_string(), + settings: settings.loader_settings, + }); + let loaded_asset = context.load_source_asset(loader_meta).await?; + let saved_asset = SavedAsset::::from_loaded(&loaded_asset).unwrap(); + let output_settings = self + .saver + .save(writer, saved_asset, &settings.saver_settings) + .await + .map_err(|error| ProcessError::AssetSaveError(error.into()))?; + Ok(output_settings) } } @@ -367,6 +364,12 @@ impl<'a> ProcessContext<'a> { Ok(loaded_asset) } + /// The path of the asset being processed. + #[inline] + pub fn path(&self) -> &AssetPath<'static> { + self.path + } + /// The source bytes of the asset being processed. #[inline] pub fn asset_bytes(&self) -> &[u8] { diff --git a/crates/bevy_asset/src/saver.rs b/crates/bevy_asset/src/saver.rs index a366338f7c376..36408dd125f29 100644 --- a/crates/bevy_asset/src/saver.rs +++ b/crates/bevy_asset/src/saver.rs @@ -1,7 +1,7 @@ use crate::transformer::TransformedAsset; use crate::{io::Writer, meta::Settings, Asset, ErasedLoadedAsset}; use crate::{AssetLoader, Handle, LabeledAsset, UntypedHandle}; -use bevy_utils::{BoxedFuture, CowArc, HashMap}; +use bevy_utils::{BoxedFuture, ConditionalSendFuture, CowArc, HashMap}; use serde::{Deserialize, Serialize}; use std::{borrow::Borrow, hash::Hash, ops::Deref}; @@ -24,7 +24,9 @@ pub trait AssetSaver: Send + Sync + 'static { writer: &'a mut Writer, asset: SavedAsset<'a, Self::Asset>, settings: &'a Self::Settings, - ) -> BoxedFuture<'a, Result<::Settings, Self::Error>>; + ) -> impl ConditionalSendFuture< + Output = Result<::Settings, Self::Error>, + >; } /// A type-erased dynamic variant of [`AssetSaver`] that allows callers to save assets without knowing the actual type of the [`AssetSaver`]. diff --git a/crates/bevy_asset/src/server/loaders.rs b/crates/bevy_asset/src/server/loaders.rs index 98cc3bce9d28f..fb161a986adb7 100644 --- a/crates/bevy_asset/src/server/loaders.rs +++ b/crates/bevy_asset/src/server/loaders.rs @@ -307,12 +307,16 @@ mod tests { use super::*; + // The compiler notices these fields are never read and raises a dead_code lint which kill CI. + #[allow(dead_code)] #[derive(Asset, TypePath, Debug)] struct A(usize); + #[allow(dead_code)] #[derive(Asset, TypePath, Debug)] struct B(usize); + #[allow(dead_code)] #[derive(Asset, TypePath, Debug)] struct C(usize); @@ -341,21 +345,19 @@ mod tests { type Error = String; - fn load<'a>( + async fn load<'a>( &'a self, - _: &'a mut crate::io::Reader, + _: &'a mut crate::io::Reader<'_>, _: &'a Self::Settings, - _: &'a mut crate::LoadContext, - ) -> bevy_utils::BoxedFuture<'a, Result> { + _: &'a mut crate::LoadContext<'_>, + ) -> Result { self.sender.send(()).unwrap(); - Box::pin(async move { - Err(format!( - "Loaded {}:{}", - std::any::type_name::(), - N - )) - }) + Err(format!( + "Loaded {}:{}", + std::any::type_name::(), + N + )) } fn extensions(&self) -> &[&str] { diff --git a/crates/bevy_asset/src/server/mod.rs b/crates/bevy_asset/src/server/mod.rs index 2be2848aece48..60536cc40509e 100644 --- a/crates/bevy_asset/src/server/mod.rs +++ b/crates/bevy_asset/src/server/mod.rs @@ -4,8 +4,8 @@ mod loaders; use crate::{ folder::LoadedFolder, io::{ - AssetReader, AssetReaderError, AssetSource, AssetSourceEvent, AssetSourceId, AssetSources, - MissingAssetSourceError, MissingProcessedAssetReaderError, Reader, + AssetReaderError, AssetSource, AssetSourceEvent, AssetSourceId, AssetSources, + ErasedAssetReader, MissingAssetSourceError, MissingProcessedAssetReaderError, Reader, }, loader::{AssetLoader, ErasedAssetLoader, LoadContext, LoadedAsset}, meta::{ @@ -30,6 +30,10 @@ use std::path::PathBuf; use std::{any::TypeId, path::Path, sync::Arc}; use thiserror::Error; +// Needed for doc string +#[allow(unused_imports)] +use crate::io::{AssetReader, AssetWriter}; + /// Loads and tracks the state of [`Asset`] values from a configured [`AssetReader`]. This can be used to kick off new asset loads and /// retrieve their current load states. /// @@ -657,7 +661,7 @@ impl AssetServer { fn load_folder<'a>( source: AssetSourceId<'static>, path: &'a Path, - reader: &'a dyn AssetReader, + reader: &'a dyn ErasedAssetReader, server: &'a AssetServer, handles: &'a mut Vec, ) -> bevy_utils::BoxedFuture<'a, Result<(), AssetLoadError>> { diff --git a/crates/bevy_asset/src/transformer.rs b/crates/bevy_asset/src/transformer.rs index 3b8ae58bc37cd..0ffddc4658a43 100644 --- a/crates/bevy_asset/src/transformer.rs +++ b/crates/bevy_asset/src/transformer.rs @@ -1,5 +1,5 @@ use crate::{meta::Settings, Asset, ErasedLoadedAsset, Handle, LabeledAsset, UntypedHandle}; -use bevy_utils::{BoxedFuture, CowArc, HashMap}; +use bevy_utils::{ConditionalSendFuture, CowArc, HashMap}; use serde::{Deserialize, Serialize}; use std::{ borrow::Borrow, @@ -25,7 +25,7 @@ pub trait AssetTransformer: Send + Sync + 'static { &'a self, asset: TransformedAsset, settings: &'a Self::Settings, - ) -> BoxedFuture<'a, Result, Self::Error>>; + ) -> impl ConditionalSendFuture, Self::Error>>; } /// An [`Asset`] (and any "sub assets") intended to be transformed diff --git a/crates/bevy_audio/src/audio_source.rs b/crates/bevy_audio/src/audio_source.rs index 8b0c7090eac14..242c6a6a7c664 100644 --- a/crates/bevy_audio/src/audio_source.rs +++ b/crates/bevy_audio/src/audio_source.rs @@ -3,7 +3,6 @@ use bevy_asset::{ Asset, AssetLoader, LoadContext, }; use bevy_reflect::TypePath; -use bevy_utils::BoxedFuture; use std::{io::Cursor, sync::Arc}; /// A source of audio data @@ -43,18 +42,16 @@ impl AssetLoader for AudioLoader { type Settings = (); type Error = std::io::Error; - fn load<'a>( + async fn load<'a>( &'a self, - reader: &'a mut Reader, + reader: &'a mut Reader<'_>, _settings: &'a Self::Settings, - _load_context: &'a mut LoadContext, - ) -> BoxedFuture<'a, Result> { - Box::pin(async move { - let mut bytes = Vec::new(); - reader.read_to_end(&mut bytes).await?; - Ok(AudioSource { - bytes: bytes.into(), - }) + _load_context: &'a mut LoadContext<'_>, + ) -> Result { + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).await?; + Ok(AudioSource { + bytes: bytes.into(), }) } diff --git a/crates/bevy_color/src/color.rs b/crates/bevy_color/src/color.rs index bfb5118712289..ad3c24f15f015 100644 --- a/crates/bevy_color/src/color.rs +++ b/crates/bevy_color/src/color.rs @@ -1,7 +1,7 @@ use crate::{ Alpha, Hsla, Hsva, Hwba, Laba, Lcha, LinearRgba, Oklaba, Oklcha, Srgba, StandardColor, Xyza, }; -use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; +use bevy_reflect::prelude::*; use serde::{Deserialize, Serialize}; /// An enumerated type that can represent any of the color types in this crate. @@ -13,7 +13,7 @@ use serde::{Deserialize, Serialize}; #[doc = include_str!("../docs/diagrams/model_graph.svg")] /// #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)] -#[reflect(PartialEq, Serialize, Deserialize)] +#[reflect(PartialEq, Serialize, Deserialize, Default)] pub enum Color { /// A color in the sRGB color space with alpha. Srgba(Srgba), @@ -266,14 +266,19 @@ impl Color { } /// Creates a new [`Color`] object storing a [`Oklaba`] color. - pub const fn oklaba(l: f32, a: f32, b: f32, alpha: f32) -> Self { - Self::Oklaba(Oklaba { l, a, b, alpha }) + pub const fn oklaba(lightness: f32, a: f32, b: f32, alpha: f32) -> Self { + Self::Oklaba(Oklaba { + lightness, + a, + b, + alpha, + }) } /// Creates a new [`Color`] object storing a [`Oklaba`] color with an alpha of 1.0. - pub const fn oklab(l: f32, a: f32, b: f32) -> Self { + pub const fn oklab(lightness: f32, a: f32, b: f32) -> Self { Self::Oklaba(Oklaba { - l, + lightness, a, b, alpha: 1.0, diff --git a/crates/bevy_color/src/color_ops.rs b/crates/bevy_color/src/color_ops.rs index efa35299055cb..460fc985d1b3b 100644 --- a/crates/bevy_color/src/color_ops.rs +++ b/crates/bevy_color/src/color_ops.rs @@ -61,3 +61,56 @@ pub trait Alpha: Sized { self.alpha() >= 1.0 } } + +/// Trait for manipulating the hue of a color. +pub trait Hue: Sized { + /// Return a new version of this color with the hue channel set to the given value. + fn with_hue(&self, hue: f32) -> Self; + + /// Return the hue of this color [0.0, 360.0]. + fn hue(&self) -> f32; + + /// Sets the hue of this color. + fn set_hue(&mut self, hue: f32); + + /// Return a new version of this color with the hue channel rotated by the given degrees. + fn rotate_hue(&self, degrees: f32) -> Self { + let rotated_hue = (self.hue() + degrees).rem_euclid(360.); + self.with_hue(rotated_hue) + } +} + +/// Trait with methods for asserting a colorspace is within bounds. +/// +/// During ordinary usage (e.g. reading images from disk, rendering images, picking colors for UI), colors should always be within their ordinary bounds (such as 0 to 1 for RGB colors). +/// However, some applications, such as high dynamic range rendering or bloom rely on unbounded colors to naturally represent a wider array of choices. +pub trait ClampColor: Sized { + /// Return a new version of this color clamped, with all fields in bounds. + fn clamped(&self) -> Self; + + /// Changes all the fields of this color to ensure they are within bounds. + fn clamp(&mut self) { + *self = self.clamped(); + } + + /// Are all the fields of this color in bounds? + fn is_within_bounds(&self) -> bool; +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Hsla; + + #[test] + fn test_rotate_hue() { + let hsla = Hsla::hsl(180.0, 1.0, 0.5); + assert_eq!(hsla.rotate_hue(90.0), Hsla::hsl(270.0, 1.0, 0.5)); + assert_eq!(hsla.rotate_hue(-90.0), Hsla::hsl(90.0, 1.0, 0.5)); + assert_eq!(hsla.rotate_hue(180.0), Hsla::hsl(0.0, 1.0, 0.5)); + assert_eq!(hsla.rotate_hue(-180.0), Hsla::hsl(0.0, 1.0, 0.5)); + assert_eq!(hsla.rotate_hue(0.0), hsla); + assert_eq!(hsla.rotate_hue(360.0), hsla); + assert_eq!(hsla.rotate_hue(-360.0), hsla); + } +} diff --git a/crates/bevy_color/src/hsla.rs b/crates/bevy_color/src/hsla.rs index 2e31d5f9b1176..37bc74fab512f 100644 --- a/crates/bevy_color/src/hsla.rs +++ b/crates/bevy_color/src/hsla.rs @@ -1,5 +1,8 @@ -use crate::{Alpha, Hsva, Hwba, Lcha, LinearRgba, Luminance, Mix, Srgba, StandardColor, Xyza}; -use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; +use crate::{ + Alpha, ClampColor, Hsva, Hue, Hwba, Lcha, LinearRgba, Luminance, Mix, Srgba, StandardColor, + Xyza, +}; +use bevy_reflect::prelude::*; use serde::{Deserialize, Serialize}; /// Color in Hue-Saturation-Lightness (HSL) color space with alpha. @@ -9,7 +12,7 @@ use serde::{Deserialize, Serialize}; #[doc = include_str!("../docs/diagrams/model_graph.svg")] /// #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)] -#[reflect(PartialEq, Serialize, Deserialize)] +#[reflect(PartialEq, Serialize, Deserialize, Default)] pub struct Hsla { /// The hue channel. [0.0, 360.0] pub hue: f32, @@ -52,11 +55,6 @@ impl Hsla { Self::new(hue, saturation, lightness, 1.0) } - /// Return a copy of this color with the hue channel set to the given value. - pub const fn with_hue(self, hue: f32) -> Self { - Self { hue, ..self } - } - /// Return a copy of this color with the saturation channel set to the given value. pub const fn with_saturation(self, saturation: f32) -> Self { Self { saturation, ..self } @@ -141,6 +139,23 @@ impl Alpha for Hsla { } } +impl Hue for Hsla { + #[inline] + fn with_hue(&self, hue: f32) -> Self { + Self { hue, ..*self } + } + + #[inline] + fn hue(&self) -> f32 { + self.hue + } + + #[inline] + fn set_hue(&mut self, hue: f32) { + self.hue = hue; + } +} + impl Luminance for Hsla { #[inline] fn with_luminance(&self, lightness: f32) -> Self { @@ -166,6 +181,24 @@ impl Luminance for Hsla { } } +impl ClampColor for Hsla { + fn clamped(&self) -> Self { + Self { + hue: self.hue.rem_euclid(360.), + saturation: self.saturation.clamp(0., 1.), + lightness: self.lightness.clamp(0., 1.), + alpha: self.alpha.clamp(0., 1.), + } + } + + fn is_within_bounds(&self) -> bool { + (0. ..=360.).contains(&self.hue) + && (0. ..=1.).contains(&self.saturation) + && (0. ..=1.).contains(&self.lightness) + && (0. ..=1.).contains(&self.alpha) + } +} + impl From for Hsva { fn from( Hsla { @@ -357,4 +390,21 @@ mod tests { assert_approx_eq!(color.hue, reference.hue, 0.001); } } + + #[test] + fn test_clamp() { + let color_1 = Hsla::hsl(361., 2., -1.); + let color_2 = Hsla::hsl(250.2762, 1., 0.67); + let mut color_3 = Hsla::hsl(-50., 1., 1.); + + assert!(!color_1.is_within_bounds()); + assert_eq!(color_1.clamped(), Hsla::hsl(1., 1., 0.)); + + assert!(color_2.is_within_bounds()); + assert_eq!(color_2, color_2.clamped()); + + color_3.clamp(); + assert!(color_3.is_within_bounds()); + assert_eq!(color_3, Hsla::hsl(310., 1., 1.)); + } } diff --git a/crates/bevy_color/src/hsva.rs b/crates/bevy_color/src/hsva.rs index bc788c894863a..dd959d266b7ff 100644 --- a/crates/bevy_color/src/hsva.rs +++ b/crates/bevy_color/src/hsva.rs @@ -1,5 +1,5 @@ -use crate::{Alpha, Hwba, Lcha, LinearRgba, Srgba, StandardColor, Xyza}; -use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; +use crate::{Alpha, ClampColor, Hue, Hwba, Lcha, LinearRgba, Srgba, StandardColor, Xyza}; +use bevy_reflect::prelude::*; use serde::{Deserialize, Serialize}; /// Color in Hue-Saturation-Value (HSV) color space with alpha. @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; #[doc = include_str!("../docs/diagrams/model_graph.svg")] /// #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)] -#[reflect(PartialEq, Serialize, Deserialize)] +#[reflect(PartialEq, Serialize, Deserialize, Default)] pub struct Hsva { /// The hue channel. [0.0, 360.0] pub hue: f32, @@ -52,11 +52,6 @@ impl Hsva { Self::new(hue, saturation, value, 1.0) } - /// Return a copy of this color with the hue channel set to the given value. - pub const fn with_hue(self, hue: f32) -> Self { - Self { hue, ..self } - } - /// Return a copy of this color with the saturation channel set to the given value. pub const fn with_saturation(self, saturation: f32) -> Self { Self { saturation, ..self } @@ -91,6 +86,41 @@ impl Alpha for Hsva { } } +impl Hue for Hsva { + #[inline] + fn with_hue(&self, hue: f32) -> Self { + Self { hue, ..*self } + } + + #[inline] + fn hue(&self) -> f32 { + self.hue + } + + #[inline] + fn set_hue(&mut self, hue: f32) { + self.hue = hue; + } +} + +impl ClampColor for Hsva { + fn clamped(&self) -> Self { + Self { + hue: self.hue.rem_euclid(360.), + saturation: self.saturation.clamp(0., 1.), + value: self.value.clamp(0., 1.), + alpha: self.alpha.clamp(0., 1.), + } + } + + fn is_within_bounds(&self) -> bool { + (0. ..=360.).contains(&self.hue) + && (0. ..=1.).contains(&self.saturation) + && (0. ..=1.).contains(&self.value) + && (0. ..=1.).contains(&self.alpha) + } +} + impl From for Hwba { fn from( Hsva { @@ -212,4 +242,21 @@ mod tests { assert_approx_eq!(color.hsv.alpha, hsv2.alpha, 0.001); } } + + #[test] + fn test_clamp() { + let color_1 = Hsva::hsv(361., 2., -1.); + let color_2 = Hsva::hsv(250.2762, 1., 0.67); + let mut color_3 = Hsva::hsv(-50., 1., 1.); + + assert!(!color_1.is_within_bounds()); + assert_eq!(color_1.clamped(), Hsva::hsv(1., 1., 0.)); + + assert!(color_2.is_within_bounds()); + assert_eq!(color_2, color_2.clamped()); + + color_3.clamp(); + assert!(color_3.is_within_bounds()); + assert_eq!(color_3, Hsva::hsv(310., 1., 1.)); + } } diff --git a/crates/bevy_color/src/hwba.rs b/crates/bevy_color/src/hwba.rs index b3b7b6bf62ef9..a83473d06af01 100644 --- a/crates/bevy_color/src/hwba.rs +++ b/crates/bevy_color/src/hwba.rs @@ -2,8 +2,8 @@ //! in [_HWB - A More Intuitive Hue-Based Color Model_] by _Smith et al_. //! //! [_HWB - A More Intuitive Hue-Based Color Model_]: https://web.archive.org/web/20240226005220/http://alvyray.com/Papers/CG/HWB_JGTv208.pdf -use crate::{Alpha, Lcha, LinearRgba, Srgba, StandardColor, Xyza}; -use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; +use crate::{Alpha, ClampColor, Hue, Lcha, LinearRgba, Srgba, StandardColor, Xyza}; +use bevy_reflect::prelude::*; use serde::{Deserialize, Serialize}; /// Color in Hue-Whiteness-Blackness (HWB) color space with alpha. @@ -13,7 +13,7 @@ use serde::{Deserialize, Serialize}; #[doc = include_str!("../docs/diagrams/model_graph.svg")] /// #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)] -#[reflect(PartialEq, Serialize, Deserialize)] +#[reflect(PartialEq, Serialize, Deserialize, Default)] pub struct Hwba { /// The hue channel. [0.0, 360.0] pub hue: f32, @@ -56,11 +56,6 @@ impl Hwba { Self::new(hue, whiteness, blackness, 1.0) } - /// Return a copy of this color with the hue channel set to the given value. - pub const fn with_hue(self, hue: f32) -> Self { - Self { hue, ..self } - } - /// Return a copy of this color with the whiteness channel set to the given value. pub const fn with_whiteness(self, whiteness: f32) -> Self { Self { whiteness, ..self } @@ -95,6 +90,41 @@ impl Alpha for Hwba { } } +impl Hue for Hwba { + #[inline] + fn with_hue(&self, hue: f32) -> Self { + Self { hue, ..*self } + } + + #[inline] + fn hue(&self) -> f32 { + self.hue + } + + #[inline] + fn set_hue(&mut self, hue: f32) { + self.hue = hue; + } +} + +impl ClampColor for Hwba { + fn clamped(&self) -> Self { + Self { + hue: self.hue.rem_euclid(360.), + whiteness: self.whiteness.clamp(0., 1.), + blackness: self.blackness.clamp(0., 1.), + alpha: self.alpha.clamp(0., 1.), + } + } + + fn is_within_bounds(&self) -> bool { + (0. ..=360.).contains(&self.hue) + && (0. ..=1.).contains(&self.whiteness) + && (0. ..=1.).contains(&self.blackness) + && (0. ..=1.).contains(&self.alpha) + } +} + impl From for Hwba { fn from( Srgba { @@ -245,4 +275,21 @@ mod tests { assert_approx_eq!(color.hwb.alpha, hwb2.alpha, 0.001); } } + + #[test] + fn test_clamp() { + let color_1 = Hwba::hwb(361., 2., -1.); + let color_2 = Hwba::hwb(250.2762, 1., 0.67); + let mut color_3 = Hwba::hwb(-50., 1., 1.); + + assert!(!color_1.is_within_bounds()); + assert_eq!(color_1.clamped(), Hwba::hwb(1., 1., 0.)); + + assert!(color_2.is_within_bounds()); + assert_eq!(color_2, color_2.clamped()); + + color_3.clamp(); + assert!(color_3.is_within_bounds()); + assert_eq!(color_3, Hwba::hwb(310., 1., 1.)); + } } diff --git a/crates/bevy_color/src/laba.rs b/crates/bevy_color/src/laba.rs index 31e8b643cc6ff..a227cc271e5d2 100644 --- a/crates/bevy_color/src/laba.rs +++ b/crates/bevy_color/src/laba.rs @@ -1,7 +1,8 @@ use crate::{ - Alpha, Hsla, Hsva, Hwba, LinearRgba, Luminance, Mix, Oklaba, Srgba, StandardColor, Xyza, + impl_componentwise_point, Alpha, ClampColor, Hsla, Hsva, Hwba, LinearRgba, Luminance, Mix, + Oklaba, Srgba, StandardColor, Xyza, }; -use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; +use bevy_reflect::prelude::*; use serde::{Deserialize, Serialize}; /// Color in LAB color space, with alpha @@ -10,7 +11,7 @@ use serde::{Deserialize, Serialize}; #[doc = include_str!("../docs/diagrams/model_graph.svg")] /// #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)] -#[reflect(PartialEq, Serialize, Deserialize)] +#[reflect(PartialEq, Serialize, Deserialize, Default)] pub struct Laba { /// The lightness channel. [0.0, 1.5] pub lightness: f32, @@ -24,6 +25,8 @@ pub struct Laba { impl StandardColor for Laba {} +impl_componentwise_point!(Laba, [lightness, a, b, alpha]); + impl Laba { /// Construct a new [`Laba`] color from components. /// @@ -110,6 +113,24 @@ impl Alpha for Laba { } } +impl ClampColor for Laba { + fn clamped(&self) -> Self { + Self { + lightness: self.lightness.clamp(0., 1.5), + a: self.a.clamp(-1.5, 1.5), + b: self.b.clamp(-1.5, 1.5), + alpha: self.alpha.clamp(0., 1.), + } + } + + fn is_within_bounds(&self) -> bool { + (0. ..=1.5).contains(&self.lightness) + && (-1.5..=1.5).contains(&self.a) + && (-1.5..=1.5).contains(&self.b) + && (0. ..=1.).contains(&self.alpha) + } +} + impl Luminance for Laba { #[inline] fn with_luminance(&self, lightness: f32) -> Self { @@ -353,4 +374,21 @@ mod tests { assert_approx_eq!(color.lab.alpha, laba.alpha, 0.001); } } + + #[test] + fn test_clamp() { + let color_1 = Laba::lab(-1., 2., -2.); + let color_2 = Laba::lab(1., 1.5, -1.2); + let mut color_3 = Laba::lab(-0.4, 1., 1.); + + assert!(!color_1.is_within_bounds()); + assert_eq!(color_1.clamped(), Laba::lab(0., 1.5, -1.5)); + + assert!(color_2.is_within_bounds()); + assert_eq!(color_2, color_2.clamped()); + + color_3.clamp(); + assert!(color_3.is_within_bounds()); + assert_eq!(color_3, Laba::lab(0., 1., 1.)); + } } diff --git a/crates/bevy_color/src/lcha.rs b/crates/bevy_color/src/lcha.rs index d59698bedc53f..7ba7dd154dcd4 100644 --- a/crates/bevy_color/src/lcha.rs +++ b/crates/bevy_color/src/lcha.rs @@ -1,5 +1,5 @@ -use crate::{Alpha, Laba, LinearRgba, Luminance, Mix, Srgba, StandardColor, Xyza}; -use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; +use crate::{Alpha, ClampColor, Hue, Laba, LinearRgba, Luminance, Mix, Srgba, StandardColor, Xyza}; +use bevy_reflect::prelude::*; use serde::{Deserialize, Serialize}; /// Color in LCH color space, with alpha @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; #[doc = include_str!("../docs/diagrams/model_graph.svg")] /// #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)] -#[reflect(PartialEq, Serialize, Deserialize)] +#[reflect(PartialEq, Serialize, Deserialize, Default)] pub struct Lcha { /// The lightness channel. [0.0, 1.5] pub lightness: f32, @@ -56,11 +56,6 @@ impl Lcha { } } - /// Return a copy of this color with the hue channel set to the given value. - pub const fn with_hue(self, hue: f32) -> Self { - Self { hue, ..self } - } - /// Return a copy of this color with the chroma channel set to the given value. pub const fn with_chroma(self, chroma: f32) -> Self { Self { chroma, ..self } @@ -137,6 +132,23 @@ impl Alpha for Lcha { } } +impl Hue for Lcha { + #[inline] + fn with_hue(&self, hue: f32) -> Self { + Self { hue, ..*self } + } + + #[inline] + fn hue(&self) -> f32 { + self.hue + } + + #[inline] + fn set_hue(&mut self, hue: f32) { + self.hue = hue; + } +} + impl Luminance for Lcha { #[inline] fn with_luminance(&self, lightness: f32) -> Self { @@ -166,6 +178,24 @@ impl Luminance for Lcha { } } +impl ClampColor for Lcha { + fn clamped(&self) -> Self { + Self { + lightness: self.lightness.clamp(0., 1.5), + chroma: self.chroma.clamp(0., 1.5), + hue: self.hue.rem_euclid(360.), + alpha: self.alpha.clamp(0., 1.), + } + } + + fn is_within_bounds(&self) -> bool { + (0. ..=1.5).contains(&self.lightness) + && (0. ..=1.5).contains(&self.chroma) + && (0. ..=360.).contains(&self.hue) + && (0. ..=1.).contains(&self.alpha) + } +} + impl From for Laba { fn from( Lcha { @@ -313,4 +343,21 @@ mod tests { assert_approx_eq!(color.lch.alpha, lcha.alpha, 0.001); } } + + #[test] + fn test_clamp() { + let color_1 = Lcha::lch(-1., 2., 400.); + let color_2 = Lcha::lch(1., 1.5, 249.54); + let mut color_3 = Lcha::lch(-0.4, 1., 1.); + + assert!(!color_1.is_within_bounds()); + assert_eq!(color_1.clamped(), Lcha::lch(0., 1.5, 40.)); + + assert!(color_2.is_within_bounds()); + assert_eq!(color_2, color_2.clamped()); + + color_3.clamp(); + assert!(color_3.is_within_bounds()); + assert_eq!(color_3, Lcha::lch(0., 1., 1.)); + } } diff --git a/crates/bevy_color/src/lib.rs b/crates/bevy_color/src/lib.rs index 27d92be1c46b2..139c381cd2062 100644 --- a/crates/bevy_color/src/lib.rs +++ b/crates/bevy_color/src/lib.rs @@ -65,6 +65,12 @@ //! types in this crate. This is useful when you need to store a color in a data structure //! that can't be generic over the color type. //! +//! Color types that are either physically or perceptually linear also implement `Add`, `Sub`, `Mul` and `Div` +//! allowing you to use them with splines. +//! +//! Please note that most often adding or subtracting colors is not what you may want. +//! Please have a look at other operations like blending, lightening or mixing colors using e.g. [`Mix`] or [`Luminance`] instead. +//! //! # Example //! //! ``` @@ -151,3 +157,85 @@ where Self: Alpha, { } + +macro_rules! impl_componentwise_point { + ($ty: ident, [$($element: ident),+]) => { + impl std::ops::Add for $ty { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + Self::Output { + $($element: self.$element + rhs.$element,)+ + } + } + } + + impl std::ops::AddAssign for $ty { + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs; + } + } + + impl std::ops::Sub for $ty { + type Output = Self; + + fn sub(self, rhs: Self) -> Self::Output { + Self::Output { + $($element: self.$element - rhs.$element,)+ + } + } + } + + impl std::ops::SubAssign for $ty { + fn sub_assign(&mut self, rhs: Self) { + *self = *self - rhs; + } + } + + impl std::ops::Mul for $ty { + type Output = Self; + + fn mul(self, rhs: f32) -> Self::Output { + Self::Output { + $($element: self.$element * rhs,)+ + } + } + } + + impl std::ops::Mul<$ty> for f32 { + type Output = $ty; + + fn mul(self, rhs: $ty) -> Self::Output { + Self::Output { + $($element: self * rhs.$element,)+ + } + } + } + + impl std::ops::MulAssign for $ty { + fn mul_assign(&mut self, rhs: f32) { + *self = *self * rhs; + } + } + + impl std::ops::Div for $ty { + type Output = Self; + + fn div(self, rhs: f32) -> Self::Output { + Self::Output { + $($element: self.$element / rhs,)+ + } + } + } + + impl std::ops::DivAssign for $ty { + fn div_assign(&mut self, rhs: f32) { + *self = *self / rhs; + } + } + + impl bevy_math::cubic_splines::Point for $ty {} + }; +} + +pub(crate) use impl_componentwise_point; diff --git a/crates/bevy_color/src/linear_rgba.rs b/crates/bevy_color/src/linear_rgba.rs index b07efa951e8ce..5e450af72f31c 100644 --- a/crates/bevy_color/src/linear_rgba.rs +++ b/crates/bevy_color/src/linear_rgba.rs @@ -1,8 +1,9 @@ -use std::ops::{Div, Mul}; - -use crate::{color_difference::EuclideanDistance, Alpha, Luminance, Mix, StandardColor}; +use crate::{ + color_difference::EuclideanDistance, impl_componentwise_point, Alpha, ClampColor, Luminance, + Mix, StandardColor, +}; use bevy_math::Vec4; -use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; +use bevy_reflect::prelude::*; use bytemuck::{Pod, Zeroable}; use serde::{Deserialize, Serialize}; @@ -12,7 +13,7 @@ use serde::{Deserialize, Serialize}; #[doc = include_str!("../docs/diagrams/model_graph.svg")] /// #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)] -#[reflect(PartialEq, Serialize, Deserialize)] +#[reflect(PartialEq, Serialize, Deserialize, Default)] #[repr(C)] pub struct LinearRgba { /// The red channel. [0.0, 1.0] @@ -27,6 +28,8 @@ pub struct LinearRgba { impl StandardColor for LinearRgba {} +impl_componentwise_point!(LinearRgba, [red, green, blue, alpha]); + impl LinearRgba { /// A fully black color with full alpha. pub const BLACK: Self = Self { @@ -256,6 +259,24 @@ impl EuclideanDistance for LinearRgba { } } +impl ClampColor for LinearRgba { + fn clamped(&self) -> Self { + Self { + red: self.red.clamp(0., 1.), + green: self.green.clamp(0., 1.), + blue: self.blue.clamp(0., 1.), + alpha: self.alpha.clamp(0., 1.), + } + } + + fn is_within_bounds(&self) -> bool { + (0. ..=1.).contains(&self.red) + && (0. ..=1.).contains(&self.green) + && (0. ..=1.).contains(&self.blue) + && (0. ..=1.).contains(&self.alpha) + } +} + impl From for [f32; 4] { fn from(color: LinearRgba) -> Self { [color.red, color.green, color.blue, color.alpha] @@ -279,48 +300,6 @@ impl From for wgpu::Color { } } -/// All color channels are scaled directly, -/// but alpha is unchanged. -/// -/// Values are not clamped. -impl Mul for LinearRgba { - type Output = Self; - - fn mul(self, rhs: f32) -> Self { - Self { - red: self.red * rhs, - green: self.green * rhs, - blue: self.blue * rhs, - alpha: self.alpha, - } - } -} - -impl Mul for f32 { - type Output = LinearRgba; - - fn mul(self, rhs: LinearRgba) -> LinearRgba { - rhs * self - } -} - -/// All color channels are scaled directly, -/// but alpha is unchanged. -/// -/// Values are not clamped. -impl Div for LinearRgba { - type Output = Self; - - fn div(self, rhs: f32) -> Self { - Self { - red: self.red / rhs, - green: self.green / rhs, - blue: self.blue / rhs, - alpha: self.alpha, - } - } -} - // [`LinearRgba`] is intended to be used with shaders // So it's the only color type that implements [`ShaderType`] to make it easier to use inside shaders impl encase::ShaderType for LinearRgba { @@ -455,4 +434,21 @@ mod tests { let twice_as_light = color.lighter(0.2); assert!(lighter2.distance_squared(&twice_as_light) < 0.0001); } + + #[test] + fn test_clamp() { + let color_1 = LinearRgba::rgb(2., -1., 0.4); + let color_2 = LinearRgba::rgb(0.031, 0.749, 1.); + let mut color_3 = LinearRgba::rgb(-1., 1., 1.); + + assert!(!color_1.is_within_bounds()); + assert_eq!(color_1.clamped(), LinearRgba::rgb(1., 0., 0.4)); + + assert!(color_2.is_within_bounds()); + assert_eq!(color_2, color_2.clamped()); + + color_3.clamp(); + assert!(color_3.is_within_bounds()); + assert_eq!(color_3, LinearRgba::rgb(0., 1., 1.)); + } } diff --git a/crates/bevy_color/src/oklaba.rs b/crates/bevy_color/src/oklaba.rs index 7bfeb8d996d5a..5501e8e4c6ec8 100644 --- a/crates/bevy_color/src/oklaba.rs +++ b/crates/bevy_color/src/oklaba.rs @@ -1,8 +1,8 @@ use crate::{ - color_difference::EuclideanDistance, Alpha, Hsla, Hsva, Hwba, Lcha, LinearRgba, Luminance, Mix, - Srgba, StandardColor, Xyza, + color_difference::EuclideanDistance, impl_componentwise_point, Alpha, ClampColor, Hsla, Hsva, + Hwba, Lcha, LinearRgba, Luminance, Mix, Srgba, StandardColor, Xyza, }; -use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; +use bevy_reflect::prelude::*; use serde::{Deserialize, Serialize}; /// Color in Oklab color space, with alpha @@ -11,10 +11,10 @@ use serde::{Deserialize, Serialize}; #[doc = include_str!("../docs/diagrams/model_graph.svg")] /// #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)] -#[reflect(PartialEq, Serialize, Deserialize)] +#[reflect(PartialEq, Serialize, Deserialize, Default)] pub struct Oklaba { - /// The 'l' channel. [0.0, 1.0] - pub l: f32, + /// The 'lightness' channel. [0.0, 1.0] + pub lightness: f32, /// The 'a' channel. [-1.0, 1.0] pub a: f32, /// The 'b' channel. [-1.0, 1.0] @@ -25,38 +25,45 @@ pub struct Oklaba { impl StandardColor for Oklaba {} +impl_componentwise_point!(Oklaba, [lightness, a, b, alpha]); + impl Oklaba { /// Construct a new [`Oklaba`] color from components. /// /// # Arguments /// - /// * `l` - Lightness channel. [0.0, 1.0] + /// * `lightness` - Lightness channel. [0.0, 1.0] /// * `a` - Green-red channel. [-1.0, 1.0] /// * `b` - Blue-yellow channel. [-1.0, 1.0] /// * `alpha` - Alpha channel. [0.0, 1.0] - pub const fn new(l: f32, a: f32, b: f32, alpha: f32) -> Self { - Self { l, a, b, alpha } + pub const fn new(lightness: f32, a: f32, b: f32, alpha: f32) -> Self { + Self { + lightness, + a, + b, + alpha, + } } /// Construct a new [`Oklaba`] color from (l, a, b) components, with the default alpha (1.0). /// /// # Arguments /// - /// * `l` - Lightness channel. [0.0, 1.0] + /// * `lightness` - Lightness channel. [0.0, 1.0] /// * `a` - Green-red channel. [-1.0, 1.0] /// * `b` - Blue-yellow channel. [-1.0, 1.0] - pub const fn lch(l: f32, a: f32, b: f32) -> Self { + pub const fn lab(lightness: f32, a: f32, b: f32) -> Self { Self { - l, + lightness, a, b, alpha: 1.0, } } - /// Return a copy of this color with the 'l' channel set to the given value. - pub const fn with_l(self, l: f32) -> Self { - Self { l, ..self } + /// Return a copy of this color with the 'lightness' channel set to the given value. + pub const fn with_lightness(self, lightness: f32) -> Self { + Self { lightness, ..self } } /// Return a copy of this color with the 'a' channel set to the given value. @@ -81,7 +88,7 @@ impl Mix for Oklaba { fn mix(&self, other: &Self, factor: f32) -> Self { let n_factor = 1.0 - factor; Self { - l: self.l * n_factor + other.l * factor, + lightness: self.lightness * n_factor + other.lightness * factor, a: self.a * n_factor + other.a * factor, b: self.b * n_factor + other.b * factor, alpha: self.alpha * n_factor + other.alpha * factor, @@ -108,27 +115,57 @@ impl Alpha for Oklaba { impl Luminance for Oklaba { #[inline] - fn with_luminance(&self, l: f32) -> Self { - Self { l, ..*self } + fn with_luminance(&self, lightness: f32) -> Self { + Self { lightness, ..*self } } fn luminance(&self) -> f32 { - self.l + self.lightness } fn darker(&self, amount: f32) -> Self { - Self::new((self.l - amount).max(0.), self.a, self.b, self.alpha) + Self::new( + (self.lightness - amount).max(0.), + self.a, + self.b, + self.alpha, + ) } fn lighter(&self, amount: f32) -> Self { - Self::new((self.l + amount).min(1.), self.a, self.b, self.alpha) + Self::new( + (self.lightness + amount).min(1.), + self.a, + self.b, + self.alpha, + ) } } impl EuclideanDistance for Oklaba { #[inline] fn distance_squared(&self, other: &Self) -> f32 { - (self.l - other.l).powi(2) + (self.a - other.a).powi(2) + (self.b - other.b).powi(2) + (self.lightness - other.lightness).powi(2) + + (self.a - other.a).powi(2) + + (self.b - other.b).powi(2) + } +} + +impl ClampColor for Oklaba { + fn clamped(&self) -> Self { + Self { + lightness: self.lightness.clamp(0., 1.), + a: self.a.clamp(-1., 1.), + b: self.b.clamp(-1., 1.), + alpha: self.alpha.clamp(0., 1.), + } + } + + fn is_within_bounds(&self) -> bool { + (0. ..=1.).contains(&self.lightness) + && (-1. ..=1.).contains(&self.a) + && (-1. ..=1.).contains(&self.b) + && (0. ..=1.).contains(&self.alpha) } } @@ -158,12 +195,17 @@ impl From for Oklaba { #[allow(clippy::excessive_precision)] impl From for LinearRgba { fn from(value: Oklaba) -> Self { - let Oklaba { l, a, b, alpha } = value; + let Oklaba { + lightness, + a, + b, + alpha, + } = value; // From https://github.com/Ogeon/palette/blob/e75eab2fb21af579353f51f6229a510d0d50a311/palette/src/oklab.rs#L312-L332 - let l_ = l + 0.3963377774 * a + 0.2158037573 * b; - let m_ = l - 0.1055613458 * a - 0.0638541728 * b; - let s_ = l - 0.0894841775 * a - 1.2914855480 * b; + let l_ = lightness + 0.3963377774 * a + 0.2158037573 * b; + let m_ = lightness - 0.1055613458 * a - 0.0638541728 * b; + let s_ = lightness - 0.0894841775 * a - 1.2914855480 * b; let l = l_ * l_ * l_; let m = m_ * m_ * m_; @@ -266,7 +308,7 @@ mod tests { let oklaba = Oklaba::new(0.5, 0.5, 0.5, 1.0); let srgba: Srgba = oklaba.into(); let oklaba2: Oklaba = srgba.into(); - assert_approx_eq!(oklaba.l, oklaba2.l, 0.001); + assert_approx_eq!(oklaba.lightness, oklaba2.lightness, 0.001); assert_approx_eq!(oklaba.a, oklaba2.a, 0.001); assert_approx_eq!(oklaba.b, oklaba2.b, 0.001); assert_approx_eq!(oklaba.alpha, oklaba2.alpha, 0.001); @@ -299,9 +341,26 @@ mod tests { let oklaba = Oklaba::new(0.5, 0.5, 0.5, 1.0); let linear: LinearRgba = oklaba.into(); let oklaba2: Oklaba = linear.into(); - assert_approx_eq!(oklaba.l, oklaba2.l, 0.001); + assert_approx_eq!(oklaba.lightness, oklaba2.lightness, 0.001); assert_approx_eq!(oklaba.a, oklaba2.a, 0.001); assert_approx_eq!(oklaba.b, oklaba2.b, 0.001); assert_approx_eq!(oklaba.alpha, oklaba2.alpha, 0.001); } + + #[test] + fn test_clamp() { + let color_1 = Oklaba::lab(-1., 2., -2.); + let color_2 = Oklaba::lab(1., 0.42, -0.4); + let mut color_3 = Oklaba::lab(-0.4, 1., 1.); + + assert!(!color_1.is_within_bounds()); + assert_eq!(color_1.clamped(), Oklaba::lab(0., 1., -1.)); + + assert!(color_2.is_within_bounds()); + assert_eq!(color_2, color_2.clamped()); + + color_3.clamp(); + assert!(color_3.is_within_bounds()); + assert_eq!(color_3, Oklaba::lab(0., 1., 1.)); + } } diff --git a/crates/bevy_color/src/oklcha.rs b/crates/bevy_color/src/oklcha.rs index 939193a4ec1b9..5a8ccc4b7a30f 100644 --- a/crates/bevy_color/src/oklcha.rs +++ b/crates/bevy_color/src/oklcha.rs @@ -1,8 +1,8 @@ use crate::{ - color_difference::EuclideanDistance, Alpha, Hsla, Hsva, Hwba, Laba, Lcha, LinearRgba, - Luminance, Mix, Oklaba, Srgba, StandardColor, Xyza, + color_difference::EuclideanDistance, Alpha, ClampColor, Hsla, Hsva, Hue, Hwba, Laba, Lcha, + LinearRgba, Luminance, Mix, Oklaba, Srgba, StandardColor, Xyza, }; -use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; +use bevy_reflect::prelude::*; use serde::{Deserialize, Serialize}; /// Color in Oklch color space, with alpha @@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize}; #[doc = include_str!("../docs/diagrams/model_graph.svg")] /// #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)] -#[reflect(PartialEq, Serialize, Deserialize)] +#[reflect(PartialEq, Serialize, Deserialize, Default)] pub struct Oklcha { /// The 'lightness' channel. [0.0, 1.0] pub lightness: f32, @@ -56,20 +56,15 @@ impl Oklcha { } /// Return a copy of this color with the 'lightness' channel set to the given value. - pub const fn with_l(self, lightness: f32) -> Self { + pub const fn with_lightness(self, lightness: f32) -> Self { Self { lightness, ..self } } /// Return a copy of this color with the 'chroma' channel set to the given value. - pub const fn with_c(self, chroma: f32) -> Self { + pub const fn with_chroma(self, chroma: f32) -> Self { Self { chroma, ..self } } - /// Return a copy of this color with the 'hue' channel set to the given value. - pub const fn with_h(self, hue: f32) -> Self { - Self { hue, ..self } - } - /// Generate a deterministic but [quasi-randomly distributed](https://en.wikipedia.org/wiki/Low-discrepancy_sequence) /// color from a provided `index`. /// @@ -136,6 +131,23 @@ impl Alpha for Oklcha { } } +impl Hue for Oklcha { + #[inline] + fn with_hue(&self, hue: f32) -> Self { + Self { hue, ..*self } + } + + #[inline] + fn hue(&self) -> f32 { + self.hue + } + + #[inline] + fn set_hue(&mut self, hue: f32) { + self.hue = hue; + } +} + impl Luminance for Oklcha { #[inline] fn with_luminance(&self, lightness: f32) -> Self { @@ -175,8 +187,14 @@ impl EuclideanDistance for Oklcha { } impl From for Oklcha { - fn from(Oklaba { l, a, b, alpha }: Oklaba) -> Self { - let lightness = l; + fn from( + Oklaba { + lightness, + a, + b, + alpha, + }: Oklaba, + ) -> Self { let chroma = a.hypot(b); let hue = b.atan2(a).to_degrees(); @@ -203,6 +221,24 @@ impl From for Oklaba { } } +impl ClampColor for Oklcha { + fn clamped(&self) -> Self { + Self { + lightness: self.lightness.clamp(0., 1.), + chroma: self.chroma.clamp(0., 1.), + hue: self.hue.rem_euclid(360.), + alpha: self.alpha.clamp(0., 1.), + } + } + + fn is_within_bounds(&self) -> bool { + (0. ..=1.).contains(&self.lightness) + && (0. ..=1.).contains(&self.chroma) + && (0. ..=360.).contains(&self.hue) + && (0. ..=1.).contains(&self.alpha) + } +} + // Derived Conversions impl From for Oklcha { @@ -349,4 +385,21 @@ mod tests { assert_approx_eq!(oklcha.hue, oklcha2.hue, 0.001); assert_approx_eq!(oklcha.alpha, oklcha2.alpha, 0.001); } + + #[test] + fn test_clamp() { + let color_1 = Oklcha::lch(-1., 2., 400.); + let color_2 = Oklcha::lch(1., 1., 249.54); + let mut color_3 = Oklcha::lch(-0.4, 1., 1.); + + assert!(!color_1.is_within_bounds()); + assert_eq!(color_1.clamped(), Oklcha::lch(0., 1., 40.)); + + assert!(color_2.is_within_bounds()); + assert_eq!(color_2, color_2.clamped()); + + color_3.clamp(); + assert!(color_3.is_within_bounds()); + assert_eq!(color_3, Oklcha::lch(0., 1., 1.)); + } } diff --git a/crates/bevy_color/src/palettes/mod.rs b/crates/bevy_color/src/palettes/mod.rs index f062ebedbabc1..d050ea685ae9a 100644 --- a/crates/bevy_color/src/palettes/mod.rs +++ b/crates/bevy_color/src/palettes/mod.rs @@ -2,3 +2,4 @@ pub mod basic; pub mod css; +pub mod tailwind; diff --git a/crates/bevy_color/src/palettes/tailwind.rs b/crates/bevy_color/src/palettes/tailwind.rs new file mode 100644 index 0000000000000..31eb9d42e06e3 --- /dev/null +++ b/crates/bevy_color/src/palettes/tailwind.rs @@ -0,0 +1,536 @@ +//! Colors from [Tailwind CSS](https://tailwindcss.com/docs/customizing-colors) (MIT License). +//! Grouped by hue with numeric lightness scale (50 is light, 950 is dark). +//! +//! Generated from Tailwind 3.4.1. + +/* +MIT License + +Copyright (c) Tailwind Labs, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +use crate::Srgba; + +///
+pub const AMBER_50: Srgba = Srgba::rgb(1.0, 0.9843137, 0.92156863); +///
+pub const AMBER_100: Srgba = Srgba::rgb(0.99607843, 0.9529412, 0.78039217); +///
+pub const AMBER_200: Srgba = Srgba::rgb(0.99215686, 0.9019608, 0.5411765); +///
+pub const AMBER_300: Srgba = Srgba::rgb(0.9882353, 0.827451, 0.3019608); +///
+pub const AMBER_400: Srgba = Srgba::rgb(0.9843137, 0.7490196, 0.14117648); +///
+pub const AMBER_500: Srgba = Srgba::rgb(0.9607843, 0.61960787, 0.043137256); +///
+pub const AMBER_600: Srgba = Srgba::rgb(0.8509804, 0.46666667, 0.023529412); +///
+pub const AMBER_700: Srgba = Srgba::rgb(0.7058824, 0.3254902, 0.03529412); +///
+pub const AMBER_800: Srgba = Srgba::rgb(0.57254905, 0.2509804, 0.05490196); +///
+pub const AMBER_900: Srgba = Srgba::rgb(0.47058824, 0.20784314, 0.05882353); +///
+pub const AMBER_950: Srgba = Srgba::rgb(0.27058825, 0.101960786, 0.011764706); + +///
+pub const BLUE_50: Srgba = Srgba::rgb(0.9372549, 0.9647059, 1.0); +///
+pub const BLUE_100: Srgba = Srgba::rgb(0.85882354, 0.91764706, 0.99607843); +///
+pub const BLUE_200: Srgba = Srgba::rgb(0.7490196, 0.85882354, 0.99607843); +///
+pub const BLUE_300: Srgba = Srgba::rgb(0.5764706, 0.77254903, 0.99215686); +///
+pub const BLUE_400: Srgba = Srgba::rgb(0.3764706, 0.64705884, 0.98039216); +///
+pub const BLUE_500: Srgba = Srgba::rgb(0.23137255, 0.50980395, 0.9647059); +///
+pub const BLUE_600: Srgba = Srgba::rgb(0.14509805, 0.3882353, 0.92156863); +///
+pub const BLUE_700: Srgba = Srgba::rgb(0.11372549, 0.30588236, 0.84705883); +///
+pub const BLUE_800: Srgba = Srgba::rgb(0.11764706, 0.2509804, 0.6862745); +///
+pub const BLUE_900: Srgba = Srgba::rgb(0.11764706, 0.22745098, 0.5411765); +///
+pub const BLUE_950: Srgba = Srgba::rgb(0.09019608, 0.14509805, 0.32941177); + +///
+pub const CYAN_50: Srgba = Srgba::rgb(0.9254902, 0.99607843, 1.0); +///
+pub const CYAN_100: Srgba = Srgba::rgb(0.8117647, 0.98039216, 0.99607843); +///
+pub const CYAN_200: Srgba = Srgba::rgb(0.64705884, 0.9529412, 0.9882353); +///
+pub const CYAN_300: Srgba = Srgba::rgb(0.40392157, 0.9098039, 0.9764706); +///
+pub const CYAN_400: Srgba = Srgba::rgb(0.13333334, 0.827451, 0.93333334); +///
+pub const CYAN_500: Srgba = Srgba::rgb(0.023529412, 0.7137255, 0.83137256); +///
+pub const CYAN_600: Srgba = Srgba::rgb(0.03137255, 0.5686275, 0.69803923); +///
+pub const CYAN_700: Srgba = Srgba::rgb(0.05490196, 0.45490196, 0.5647059); +///
+pub const CYAN_800: Srgba = Srgba::rgb(0.08235294, 0.36862746, 0.45882353); +///
+pub const CYAN_900: Srgba = Srgba::rgb(0.08627451, 0.30588236, 0.3882353); +///
+pub const CYAN_950: Srgba = Srgba::rgb(0.03137255, 0.2, 0.26666668); + +///
+pub const EMERALD_50: Srgba = Srgba::rgb(0.9254902, 0.99215686, 0.9607843); +///
+pub const EMERALD_100: Srgba = Srgba::rgb(0.81960785, 0.98039216, 0.8980392); +///
+pub const EMERALD_200: Srgba = Srgba::rgb(0.654902, 0.9529412, 0.8156863); +///
+pub const EMERALD_300: Srgba = Srgba::rgb(0.43137255, 0.90588236, 0.7176471); +///
+pub const EMERALD_400: Srgba = Srgba::rgb(0.20392157, 0.827451, 0.6); +///
+pub const EMERALD_500: Srgba = Srgba::rgb(0.0627451, 0.7254902, 0.5058824); +///
+pub const EMERALD_600: Srgba = Srgba::rgb(0.019607844, 0.5882353, 0.4117647); +///
+pub const EMERALD_700: Srgba = Srgba::rgb(0.015686275, 0.47058824, 0.34117648); +///
+pub const EMERALD_800: Srgba = Srgba::rgb(0.023529412, 0.37254903, 0.27450982); +///
+pub const EMERALD_900: Srgba = Srgba::rgb(0.023529412, 0.30588236, 0.23137255); +///
+pub const EMERALD_950: Srgba = Srgba::rgb(0.007843138, 0.17254902, 0.13333334); + +///
+pub const FUCHSIA_50: Srgba = Srgba::rgb(0.99215686, 0.95686275, 1.0); +///
+pub const FUCHSIA_100: Srgba = Srgba::rgb(0.98039216, 0.9098039, 1.0); +///
+pub const FUCHSIA_200: Srgba = Srgba::rgb(0.9607843, 0.8156863, 0.99607843); +///
+pub const FUCHSIA_300: Srgba = Srgba::rgb(0.9411765, 0.67058825, 0.9882353); +///
+pub const FUCHSIA_400: Srgba = Srgba::rgb(0.9098039, 0.4745098, 0.9764706); +///
+pub const FUCHSIA_500: Srgba = Srgba::rgb(0.8509804, 0.27450982, 0.9372549); +///
+pub const FUCHSIA_600: Srgba = Srgba::rgb(0.7529412, 0.14901961, 0.827451); +///
+pub const FUCHSIA_700: Srgba = Srgba::rgb(0.63529414, 0.10980392, 0.6862745); +///
+pub const FUCHSIA_800: Srgba = Srgba::rgb(0.5254902, 0.09803922, 0.56078434); +///
+pub const FUCHSIA_900: Srgba = Srgba::rgb(0.4392157, 0.101960786, 0.45882353); +///
+pub const FUCHSIA_950: Srgba = Srgba::rgb(0.2901961, 0.015686275, 0.30588236); + +///
+pub const GRAY_50: Srgba = Srgba::rgb(0.9764706, 0.98039216, 0.9843137); +///
+pub const GRAY_100: Srgba = Srgba::rgb(0.9529412, 0.95686275, 0.9647059); +///
+pub const GRAY_200: Srgba = Srgba::rgb(0.8980392, 0.90588236, 0.92156863); +///
+pub const GRAY_300: Srgba = Srgba::rgb(0.81960785, 0.8352941, 0.85882354); +///
+pub const GRAY_400: Srgba = Srgba::rgb(0.6117647, 0.6392157, 0.6862745); +///
+pub const GRAY_500: Srgba = Srgba::rgb(0.41960785, 0.44705883, 0.5019608); +///
+pub const GRAY_600: Srgba = Srgba::rgb(0.29411766, 0.33333334, 0.3882353); +///
+pub const GRAY_700: Srgba = Srgba::rgb(0.21568628, 0.25490198, 0.31764707); +///
+pub const GRAY_800: Srgba = Srgba::rgb(0.12156863, 0.16078432, 0.21568628); +///
+pub const GRAY_900: Srgba = Srgba::rgb(0.06666667, 0.09411765, 0.15294118); +///
+pub const GRAY_950: Srgba = Srgba::rgb(0.011764706, 0.02745098, 0.07058824); + +///
+pub const GREEN_50: Srgba = Srgba::rgb(0.9411765, 0.99215686, 0.95686275); +///
+pub const GREEN_100: Srgba = Srgba::rgb(0.8627451, 0.9882353, 0.90588236); +///
+pub const GREEN_200: Srgba = Srgba::rgb(0.73333335, 0.96862745, 0.8156863); +///
+pub const GREEN_300: Srgba = Srgba::rgb(0.5254902, 0.9372549, 0.6745098); +///
+pub const GREEN_400: Srgba = Srgba::rgb(0.2901961, 0.87058824, 0.5019608); +///
+pub const GREEN_500: Srgba = Srgba::rgb(0.13333334, 0.77254903, 0.36862746); +///
+pub const GREEN_600: Srgba = Srgba::rgb(0.08627451, 0.6392157, 0.2901961); +///
+pub const GREEN_700: Srgba = Srgba::rgb(0.08235294, 0.5019608, 0.23921569); +///
+pub const GREEN_800: Srgba = Srgba::rgb(0.08627451, 0.39607844, 0.20392157); +///
+pub const GREEN_900: Srgba = Srgba::rgb(0.078431375, 0.3254902, 0.1764706); +///
+pub const GREEN_950: Srgba = Srgba::rgb(0.019607844, 0.18039216, 0.08627451); + +///
+pub const INDIGO_50: Srgba = Srgba::rgb(0.93333334, 0.9490196, 1.0); +///
+pub const INDIGO_100: Srgba = Srgba::rgb(0.8784314, 0.90588236, 1.0); +///
+pub const INDIGO_200: Srgba = Srgba::rgb(0.78039217, 0.8235294, 0.99607843); +///
+pub const INDIGO_300: Srgba = Srgba::rgb(0.64705884, 0.7058824, 0.9882353); +///
+pub const INDIGO_400: Srgba = Srgba::rgb(0.5058824, 0.54901963, 0.972549); +///
+pub const INDIGO_500: Srgba = Srgba::rgb(0.3882353, 0.4, 0.94509804); +///
+pub const INDIGO_600: Srgba = Srgba::rgb(0.30980393, 0.27450982, 0.8980392); +///
+pub const INDIGO_700: Srgba = Srgba::rgb(0.2627451, 0.21960784, 0.7921569); +///
+pub const INDIGO_800: Srgba = Srgba::rgb(0.21568628, 0.1882353, 0.6392157); +///
+pub const INDIGO_900: Srgba = Srgba::rgb(0.19215687, 0.18039216, 0.5058824); +///
+pub const INDIGO_950: Srgba = Srgba::rgb(0.11764706, 0.105882354, 0.29411766); + +///
+pub const LIME_50: Srgba = Srgba::rgb(0.96862745, 0.99607843, 0.90588236); +///
+pub const LIME_100: Srgba = Srgba::rgb(0.9254902, 0.9882353, 0.79607844); +///
+pub const LIME_200: Srgba = Srgba::rgb(0.8509804, 0.9764706, 0.6156863); +///
+pub const LIME_300: Srgba = Srgba::rgb(0.74509805, 0.9490196, 0.39215687); +///
+pub const LIME_400: Srgba = Srgba::rgb(0.6392157, 0.9019608, 0.20784314); +///
+pub const LIME_500: Srgba = Srgba::rgb(0.5176471, 0.8, 0.08627451); +///
+pub const LIME_600: Srgba = Srgba::rgb(0.39607844, 0.6392157, 0.050980393); +///
+pub const LIME_700: Srgba = Srgba::rgb(0.3019608, 0.4862745, 0.05882353); +///
+pub const LIME_800: Srgba = Srgba::rgb(0.24705882, 0.38431373, 0.07058824); +///
+pub const LIME_900: Srgba = Srgba::rgb(0.21176471, 0.3254902, 0.078431375); +///
+pub const LIME_950: Srgba = Srgba::rgb(0.101960786, 0.18039216, 0.019607844); + +///
+pub const NEUTRAL_50: Srgba = Srgba::rgb(0.98039216, 0.98039216, 0.98039216); +///
+pub const NEUTRAL_100: Srgba = Srgba::rgb(0.9607843, 0.9607843, 0.9607843); +///
+pub const NEUTRAL_200: Srgba = Srgba::rgb(0.8980392, 0.8980392, 0.8980392); +///
+pub const NEUTRAL_300: Srgba = Srgba::rgb(0.83137256, 0.83137256, 0.83137256); +///
+pub const NEUTRAL_400: Srgba = Srgba::rgb(0.6392157, 0.6392157, 0.6392157); +///
+pub const NEUTRAL_500: Srgba = Srgba::rgb(0.4509804, 0.4509804, 0.4509804); +///
+pub const NEUTRAL_600: Srgba = Srgba::rgb(0.32156864, 0.32156864, 0.32156864); +///
+pub const NEUTRAL_700: Srgba = Srgba::rgb(0.2509804, 0.2509804, 0.2509804); +///
+pub const NEUTRAL_800: Srgba = Srgba::rgb(0.14901961, 0.14901961, 0.14901961); +///
+pub const NEUTRAL_900: Srgba = Srgba::rgb(0.09019608, 0.09019608, 0.09019608); +///
+pub const NEUTRAL_950: Srgba = Srgba::rgb(0.039215688, 0.039215688, 0.039215688); + +///
+pub const ORANGE_50: Srgba = Srgba::rgb(1.0, 0.96862745, 0.92941177); +///
+pub const ORANGE_100: Srgba = Srgba::rgb(1.0, 0.92941177, 0.8352941); +///
+pub const ORANGE_200: Srgba = Srgba::rgb(0.99607843, 0.84313726, 0.6666667); +///
+pub const ORANGE_300: Srgba = Srgba::rgb(0.99215686, 0.7294118, 0.45490196); +///
+pub const ORANGE_400: Srgba = Srgba::rgb(0.9843137, 0.57254905, 0.23529412); +///
+pub const ORANGE_500: Srgba = Srgba::rgb(0.9764706, 0.4509804, 0.08627451); +///
+pub const ORANGE_600: Srgba = Srgba::rgb(0.91764706, 0.34509805, 0.047058824); +///
+pub const ORANGE_700: Srgba = Srgba::rgb(0.7607843, 0.25490198, 0.047058824); +///
+pub const ORANGE_800: Srgba = Srgba::rgb(0.6039216, 0.20392157, 0.07058824); +///
+pub const ORANGE_900: Srgba = Srgba::rgb(0.4862745, 0.1764706, 0.07058824); +///
+pub const ORANGE_950: Srgba = Srgba::rgb(0.2627451, 0.078431375, 0.02745098); + +///
+pub const PINK_50: Srgba = Srgba::rgb(0.99215686, 0.9490196, 0.972549); +///
+pub const PINK_100: Srgba = Srgba::rgb(0.9882353, 0.90588236, 0.9529412); +///
+pub const PINK_200: Srgba = Srgba::rgb(0.9843137, 0.8117647, 0.9098039); +///
+pub const PINK_300: Srgba = Srgba::rgb(0.9764706, 0.65882355, 0.83137256); +///
+pub const PINK_400: Srgba = Srgba::rgb(0.95686275, 0.44705883, 0.7137255); +///
+pub const PINK_500: Srgba = Srgba::rgb(0.9254902, 0.28235295, 0.6); +///
+pub const PINK_600: Srgba = Srgba::rgb(0.85882354, 0.15294118, 0.46666667); +///
+pub const PINK_700: Srgba = Srgba::rgb(0.74509805, 0.09411765, 0.3647059); +///
+pub const PINK_800: Srgba = Srgba::rgb(0.6156863, 0.09019608, 0.3019608); +///
+pub const PINK_900: Srgba = Srgba::rgb(0.5137255, 0.09411765, 0.2627451); +///
+pub const PINK_950: Srgba = Srgba::rgb(0.3137255, 0.02745098, 0.14117648); + +///
+pub const PURPLE_50: Srgba = Srgba::rgb(0.98039216, 0.9607843, 1.0); +///
+pub const PURPLE_100: Srgba = Srgba::rgb(0.9529412, 0.9098039, 1.0); +///
+pub const PURPLE_200: Srgba = Srgba::rgb(0.9137255, 0.8352941, 1.0); +///
+pub const PURPLE_300: Srgba = Srgba::rgb(0.84705883, 0.7058824, 0.99607843); +///
+pub const PURPLE_400: Srgba = Srgba::rgb(0.7529412, 0.5176471, 0.9882353); +///
+pub const PURPLE_500: Srgba = Srgba::rgb(0.65882355, 0.33333334, 0.96862745); +///
+pub const PURPLE_600: Srgba = Srgba::rgb(0.5764706, 0.2, 0.91764706); +///
+pub const PURPLE_700: Srgba = Srgba::rgb(0.49411765, 0.13333334, 0.80784315); +///
+pub const PURPLE_800: Srgba = Srgba::rgb(0.41960785, 0.12941177, 0.65882355); +///
+pub const PURPLE_900: Srgba = Srgba::rgb(0.34509805, 0.10980392, 0.5294118); +///
+pub const PURPLE_950: Srgba = Srgba::rgb(0.23137255, 0.02745098, 0.39215687); + +///
+pub const RED_50: Srgba = Srgba::rgb(0.99607843, 0.9490196, 0.9490196); +///
+pub const RED_100: Srgba = Srgba::rgb(0.99607843, 0.8862745, 0.8862745); +///
+pub const RED_200: Srgba = Srgba::rgb(0.99607843, 0.7921569, 0.7921569); +///
+pub const RED_300: Srgba = Srgba::rgb(0.9882353, 0.64705884, 0.64705884); +///
+pub const RED_400: Srgba = Srgba::rgb(0.972549, 0.44313726, 0.44313726); +///
+pub const RED_500: Srgba = Srgba::rgb(0.9372549, 0.26666668, 0.26666668); +///
+pub const RED_600: Srgba = Srgba::rgb(0.8627451, 0.14901961, 0.14901961); +///
+pub const RED_700: Srgba = Srgba::rgb(0.7254902, 0.10980392, 0.10980392); +///
+pub const RED_800: Srgba = Srgba::rgb(0.6, 0.105882354, 0.105882354); +///
+pub const RED_900: Srgba = Srgba::rgb(0.49803922, 0.11372549, 0.11372549); +///
+pub const RED_950: Srgba = Srgba::rgb(0.27058825, 0.039215688, 0.039215688); + +///
+pub const ROSE_50: Srgba = Srgba::rgb(1.0, 0.94509804, 0.9490196); +///
+pub const ROSE_100: Srgba = Srgba::rgb(1.0, 0.89411765, 0.9019608); +///
+pub const ROSE_200: Srgba = Srgba::rgb(0.99607843, 0.8039216, 0.827451); +///
+pub const ROSE_300: Srgba = Srgba::rgb(0.99215686, 0.6431373, 0.6862745); +///
+pub const ROSE_400: Srgba = Srgba::rgb(0.9843137, 0.44313726, 0.52156866); +///
+pub const ROSE_500: Srgba = Srgba::rgb(0.95686275, 0.24705882, 0.36862746); +///
+pub const ROSE_600: Srgba = Srgba::rgb(0.88235295, 0.11372549, 0.28235295); +///
+pub const ROSE_700: Srgba = Srgba::rgb(0.74509805, 0.07058824, 0.23529412); +///
+pub const ROSE_800: Srgba = Srgba::rgb(0.62352943, 0.07058824, 0.22352941); +///
+pub const ROSE_900: Srgba = Srgba::rgb(0.53333336, 0.07450981, 0.21568628); +///
+pub const ROSE_950: Srgba = Srgba::rgb(0.29803923, 0.019607844, 0.09803922); + +///
+pub const SKY_50: Srgba = Srgba::rgb(0.9411765, 0.9764706, 1.0); +///
+pub const SKY_100: Srgba = Srgba::rgb(0.8784314, 0.9490196, 0.99607843); +///
+pub const SKY_200: Srgba = Srgba::rgb(0.7294118, 0.9019608, 0.99215686); +///
+pub const SKY_300: Srgba = Srgba::rgb(0.49019608, 0.827451, 0.9882353); +///
+pub const SKY_400: Srgba = Srgba::rgb(0.21960784, 0.7411765, 0.972549); +///
+pub const SKY_500: Srgba = Srgba::rgb(0.05490196, 0.64705884, 0.9137255); +///
+pub const SKY_600: Srgba = Srgba::rgb(0.007843138, 0.5176471, 0.78039217); +///
+pub const SKY_700: Srgba = Srgba::rgb(0.011764706, 0.4117647, 0.6313726); +///
+pub const SKY_800: Srgba = Srgba::rgb(0.02745098, 0.34901962, 0.52156866); +///
+pub const SKY_900: Srgba = Srgba::rgb(0.047058824, 0.2901961, 0.43137255); +///
+pub const SKY_950: Srgba = Srgba::rgb(0.03137255, 0.18431373, 0.28627452); + +///
+pub const SLATE_50: Srgba = Srgba::rgb(0.972549, 0.98039216, 0.9882353); +///
+pub const SLATE_100: Srgba = Srgba::rgb(0.94509804, 0.9607843, 0.9764706); +///
+pub const SLATE_200: Srgba = Srgba::rgb(0.8862745, 0.9098039, 0.9411765); +///
+pub const SLATE_300: Srgba = Srgba::rgb(0.79607844, 0.8352941, 0.88235295); +///
+pub const SLATE_400: Srgba = Srgba::rgb(0.5803922, 0.6392157, 0.72156864); +///
+pub const SLATE_500: Srgba = Srgba::rgb(0.39215687, 0.45490196, 0.54509807); +///
+pub const SLATE_600: Srgba = Srgba::rgb(0.2784314, 0.33333334, 0.4117647); +///
+pub const SLATE_700: Srgba = Srgba::rgb(0.2, 0.25490198, 0.33333334); +///
+pub const SLATE_800: Srgba = Srgba::rgb(0.11764706, 0.16078432, 0.23137255); +///
+pub const SLATE_900: Srgba = Srgba::rgb(0.05882353, 0.09019608, 0.16470589); +///
+pub const SLATE_950: Srgba = Srgba::rgb(0.007843138, 0.023529412, 0.09019608); + +///
+pub const STONE_50: Srgba = Srgba::rgb(0.98039216, 0.98039216, 0.9764706); +///
+pub const STONE_100: Srgba = Srgba::rgb(0.9607843, 0.9607843, 0.95686275); +///
+pub const STONE_200: Srgba = Srgba::rgb(0.90588236, 0.8980392, 0.89411765); +///
+pub const STONE_300: Srgba = Srgba::rgb(0.8392157, 0.827451, 0.81960785); +///
+pub const STONE_400: Srgba = Srgba::rgb(0.65882355, 0.63529414, 0.61960787); +///
+pub const STONE_500: Srgba = Srgba::rgb(0.47058824, 0.44313726, 0.42352942); +///
+pub const STONE_600: Srgba = Srgba::rgb(0.34117648, 0.3254902, 0.30588236); +///
+pub const STONE_700: Srgba = Srgba::rgb(0.26666668, 0.2509804, 0.23529412); +///
+pub const STONE_800: Srgba = Srgba::rgb(0.16078432, 0.14509805, 0.14117648); +///
+pub const STONE_900: Srgba = Srgba::rgb(0.10980392, 0.09803922, 0.09019608); +///
+pub const STONE_950: Srgba = Srgba::rgb(0.047058824, 0.039215688, 0.03529412); + +///
+pub const TEAL_50: Srgba = Srgba::rgb(0.9411765, 0.99215686, 0.98039216); +///
+pub const TEAL_100: Srgba = Srgba::rgb(0.8, 0.9843137, 0.94509804); +///
+pub const TEAL_200: Srgba = Srgba::rgb(0.6, 0.9647059, 0.89411765); +///
+pub const TEAL_300: Srgba = Srgba::rgb(0.36862746, 0.91764706, 0.83137256); +///
+pub const TEAL_400: Srgba = Srgba::rgb(0.1764706, 0.83137256, 0.7490196); +///
+pub const TEAL_500: Srgba = Srgba::rgb(0.078431375, 0.72156864, 0.6509804); +///
+pub const TEAL_600: Srgba = Srgba::rgb(0.050980393, 0.5803922, 0.53333336); +///
+pub const TEAL_700: Srgba = Srgba::rgb(0.05882353, 0.4627451, 0.43137255); +///
+pub const TEAL_800: Srgba = Srgba::rgb(0.06666667, 0.36862746, 0.34901962); +///
+pub const TEAL_900: Srgba = Srgba::rgb(0.07450981, 0.30588236, 0.2901961); +///
+pub const TEAL_950: Srgba = Srgba::rgb(0.015686275, 0.18431373, 0.18039216); + +///
+pub const VIOLET_50: Srgba = Srgba::rgb(0.9607843, 0.9529412, 1.0); +///
+pub const VIOLET_100: Srgba = Srgba::rgb(0.92941177, 0.9137255, 0.99607843); +///
+pub const VIOLET_200: Srgba = Srgba::rgb(0.8666667, 0.8392157, 0.99607843); +///
+pub const VIOLET_300: Srgba = Srgba::rgb(0.76862746, 0.70980394, 0.99215686); +///
+pub const VIOLET_400: Srgba = Srgba::rgb(0.654902, 0.54509807, 0.98039216); +///
+pub const VIOLET_500: Srgba = Srgba::rgb(0.54509807, 0.36078432, 0.9647059); +///
+pub const VIOLET_600: Srgba = Srgba::rgb(0.4862745, 0.22745098, 0.92941177); +///
+pub const VIOLET_700: Srgba = Srgba::rgb(0.42745098, 0.15686275, 0.8509804); +///
+pub const VIOLET_800: Srgba = Srgba::rgb(0.35686275, 0.12941177, 0.7137255); +///
+pub const VIOLET_900: Srgba = Srgba::rgb(0.29803923, 0.11372549, 0.58431375); +///
+pub const VIOLET_950: Srgba = Srgba::rgb(0.18039216, 0.0627451, 0.39607844); + +///
+pub const YELLOW_50: Srgba = Srgba::rgb(0.99607843, 0.9882353, 0.9098039); +///
+pub const YELLOW_100: Srgba = Srgba::rgb(0.99607843, 0.9764706, 0.7647059); +///
+pub const YELLOW_200: Srgba = Srgba::rgb(0.99607843, 0.9411765, 0.5411765); +///
+pub const YELLOW_300: Srgba = Srgba::rgb(0.99215686, 0.8784314, 0.2784314); +///
+pub const YELLOW_400: Srgba = Srgba::rgb(0.98039216, 0.8, 0.08235294); +///
+pub const YELLOW_500: Srgba = Srgba::rgb(0.91764706, 0.7019608, 0.03137255); +///
+pub const YELLOW_600: Srgba = Srgba::rgb(0.7921569, 0.5411765, 0.015686275); +///
+pub const YELLOW_700: Srgba = Srgba::rgb(0.6313726, 0.38431373, 0.02745098); +///
+pub const YELLOW_800: Srgba = Srgba::rgb(0.52156866, 0.3019608, 0.05490196); +///
+pub const YELLOW_900: Srgba = Srgba::rgb(0.44313726, 0.24705882, 0.07058824); +///
+pub const YELLOW_950: Srgba = Srgba::rgb(0.25882354, 0.1254902, 0.023529412); + +///
+pub const ZINC_50: Srgba = Srgba::rgb(0.98039216, 0.98039216, 0.98039216); +///
+pub const ZINC_100: Srgba = Srgba::rgb(0.95686275, 0.95686275, 0.9607843); +///
+pub const ZINC_200: Srgba = Srgba::rgb(0.89411765, 0.89411765, 0.90588236); +///
+pub const ZINC_300: Srgba = Srgba::rgb(0.83137256, 0.83137256, 0.84705883); +///
+pub const ZINC_400: Srgba = Srgba::rgb(0.6313726, 0.6313726, 0.6666667); +///
+pub const ZINC_500: Srgba = Srgba::rgb(0.44313726, 0.44313726, 0.47843137); +///
+pub const ZINC_600: Srgba = Srgba::rgb(0.32156864, 0.32156864, 0.35686275); +///
+pub const ZINC_700: Srgba = Srgba::rgb(0.24705882, 0.24705882, 0.27450982); +///
+pub const ZINC_800: Srgba = Srgba::rgb(0.15294118, 0.15294118, 0.16470589); +///
+pub const ZINC_900: Srgba = Srgba::rgb(0.09411765, 0.09411765, 0.105882354); +///
+pub const ZINC_950: Srgba = Srgba::rgb(0.03529412, 0.03529412, 0.043137256); diff --git a/crates/bevy_color/src/srgba.rs b/crates/bevy_color/src/srgba.rs index 8479af4781daa..606a6e2363ed1 100644 --- a/crates/bevy_color/src/srgba.rs +++ b/crates/bevy_color/src/srgba.rs @@ -1,9 +1,9 @@ use std::ops::{Div, Mul}; use crate::color_difference::EuclideanDistance; -use crate::{Alpha, LinearRgba, Luminance, Mix, StandardColor, Xyza}; +use crate::{Alpha, ClampColor, LinearRgba, Luminance, Mix, StandardColor, Xyza}; use bevy_math::Vec4; -use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; +use bevy_reflect::prelude::*; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -13,7 +13,7 @@ use thiserror::Error; #[doc = include_str!("../docs/diagrams/model_graph.svg")] /// #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)] -#[reflect(PartialEq, Serialize, Deserialize)] +#[reflect(PartialEq, Serialize, Deserialize, Default)] pub struct Srgba { /// The red channel. [0.0, 1.0] pub red: f32, @@ -307,6 +307,24 @@ impl EuclideanDistance for Srgba { } } +impl ClampColor for Srgba { + fn clamped(&self) -> Self { + Self { + red: self.red.clamp(0., 1.), + green: self.green.clamp(0., 1.), + blue: self.blue.clamp(0., 1.), + alpha: self.alpha.clamp(0., 1.), + } + } + + fn is_within_bounds(&self) -> bool { + (0. ..=1.).contains(&self.red) + && (0. ..=1.).contains(&self.green) + && (0. ..=1.).contains(&self.blue) + && (0. ..=1.).contains(&self.alpha) + } +} + impl From for Srgba { #[inline] fn from(value: LinearRgba) -> Self { @@ -490,4 +508,21 @@ mod tests { assert!(matches!(Srgba::hex("yyy"), Err(HexColorError::Parse(_)))); assert!(matches!(Srgba::hex("##fff"), Err(HexColorError::Parse(_)))); } + + #[test] + fn test_clamp() { + let color_1 = Srgba::rgb(2., -1., 0.4); + let color_2 = Srgba::rgb(0.031, 0.749, 1.); + let mut color_3 = Srgba::rgb(-1., 1., 1.); + + assert!(!color_1.is_within_bounds()); + assert_eq!(color_1.clamped(), Srgba::rgb(1., 0., 0.4)); + + assert!(color_2.is_within_bounds()); + assert_eq!(color_2, color_2.clamped()); + + color_3.clamp(); + assert!(color_3.is_within_bounds()); + assert_eq!(color_3, Srgba::rgb(0., 1., 1.)); + } } diff --git a/crates/bevy_color/src/xyza.rs b/crates/bevy_color/src/xyza.rs index 76bf27cef04b5..008b42fb12b24 100644 --- a/crates/bevy_color/src/xyza.rs +++ b/crates/bevy_color/src/xyza.rs @@ -1,5 +1,7 @@ -use crate::{Alpha, LinearRgba, Luminance, Mix, StandardColor}; -use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; +use crate::{ + impl_componentwise_point, Alpha, ClampColor, LinearRgba, Luminance, Mix, StandardColor, +}; +use bevy_reflect::prelude::*; use serde::{Deserialize, Serialize}; /// [CIE 1931](https://en.wikipedia.org/wiki/CIE_1931_color_space) color space, also known as XYZ, with an alpha channel. @@ -8,7 +10,7 @@ use serde::{Deserialize, Serialize}; #[doc = include_str!("../docs/diagrams/model_graph.svg")] /// #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)] -#[reflect(PartialEq, Serialize, Deserialize)] +#[reflect(PartialEq, Serialize, Deserialize, Default)] pub struct Xyza { /// The x-axis. [0.0, 1.0] pub x: f32, @@ -22,6 +24,8 @@ pub struct Xyza { impl StandardColor for Xyza {} +impl_componentwise_point!(Xyza, [x, y, z, alpha]); + impl Xyza { /// Construct a new [`Xyza`] color from components. /// @@ -134,6 +138,24 @@ impl Mix for Xyza { } } +impl ClampColor for Xyza { + fn clamped(&self) -> Self { + Self { + x: self.x.clamp(0., 1.), + y: self.y.clamp(0., 1.), + z: self.z.clamp(0., 1.), + alpha: self.alpha.clamp(0., 1.), + } + } + + fn is_within_bounds(&self) -> bool { + (0. ..=1.).contains(&self.x) + && (0. ..=1.).contains(&self.y) + && (0. ..=1.).contains(&self.z) + && (0. ..=1.).contains(&self.alpha) + } +} + impl From for Xyza { fn from( LinearRgba { @@ -208,4 +230,21 @@ mod tests { assert_approx_eq!(color.xyz.alpha, xyz2.alpha, 0.001); } } + + #[test] + fn test_clamp() { + let color_1 = Xyza::xyz(2., -1., 0.4); + let color_2 = Xyza::xyz(0.031, 0.749, 1.); + let mut color_3 = Xyza::xyz(-1., 1., 1.); + + assert!(!color_1.is_within_bounds()); + assert_eq!(color_1.clamped(), Xyza::xyz(1., 0., 0.4)); + + assert!(color_2.is_within_bounds()); + assert_eq!(color_2, color_2.clamped()); + + color_3.clamp(); + assert!(color_3.is_within_bounds()); + assert_eq!(color_3, Xyza::xyz(0., 1., 1.)); + } } diff --git a/crates/bevy_core_pipeline/src/bloom/mod.rs b/crates/bevy_core_pipeline/src/bloom/mod.rs index 55d5a70719b5a..382e58a48675f 100644 --- a/crates/bevy_core_pipeline/src/bloom/mod.rs +++ b/crates/bevy_core_pipeline/src/bloom/mod.rs @@ -15,6 +15,7 @@ use bevy_ecs::{prelude::*, query::QueryItem}; use bevy_math::UVec2; use bevy_render::{ camera::ExtractedCamera, + diagnostic::RecordDiagnostics, extract_component::{ ComponentUniforms, DynamicUniformIndex, ExtractComponentPlugin, UniformComponentPlugin, }, @@ -156,6 +157,9 @@ impl ViewNode for BloomNode { render_context.command_encoder().push_debug_group("bloom"); + let diagnostics = render_context.diagnostic_recorder(); + let time_span = diagnostics.time_span(render_context.command_encoder(), "bloom"); + // First downsample pass { let downsampling_first_bind_group = render_context.render_device().create_bind_group( @@ -275,6 +279,7 @@ impl ViewNode for BloomNode { upsampling_final_pass.draw(0..3, 0..1); } + time_span.end(render_context.command_encoder()); render_context.command_encoder().pop_debug_group(); Ok(()) diff --git a/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs b/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs index 86e636d1a6b7d..83cfdfe3b3a46 100644 --- a/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs +++ b/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs @@ -22,6 +22,10 @@ pub struct Camera2d; pub struct Camera2dBundle { pub camera: Camera, pub camera_render_graph: CameraRenderGraph, + /// Note: default value for `OrthographicProjection.near` is `0.0` + /// which makes objects on the screen plane invisible to 2D camera. + /// `Camera2dBundle::default()` sets `near` to negative value, + /// so be careful when initializing this field manually. pub projection: OrthographicProjection, pub visible_entities: VisibleEntities, pub frustum: Frustum, diff --git a/crates/bevy_core_pipeline/src/core_2d/main_pass_2d_node.rs b/crates/bevy_core_pipeline/src/core_2d/main_pass_2d_node.rs index 4013db7b3bdfe..5e61f475af6cb 100644 --- a/crates/bevy_core_pipeline/src/core_2d/main_pass_2d_node.rs +++ b/crates/bevy_core_pipeline/src/core_2d/main_pass_2d_node.rs @@ -2,6 +2,7 @@ use crate::core_2d::Transparent2d; use bevy_ecs::prelude::*; use bevy_render::{ camera::ExtractedCamera, + diagnostic::RecordDiagnostics, render_graph::{Node, NodeRunError, RenderGraphContext}, render_phase::RenderPhase, render_resource::RenderPassDescriptor, @@ -52,6 +53,8 @@ impl Node for MainPass2dNode { #[cfg(feature = "trace")] let _main_pass_2d = info_span!("main_pass_2d").entered(); + let diagnostics = render_context.diagnostic_recorder(); + let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { label: Some("main_pass_2d"), color_attachments: &[Some(target.get_color_attachment())], @@ -60,11 +63,15 @@ impl Node for MainPass2dNode { occlusion_query_set: None, }); + let pass_span = diagnostics.pass_span(&mut render_pass, "main_pass_2d"); + if let Some(viewport) = camera.viewport.as_ref() { render_pass.set_camera_viewport(viewport); } transparent_phase.render(&mut render_pass, world, view_entity); + + pass_span.end(&mut render_pass); } // WebGL2 quirk: if ending with a render pass with a custom viewport, the viewport isn't diff --git a/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs b/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs index 5b7d1315e8849..53e732ca7d917 100644 --- a/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs +++ b/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs @@ -5,6 +5,7 @@ use crate::{ use bevy_ecs::{prelude::World, query::QueryItem}; use bevy_render::{ camera::ExtractedCamera, + diagnostic::RecordDiagnostics, render_graph::{NodeRunError, RenderGraphContext, ViewNode}, render_phase::{RenderPhase, TrackedRenderPass}, render_resource::{CommandEncoderDescriptor, PipelineCache, RenderPassDescriptor, StoreOp}, @@ -47,6 +48,8 @@ impl ViewNode for MainOpaquePass3dNode { ): QueryItem<'w, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { + let diagnostics = render_context.diagnostic_recorder(); + let color_attachments = [Some(target.get_color_attachment())]; let depth_stencil_attachment = Some(depth.get_attachment(StoreOp::Store)); @@ -70,6 +73,8 @@ impl ViewNode for MainOpaquePass3dNode { occlusion_query_set: None, }); let mut render_pass = TrackedRenderPass::new(&render_device, render_pass); + let pass_span = diagnostics.pass_span(&mut render_pass, "main_opaque_pass_3d"); + if let Some(viewport) = camera.viewport.as_ref() { render_pass.set_camera_viewport(viewport); } @@ -104,6 +109,7 @@ impl ViewNode for MainOpaquePass3dNode { } } + pass_span.end(&mut render_pass); drop(render_pass); command_encoder.finish() }); diff --git a/crates/bevy_core_pipeline/src/core_3d/main_transparent_pass_3d_node.rs b/crates/bevy_core_pipeline/src/core_3d/main_transparent_pass_3d_node.rs index 4057e40ee721d..18a19cd2fe9c7 100644 --- a/crates/bevy_core_pipeline/src/core_3d/main_transparent_pass_3d_node.rs +++ b/crates/bevy_core_pipeline/src/core_3d/main_transparent_pass_3d_node.rs @@ -2,6 +2,7 @@ use crate::core_3d::Transparent3d; use bevy_ecs::{prelude::*, query::QueryItem}; use bevy_render::{ camera::ExtractedCamera, + diagnostic::RecordDiagnostics, render_graph::{NodeRunError, RenderGraphContext, ViewNode}, render_phase::RenderPhase, render_resource::{RenderPassDescriptor, StoreOp}, @@ -37,6 +38,8 @@ impl ViewNode for MainTransparentPass3dNode { #[cfg(feature = "trace")] let _main_transparent_pass_3d_span = info_span!("main_transparent_pass_3d").entered(); + let diagnostics = render_context.diagnostic_recorder(); + let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { label: Some("main_transparent_pass_3d"), color_attachments: &[Some(target.get_color_attachment())], @@ -51,11 +54,15 @@ impl ViewNode for MainTransparentPass3dNode { occlusion_query_set: None, }); + let pass_span = diagnostics.pass_span(&mut render_pass, "main_transparent_pass_3d"); + if let Some(viewport) = camera.viewport.as_ref() { render_pass.set_camera_viewport(viewport); } transparent_phase.render(&mut render_pass, world, view_entity); + + pass_span.end(&mut render_pass); } // WebGL2 quirk: if ending with a render pass with a custom viewport, the viewport isn't diff --git a/crates/bevy_core_pipeline/src/prepass/node.rs b/crates/bevy_core_pipeline/src/prepass/node.rs index 928183ad7d010..b22fdf1fb69a5 100644 --- a/crates/bevy_core_pipeline/src/prepass/node.rs +++ b/crates/bevy_core_pipeline/src/prepass/node.rs @@ -2,6 +2,7 @@ use bevy_ecs::prelude::*; use bevy_ecs::query::QueryItem; use bevy_render::{ camera::ExtractedCamera, + diagnostic::RecordDiagnostics, render_graph::{NodeRunError, RenderGraphContext, ViewNode}, render_phase::{RenderPhase, TrackedRenderPass}, render_resource::{CommandEncoderDescriptor, RenderPassDescriptor, StoreOp}, @@ -43,6 +44,8 @@ impl ViewNode for PrepassNode { ): QueryItem<'w, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { + let diagnostics = render_context.diagnostic_recorder(); + let mut color_attachments = vec![ view_prepass_textures .normal @@ -83,7 +86,10 @@ impl ViewNode for PrepassNode { timestamp_writes: None, occlusion_query_set: None, }); + let mut render_pass = TrackedRenderPass::new(&render_device, render_pass); + let pass_span = diagnostics.pass_span(&mut render_pass, "prepass"); + if let Some(viewport) = camera.viewport.as_ref() { render_pass.set_camera_viewport(viewport); } @@ -102,6 +108,7 @@ impl ViewNode for PrepassNode { alpha_mask_prepass_phase.render(&mut render_pass, world, view_entity); } + pass_span.end(&mut render_pass); drop(render_pass); // Copy prepass depth to the main depth texture if deferred isn't going to diff --git a/crates/bevy_dev_tools/Cargo.toml b/crates/bevy_dev_tools/Cargo.toml index 7be2541583148..62ab2c8fcc8ae 100644 --- a/crates/bevy_dev_tools/Cargo.toml +++ b/crates/bevy_dev_tools/Cargo.toml @@ -9,22 +9,31 @@ license = "MIT OR Apache-2.0" keywords = ["bevy"] [features] +default = ["bevy_ui_debug"] bevy_ci_testing = ["serde", "ron"] +bevy_ui_debug = [] [dependencies] # bevy bevy_app = { path = "../bevy_app", version = "0.14.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.14.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.14.0-dev" } +bevy_core = { path = "../bevy_core", version = "0.14.0-dev" } +bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.14.0-dev" } +bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.14.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" } +bevy_gizmos = { path = "../bevy_gizmos", version = "0.14.0-dev" } +bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.14.0-dev" } +bevy_input = { path = "../bevy_input", version = "0.14.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.14.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev" } bevy_render = { path = "../bevy_render", version = "0.14.0-dev" } bevy_time = { path = "../bevy_time", version = "0.14.0-dev" } -bevy_window = { path = "../bevy_window", version = "0.14.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.14.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.14.0-dev" } bevy_ui = { path = "../bevy_ui", version = "0.14.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" } +bevy_window = { path = "../bevy_window", version = "0.14.0-dev" } bevy_text = { path = "../bevy_text", version = "0.14.0-dev" } -bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.14.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.14.0-dev" } -bevy_input = { path = "../bevy_input", version = "0.14.0-dev" } # other serde = { version = "1.0", features = ["derive"], optional = true } diff --git a/crates/bevy_dev_tools/src/debug_overlay/inset.rs b/crates/bevy_dev_tools/src/debug_overlay/inset.rs new file mode 100644 index 0000000000000..86be2146c73d7 --- /dev/null +++ b/crates/bevy_dev_tools/src/debug_overlay/inset.rs @@ -0,0 +1,192 @@ +use bevy_color::Color; +use bevy_gizmos::{config::GizmoConfigGroup, prelude::Gizmos}; +use bevy_math::{Vec2, Vec2Swizzles}; +use bevy_reflect::Reflect; +use bevy_transform::prelude::GlobalTransform; +use bevy_utils::HashMap; + +use super::{CameraQuery, LayoutRect}; + +// Function used here so we don't need to redraw lines that are fairly close to each other. +fn approx_eq(compared: f32, other: f32) -> bool { + (compared - other).abs() < 0.001 +} + +fn rect_border_axis(rect: LayoutRect) -> (f32, f32, f32, f32) { + let pos = rect.pos; + let size = rect.size; + let offset = pos + size; + (pos.x, offset.x, pos.y, offset.y) +} + +#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug)] +enum Dir { + Start, + End, +} +impl Dir { + const fn increments(self) -> i64 { + match self { + Dir::Start => 1, + Dir::End => -1, + } + } +} +impl From for Dir { + fn from(value: i64) -> Self { + if value.is_positive() { + Dir::Start + } else { + Dir::End + } + } +} +/// Collection of axis aligned "lines" (actually just their coordinate on +/// a given axis). +#[derive(Debug, Clone)] +struct DrawnLines { + lines: HashMap, + width: f32, +} +#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)] +impl DrawnLines { + fn new(width: f32) -> Self { + DrawnLines { + lines: HashMap::new(), + width, + } + } + /// Return `value` offset by as many `increment`s as necessary to make it + /// not overlap with already drawn lines. + fn inset(&self, value: f32) -> f32 { + let scaled = value / self.width; + let fract = scaled.fract(); + let mut on_grid = scaled.floor() as i64; + for _ in 0..10 { + let Some(dir) = self.lines.get(&on_grid) else { + break; + }; + // TODO(clean): This fixes a panic, but I'm not sure how valid this is + let Some(added) = on_grid.checked_add(dir.increments()) else { + break; + }; + on_grid = added; + } + ((on_grid as f32) + fract) * self.width + } + /// Remove a line from the collection of drawn lines. + /// + /// Typically, we only care for pre-existing lines when drawing the children + /// of a container, nothing more. So we remove it after we are done with + /// the children. + fn remove(&mut self, value: f32, increment: i64) { + let mut on_grid = (value / self.width).floor() as i64; + loop { + // TODO(clean): This fixes a panic, but I'm not sure how valid this is + let Some(next_cell) = on_grid.checked_add(increment) else { + return; + }; + if !self.lines.contains_key(&next_cell) { + self.lines.remove(&on_grid); + return; + } + on_grid = next_cell; + } + } + /// Add a line from the collection of drawn lines. + fn add(&mut self, value: f32, increment: i64) { + let mut on_grid = (value / self.width).floor() as i64; + loop { + let old_value = self.lines.insert(on_grid, increment.into()); + if old_value.is_none() { + return; + } + // TODO(clean): This fixes a panic, but I'm not sure how valid this is + let Some(added) = on_grid.checked_add(increment) else { + return; + }; + on_grid = added; + } + } +} + +#[derive(GizmoConfigGroup, Reflect, Default)] +pub struct UiGizmosDebug; + +pub(super) struct InsetGizmo<'w, 's> { + draw: Gizmos<'w, 's, UiGizmosDebug>, + cam: CameraQuery<'w, 's>, + known_y: DrawnLines, + known_x: DrawnLines, +} +impl<'w, 's> InsetGizmo<'w, 's> { + pub(super) fn new( + draw: Gizmos<'w, 's, UiGizmosDebug>, + cam: CameraQuery<'w, 's>, + line_width: f32, + ) -> Self { + InsetGizmo { + draw, + cam, + known_y: DrawnLines::new(line_width), + known_x: DrawnLines::new(line_width), + } + } + fn relative(&self, mut position: Vec2) -> Vec2 { + let zero = GlobalTransform::IDENTITY; + let Ok(cam) = self.cam.get_single() else { + return Vec2::ZERO; + }; + if let Some(new_position) = cam.world_to_viewport(&zero, position.extend(0.)) { + position = new_position; + }; + position.xy() + } + fn line_2d(&mut self, mut start: Vec2, mut end: Vec2, color: Color) { + if approx_eq(start.x, end.x) { + start.x = self.known_x.inset(start.x); + end.x = start.x; + } else if approx_eq(start.y, end.y) { + start.y = self.known_y.inset(start.y); + end.y = start.y; + } + let (start, end) = (self.relative(start), self.relative(end)); + self.draw.line_2d(start, end, color); + } + pub(super) fn set_scope(&mut self, rect: LayoutRect) { + let (left, right, top, bottom) = rect_border_axis(rect); + self.known_x.add(left, 1); + self.known_x.add(right, -1); + self.known_y.add(top, 1); + self.known_y.add(bottom, -1); + } + pub(super) fn clear_scope(&mut self, rect: LayoutRect) { + let (left, right, top, bottom) = rect_border_axis(rect); + self.known_x.remove(left, 1); + self.known_x.remove(right, -1); + self.known_y.remove(top, 1); + self.known_y.remove(bottom, -1); + } + pub(super) fn rect_2d(&mut self, rect: LayoutRect, color: Color) { + let (left, right, top, bottom) = rect_border_axis(rect); + if approx_eq(left, right) { + self.line_2d(Vec2::new(left, top), Vec2::new(left, bottom), color); + } else if approx_eq(top, bottom) { + self.line_2d(Vec2::new(left, top), Vec2::new(right, top), color); + } else { + let inset_x = |v| self.known_x.inset(v); + let inset_y = |v| self.known_y.inset(v); + let (left, right) = (inset_x(left), inset_x(right)); + let (top, bottom) = (inset_y(top), inset_y(bottom)); + let strip = [ + Vec2::new(left, top), + Vec2::new(left, bottom), + Vec2::new(right, bottom), + Vec2::new(right, top), + Vec2::new(left, top), + ]; + self.draw + .linestrip_2d(strip.map(|v| self.relative(v)), color); + } + } +} diff --git a/crates/bevy_dev_tools/src/debug_overlay/mod.rs b/crates/bevy_dev_tools/src/debug_overlay/mod.rs new file mode 100644 index 0000000000000..952539850638d --- /dev/null +++ b/crates/bevy_dev_tools/src/debug_overlay/mod.rs @@ -0,0 +1,280 @@ +//! A visual representation of UI node sizes. +use std::any::{Any, TypeId}; + +use bevy_app::{App, Plugin, PostUpdate}; +use bevy_color::Hsla; +use bevy_core::Name; +use bevy_core_pipeline::core_2d::Camera2dBundle; +use bevy_ecs::{prelude::*, system::SystemParam}; +use bevy_gizmos::{config::GizmoConfigStore, prelude::Gizmos, AppGizmoBuilder}; +use bevy_hierarchy::{Children, Parent}; +use bevy_math::{Vec2, Vec3Swizzles}; +use bevy_render::{ + camera::RenderTarget, + prelude::*, + view::{RenderLayers, VisibilitySystems}, +}; +use bevy_transform::{prelude::GlobalTransform, TransformSystem}; +use bevy_ui::{DefaultUiCamera, Display, Node, Style, TargetCamera, UiScale}; +use bevy_utils::{default, warn_once}; +use bevy_window::{PrimaryWindow, Window, WindowRef}; + +use inset::InsetGizmo; + +use self::inset::UiGizmosDebug; + +mod inset; + +/// The [`Camera::order`] index used by the layout debug camera. +pub const LAYOUT_DEBUG_CAMERA_ORDER: isize = 255; +/// The [`RenderLayers`] used by the debug gizmos and the debug camera. +pub const LAYOUT_DEBUG_LAYERS: RenderLayers = RenderLayers::none().with(16); + +#[derive(Clone, Copy)] +struct LayoutRect { + pos: Vec2, + size: Vec2, +} + +impl LayoutRect { + fn new(trans: &GlobalTransform, node: &Node, scale: f32) -> Self { + let mut this = Self { + pos: trans.translation().xy() * scale, + size: node.size() * scale, + }; + this.pos -= this.size / 2.; + this + } +} + +#[derive(Component, Debug, Clone, Default)] +struct DebugOverlayCamera; + +/// The debug overlay options. +#[derive(Resource, Clone, Default)] +pub struct UiDebugOptions { + /// Whether the overlay is enabled. + pub enabled: bool, + layout_gizmos_camera: Option, +} +impl UiDebugOptions { + /// This will toggle the enabled field, setting it to false if true and true if false. + pub fn toggle(&mut self) { + self.enabled = !self.enabled; + } +} + +/// The system responsible to change the [`Camera`] config based on changes in [`UiDebugOptions`] and [`GizmoConfig`](bevy_gizmos::prelude::GizmoConfig). +fn update_debug_camera( + mut gizmo_config: ResMut, + mut options: ResMut, + mut cmds: Commands, + mut debug_cams: Query<&mut Camera, With>, +) { + if !options.is_changed() && !gizmo_config.is_changed() { + return; + } + if !options.enabled { + let Some(cam) = options.layout_gizmos_camera else { + return; + }; + let Ok(mut cam) = debug_cams.get_mut(cam) else { + return; + }; + cam.is_active = false; + if let Some((config, _)) = gizmo_config.get_config_mut_dyn(&TypeId::of::()) { + config.enabled = false; + } + } else { + let spawn_cam = || { + cmds.spawn(( + Camera2dBundle { + projection: OrthographicProjection { + far: 1000.0, + viewport_origin: Vec2::new(0.0, 0.0), + ..default() + }, + camera: Camera { + order: LAYOUT_DEBUG_CAMERA_ORDER, + clear_color: ClearColorConfig::None, + ..default() + }, + ..default() + }, + LAYOUT_DEBUG_LAYERS, + DebugOverlayCamera, + Name::new("Layout Debug Camera"), + )) + .id() + }; + if let Some((config, _)) = gizmo_config.get_config_mut_dyn(&TypeId::of::()) { + config.enabled = true; + config.render_layers = LAYOUT_DEBUG_LAYERS; + } + let cam = *options.layout_gizmos_camera.get_or_insert_with(spawn_cam); + let Ok(mut cam) = debug_cams.get_mut(cam) else { + return; + }; + cam.is_active = true; + } +} + +/// The function that goes over every children of given [`Entity`], skipping the not visible ones and drawing the gizmos outlines. +fn outline_nodes(outline: &OutlineParam, draw: &mut InsetGizmo, this_entity: Entity, scale: f32) { + let Ok(to_iter) = outline.children.get(this_entity) else { + return; + }; + + for (entity, trans, node, style, children) in outline.nodes.iter_many(to_iter) { + if style.is_none() || style.is_some_and(|s| matches!(s.display, Display::None)) { + continue; + } + + if let Ok(view_visibility) = outline.view_visibility.get(entity) { + if !view_visibility.get() { + continue; + } + } + let rect = LayoutRect::new(trans, node, scale); + outline_node(entity, rect, draw); + if children.is_some() { + outline_nodes(outline, draw, entity, scale); + } + draw.clear_scope(rect); + } +} + +type NodesQuery = ( + Entity, + &'static GlobalTransform, + &'static Node, + Option<&'static Style>, + Option<&'static Children>, +); + +#[derive(SystemParam)] +struct OutlineParam<'w, 's> { + gizmo_config: Res<'w, GizmoConfigStore>, + children: Query<'w, 's, &'static Children>, + nodes: Query<'w, 's, NodesQuery>, + view_visibility: Query<'w, 's, &'static ViewVisibility>, + ui_scale: Res<'w, UiScale>, +} + +type CameraQuery<'w, 's> = Query<'w, 's, &'static Camera, With>; + +#[derive(SystemParam)] +struct CameraParam<'w, 's> { + debug_camera: Query<'w, 's, &'static Camera, With>, + cameras: Query<'w, 's, &'static Camera, Without>, + primary_window: Query<'w, 's, &'static Window, With>, + default_ui_camera: DefaultUiCamera<'w, 's>, +} + +/// system responsible for drawing the gizmos lines around all the node roots, iterating recursively through all visible children. +fn outline_roots( + outline: OutlineParam, + draw: Gizmos, + cam: CameraParam, + roots: Query< + ( + Entity, + &GlobalTransform, + &Node, + Option<&ViewVisibility>, + Option<&TargetCamera>, + ), + Without, + >, + window: Query<&Window, With>, + nonprimary_windows: Query<&Window, Without>, + options: Res, +) { + if !options.enabled { + return; + } + if !nonprimary_windows.is_empty() { + warn_once!( + "The layout debug view only uses the primary window scale, \ + you might notice gaps between container lines" + ); + } + let window_scale = window.get_single().map_or(1., Window::scale_factor); + let scale_factor = window_scale * outline.ui_scale.0; + + // We let the line be defined by the window scale alone + let line_width = outline + .gizmo_config + .get_config_dyn(&UiGizmosDebug.type_id()) + .map_or(2., |(config, _)| config.line_width) + / window_scale; + let mut draw = InsetGizmo::new(draw, cam.debug_camera, line_width); + for (entity, trans, node, view_visibility, maybe_target_camera) in &roots { + if let Some(view_visibility) = view_visibility { + // If the entity isn't visible, we will not draw any lines. + if !view_visibility.get() { + continue; + } + } + // We skip ui in other windows that are not the primary one + if let Some(camera_entity) = maybe_target_camera + .map(|target| target.0) + .or(cam.default_ui_camera.get()) + { + let Ok(camera) = cam.cameras.get(camera_entity) else { + // The camera wasn't found. Either the Camera don't exist or the Camera is the debug Camera, that we want to skip and warn + warn_once!("Camera {:?} wasn't found for debug overlay", camera_entity); + continue; + }; + match camera.target { + RenderTarget::Window(window_ref) => { + if let WindowRef::Entity(window_entity) = window_ref { + if cam.primary_window.get(window_entity).is_err() { + // This window isn't the primary, so we skip this root. + continue; + } + } + } + // Hard to know the results of this, better skip this target. + _ => continue, + } + } + + let rect = LayoutRect::new(trans, node, scale_factor); + outline_node(entity, rect, &mut draw); + outline_nodes(&outline, &mut draw, entity, scale_factor); + } +} + +/// Function responsible for drawing the gizmos lines around the given Entity +fn outline_node(entity: Entity, rect: LayoutRect, draw: &mut InsetGizmo) { + let color = Hsla::sequential_dispersed(entity.index()); + + draw.rect_2d(rect, color.into()); + draw.set_scope(rect); +} + +/// The debug overlay plugin. +/// +/// This spawns a new camera with a low order, and draws gizmo. +/// +/// Note that due to limitation with [`bevy_gizmos`], multiple windows with this feature +/// enabled isn't supported and the lines are only drawn in the [`PrimaryWindow`] +pub struct DebugUiPlugin; +impl Plugin for DebugUiPlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .init_gizmo_group::() + .add_systems( + PostUpdate, + ( + update_debug_camera, + outline_roots + .after(TransformSystem::TransformPropagate) + // This needs to run before VisibilityPropagate so it can relies on ViewVisibility + .before(VisibilitySystems::VisibilityPropagate), + ) + .chain(), + ); + } +} diff --git a/crates/bevy_dev_tools/src/fps_overlay.rs b/crates/bevy_dev_tools/src/fps_overlay.rs index 51b8e7886dcf9..d3f57f998411b 100644 --- a/crates/bevy_dev_tools/src/fps_overlay.rs +++ b/crates/bevy_dev_tools/src/fps_overlay.rs @@ -10,8 +10,18 @@ use bevy_ecs::{ schedule::{common_conditions::resource_changed, IntoSystemConfigs}, system::{Commands, Query, Res, Resource}, }; +use bevy_hierarchy::BuildChildren; use bevy_text::{Font, Text, TextSection, TextStyle}; -use bevy_ui::node_bundles::TextBundle; +use bevy_ui::{ + node_bundles::{NodeBundle, TextBundle}, + PositionType, Style, ZIndex, +}; +use bevy_utils::default; + +/// Global [`ZIndex`] used to render the fps overlay. +/// +/// We use a number slightly under `i32::MAX` so you can render on top of it if you really need to. +pub const FPS_OVERLAY_ZINDEX: i32 = i32::MAX - 32; /// A plugin that adds an FPS overlay to the Bevy application. /// @@ -67,13 +77,26 @@ impl Default for FpsOverlayConfig { struct FpsText; fn setup(mut commands: Commands, overlay_config: Res) { - commands.spawn(( - TextBundle::from_sections([ - TextSection::new("FPS: ", overlay_config.text_config.clone()), - TextSection::from_style(overlay_config.text_config.clone()), - ]), - FpsText, - )); + commands + .spawn(NodeBundle { + style: Style { + // We need to make sure the overlay doesn't affect the position of other UI nodes + position_type: PositionType::Absolute, + ..default() + }, + // Render overlay on top of everything + z_index: ZIndex::Global(FPS_OVERLAY_ZINDEX), + ..default() + }) + .with_children(|c| { + c.spawn(( + TextBundle::from_sections([ + TextSection::new("FPS: ", overlay_config.text_config.clone()), + TextSection::from_style(overlay_config.text_config.clone()), + ]), + FpsText, + )); + }); } fn update_text(diagnostic: Res, mut query: Query<&mut Text, With>) { diff --git a/crates/bevy_dev_tools/src/lib.rs b/crates/bevy_dev_tools/src/lib.rs index adad8cec9030b..8bb9b0b7d2b98 100644 --- a/crates/bevy_dev_tools/src/lib.rs +++ b/crates/bevy_dev_tools/src/lib.rs @@ -8,6 +8,9 @@ use bevy_app::prelude::*; pub mod ci_testing; pub mod fps_overlay; +#[cfg(feature = "bevy_ui_debug")] +pub mod debug_overlay; + /// Enables developer tools in an [`App`]. This plugin is added automatically with `bevy_dev_tools` /// feature. /// diff --git a/crates/bevy_diagnostic/src/diagnostic.rs b/crates/bevy_diagnostic/src/diagnostic.rs index f3b0bb04c0934..d202ffff33207 100644 --- a/crates/bevy_diagnostic/src/diagnostic.rs +++ b/crates/bevy_diagnostic/src/diagnostic.rs @@ -130,7 +130,9 @@ pub struct Diagnostic { impl Diagnostic { /// Add a new value as a [`DiagnosticMeasurement`]. pub fn add_measurement(&mut self, measurement: DiagnosticMeasurement) { - if let Some(previous) = self.measurement() { + if measurement.value.is_nan() { + // Skip calculating the moving average. + } else if let Some(previous) = self.measurement() { let delta = (measurement.time - previous.time).as_secs_f64(); let alpha = (delta / self.ema_smoothing_factor).clamp(0.0, 1.0); self.ema += alpha * (measurement.value - self.ema); @@ -139,16 +141,24 @@ impl Diagnostic { } if self.max_history_length > 1 { - if self.history.len() == self.max_history_length { + if self.history.len() >= self.max_history_length { if let Some(removed_diagnostic) = self.history.pop_front() { - self.sum -= removed_diagnostic.value; + if !removed_diagnostic.value.is_nan() { + self.sum -= removed_diagnostic.value; + } } } - self.sum += measurement.value; + if measurement.value.is_finite() { + self.sum += measurement.value; + } } else { self.history.clear(); - self.sum = measurement.value; + if measurement.value.is_nan() { + self.sum = 0.0; + } else { + self.sum = measurement.value; + } } self.history.push_back(measurement); @@ -172,8 +182,13 @@ impl Diagnostic { #[must_use] pub fn with_max_history_length(mut self, max_history_length: usize) -> Self { self.max_history_length = max_history_length; - self.history.reserve(self.max_history_length); - self.history.shrink_to(self.max_history_length); + + // reserve/reserve_exact reserve space for n *additional* elements. + let expected_capacity = self + .max_history_length + .saturating_sub(self.history.capacity()); + self.history.reserve_exact(expected_capacity); + self.history.shrink_to(expected_capacity); self } diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml index 11fd506a382ea..06e099b0059ce 100644 --- a/crates/bevy_ecs/Cargo.toml +++ b/crates/bevy_ecs/Cargo.toml @@ -25,7 +25,7 @@ petgraph = "0.6" bitflags = "2.3" concurrent-queue = "2.4.0" -fixedbitset = "0.4.2" +fixedbitset = "0.5" rustc-hash = "1.1" serde = "1" thiserror = "1.0" diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs index 9dbc26ac1a8cc..3013b9bb8a292 100644 --- a/crates/bevy_ecs/src/entity/mod.rs +++ b/crates/bevy_ecs/src/entity/mod.rs @@ -139,7 +139,7 @@ type IdCursor = isize; /// [`Query::get`]: crate::system::Query::get /// [`World`]: crate::world::World /// [SemVer]: https://semver.org/ -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr( feature = "bevy_reflect", @@ -384,9 +384,15 @@ impl<'de> Deserialize<'de> for Entity { } } -impl fmt::Debug for Entity { +impl fmt::Display for Entity { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}v{}", self.index(), self.generation()) + write!( + f, + "{}v{}|{}", + self.index(), + self.generation(), + self.to_bits() + ) } } @@ -1147,4 +1153,14 @@ mod tests { assert_ne!(hash, first_hash); } } + + #[test] + fn entity_display() { + let entity = Entity::from_raw(42); + let string = format!("{}", entity); + let bits = entity.to_bits().to_string(); + assert!(string.contains("42")); + assert!(string.contains("v1")); + assert!(string.contains(&bits)); + } } diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 17c5693d59269..245e2bcd64786 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -84,6 +84,7 @@ mod tests { #[derive(Component, Debug, PartialEq, Eq, Clone, Copy)] struct C; + #[allow(dead_code)] #[derive(Default)] struct NonSendA(usize, PhantomData<*mut ()>); @@ -102,6 +103,8 @@ mod tests { } } + // TODO: The compiler says the Debug and Clone are removed during dead code analysis. Investigate. + #[allow(dead_code)] #[derive(Component, Clone, Debug)] #[component(storage = "SparseSet")] struct DropCkSparse(DropCk); @@ -1724,9 +1727,12 @@ mod tests { ); } + // These fields are never read so we get a dead code lint here. + #[allow(dead_code)] #[derive(Component)] struct ComponentA(u32); + #[allow(dead_code)] #[derive(Component)] struct ComponentB(u32); diff --git a/crates/bevy_ecs/src/query/access.rs b/crates/bevy_ecs/src/query/access.rs index 848309be77ecc..73d14880199a7 100644 --- a/crates/bevy_ecs/src/query/access.rs +++ b/crates/bevy_ecs/src/query/access.rs @@ -97,26 +97,17 @@ impl Access { } } - /// Increases the set capacity to the specified amount. - /// - /// Does nothing if `capacity` is less than or equal to the current value. - pub fn grow(&mut self, capacity: usize) { - self.reads_and_writes.grow(capacity); - self.writes.grow(capacity); - } - /// Adds access to the element given by `index`. pub fn add_read(&mut self, index: T) { - self.reads_and_writes.grow(index.sparse_set_index() + 1); - self.reads_and_writes.insert(index.sparse_set_index()); + self.reads_and_writes + .grow_and_insert(index.sparse_set_index()); } /// Adds exclusive access to the element given by `index`. pub fn add_write(&mut self, index: T) { - self.reads_and_writes.grow(index.sparse_set_index() + 1); - self.reads_and_writes.insert(index.sparse_set_index()); - self.writes.grow(index.sparse_set_index() + 1); - self.writes.insert(index.sparse_set_index()); + self.reads_and_writes + .grow_and_insert(index.sparse_set_index()); + self.writes.grow_and_insert(index.sparse_set_index()); } /// Adds an archetypal (indirect) access to the element given by `index`. @@ -128,8 +119,7 @@ impl Access { /// /// [`Has`]: crate::query::Has pub fn add_archetypal(&mut self, index: T) { - self.archetypal.grow(index.sparse_set_index() + 1); - self.archetypal.insert(index.sparse_set_index()); + self.archetypal.grow_and_insert(index.sparse_set_index()); } /// Returns `true` if this can access the element given by `index`. @@ -389,9 +379,7 @@ impl FilteredAccess { } fn add_required(&mut self, index: T) { - let index = index.sparse_set_index(); - self.required.grow(index + 1); - self.required.insert(index); + self.required.grow_and_insert(index.sparse_set_index()); } /// Adds a `With` filter: corresponds to a conjunction (AND) operation. @@ -399,10 +387,8 @@ impl FilteredAccess { /// Suppose we begin with `Or<(With
, With)>`, which is represented by an array of two `AccessFilter` instances. /// Adding `AND With` via this method transforms it into the equivalent of `Or<((With, With), (With, With))>`. pub fn and_with(&mut self, index: T) { - let index = index.sparse_set_index(); for filter in &mut self.filter_sets { - filter.with.grow(index + 1); - filter.with.insert(index); + filter.with.grow_and_insert(index.sparse_set_index()); } } @@ -411,10 +397,8 @@ impl FilteredAccess { /// Suppose we begin with `Or<(With, With)>`, which is represented by an array of two `AccessFilter` instances. /// Adding `AND Without` via this method transforms it into the equivalent of `Or<((With, Without), (With, Without))>`. pub fn and_without(&mut self, index: T) { - let index = index.sparse_set_index(); for filter in &mut self.filter_sets { - filter.without.grow(index + 1); - filter.without.insert(index); + filter.without.grow_and_insert(index.sparse_set_index()); } } @@ -689,7 +673,6 @@ mod tests { fn read_all_access_conflicts() { // read_all / single write let mut access_a = Access::::default(); - access_a.grow(10); access_a.add_write(0); let mut access_b = Access::::default(); @@ -699,7 +682,6 @@ mod tests { // read_all / read_all let mut access_a = Access::::default(); - access_a.grow(10); access_a.read_all(); let mut access_b = Access::::default(); diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index 1a97e2bd70b4e..75da60c8d50db 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -31,7 +31,6 @@ pub struct QueryState { pub(crate) archetype_generation: ArchetypeGeneration, pub(crate) matched_tables: FixedBitSet, pub(crate) matched_archetypes: FixedBitSet, - pub(crate) archetype_component_access: Access, pub(crate) component_access: FilteredAccess, // NOTE: we maintain both a TableId bitset and a vec because iterating the vec is faster pub(crate) matched_table_ids: Vec, @@ -96,11 +95,6 @@ impl QueryState { &*ptr::from_ref(self).cast::>() } - /// Returns the archetype components accessed by this query. - pub fn archetype_component_access(&self) -> &Access { - &self.archetype_component_access - } - /// Returns the components accessed by this query. pub fn component_access(&self) -> &FilteredAccess { &self.component_access @@ -120,6 +114,31 @@ impl QueryState { impl QueryState { /// Creates a new [`QueryState`] from a given [`World`] and inherits the result of `world.id()`. pub fn new(world: &mut World) -> Self { + let mut state = Self::new_uninitialized(world); + state.update_archetypes(world); + state + } + + /// Identical to `new`, but it populates the provided `access` with the matched results. + pub(crate) fn new_with_access( + world: &mut World, + access: &mut Access, + ) -> Self { + let mut state = Self::new_uninitialized(world); + for archetype in world.archetypes.iter() { + if state.new_archetype_internal(archetype) { + state.update_archetype_component_access(archetype, access); + } + } + state.archetype_generation = world.archetypes.generation(); + state + } + + /// Creates a new [`QueryState`] but does not populate it with the matched results from the World yet + /// + /// `new_archetype` and it's variants must be called on all of the World's archetypes before the + /// state can return valid query results. + fn new_uninitialized(world: &mut World) -> Self { let fetch_state = D::init_state(world); let filter_state = F::init_state(world); @@ -136,7 +155,7 @@ impl QueryState { // properly considered in a global "cross-query" context (both within systems and across systems). component_access.extend(&filter_component_access); - let mut state = Self { + Self { world_id: world.id(), archetype_generation: ArchetypeGeneration::initial(), matched_table_ids: Vec::new(), @@ -146,16 +165,13 @@ impl QueryState { component_access, matched_tables: Default::default(), matched_archetypes: Default::default(), - archetype_component_access: Default::default(), #[cfg(feature = "trace")] par_iter_span: bevy_utils::tracing::info_span!( "par_for_each", query = std::any::type_name::(), filter = std::any::type_name::(), ), - }; - state.update_archetypes(world); - state + } } /// Creates a new [`QueryState`] from a given [`QueryBuilder`] and inherits it's [`FilteredAccess`]. @@ -174,7 +190,6 @@ impl QueryState { component_access: builder.access().clone(), matched_tables: Default::default(), matched_archetypes: Default::default(), - archetype_component_access: Default::default(), #[cfg(feature = "trace")] par_iter_span: bevy_utils::tracing::info_span!( "par_for_each", @@ -188,9 +203,17 @@ impl QueryState { /// Checks if the query is empty for the given [`World`], where the last change and current tick are given. /// + /// This is equivalent to `self.iter().next().is_none()`, and thus the worst case runtime will be `O(n)` + /// where `n` is the number of *potential* matches. This can be notably expensive for queries that rely + /// on non-archetypal filters such as [`Added`] or [`Changed`] which must individually check each query + /// result for a match. + /// /// # Panics /// /// If `world` does not match the one used to call `QueryState::new` for this instance. + /// + /// [`Added`]: crate::query::Added + /// [`Changed`]: crate::query::Changed #[inline] pub fn is_empty(&self, world: &World, last_run: Tick, this_run: Tick) -> bool { self.validate_world(world.id()); @@ -268,7 +291,7 @@ impl QueryState { std::mem::replace(&mut self.archetype_generation, archetypes.generation()); for archetype in &archetypes[old_generation..] { - self.new_archetype(archetype); + self.new_archetype_internal(archetype); } } @@ -295,25 +318,36 @@ impl QueryState { /// Update the current [`QueryState`] with information from the provided [`Archetype`] /// (if applicable, i.e. if the archetype has any intersecting [`ComponentId`] with the current [`QueryState`]). - pub fn new_archetype(&mut self, archetype: &Archetype) { + /// + /// The passed in `access` will be updated with any new accesses introduced by the new archetype. + pub fn new_archetype( + &mut self, + archetype: &Archetype, + access: &mut Access, + ) { + if self.new_archetype_internal(archetype) { + self.update_archetype_component_access(archetype, access); + } + } + + fn new_archetype_internal(&mut self, archetype: &Archetype) -> bool { if D::matches_component_set(&self.fetch_state, &|id| archetype.contains(id)) && F::matches_component_set(&self.filter_state, &|id| archetype.contains(id)) && self.matches_component_set(&|id| archetype.contains(id)) { - self.update_archetype_component_access(archetype); - let archetype_index = archetype.id().index(); if !self.matched_archetypes.contains(archetype_index) { - self.matched_archetypes.grow(archetype_index + 1); - self.matched_archetypes.set(archetype_index, true); + self.matched_archetypes.grow_and_insert(archetype_index); self.matched_archetype_ids.push(archetype.id()); } let table_index = archetype.table_id().as_usize(); if !self.matched_tables.contains(table_index) { - self.matched_tables.grow(table_index + 1); - self.matched_tables.set(table_index, true); + self.matched_tables.grow_and_insert(table_index); self.matched_table_ids.push(archetype.table_id()); } + true + } else { + false } } @@ -331,15 +365,21 @@ impl QueryState { } /// For the given `archetype`, adds any component accessed used by this query's underlying [`FilteredAccess`] to `access`. - pub fn update_archetype_component_access(&mut self, archetype: &Archetype) { + /// + /// The passed in `access` will be updated with any new accesses introduced by the new archetype. + pub fn update_archetype_component_access( + &mut self, + archetype: &Archetype, + access: &mut Access, + ) { self.component_access.access.reads().for_each(|id| { if let Some(id) = archetype.get_archetype_component_id(id) { - self.archetype_component_access.add_read(id); + access.add_read(id); } }); self.component_access.access.writes().for_each(|id| { if let Some(id) = archetype.get_archetype_component_id(id) { - self.archetype_component_access.add_write(id); + access.add_write(id); } }); } @@ -391,7 +431,6 @@ impl QueryState { component_access: self.component_access.clone(), matched_tables: self.matched_tables.clone(), matched_archetypes: self.matched_archetypes.clone(), - archetype_component_access: self.archetype_component_access.clone(), #[cfg(feature = "trace")] par_iter_span: bevy_utils::tracing::info_span!( "par_for_each", @@ -499,7 +538,6 @@ impl QueryState { component_access: joined_component_access, matched_tables, matched_archetypes, - archetype_component_access: self.archetype_component_access.clone(), #[cfg(feature = "trace")] par_iter_span: bevy_utils::tracing::info_span!( "par_for_each", diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index 408fae9b1d615..95a580c425790 100644 --- a/crates/bevy_ecs/src/schedule/schedule.rs +++ b/crates/bevy_ecs/src/schedule/schedule.rs @@ -661,6 +661,42 @@ impl ScheduleGraph { &self.conflicting_systems } + fn process_config( + &mut self, + config: NodeConfig, + collect_nodes: bool, + ) -> ProcessConfigsResult { + ProcessConfigsResult { + densely_chained: true, + nodes: collect_nodes + .then_some(T::process_config(self, config)) + .into_iter() + .collect(), + } + } + + fn apply_collective_conditions( + &mut self, + configs: &mut [NodeConfigs], + collective_conditions: Vec, + ) { + if !collective_conditions.is_empty() { + if let [config] = configs { + for condition in collective_conditions { + config.run_if_dyn(condition); + } + } else { + let set = self.create_anonymous_set(); + for config in configs.iter_mut() { + config.in_set_inner(set.intern()); + } + let mut set_config = SystemSetConfig::new(set.intern()); + set_config.conditions.extend(collective_conditions); + self.configure_set_inner(set_config).unwrap(); + } + } + } + /// Adds the config nodes to the graph. /// /// `collect_nodes` controls whether the `NodeId`s of the processed config nodes are stored in the returned [`ProcessConfigsResult`]. @@ -676,157 +712,75 @@ impl ScheduleGraph { collect_nodes: bool, ) -> ProcessConfigsResult { match configs { - NodeConfigs::NodeConfig(config) => { - let node_id = T::process_config(self, config); - if collect_nodes { - ProcessConfigsResult { - densely_chained: true, - nodes: vec![node_id], - } - } else { - ProcessConfigsResult { - densely_chained: true, - nodes: Vec::new(), - } - } - } + NodeConfigs::NodeConfig(config) => self.process_config(config, collect_nodes), NodeConfigs::Configs { mut configs, collective_conditions, chained, } => { - let more_than_one_entry = configs.len() > 1; - if !collective_conditions.is_empty() { - if more_than_one_entry { - let set = self.create_anonymous_set(); - for config in &mut configs { - config.in_set_inner(set.intern()); - } - let mut set_config = SystemSetConfig::new(set.intern()); - set_config.conditions.extend(collective_conditions); - self.configure_set_inner(set_config).unwrap(); - } else { - for condition in collective_conditions { - configs[0].run_if_dyn(condition); - } - } - } - let mut config_iter = configs.into_iter(); - let mut nodes_in_scope = Vec::new(); - let mut densely_chained = true; - if chained == Chain::Yes || chained == Chain::YesIgnoreDeferred { - let Some(prev) = config_iter.next() else { - return ProcessConfigsResult { - nodes: Vec::new(), - densely_chained: true, - }; + self.apply_collective_conditions(&mut configs, collective_conditions); + + let ignore_deferred = matches!(chained, Chain::YesIgnoreDeferred); + let chained = matches!(chained, Chain::Yes | Chain::YesIgnoreDeferred); + + // Densely chained if + // * chained and all configs in the chain are densely chained, or + // * unchained with a single densely chained config + let mut densely_chained = chained || configs.len() == 1; + let mut configs = configs.into_iter(); + let mut nodes = Vec::new(); + + let Some(first) = configs.next() else { + return ProcessConfigsResult { + nodes: Vec::new(), + densely_chained, }; - let mut previous_result = self.process_configs(prev, true); - densely_chained = previous_result.densely_chained; - for current in config_iter { - let current_result = self.process_configs(current, true); - densely_chained = densely_chained && current_result.densely_chained; - match ( - previous_result.densely_chained, - current_result.densely_chained, - ) { - // Both groups are "densely" chained, so we can simplify the graph by only - // chaining the last in the previous list to the first in the current list - (true, true) => { - let last_in_prev = previous_result.nodes.last().unwrap(); - let first_in_current = current_result.nodes.first().unwrap(); - self.dependency.graph.add_edge( - *last_in_prev, - *first_in_current, - (), - ); - - if chained == Chain::YesIgnoreDeferred { - self.no_sync_edges - .insert((*last_in_prev, *first_in_current)); - } - } - // The previous group is "densely" chained, so we can simplify the graph by only - // chaining the last item from the previous list to every item in the current list - (true, false) => { - let last_in_prev = previous_result.nodes.last().unwrap(); - for current_node in ¤t_result.nodes { - self.dependency.graph.add_edge( - *last_in_prev, - *current_node, - (), - ); - - if chained == Chain::YesIgnoreDeferred { - self.no_sync_edges.insert((*last_in_prev, *current_node)); - } - } - } - // The current list is currently "densely" chained, so we can simplify the graph by - // only chaining every item in the previous list to the first item in the current list - (false, true) => { - let first_in_current = current_result.nodes.first().unwrap(); - for previous_node in &previous_result.nodes { - self.dependency.graph.add_edge( - *previous_node, - *first_in_current, - (), - ); - - if chained == Chain::YesIgnoreDeferred { - self.no_sync_edges - .insert((*previous_node, *first_in_current)); - } - } - } - // Neither of the lists are "densely" chained, so we must chain every item in the first - // list to every item in the second list - (false, false) => { - for previous_node in &previous_result.nodes { - for current_node in ¤t_result.nodes { - self.dependency.graph.add_edge( - *previous_node, - *current_node, - (), - ); - - if chained == Chain::YesIgnoreDeferred { - self.no_sync_edges - .insert((*previous_node, *current_node)); - } - } + }; + let mut previous_result = self.process_configs(first, collect_nodes || chained); + densely_chained &= previous_result.densely_chained; + + for current in configs { + let current_result = self.process_configs(current, collect_nodes || chained); + densely_chained &= current_result.densely_chained; + + if chained { + // if the current result is densely chained, we only need to chain the first node + let current_nodes = if current_result.densely_chained { + ¤t_result.nodes[..1] + } else { + ¤t_result.nodes + }; + // if the previous result was densely chained, we only need to chain the last node + let previous_nodes = if previous_result.densely_chained { + &previous_result.nodes[previous_result.nodes.len() - 1..] + } else { + &previous_result.nodes + }; + + for previous_node in previous_nodes { + for current_node in current_nodes { + self.dependency + .graph + .add_edge(*previous_node, *current_node, ()); + + if ignore_deferred { + self.no_sync_edges.insert((*previous_node, *current_node)); } } } - - if collect_nodes { - nodes_in_scope.append(&mut previous_result.nodes); - } - - previous_result = current_result; } - - // ensure the last config's nodes are added if collect_nodes { - nodes_in_scope.append(&mut previous_result.nodes); - } - } else { - for config in config_iter { - let result = self.process_configs(config, collect_nodes); - densely_chained = densely_chained && result.densely_chained; - if collect_nodes { - nodes_in_scope.extend(result.nodes); - } + nodes.append(&mut previous_result.nodes); } - // an "unchained" SystemConfig is only densely chained if it has exactly one densely chained entry - if more_than_one_entry { - densely_chained = false; - } + previous_result = current_result; + } + if collect_nodes { + nodes.append(&mut previous_result.nodes); } ProcessConfigsResult { - nodes: nodes_in_scope, + nodes, densely_chained, } } diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index a8d7fe9cc3116..acf5e39f683fa 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -1092,6 +1092,7 @@ mod tests { Arc, }; + #[allow(dead_code)] #[derive(Component)] #[component(storage = "SparseSet")] struct SparseDropCk(DropCk); diff --git a/crates/bevy_ecs/src/system/mod.rs b/crates/bevy_ecs/src/system/mod.rs index 775163f4fc2b9..19785a7243c75 100644 --- a/crates/bevy_ecs/src/system/mod.rs +++ b/crates/bevy_ecs/src/system/mod.rs @@ -844,7 +844,9 @@ mod tests { let mut world = World::default(); world.insert_resource(SystemRan::No); + #[allow(dead_code)] struct NotSend1(std::rc::Rc); + #[allow(dead_code)] struct NotSend2(std::rc::Rc); world.insert_non_send_resource(NotSend1(std::rc::Rc::new(0))); @@ -867,7 +869,9 @@ mod tests { let mut world = World::default(); world.insert_resource(SystemRan::No); + #[allow(dead_code)] struct NotSend1(std::rc::Rc); + #[allow(dead_code)] struct NotSend2(std::rc::Rc); world.insert_non_send_resource(NotSend1(std::rc::Rc::new(1))); diff --git a/crates/bevy_ecs/src/system/query.rs b/crates/bevy_ecs/src/system/query.rs index e34557dbda268..fb87bb2e018ef 100644 --- a/crates/bevy_ecs/src/system/query.rs +++ b/crates/bevy_ecs/src/system/query.rs @@ -1208,6 +1208,11 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// Returns `true` if there are no query items. /// + /// This is equivalent to `self.iter().next().is_none()`, and thus the worst case runtime will be `O(n)` + /// where `n` is the number of *potential* matches. This can be notably expensive for queries that rely + /// on non-archetypal filters such as [`Added`] or [`Changed`] which must individually check each query + /// result for a match. + /// /// # Example /// /// Here, the score is increased only if an entity with a `Player` component is present in the world: @@ -1226,6 +1231,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// } /// # bevy_ecs::system::assert_is_system(update_score_system); /// ``` + /// + /// [`Added`]: crate::query::Added + /// [`Changed`]: crate::query::Changed #[inline] pub fn is_empty(&self) -> bool { // SAFETY: diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index f9ecc9aaf62d6..eabe8d5d19983 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -193,7 +193,7 @@ unsafe impl SystemParam for Qu type Item<'w, 's> = Query<'w, 's, D, F>; fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - let state = QueryState::new(world); + let state = QueryState::new_with_access(world, &mut system_meta.archetype_component_access); assert_component_access_compatibility( &system_meta.name, std::any::type_name::(), @@ -205,17 +205,11 @@ unsafe impl SystemParam for Qu system_meta .component_access_set .add(state.component_access.clone()); - system_meta - .archetype_component_access - .extend(&state.archetype_component_access); state } fn new_archetype(state: &mut Self::State, archetype: &Archetype, system_meta: &mut SystemMeta) { - state.new_archetype(archetype); - system_meta - .archetype_component_access - .extend(&state.archetype_component_access); + state.new_archetype(archetype, &mut system_meta.archetype_component_access); } #[inline] @@ -1584,6 +1578,7 @@ mod tests { // Compile test for https://github.com/bevyengine/bevy/pull/7001. #[test] fn system_param_const_generics() { + #[allow(dead_code)] #[derive(SystemParam)] pub struct ConstGenericParam<'w, const I: usize>(Res<'w, R>); @@ -1641,6 +1636,7 @@ mod tests { #[derive(SystemParam)] pub struct UnitParam; + #[allow(dead_code)] #[derive(SystemParam)] pub struct TupleParam<'w, 's, R: Resource, L: FromWorld + Send + 'static>( Res<'w, R>, @@ -1657,6 +1653,7 @@ mod tests { #[derive(Resource)] struct PrivateResource; + #[allow(dead_code)] #[derive(SystemParam)] pub struct EncapsulatedParam<'w>(Res<'w, PrivateResource>); diff --git a/crates/bevy_ecs/src/system/system_registry.rs b/crates/bevy_ecs/src/system/system_registry.rs index fa0dd6c4c821d..bb4c2fc2d8170 100644 --- a/crates/bevy_ecs/src/system/system_registry.rs +++ b/crates/bevy_ecs/src/system/system_registry.rs @@ -38,9 +38,11 @@ impl RemovedSystem { /// /// These are opaque identifiers, keyed to a specific [`World`], /// and are created via [`World::register_system`]. -#[derive(Eq)] pub struct SystemId(Entity, std::marker::PhantomData O>); +// A manual impl is used because the trait bounds should ignore the `I` and `O` phantom parameters. +impl Eq for SystemId {} + // A manual impl is used because the trait bounds should ignore the `I` and `O` phantom parameters. impl Copy for SystemId {} diff --git a/crates/bevy_ecs/src/world/command_queue.rs b/crates/bevy_ecs/src/world/command_queue.rs index 3103e3486ce2d..27ac752139639 100644 --- a/crates/bevy_ecs/src/world/command_queue.rs +++ b/crates/bevy_ecs/src/world/command_queue.rs @@ -344,6 +344,7 @@ mod test { // This has an arbitrary value `String` stored to ensure // when then command gets pushed, the `bytes` vector gets // some data added to it. + #[allow(dead_code)] struct PanicCommand(String); impl Command for PanicCommand { fn apply(self, _: &mut World) { @@ -392,6 +393,7 @@ mod test { assert_is_send(SpawnCommand); } + #[allow(dead_code)] struct CommandWithPadding(u8, u16); impl Command for CommandWithPadding { fn apply(self, _: &mut World) {} diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 7448dddd8bec2..d92029154f1a6 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -348,6 +348,16 @@ impl<'w> EntityMut<'w> { self.as_readonly().get() } + /// Consumes `self` and gets access to the component of type `T` with the + /// world `'w` lifetime for the current entity. + /// + /// Returns `None` if the entity does not have a component of type `T`. + #[inline] + pub fn into_borrow(self) -> Option<&'w T> { + // SAFETY: consuming `self` implies exclusive access + unsafe { self.0.get() } + } + /// Gets access to the component of type `T` for the current entity, /// including change detection information as a [`Ref`]. /// @@ -357,6 +367,17 @@ impl<'w> EntityMut<'w> { self.as_readonly().get_ref() } + /// Consumes `self` and gets access to the component of type `T` with world + /// `'w` lifetime for the current entity, including change detection information + /// as a [`Ref<'w>`]. + /// + /// Returns `None` if the entity does not have a component of type `T`. + #[inline] + pub fn into_ref(self) -> Option> { + // SAFETY: consuming `self` implies exclusive access + unsafe { self.0.get_ref() } + } + /// Gets mutable access to the component of type `T` for the current entity. /// Returns `None` if the entity does not have a component of type `T`. #[inline] @@ -365,6 +386,15 @@ impl<'w> EntityMut<'w> { unsafe { self.0.get_mut() } } + /// Consumes self and gets mutable access to the component of type `T` + /// with the world `'w` lifetime for the current entity. + /// Returns `None` if the entity does not have a component of type `T`. + #[inline] + pub fn into_mut(self) -> Option> { + // SAFETY: consuming `self` implies exclusive access + unsafe { self.0.get_mut() } + } + /// Retrieves the change ticks for the given component. This can be useful for implementing change /// detection in custom runtimes. #[inline] @@ -396,6 +426,19 @@ impl<'w> EntityMut<'w> { self.as_readonly().get_by_id(component_id) } + /// Consumes `self` and gets the component of the given [`ComponentId`] with + /// world `'w` lifetime from the entity. + /// + /// **You should prefer to use the typed API [`EntityWorldMut::into_borrow`] where possible and only + /// use this in cases where the actual component types are not known at + /// compile time.** + #[inline] + pub fn into_borrow_by_id(self, component_id: ComponentId) -> Option> { + // SAFETY: + // consuming `self` ensures that no references exist to this entity's components. + unsafe { self.0.get_by_id(component_id) } + } + /// Gets a [`MutUntyped`] of the component of the given [`ComponentId`] from the entity. /// /// **You should prefer to use the typed API [`EntityMut::get_mut`] where possible and only @@ -411,6 +454,19 @@ impl<'w> EntityMut<'w> { // - `as_unsafe_world_cell` gives mutable permission for all components on this entity unsafe { self.0.get_mut_by_id(component_id) } } + + /// Consumes `self` and gets a [`MutUntyped<'w>`] of the component of the given [`ComponentId`] + /// with world `'w` lifetime from the entity. + /// + /// **You should prefer to use the typed API [`EntityMut::into_mut`] where possible and only + /// use this in cases where the actual component types are not known at + /// compile time.** + #[inline] + pub fn into_mut_by_id(self, component_id: ComponentId) -> Option> { + // SAFETY: + // consuming `self` ensures that no references exist to this entity's components. + unsafe { self.0.get_mut_by_id(component_id) } + } } impl<'w> From> for EntityMut<'w> { @@ -583,6 +639,15 @@ impl<'w> EntityWorldMut<'w> { EntityRef::from(self).get() } + /// Consumes `self` and gets access to the component of type `T` with + /// the world `'w` lifetime for the current entity. + /// Returns `None` if the entity does not have a component of type `T`. + #[inline] + pub fn into_borrow(self) -> Option<&'w T> { + // SAFETY: consuming `self` implies exclusive access + unsafe { self.into_unsafe_entity_cell().get() } + } + /// Gets access to the component of type `T` for the current entity, /// including change detection information as a [`Ref`]. /// @@ -592,6 +657,16 @@ impl<'w> EntityWorldMut<'w> { EntityRef::from(self).get_ref() } + /// Consumes `self` and gets access to the component of type `T` + /// with the world `'w` lifetime for the current entity, + /// including change detection information as a [`Ref`]. + /// + /// Returns `None` if the entity does not have a component of type `T`. + #[inline] + pub fn into_ref(self) -> Option> { + EntityRef::from(self).get_ref() + } + /// Gets mutable access to the component of type `T` for the current entity. /// Returns `None` if the entity does not have a component of type `T`. #[inline] @@ -600,6 +675,15 @@ impl<'w> EntityWorldMut<'w> { unsafe { self.as_unsafe_entity_cell().get_mut() } } + /// Consumes `self` and gets mutable access to the component of type `T` + /// with the world `'w` lifetime for the current entity. + /// Returns `None` if the entity does not have a component of type `T`. + #[inline] + pub fn into_mut(self) -> Option> { + // SAFETY: consuming `self` implies exclusive access + unsafe { self.into_unsafe_entity_cell().get_mut() } + } + /// Retrieves the change ticks for the given component. This can be useful for implementing change /// detection in custom runtimes. #[inline] @@ -631,6 +715,18 @@ impl<'w> EntityWorldMut<'w> { EntityRef::from(self).get_by_id(component_id) } + /// Consumes `self` and gets the component of the given [`ComponentId`] with + /// with world `'w` lifetime from the entity. + /// + /// **You should prefer to use the typed API [`EntityWorldMut::into_borrow`] where + /// possible and only use this in cases where the actual component types are not + /// known at compile time.** + #[inline] + pub fn into_borrow_by_id(self, component_id: ComponentId) -> Option> { + // SAFETY: consuming `self` implies exclusive access + unsafe { self.into_unsafe_entity_cell().get_by_id(component_id) } + } + /// Gets a [`MutUntyped`] of the component of the given [`ComponentId`] from the entity. /// /// **You should prefer to use the typed API [`EntityWorldMut::get_mut`] where possible and only @@ -647,6 +743,19 @@ impl<'w> EntityWorldMut<'w> { unsafe { self.as_unsafe_entity_cell().get_mut_by_id(component_id) } } + /// Consumes `self` and gets a [`MutUntyped<'w>`] of the component with the world `'w` lifetime + /// of the given [`ComponentId`] from the entity. + /// + /// **You should prefer to use the typed API [`EntityWorldMut::into_mut`] where possible and only + /// use this in cases where the actual component types are not known at + /// compile time.** + #[inline] + pub fn into_mut_by_id(self, component_id: ComponentId) -> Option> { + // SAFETY: + // consuming `self` ensures that no references exist to this entity's components. + unsafe { self.into_unsafe_entity_cell().get_mut_by_id(component_id) } + } + /// Adds a [`Bundle`] of components to the entity. /// /// This will overwrite any previous value(s) of the same component type. diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 74c7ffb7f5cdb..3f87d8026db27 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -6,7 +6,6 @@ mod entity_ref; pub mod error; mod spawn_batch; pub mod unsafe_world_cell; -mod world_cell; pub use crate::change_detection::{Mut, Ref, CHECK_TICK_THRESHOLD}; pub use crate::world::command_queue::CommandQueue; @@ -16,7 +15,6 @@ pub use entity_ref::{ OccupiedEntry, VacantEntry, }; pub use spawn_batch::*; -pub use world_cell::*; use crate::{ archetype::{ArchetypeComponentId, ArchetypeId, ArchetypeRow, Archetypes}, @@ -111,8 +109,6 @@ pub struct World { pub(crate) storages: Storages, pub(crate) bundles: Bundles, pub(crate) removed_components: RemovedComponentEvents, - /// Access cache used by [`WorldCell`]. Is only accessed in the `Drop` impl of `WorldCell`. - pub(crate) archetype_component_access: ArchetypeComponentAccess, pub(crate) change_tick: AtomicU32, pub(crate) last_change_tick: Tick, pub(crate) last_check_tick: Tick, @@ -129,7 +125,6 @@ impl Default for World { storages: Default::default(), bundles: Default::default(), removed_components: Default::default(), - archetype_component_access: Default::default(), // Default value is `1`, and `last_change_tick`s default to `0`, such that changes // are detected on first system runs and for direct world queries. change_tick: AtomicU32::new(1), @@ -217,13 +212,6 @@ impl World { &self.removed_components } - /// Retrieves a [`WorldCell`], which safely enables multiple mutable World accesses at the same - /// time, provided those accesses do not conflict with each other. - #[inline] - pub fn cell(&mut self) -> WorldCell<'_> { - WorldCell::new(self) - } - /// Creates a new [`Commands`] instance that writes to the world's command queue /// Use [`World::flush_commands`] to apply all queued commands #[inline] diff --git a/crates/bevy_ecs/src/world/unsafe_world_cell.rs b/crates/bevy_ecs/src/world/unsafe_world_cell.rs index 9fbb6e52de668..429a9658fdf78 100644 --- a/crates/bevy_ecs/src/world/unsafe_world_cell.rs +++ b/crates/bevy_ecs/src/world/unsafe_world_cell.rs @@ -4,7 +4,7 @@ use super::{command_queue::CommandQueue, Mut, Ref, World, WorldId}; use crate::{ - archetype::{Archetype, ArchetypeComponentId, Archetypes}, + archetype::{Archetype, Archetypes}, bundle::Bundles, change_detection::{MutUntyped, Ticks, TicksMut}, component::{ComponentId, ComponentTicks, Components, StorageType, Tick, TickCells}, @@ -284,36 +284,6 @@ impl<'w> UnsafeWorldCell<'w> { &unsafe { self.unsafe_world() }.storages } - /// Shorthand helper function for getting the [`ArchetypeComponentId`] for a resource. - #[inline] - pub(crate) fn get_resource_archetype_component_id( - self, - component_id: ComponentId, - ) -> Option { - // SAFETY: - // - we only access world metadata - let resource = unsafe { self.world_metadata() } - .storages - .resources - .get(component_id)?; - Some(resource.id()) - } - - /// Shorthand helper function for getting the [`ArchetypeComponentId`] for a resource. - #[inline] - pub(crate) fn get_non_send_archetype_component_id( - self, - component_id: ComponentId, - ) -> Option { - // SAFETY: - // - we only access world metadata - let resource = unsafe { self.world_metadata() } - .storages - .non_send_resources - .get(component_id)?; - Some(resource.id()) - } - /// Retrieves an [`UnsafeEntityCell`] that exposes read and write operations for the given `entity`. /// Similar to the [`UnsafeWorldCell`], you are in charge of making sure that no aliasing rules are violated. #[inline] diff --git a/crates/bevy_ecs/src/world/world_cell.rs b/crates/bevy_ecs/src/world/world_cell.rs deleted file mode 100644 index 2ae79b32306e6..0000000000000 --- a/crates/bevy_ecs/src/world/world_cell.rs +++ /dev/null @@ -1,479 +0,0 @@ -use bevy_utils::tracing::error; - -use crate::{ - archetype::ArchetypeComponentId, - event::{Event, Events}, - storage::SparseSet, - system::Resource, - world::{Mut, World}, -}; -use std::{ - any::TypeId, - cell::RefCell, - ops::{Deref, DerefMut}, - rc::Rc, -}; - -use super::unsafe_world_cell::UnsafeWorldCell; - -/// Exposes safe mutable access to multiple resources at a time in a World. Attempting to access -/// World in a way that violates Rust's mutability rules will panic thanks to runtime checks. -pub struct WorldCell<'w> { - pub(crate) world: UnsafeWorldCell<'w>, - pub(crate) access: Rc>, -} - -pub(crate) struct ArchetypeComponentAccess { - access: SparseSet, -} - -impl Default for ArchetypeComponentAccess { - fn default() -> Self { - Self { - access: SparseSet::new(), - } - } -} - -const UNIQUE_ACCESS: usize = 0; -const BASE_ACCESS: usize = 1; -impl ArchetypeComponentAccess { - const fn new() -> Self { - Self { - access: SparseSet::new(), - } - } - - fn read(&mut self, id: ArchetypeComponentId) -> bool { - let id_access = self.access.get_or_insert_with(id, || BASE_ACCESS); - if *id_access == UNIQUE_ACCESS { - false - } else { - *id_access += 1; - true - } - } - - fn drop_read(&mut self, id: ArchetypeComponentId) { - let id_access = self.access.get_or_insert_with(id, || BASE_ACCESS); - *id_access -= 1; - } - - fn write(&mut self, id: ArchetypeComponentId) -> bool { - let id_access = self.access.get_or_insert_with(id, || BASE_ACCESS); - if *id_access == BASE_ACCESS { - *id_access = UNIQUE_ACCESS; - true - } else { - false - } - } - - fn drop_write(&mut self, id: ArchetypeComponentId) { - let id_access = self.access.get_or_insert_with(id, || BASE_ACCESS); - *id_access = BASE_ACCESS; - } -} - -impl<'w> Drop for WorldCell<'w> { - fn drop(&mut self) { - let mut access = self.access.borrow_mut(); - - { - // SAFETY: `WorldCell` does not hand out `UnsafeWorldCell` to anywhere else so this is the only - // `UnsafeWorldCell` and we have exclusive access to it. - let world = unsafe { self.world.world_mut() }; - let world_cached_access = &mut world.archetype_component_access; - - // give world ArchetypeComponentAccess back to reuse allocations - std::mem::swap(world_cached_access, &mut *access); - } - } -} - -/// A read-only borrow of some data stored in a [`World`]. This type is returned by [`WorldCell`], -/// which uses run-time checks to ensure that the borrow does not violate Rust's aliasing rules. -pub struct WorldBorrow<'w, T> { - value: &'w T, - archetype_component_id: ArchetypeComponentId, - access: Rc>, -} - -impl<'w, T> WorldBorrow<'w, T> { - fn try_new( - value: impl FnOnce() -> Option<&'w T>, - archetype_component_id: ArchetypeComponentId, - access: Rc>, - ) -> Option { - assert!( - access.borrow_mut().read(archetype_component_id), - "Attempted to immutably access {}, but it is already mutably borrowed", - std::any::type_name::(), - ); - let value = value()?; - Some(Self { - value, - archetype_component_id, - access, - }) - } -} - -impl<'w, T> Deref for WorldBorrow<'w, T> { - type Target = T; - - #[inline] - fn deref(&self) -> &Self::Target { - self.value - } -} - -impl<'w, T> Drop for WorldBorrow<'w, T> { - fn drop(&mut self) { - let mut access = self.access.borrow_mut(); - access.drop_read(self.archetype_component_id); - } -} - -/// A mutable borrow of some data stored in a [`World`]. This type is returned by [`WorldCell`], -/// which uses run-time checks to ensure that the borrow does not violate Rust's aliasing rules. -pub struct WorldBorrowMut<'w, T> { - value: Mut<'w, T>, - archetype_component_id: ArchetypeComponentId, - access: Rc>, -} - -impl<'w, T> WorldBorrowMut<'w, T> { - fn try_new( - value: impl FnOnce() -> Option>, - archetype_component_id: ArchetypeComponentId, - access: Rc>, - ) -> Option { - assert!( - access.borrow_mut().write(archetype_component_id), - "Attempted to mutably access {}, but it is already mutably borrowed", - std::any::type_name::(), - ); - let value = value()?; - Some(Self { - value, - archetype_component_id, - access, - }) - } -} - -impl<'w, T> Deref for WorldBorrowMut<'w, T> { - type Target = T; - - #[inline] - fn deref(&self) -> &Self::Target { - self.value.deref() - } -} - -impl<'w, T> DerefMut for WorldBorrowMut<'w, T> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.value - } -} - -impl<'w, T> Drop for WorldBorrowMut<'w, T> { - fn drop(&mut self) { - let mut access = self.access.borrow_mut(); - access.drop_write(self.archetype_component_id); - } -} - -impl<'w> WorldCell<'w> { - pub(crate) fn new(world: &'w mut World) -> Self { - // this is cheap because ArchetypeComponentAccess::new() is const / allocation free - let access = std::mem::replace( - &mut world.archetype_component_access, - ArchetypeComponentAccess::new(), - ); - // world's ArchetypeComponentAccess is recycled to cut down on allocations - Self { - world: world.as_unsafe_world_cell(), - access: Rc::new(RefCell::new(access)), - } - } - - /// Gets a reference to the resource of the given type - pub fn get_resource(&self) -> Option> { - let component_id = self.world.components().get_resource_id(TypeId::of::())?; - - let archetype_component_id = self - .world - .get_resource_archetype_component_id(component_id)?; - - WorldBorrow::try_new( - // SAFETY: access is checked by WorldBorrow - || unsafe { self.world.get_resource::() }, - archetype_component_id, - self.access.clone(), - ) - } - - /// Gets a reference to the resource of the given type - /// - /// # Panics - /// - /// Panics if the resource does not exist. Use [`get_resource`](WorldCell::get_resource) instead - /// if you want to handle this case. - pub fn resource(&self) -> WorldBorrow<'_, T> { - match self.get_resource() { - Some(x) => x, - None => panic!( - "Requested resource {} does not exist in the `World`. - Did you forget to add it using `app.insert_resource` / `app.init_resource`? - Resources are also implicitly added via `app.add_event`, - and can be added by plugins.", - std::any::type_name::() - ), - } - } - - /// Gets a mutable reference to the resource of the given type - pub fn get_resource_mut(&self) -> Option> { - let component_id = self.world.components().get_resource_id(TypeId::of::())?; - - let archetype_component_id = self - .world - .get_resource_archetype_component_id(component_id)?; - WorldBorrowMut::try_new( - // SAFETY: access is checked by WorldBorrowMut - || unsafe { self.world.get_resource_mut::() }, - archetype_component_id, - self.access.clone(), - ) - } - - /// Gets a mutable reference to the resource of the given type - /// - /// # Panics - /// - /// Panics if the resource does not exist. Use [`get_resource_mut`](WorldCell::get_resource_mut) - /// instead if you want to handle this case. - pub fn resource_mut(&self) -> WorldBorrowMut<'_, T> { - match self.get_resource_mut() { - Some(x) => x, - None => panic!( - "Requested resource {} does not exist in the `World`. - Did you forget to add it using `app.insert_resource` / `app.init_resource`? - Resources are also implicitly added via `app.add_event`, - and can be added by plugins.", - std::any::type_name::() - ), - } - } - - /// Gets an immutable reference to the non-send resource of the given type, if it exists. - pub fn get_non_send_resource(&self) -> Option> { - let component_id = self.world.components().get_resource_id(TypeId::of::())?; - - let archetype_component_id = self - .world - .get_non_send_archetype_component_id(component_id)?; - WorldBorrow::try_new( - // SAFETY: access is checked by WorldBorrowMut - || unsafe { self.world.get_non_send_resource::() }, - archetype_component_id, - self.access.clone(), - ) - } - - /// Gets an immutable reference to the non-send resource of the given type, if it exists. - /// - /// # Panics - /// - /// Panics if the resource does not exist. Use - /// [`get_non_send_resource`](WorldCell::get_non_send_resource) instead if you want to handle - /// this case. - pub fn non_send_resource(&self) -> WorldBorrow<'_, T> { - match self.get_non_send_resource() { - Some(x) => x, - None => panic!( - "Requested non-send resource {} does not exist in the `World`. - Did you forget to add it using `app.insert_non_send_resource` / `app.init_non_send_resource`? - Non-send resources can also be added by plugins.", - std::any::type_name::() - ), - } - } - - /// Gets a mutable reference to the non-send resource of the given type, if it exists. - pub fn get_non_send_resource_mut(&self) -> Option> { - let component_id = self.world.components().get_resource_id(TypeId::of::())?; - - let archetype_component_id = self - .world - .get_non_send_archetype_component_id(component_id)?; - WorldBorrowMut::try_new( - // SAFETY: access is checked by WorldBorrowMut - || unsafe { self.world.get_non_send_resource_mut::() }, - archetype_component_id, - self.access.clone(), - ) - } - - /// Gets a mutable reference to the non-send resource of the given type, if it exists. - /// - /// # Panics - /// - /// Panics if the resource does not exist. Use - /// [`get_non_send_resource_mut`](WorldCell::get_non_send_resource_mut) instead if you want to - /// handle this case. - pub fn non_send_resource_mut(&self) -> WorldBorrowMut<'_, T> { - match self.get_non_send_resource_mut() { - Some(x) => x, - None => panic!( - "Requested non-send resource {} does not exist in the `World`. - Did you forget to add it using `app.insert_non_send_resource` / `app.init_non_send_resource`? - Non-send resources can also be added by plugins.", - std::any::type_name::() - ), - } - } - - /// Sends an [`Event`]. - #[inline] - pub fn send_event(&self, event: E) { - self.send_event_batch(std::iter::once(event)); - } - - /// Sends the default value of the [`Event`] of type `E`. - #[inline] - pub fn send_event_default(&self) { - self.send_event_batch(std::iter::once(E::default())); - } - - /// Sends a batch of [`Event`]s from an iterator. - #[inline] - pub fn send_event_batch(&self, events: impl Iterator) { - match self.get_resource_mut::>() { - Some(mut events_resource) => events_resource.extend(events), - None => error!( - "Unable to send event `{}`\n\tEvent must be added to the app with `add_event()`\n\thttps://docs.rs/bevy/*/bevy/app/struct.App.html#method.add_event ", - std::any::type_name::() - ), - } - } -} - -#[cfg(test)] -mod tests { - use super::BASE_ACCESS; - use crate as bevy_ecs; - use crate::{system::Resource, world::World}; - use std::any::TypeId; - - #[derive(Resource)] - struct A(u32); - - #[derive(Resource)] - struct B(u64); - - #[test] - fn world_cell() { - let mut world = World::default(); - world.insert_resource(A(1)); - world.insert_resource(B(1)); - let cell = world.cell(); - { - let mut a = cell.resource_mut::(); - assert_eq!(1, a.0); - a.0 = 2; - } - { - let a = cell.resource::(); - assert_eq!(2, a.0, "ensure access is dropped"); - - let a2 = cell.resource::(); - assert_eq!( - 2, a2.0, - "ensure multiple immutable accesses can occur at the same time" - ); - } - { - let a = cell.resource_mut::(); - assert_eq!( - 2, a.0, - "ensure both immutable accesses are dropped, enabling a new mutable access" - ); - - let b = cell.resource::(); - assert_eq!( - 1, b.0, - "ensure multiple non-conflicting mutable accesses can occur at the same time" - ); - } - } - - #[test] - fn world_access_reused() { - let mut world = World::default(); - world.insert_resource(A(1)); - { - let cell = world.cell(); - { - let mut a = cell.resource_mut::(); - assert_eq!(1, a.0); - a.0 = 2; - } - } - - let u32_component_id = world.components.get_resource_id(TypeId::of::()).unwrap(); - let u32_archetype_component_id = world - .get_resource_archetype_component_id(u32_component_id) - .unwrap(); - assert_eq!(world.archetype_component_access.access.len(), 1); - assert_eq!( - world - .archetype_component_access - .access - .get(u32_archetype_component_id), - Some(&BASE_ACCESS), - "reused access count is 'base'" - ); - } - - #[test] - #[should_panic] - fn world_cell_double_mut() { - let mut world = World::default(); - world.insert_resource(A(1)); - let cell = world.cell(); - let _value_a = cell.resource_mut::(); - let _value_b = cell.resource_mut::(); - } - - #[test] - #[should_panic] - fn world_cell_ref_and_mut() { - let mut world = World::default(); - world.insert_resource(A(1)); - let cell = world.cell(); - let _value_a = cell.resource::(); - let _value_b = cell.resource_mut::(); - } - - #[test] - #[should_panic] - fn world_cell_mut_and_ref() { - let mut world = World::default(); - world.insert_resource(A(1)); - let cell = world.cell(); - let _value_a = cell.resource_mut::(); - let _value_b = cell.resource::(); - } - - #[test] - fn world_cell_ref_and_ref() { - let mut world = World::default(); - world.insert_resource(A(1)); - let cell = world.cell(); - let _value_a = cell.resource::(); - let _value_b = cell.resource::(); - } -} diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/query_exact_sized_iterator_safety.stderr b/crates/bevy_ecs_compile_fail_tests/tests/ui/query_exact_sized_iterator_safety.stderr index e4c9574ad49d2..ddc1c9e78a91b 100644 --- a/crates/bevy_ecs_compile_fail_tests/tests/ui/query_exact_sized_iterator_safety.stderr +++ b/crates/bevy_ecs_compile_fail_tests/tests/ui/query_exact_sized_iterator_safety.stderr @@ -2,7 +2,7 @@ error[E0277]: the trait bound `bevy_ecs::query::Changed: ArchetypeFilter` i --> tests/ui/query_exact_sized_iterator_safety.rs:8:28 | 8 | is_exact_size_iterator(query.iter()); - | ---------------------- ^^^^^^^^^^^^ the trait `ArchetypeFilter` is not implemented for `bevy_ecs::query::Changed` + | ---------------------- ^^^^^^^^^^^^ the trait `ArchetypeFilter` is not implemented for `bevy_ecs::query::Changed`, which is required by `QueryIter<'_, '_, &Foo, bevy_ecs::query::Changed>: ExactSizeIterator` | | | required by a bound introduced by this call | @@ -27,7 +27,7 @@ error[E0277]: the trait bound `bevy_ecs::query::Added: ArchetypeFilter` is --> tests/ui/query_exact_sized_iterator_safety.rs:13:28 | 13 | is_exact_size_iterator(query.iter()); - | ---------------------- ^^^^^^^^^^^^ the trait `ArchetypeFilter` is not implemented for `bevy_ecs::query::Added` + | ---------------------- ^^^^^^^^^^^^ the trait `ArchetypeFilter` is not implemented for `bevy_ecs::query::Added`, which is required by `QueryIter<'_, '_, &Foo, bevy_ecs::query::Added>: ExactSizeIterator` | | | required by a bound introduced by this call | diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/query_iter_combinations_mut_iterator_safety.stderr b/crates/bevy_ecs_compile_fail_tests/tests/ui/query_iter_combinations_mut_iterator_safety.stderr index a2059f29b57ce..b005ce76291d1 100644 --- a/crates/bevy_ecs_compile_fail_tests/tests/ui/query_iter_combinations_mut_iterator_safety.stderr +++ b/crates/bevy_ecs_compile_fail_tests/tests/ui/query_iter_combinations_mut_iterator_safety.stderr @@ -2,7 +2,7 @@ error[E0277]: the trait bound `&mut A: ReadOnlyQueryData` is not satisfied --> tests/ui/query_iter_combinations_mut_iterator_safety.rs:10:17 | 10 | is_iterator(iter) - | ----------- ^^^^ the trait `ReadOnlyQueryData` is not implemented for `&mut A` + | ----------- ^^^^ the trait `ReadOnlyQueryData` is not implemented for `&mut A`, which is required by `QueryCombinationIter<'_, '_, &mut A, (), _>: Iterator` | | | required by a bound introduced by this call | diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/query_iter_many_mut_iterator_safety.stderr b/crates/bevy_ecs_compile_fail_tests/tests/ui/query_iter_many_mut_iterator_safety.stderr index 959e4d126eedd..d8f8d91855b8a 100644 --- a/crates/bevy_ecs_compile_fail_tests/tests/ui/query_iter_many_mut_iterator_safety.stderr +++ b/crates/bevy_ecs_compile_fail_tests/tests/ui/query_iter_many_mut_iterator_safety.stderr @@ -2,7 +2,7 @@ error[E0277]: the trait bound `&mut A: ReadOnlyQueryData` is not satisfied --> tests/ui/query_iter_many_mut_iterator_safety.rs:10:17 | 10 | is_iterator(iter) - | ----------- ^^^^ the trait `ReadOnlyQueryData` is not implemented for `&mut A` + | ----------- ^^^^ the trait `ReadOnlyQueryData` is not implemented for `&mut A`, which is required by `QueryManyIter<'_, '_, &mut A, (), std::array::IntoIter>: Iterator` | | | required by a bound introduced by this call | diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/system_param_derive_readonly.stderr b/crates/bevy_ecs_compile_fail_tests/tests/ui/system_param_derive_readonly.stderr index 49120c73bb565..dab85816fd6ea 100644 --- a/crates/bevy_ecs_compile_fail_tests/tests/ui/system_param_derive_readonly.stderr +++ b/crates/bevy_ecs_compile_fail_tests/tests/ui/system_param_derive_readonly.stderr @@ -10,7 +10,7 @@ error[E0277]: the trait bound `&'static mut Foo: ReadOnlyQueryData` is not satis --> tests/ui/system_param_derive_readonly.rs:18:23 | 18 | assert_readonly::(); - | ^^^^^^^ the trait `ReadOnlyQueryData` is not implemented for `&'static mut Foo` + | ^^^^^^^ the trait `ReadOnlyQueryData` is not implemented for `&'static mut Foo`, which is required by `Mutable<'_, '_>: ReadOnlySystemParam` | = help: the following other types implement trait `ReadOnlyQueryData`: bevy_ecs::change_detection::Ref<'__w, T> diff --git a/crates/bevy_gizmos/src/grid.rs b/crates/bevy_gizmos/src/grid.rs index c7b007746849c..a00a8c4fa19bc 100644 --- a/crates/bevy_gizmos/src/grid.rs +++ b/crates/bevy_gizmos/src/grid.rs @@ -5,29 +5,91 @@ use crate::prelude::{GizmoConfigGroup, Gizmos}; use bevy_color::LinearRgba; -use bevy_math::{Quat, UVec2, Vec2, Vec3}; +use bevy_math::{Quat, UVec2, UVec3, Vec2, Vec3}; +/// A builder returned by [`Gizmos::grid_3d`] +pub struct GridBuilder3d<'a, 'w, 's, T: GizmoConfigGroup> { + gizmos: &'a mut Gizmos<'w, 's, T>, + position: Vec3, + rotation: Quat, + spacing: Vec3, + cell_count: UVec3, + skew: Vec3, + outer_edges: [bool; 3], + color: LinearRgba, +} /// A builder returned by [`Gizmos::grid`] and [`Gizmos::grid_2d`] -pub struct GridBuilder<'a, 'w, 's, T: GizmoConfigGroup> { +pub struct GridBuilder2d<'a, 'w, 's, T: GizmoConfigGroup> { gizmos: &'a mut Gizmos<'w, 's, T>, position: Vec3, rotation: Quat, spacing: Vec2, cell_count: UVec2, skew: Vec2, - outer_edges: bool, + outer_edges: [bool; 2], color: LinearRgba, } -impl GridBuilder<'_, '_, '_, T> { +impl GridBuilder3d<'_, '_, '_, T> { /// Skews the grid by `tan(skew)` in the x direction. /// `skew` is in radians pub fn skew_x(mut self, skew: f32) -> Self { self.skew.x = skew; self } + /// Skews the grid by `tan(skew)` in the y direction. + /// `skew` is in radians + pub fn skew_y(mut self, skew: f32) -> Self { + self.skew.y = skew; + self + } + /// Skews the grid by `tan(skew)` in the z direction. + /// `skew` is in radians + pub fn skew_z(mut self, skew: f32) -> Self { + self.skew.z = skew; + self + } + /// Skews the grid by `tan(skew)` in the x, y and z directions. + /// `skew` is in radians + pub fn skew(mut self, skew: Vec3) -> Self { + self.skew = skew; + self + } + + /// Declare that the outer edges of the grid along the x axis should be drawn. + /// By default, the outer edges will not be drawn. + pub fn outer_edges_x(mut self) -> Self { + self.outer_edges[0] = true; + self + } + /// Declare that the outer edges of the grid along the y axis should be drawn. + /// By default, the outer edges will not be drawn. + pub fn outer_edges_y(mut self) -> Self { + self.outer_edges[1] = true; + self + } + /// Declare that the outer edges of the grid along the z axis should be drawn. + /// By default, the outer edges will not be drawn. + pub fn outer_edges_z(mut self) -> Self { + self.outer_edges[2] = true; + self + } + /// Declare that all outer edges of the grid should be drawn. + /// By default, the outer edges will not be drawn. + pub fn outer_edges(mut self) -> Self { + self.outer_edges.fill(true); + self + } +} +impl GridBuilder2d<'_, '_, '_, T> { /// Skews the grid by `tan(skew)` in the x direction. /// `skew` is in radians + pub fn skew_x(mut self, skew: f32) -> Self { + self.skew.x = skew; + self + } + /// Skews the grid by `tan(skew)` in the y direction. + /// `skew` is in radians pub fn skew_y(mut self, skew: f32) -> Self { self.skew.y = skew; self @@ -39,70 +101,54 @@ impl GridBuilder<'_, '_, '_, T> { self } - /// Toggle whether the outer edges of the grid should be drawn. + /// Declare that the outer edges of the grid along the x axis should be drawn. + /// By default, the outer edges will not be drawn. + pub fn outer_edges_x(mut self) -> Self { + self.outer_edges[0] = true; + self + } + /// Declare that the outer edges of the grid along the y axis should be drawn. /// By default, the outer edges will not be drawn. - pub fn outer_edges(mut self, outer_edges: bool) -> Self { - self.outer_edges = outer_edges; + pub fn outer_edges_y(mut self) -> Self { + self.outer_edges[1] = true; + self + } + /// Declare that all outer edges of the grid should be drawn. + /// By default, the outer edges will not be drawn. + pub fn outer_edges(mut self) -> Self { + self.outer_edges.fill(true); self } } -impl Drop for GridBuilder<'_, '_, '_, T> { - /// Draws a grid, by drawing lines with the stored [`Gizmos`] +impl Drop for GridBuilder3d<'_, '_, '_, T> { fn drop(&mut self) { - if !self.gizmos.enabled { - return; - } - - // Offset between two adjacent grid cells along the x/y-axis and accounting for skew. - let dx = Vec3::new(self.spacing.x, self.spacing.x * self.skew.y.tan(), 0.); - let dy = Vec3::new(self.spacing.y * self.skew.x.tan(), self.spacing.y, 0.); - - // Bottom-left corner of the grid - let grid_start = self.position - - self.cell_count.x as f32 / 2.0 * dx - - self.cell_count.y as f32 / 2.0 * dy; - - let (line_count, vertical_start, horizontal_start) = if self.outer_edges { - (self.cell_count + UVec2::ONE, grid_start, grid_start) - } else { - ( - self.cell_count.saturating_sub(UVec2::ONE), - grid_start + dx, - grid_start + dy, - ) - }; - - // Vertical lines - let dline = dy * self.cell_count.y as f32; - for i in 0..line_count.x { - let i = i as f32; - let line_start = vertical_start + i * dx; - let line_end = line_start + dline; - - self.gizmos.line( - self.rotation * line_start, - self.rotation * line_end, - self.color, - ); - } - - // Horizontal lines - let dline = dx * self.cell_count.x as f32; - for i in 0..line_count.y { - let i = i as f32; - let line_start = horizontal_start + i * dy; - let line_end = line_start + dline; - - self.gizmos.line( - self.rotation * line_start, - self.rotation * line_end, - self.color, - ); - } + draw_grid( + self.gizmos, + self.position, + self.rotation, + self.spacing, + self.cell_count, + self.skew, + self.outer_edges, + self.color, + ); + } +} +impl Drop for GridBuilder2d<'_, '_, '_, T> { + fn drop(&mut self) { + draw_grid( + self.gizmos, + self.position, + self.rotation, + self.spacing.extend(0.), + self.cell_count.extend(0), + self.skew.extend(0.), + [self.outer_edges[0], self.outer_edges[1], true], + self.color, + ); } } - impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { /// Draw a 2D grid in 3D. /// @@ -119,7 +165,7 @@ impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { /// # Builder methods /// /// - The skew of the grid can be adjusted using the `.skew(...)`, `.skew_x(...)` or `.skew_y(...)` methods. They behave very similar to their CSS equivalents. - /// - The outer edges can be toggled on or off using `.outer_edges(...)`. + /// - All outer edges can be toggled on or off using `.outer_edges(...)`. Alternatively you can use `.outer_edges_x(...)` or `.outer_edges_y(...)` to toggle the outer edges along an axis. /// /// # Example /// ``` @@ -136,7 +182,7 @@ impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { /// GREEN /// ) /// .skew_x(0.25) - /// .outer_edges(true); + /// .outer_edges(); /// } /// # bevy_ecs::system::assert_is_system(system); /// ``` @@ -147,15 +193,71 @@ impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { cell_count: UVec2, spacing: Vec2, color: impl Into, - ) -> GridBuilder<'_, 'w, 's, T> { - GridBuilder { + ) -> GridBuilder2d<'_, 'w, 's, T> { + GridBuilder2d { gizmos: self, position, rotation, spacing, cell_count, skew: Vec2::ZERO, - outer_edges: false, + outer_edges: [false, false], + color: color.into(), + } + } + + /// Draw a 3D grid of voxel-like cells. + /// + /// This should be called for each frame the grid needs to be rendered. + /// + /// # 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. + /// - `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 + /// + /// # Builder methods + /// + /// - The skew of the grid can be adjusted using the `.skew(...)`, `.skew_x(...)`, `.skew_y(...)` or `.skew_z(...)` methods. They behave very similar to their CSS equivalents. + /// - All outer edges can be toggled on or off using `.outer_edges(...)`. Alternatively you can use `.outer_edges_x(...)`, `.outer_edges_y(...)` or `.outer_edges_z(...)` to toggle the outer edges along an axis. + /// + /// # Example + /// ``` + /// # use bevy_gizmos::prelude::*; + /// # use bevy_render::prelude::*; + /// # use bevy_math::prelude::*; + /// # use bevy_color::palettes::basic::GREEN; + /// fn system(mut gizmos: Gizmos) { + /// gizmos.grid_3d( + /// Vec3::ZERO, + /// Quat::IDENTITY, + /// UVec3::new(10, 2, 10), + /// Vec3::splat(2.), + /// GREEN + /// ) + /// .skew_x(0.25) + /// .outer_edges(); + /// } + /// # bevy_ecs::system::assert_is_system(system); + /// ``` + pub fn grid_3d( + &mut self, + position: Vec3, + rotation: Quat, + cell_count: UVec3, + spacing: Vec3, + color: impl Into, + ) -> GridBuilder3d<'_, 'w, 's, T> { + GridBuilder3d { + gizmos: self, + position, + rotation, + spacing, + cell_count, + skew: Vec3::ZERO, + outer_edges: [false, false, false], color: color.into(), } } @@ -175,7 +277,7 @@ impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { /// # Builder methods /// /// - The skew of the grid can be adjusted using the `.skew(...)`, `.skew_x(...)` or `.skew_y(...)` methods. They behave very similar to their CSS equivalents. - /// - The outer edges can be toggled on or off using `.outer_edges(...)`. + /// - All outer edges can be toggled on or off using `.outer_edges(...)`. Alternatively you can use `.outer_edges_x(...)` or `.outer_edges_y(...)` to toggle the outer edges along an axis. /// /// # Example /// ``` @@ -192,7 +294,7 @@ impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { /// GREEN /// ) /// .skew_x(0.25) - /// .outer_edges(true); + /// .outer_edges(); /// } /// # bevy_ecs::system::assert_is_system(system); /// ``` @@ -203,16 +305,107 @@ impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { cell_count: UVec2, spacing: Vec2, color: impl Into, - ) -> GridBuilder<'_, 'w, 's, T> { - GridBuilder { + ) -> GridBuilder2d<'_, 'w, 's, T> { + GridBuilder2d { gizmos: self, position: position.extend(0.), rotation: Quat::from_rotation_z(rotation), spacing, cell_count, skew: Vec2::ZERO, - outer_edges: false, + outer_edges: [false, false], color: color.into(), } } } + +#[allow(clippy::too_many_arguments)] +fn draw_grid( + gizmos: &mut Gizmos<'_, '_, T>, + position: Vec3, + rotation: Quat, + spacing: Vec3, + cell_count: UVec3, + skew: Vec3, + outer_edges: [bool; 3], + color: LinearRgba, +) { + if !gizmos.enabled { + return; + } + + // Offset between two adjacent grid cells along the x/y-axis and accounting for skew. + let dx = spacing.x + * Vec3::new(1., skew.y.tan(), skew.z.tan()) + * if cell_count.x != 0 { 1. } else { 0. }; + let dy = spacing.y + * Vec3::new(skew.x.tan(), 1., skew.z.tan()) + * if cell_count.y != 0 { 1. } else { 0. }; + let dz = spacing.z + * Vec3::new(skew.x.tan(), skew.y.tan(), 1.) + * if cell_count.z != 0 { 1. } else { 0. }; + + // Bottom-left-front corner of the grid + let grid_start = position + - cell_count.x as f32 / 2.0 * dx + - cell_count.y as f32 / 2.0 * dy + - cell_count.z as f32 / 2.0 * dz; + + let line_count = UVec3::new( + if outer_edges[0] { + cell_count.x + 1 + } else { + cell_count.x.saturating_sub(1) + }, + if outer_edges[1] { + cell_count.y + 1 + } else { + cell_count.y.saturating_sub(1) + }, + if outer_edges[2] { + cell_count.z + 1 + } else { + cell_count.z.saturating_sub(1) + }, + ); + let x_start = grid_start + if outer_edges[0] { Vec3::ZERO } else { dy + dz }; + let y_start = grid_start + if outer_edges[1] { Vec3::ZERO } else { dx + dz }; + let z_start = grid_start + if outer_edges[2] { Vec3::ZERO } else { dx + dy }; + + // Lines along the x direction + let dline = dx * cell_count.x as f32; + for iy in 0..line_count.y { + let iy = iy as f32; + for iz in 0..line_count.z { + let iz = iz as f32; + let line_start = x_start + iy * dy + iz * dz; + let line_end = line_start + dline; + + gizmos.line(rotation * line_start, rotation * line_end, color); + } + } + // Lines along the y direction + let dline = dy * cell_count.y as f32; + for ix in 0..line_count.x { + let ix = ix as f32; + for iz in 0..line_count.z { + let iz = iz as f32; + let line_start = y_start + ix * dx + iz * dz; + let line_end = line_start + dline; + + gizmos.line(rotation * line_start, rotation * line_end, color); + } + } + // Lines along the z direction + let dline = dz * cell_count.z as f32; + for ix in 0..line_count.x { + let ix = ix as f32; + for iy in 0..line_count.y { + let iy = iy as f32; + let line_start = z_start + ix * dx + iy * dy; + let line_end = line_start + dline; + + gizmos.line(rotation * line_start, rotation * line_end, color); + } + } +} diff --git a/crates/bevy_gltf/Cargo.toml b/crates/bevy_gltf/Cargo.toml index 9cea2c7cb56bb..a7f94ef82d217 100644 --- a/crates/bevy_gltf/Cargo.toml +++ b/crates/bevy_gltf/Cargo.toml @@ -50,7 +50,7 @@ gltf = { version = "1.4.0", default-features = false, features = [ "utils", ] } thiserror = "1.0" -base64 = "0.21.5" +base64 = "0.22.0" percent-encoding = "2.1" serde = { version = "1.0", features = ["derive"] } serde_json = "1" diff --git a/crates/bevy_gltf/src/loader.rs b/crates/bevy_gltf/src/loader.rs index 6f2f27969805c..1a8caa4531a95 100644 --- a/crates/bevy_gltf/src/loader.rs +++ b/crates/bevy_gltf/src/loader.rs @@ -162,17 +162,15 @@ impl AssetLoader for GltfLoader { type Asset = Gltf; type Settings = GltfLoaderSettings; type Error = GltfError; - fn load<'a>( + async fn load<'a>( &'a self, - reader: &'a mut Reader, + reader: &'a mut Reader<'_>, settings: &'a GltfLoaderSettings, - load_context: &'a mut LoadContext, - ) -> bevy_utils::BoxedFuture<'a, Result> { - Box::pin(async move { - let mut bytes = Vec::new(); - reader.read_to_end(&mut bytes).await?; - load_gltf(self, &bytes, load_context, settings).await - }) + load_context: &'a mut LoadContext<'_>, + ) -> Result { + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).await?; + load_gltf(self, &bytes, load_context, settings).await } fn extensions(&self) -> &[&str] { diff --git a/crates/bevy_hierarchy/src/child_builder.rs b/crates/bevy_hierarchy/src/child_builder.rs index f8d206dee5580..9f36fe679e962 100644 --- a/crates/bevy_hierarchy/src/child_builder.rs +++ b/crates/bevy_hierarchy/src/child_builder.rs @@ -851,6 +851,7 @@ mod tests { ); } + #[allow(dead_code)] #[derive(Component)] struct C(u32); diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 68cb7421d3106..fc7eb39ff1ae9 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -175,6 +175,7 @@ bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" } bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.14.0-dev" } bevy_input = { path = "../bevy_input", version = "0.14.0-dev" } bevy_log = { path = "../bevy_log", version = "0.14.0-dev" } +bevy_panic_handler = { path = "../bevy_panic_handler", version = "0.14.0-dev" } bevy_math = { path = "../bevy_math", version = "0.14.0-dev" } bevy_ptr = { path = "../bevy_ptr", version = "0.14.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", features = [ @@ -202,7 +203,7 @@ bevy_ui = { path = "../bevy_ui", optional = true, version = "0.14.0-dev" } bevy_winit = { path = "../bevy_winit", optional = true, version = "0.14.0-dev" } bevy_gilrs = { path = "../bevy_gilrs", optional = true, version = "0.14.0-dev" } bevy_gizmos = { path = "../bevy_gizmos", optional = true, version = "0.14.0-dev", default-features = false } -bevy_dev_tools = { path = "../bevy_dev_tools/", optional = true, version = "0.14.0-dev" } +bevy_dev_tools = { path = "../bevy_dev_tools", optional = true, version = "0.14.0-dev" } [lints] workspace = true diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index dcea35be338e3..fdc33428e58eb 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -1,6 +1,7 @@ use bevy_app::{Plugin, PluginGroup, PluginGroupBuilder}; /// This plugin group will add all the default plugins for a *Bevy* application: +/// * [`PanicHandlerPlugin`](crate::panic_handler::PanicHandlerPlugin) /// * [`LogPlugin`](crate::log::LogPlugin) /// * [`TaskPoolPlugin`](crate::core::TaskPoolPlugin) /// * [`TypeRegistrationPlugin`](crate::core::TypeRegistrationPlugin) @@ -42,6 +43,7 @@ impl PluginGroup for DefaultPlugins { fn build(self) -> PluginGroupBuilder { let mut group = PluginGroupBuilder::start::(); group = group + .add(bevy_panic_handler::PanicHandlerPlugin) .add(bevy_log::LogPlugin::default()) .add(bevy_core::TaskPoolPlugin::default()) .add(bevy_core::TypeRegistrationPlugin) diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index 896091c404c98..e7a46f246c808 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -53,6 +53,11 @@ pub mod log { pub use bevy_log::*; } +pub mod panic_handler { + //! Platform-specific panic handlers + pub use bevy_panic_handler::*; +} + pub mod math { //! Math types (Vec3, Mat4, Quat, etc) and helpers. pub use bevy_math::*; diff --git a/crates/bevy_log/Cargo.toml b/crates/bevy_log/Cargo.toml index a5229d61ac934..eaf53d1f6cdeb 100644 --- a/crates/bevy_log/Cargo.toml +++ b/crates/bevy_log/Cargo.toml @@ -34,7 +34,6 @@ tracy-client = { version = "0.17.0", optional = true } android_log-sys = "0.3.0" [target.'cfg(target_arch = "wasm32")'.dependencies] -console_error_panic_hook = "0.1.6" tracing-wasm = "0.2.1" [lints] diff --git a/crates/bevy_log/src/lib.rs b/crates/bevy_log/src/lib.rs index eb3cef046a4ff..b6576acb172d9 100644 --- a/crates/bevy_log/src/lib.rs +++ b/crates/bevy_log/src/lib.rs @@ -205,7 +205,6 @@ impl Plugin for LogPlugin { #[cfg(target_arch = "wasm32")] { - console_error_panic_hook::set_once(); finished_subscriber = subscriber.with(tracing_wasm::WASMLayer::new( tracing_wasm::WASMLayerConfig::default(), )); diff --git a/crates/bevy_macros_compile_fail_tests/tests/deref_mut_derive/missing_deref.fail.stderr b/crates/bevy_macros_compile_fail_tests/tests/deref_mut_derive/missing_deref.fail.stderr index 3e11d49532e94..c969892c67ee8 100644 --- a/crates/bevy_macros_compile_fail_tests/tests/deref_mut_derive/missing_deref.fail.stderr +++ b/crates/bevy_macros_compile_fail_tests/tests/deref_mut_derive/missing_deref.fail.stderr @@ -7,6 +7,14 @@ error[E0277]: the trait bound `TupleStruct: Deref` is not satisfied note: required by a bound in `DerefMut` --> $RUST/core/src/ops/deref.rs +error[E0277]: the trait bound `TupleStruct: Deref` is not satisfied + --> tests/deref_mut_derive/missing_deref.fail.rs:3:10 + | +3 | #[derive(DerefMut)] + | ^^^^^^^^ the trait `Deref` is not implemented for `TupleStruct` + | + = note: this error originates in the derive macro `DerefMut` (in Nightly builds, run with -Z macro-backtrace for more info) + error[E0277]: the trait bound `Struct: Deref` is not satisfied --> tests/deref_mut_derive/missing_deref.fail.rs:7:8 | @@ -16,14 +24,6 @@ error[E0277]: the trait bound `Struct: Deref` is not satisfied note: required by a bound in `DerefMut` --> $RUST/core/src/ops/deref.rs -error[E0277]: the trait bound `TupleStruct: Deref` is not satisfied - --> tests/deref_mut_derive/missing_deref.fail.rs:3:10 - | -3 | #[derive(DerefMut)] - | ^^^^^^^^ the trait `Deref` is not implemented for `TupleStruct` - | - = note: this error originates in the derive macro `DerefMut` (in Nightly builds, run with -Z macro-backtrace for more info) - error[E0277]: the trait bound `Struct: Deref` is not satisfied --> tests/deref_mut_derive/missing_deref.fail.rs:6:10 | diff --git a/crates/bevy_math/Cargo.toml b/crates/bevy_math/Cargo.toml index 128e8529d3d2c..3423759ebec8c 100644 --- a/crates/bevy_math/Cargo.toml +++ b/crates/bevy_math/Cargo.toml @@ -14,11 +14,18 @@ thiserror = "1.0" serde = { version = "1", features = ["derive"], optional = true } libm = { version = "0.2", optional = true } approx = { version = "0.5", optional = true } +rand = { version = "0.8", features = [ + "alloc", +], default-features = false, optional = true } [dev-dependencies] approx = "0.5" +# Supply rngs for examples and tests +rand = "0.8" +rand_chacha = "0.3" [features] +default = ["rand"] serialize = ["dep:serde", "glam/serde"] # Enable approx for glam types to approximate floating point equality comparisons and assertions approx = ["dep:approx", "glam/approx"] @@ -31,6 +38,8 @@ libm = ["dep:libm", "glam/libm"] glam_assert = ["glam/glam-assert"] # Enable assertions in debug builds to check the validity of parameters passed to glam debug_glam_assert = ["glam/debug-glam-assert"] +# Enable the rand dependency for shape_sampling +rand = ["dep:rand"] [lints] workspace = true diff --git a/crates/bevy_math/src/cubic_splines.rs b/crates/bevy_math/src/cubic_splines.rs index 422d5767e2b6a..b7796b68f036c 100644 --- a/crates/bevy_math/src/cubic_splines.rs +++ b/crates/bevy_math/src/cubic_splines.rs @@ -2,6 +2,7 @@ use std::{ fmt::Debug, + iter::once, ops::{Add, Div, Mul, Sub}, }; @@ -172,7 +173,8 @@ impl CubicGenerator

for CubicHermite

{ } /// A spline interpolated continuously across the nearest four control points, with the position of -/// the curve specified at every control point and the tangents computed automatically. +/// the curve specified at every control point and the tangents computed automatically. The associated [`CubicCurve`] +/// has one segment between each pair of adjacent control points. /// /// **Note** the Catmull-Rom spline is a special case of Cardinal spline where the tension is 0.5. /// @@ -183,8 +185,8 @@ impl CubicGenerator

for CubicHermite

{ /// Tangents are automatically computed based on the positions of control points. /// /// ### Continuity -/// The curve is at minimum C0 continuous, meaning it has no holes or jumps. It is also C1, meaning the -/// tangent vector has no sudden jumps. +/// The curve is at minimum C1, meaning that it is continuous (it has no holes or jumps), and its tangent +/// vector is also well-defined everywhere, without sudden jumps. /// /// ### Usage /// @@ -232,10 +234,28 @@ impl CubicGenerator

for CubicCardinalSpline

{ [-s, 2. - s, s - 2., s], ]; - let segments = self - .control_points + let length = self.control_points.len(); + + // Early return to avoid accessing an invalid index + if length < 2 { + return CubicCurve { segments: vec![] }; + } + + // Extend the list of control points by mirroring the last second-to-last control points on each end; + // this allows tangents for the endpoints to be provided, and the overall effect is that the tangent + // at an endpoint is proportional to twice the vector between it and its adjacent control point. + // + // The expression used here is P_{-1} := P_0 - (P_1 - P_0) = 2P_0 - P_1. (Analogously at the other end.) + let mirrored_first = self.control_points[0] * 2. - self.control_points[1]; + let mirrored_last = self.control_points[length - 1] * 2. - self.control_points[length - 2]; + let extended_control_points = once(&mirrored_first) + .chain(self.control_points.iter()) + .chain(once(&mirrored_last)) + .collect::>(); + + let segments = extended_control_points .windows(4) - .map(|p| CubicSegment::coefficients([p[0], p[1], p[2], p[3]], char_matrix)) + .map(|p| CubicSegment::coefficients([*p[0], *p[1], *p[2], *p[3]], char_matrix)) .collect(); CubicCurve { segments } @@ -1275,6 +1295,37 @@ mod tests { assert_eq!(bezier.ease(1.0), 1.0); } + /// Test that a simple cardinal spline passes through all of its control points with + /// the correct tangents. + #[test] + fn cardinal_control_pts() { + use super::CubicCardinalSpline; + + let tension = 0.2; + let [p0, p1, p2, p3] = [vec2(-1., -2.), vec2(0., 1.), vec2(1., 2.), vec2(-2., 1.)]; + let curve = CubicCardinalSpline::new(tension, [p0, p1, p2, p3]).to_curve(); + + // Positions at segment endpoints + assert!(curve.position(0.).abs_diff_eq(p0, FLOAT_EQ)); + assert!(curve.position(1.).abs_diff_eq(p1, FLOAT_EQ)); + assert!(curve.position(2.).abs_diff_eq(p2, FLOAT_EQ)); + assert!(curve.position(3.).abs_diff_eq(p3, FLOAT_EQ)); + + // Tangents at segment endpoints + assert!(curve + .velocity(0.) + .abs_diff_eq((p1 - p0) * tension * 2., FLOAT_EQ)); + assert!(curve + .velocity(1.) + .abs_diff_eq((p2 - p0) * tension, FLOAT_EQ)); + assert!(curve + .velocity(2.) + .abs_diff_eq((p3 - p1) * tension, FLOAT_EQ)); + assert!(curve + .velocity(3.) + .abs_diff_eq((p3 - p2) * tension * 2., FLOAT_EQ)); + } + /// Test that [`RationalCurve`] properly generalizes [`CubicCurve`]. A Cubic upgraded to a rational /// should produce pretty much the same output. #[test] diff --git a/crates/bevy_math/src/direction.rs b/crates/bevy_math/src/direction.rs index c98736960c840..150a9105b86e1 100644 --- a/crates/bevy_math/src/direction.rs +++ b/crates/bevy_math/src/direction.rs @@ -136,6 +136,11 @@ impl Dir2 { pub fn from_xy(x: f32, y: f32) -> Result { Self::new(Vec2::new(x, y)) } + + /// Returns the inner [`Vec2`] + pub const fn as_vec2(&self) -> Vec2 { + self.0 + } } impl TryFrom for Dir2 { @@ -146,6 +151,12 @@ impl TryFrom for Dir2 { } } +impl From for Vec2 { + fn from(value: Dir2) -> Self { + value.as_vec2() + } +} + impl std::ops::Deref for Dir2 { type Target = Vec2; fn deref(&self) -> &Self::Target { @@ -287,6 +298,11 @@ impl Dir3 { pub fn from_xyz(x: f32, y: f32, z: f32) -> Result { Self::new(Vec3::new(x, y, z)) } + + /// Returns the inner [`Vec3`] + pub const fn as_vec3(&self) -> Vec3 { + self.0 + } } impl TryFrom for Dir3 { @@ -447,6 +463,11 @@ impl Dir3A { pub fn from_xyz(x: f32, y: f32, z: f32) -> Result { Self::new(Vec3A::new(x, y, z)) } + + /// Returns the inner [`Vec3A`] + pub const fn as_vec3a(&self) -> Vec3A { + self.0 + } } impl TryFrom for Dir3A { diff --git a/crates/bevy_math/src/lib.rs b/crates/bevy_math/src/lib.rs index 604a299ab282c..0d76500703818 100644 --- a/crates/bevy_math/src/lib.rs +++ b/crates/bevy_math/src/lib.rs @@ -14,6 +14,8 @@ pub mod primitives; mod ray; mod rects; mod rotation2d; +#[cfg(feature = "rand")] +mod shape_sampling; pub use affine3::*; pub use aspect_ratio::AspectRatio; @@ -21,9 +23,14 @@ pub use direction::*; pub use ray::{Ray2d, Ray3d}; pub use rects::*; pub use rotation2d::Rotation2d; +#[cfg(feature = "rand")] +pub use shape_sampling::ShapeSample; /// The `bevy_math` prelude. pub mod prelude { + #[doc(hidden)] + #[cfg(feature = "rand")] + pub use crate::shape_sampling::ShapeSample; #[doc(hidden)] pub use crate::{ cubic_splines::{ diff --git a/crates/bevy_math/src/shape_sampling.rs b/crates/bevy_math/src/shape_sampling.rs new file mode 100644 index 0000000000000..eff4a0898f37b --- /dev/null +++ b/crates/bevy_math/src/shape_sampling.rs @@ -0,0 +1,345 @@ +use std::f32::consts::{PI, TAU}; + +use crate::{primitives::*, Vec2, Vec3}; +use rand::{ + distributions::{Distribution, WeightedIndex}, + Rng, +}; + +/// Exposes methods to uniformly sample a variety of primitive shapes. +pub trait ShapeSample { + /// The type of vector returned by the sample methods, [`Vec2`] for 2D shapes and [`Vec3`] for 3D shapes. + type Output; + + /// Uniformly sample a point from inside the area/volume of this shape, centered on 0. + /// + /// Shapes like [`Cylinder`], [`Capsule2d`] and [`Capsule3d`] are oriented along the y-axis. + /// + /// # Example + /// ``` + /// # use bevy_math::prelude::*; + /// let square = Rectangle::new(2.0, 2.0); + /// + /// // Returns a Vec2 with both x and y between -1 and 1. + /// println!("{:?}", square.sample_interior(&mut rand::thread_rng())); + /// ``` + fn sample_interior(&self, rng: &mut R) -> Self::Output; + + /// Uniformly sample a point from the surface of this shape, centered on 0. + /// + /// Shapes like [`Cylinder`], [`Capsule2d`] and [`Capsule3d`] are oriented along the y-axis. + /// + /// # Example + /// ``` + /// # use bevy_math::prelude::*; + /// let square = Rectangle::new(2.0, 2.0); + /// + /// // Returns a Vec2 where one of the coordinates is at ±1, + /// // and the other is somewhere between -1 and 1. + /// println!("{:?}", square.sample_boundary(&mut rand::thread_rng())); + /// ``` + fn sample_boundary(&self, rng: &mut R) -> Self::Output; +} + +impl ShapeSample for Circle { + type Output = Vec2; + + fn sample_interior(&self, rng: &mut R) -> Vec2 { + // https://mathworld.wolfram.com/DiskPointPicking.html + let theta = rng.gen_range(0.0..TAU); + let r_squared = rng.gen_range(0.0..=(self.radius * self.radius)); + let r = r_squared.sqrt(); + Vec2::new(r * theta.cos(), r * theta.sin()) + } + + fn sample_boundary(&self, rng: &mut R) -> Vec2 { + let theta = rng.gen_range(0.0..TAU); + Vec2::new(self.radius * theta.cos(), self.radius * theta.sin()) + } +} + +impl ShapeSample for Sphere { + type Output = Vec3; + + fn sample_interior(&self, rng: &mut R) -> Vec3 { + // https://mathworld.wolfram.com/SpherePointPicking.html + let theta = rng.gen_range(0.0..TAU); + let phi = rng.gen_range(-1.0_f32..1.0).acos(); + let r_cubed = rng.gen_range(0.0..=(self.radius * self.radius * self.radius)); + let r = r_cubed.cbrt(); + Vec3 { + x: r * phi.sin() * theta.cos(), + y: r * phi.sin() * theta.sin(), + z: r * phi.cos(), + } + } + + fn sample_boundary(&self, rng: &mut R) -> Vec3 { + let theta = rng.gen_range(0.0..TAU); + let phi = rng.gen_range(-1.0_f32..1.0).acos(); + Vec3 { + x: self.radius * phi.sin() * theta.cos(), + y: self.radius * phi.sin() * theta.sin(), + z: self.radius * phi.cos(), + } + } +} + +impl ShapeSample for Rectangle { + type Output = Vec2; + + fn sample_interior(&self, rng: &mut R) -> Vec2 { + let x = rng.gen_range(-self.half_size.x..=self.half_size.x); + let y = rng.gen_range(-self.half_size.y..=self.half_size.y); + Vec2::new(x, y) + } + + fn sample_boundary(&self, rng: &mut R) -> Vec2 { + let primary_side = rng.gen_range(-1.0..1.0); + let other_side = if rng.gen() { -1.0 } else { 1.0 }; + + if self.half_size.x + self.half_size.y > 0.0 { + if rng.gen_bool((self.half_size.x / (self.half_size.x + self.half_size.y)) as f64) { + Vec2::new(primary_side, other_side) * self.half_size + } else { + Vec2::new(other_side, primary_side) * self.half_size + } + } else { + Vec2::ZERO + } + } +} + +impl ShapeSample for Cuboid { + type Output = Vec3; + + fn sample_interior(&self, rng: &mut R) -> Vec3 { + let x = rng.gen_range(-self.half_size.x..=self.half_size.x); + let y = rng.gen_range(-self.half_size.y..=self.half_size.y); + let z = rng.gen_range(-self.half_size.z..=self.half_size.z); + Vec3::new(x, y, z) + } + + fn sample_boundary(&self, rng: &mut R) -> Vec3 { + let primary_side1 = rng.gen_range(-1.0..1.0); + let primary_side2 = rng.gen_range(-1.0..1.0); + let other_side = if rng.gen() { -1.0 } else { 1.0 }; + + if let Ok(dist) = WeightedIndex::new([ + self.half_size.y * self.half_size.z, + self.half_size.x * self.half_size.z, + self.half_size.x * self.half_size.y, + ]) { + match dist.sample(rng) { + 0 => Vec3::new(other_side, primary_side1, primary_side2) * self.half_size, + 1 => Vec3::new(primary_side1, other_side, primary_side2) * self.half_size, + 2 => Vec3::new(primary_side1, primary_side2, other_side) * self.half_size, + _ => unreachable!(), + } + } else { + Vec3::ZERO + } + } +} + +impl ShapeSample for Cylinder { + type Output = Vec3; + + fn sample_interior(&self, rng: &mut R) -> Vec3 { + let Vec2 { x, y: z } = self.base().sample_interior(rng); + let y = rng.gen_range(-self.half_height..=self.half_height); + Vec3::new(x, y, z) + } + + fn sample_boundary(&self, rng: &mut R) -> Vec3 { + // This uses the area of the ends divided by the overall surface area (optimised) + // [2 (\pi r^2)]/[2 (\pi r^2) + 2 \pi r h] = r/(r + h) + if self.radius + 2.0 * self.half_height > 0.0 { + if rng.gen_bool((self.radius / (self.radius + 2.0 * self.half_height)) as f64) { + let Vec2 { x, y: z } = self.base().sample_interior(rng); + if rng.gen() { + Vec3::new(x, self.half_height, z) + } else { + Vec3::new(x, -self.half_height, z) + } + } else { + let Vec2 { x, y: z } = self.base().sample_boundary(rng); + let y = rng.gen_range(-self.half_height..=self.half_height); + Vec3::new(x, y, z) + } + } else { + Vec3::ZERO + } + } +} + +impl ShapeSample for Capsule2d { + type Output = Vec2; + + fn sample_interior(&self, rng: &mut R) -> Vec2 { + let rectangle_area = self.half_length * self.radius * 4.0; + let capsule_area = rectangle_area + PI * self.radius * self.radius; + if capsule_area > 0.0 { + // Check if the random point should be inside the rectangle + if rng.gen_bool((rectangle_area / capsule_area) as f64) { + let rectangle = Rectangle::new(self.radius, self.half_length * 2.0); + rectangle.sample_interior(rng) + } else { + let circle = Circle::new(self.radius); + let point = circle.sample_interior(rng); + // Add half length if it is the top semi-circle, otherwise subtract half + if point.y > 0.0 { + point + Vec2::Y * self.half_length + } else { + point - Vec2::Y * self.half_length + } + } + } else { + Vec2::ZERO + } + } + + fn sample_boundary(&self, rng: &mut R) -> Vec2 { + let rectangle_surface = 4.0 * self.half_length; + let capsule_surface = rectangle_surface + TAU * self.radius; + if capsule_surface > 0.0 { + if rng.gen_bool((rectangle_surface / capsule_surface) as f64) { + let side_distance = + rng.gen_range((-2.0 * self.half_length)..=(2.0 * self.half_length)); + if side_distance < 0.0 { + Vec2::new(self.radius, side_distance + self.half_length) + } else { + Vec2::new(-self.radius, side_distance - self.half_length) + } + } else { + let circle = Circle::new(self.radius); + let point = circle.sample_boundary(rng); + // Add half length if it is the top semi-circle, otherwise subtract half + if point.y > 0.0 { + point + Vec2::Y * self.half_length + } else { + point - Vec2::Y * self.half_length + } + } + } else { + Vec2::ZERO + } + } +} + +impl ShapeSample for Capsule3d { + type Output = Vec3; + + fn sample_interior(&self, rng: &mut R) -> Vec3 { + let cylinder_vol = PI * self.radius * self.radius * 2.0 * self.half_length; + // Add 4/3 pi r^3 + let capsule_vol = cylinder_vol + 4.0 / 3.0 * PI * self.radius * self.radius * self.radius; + if capsule_vol > 0.0 { + // Check if the random point should be inside the cylinder + if rng.gen_bool((cylinder_vol / capsule_vol) as f64) { + self.to_cylinder().sample_interior(rng) + } else { + let sphere = Sphere::new(self.radius); + let point = sphere.sample_interior(rng); + // Add half length if it is the top semi-sphere, otherwise subtract half + if point.y > 0.0 { + point + Vec3::Y * self.half_length + } else { + point - Vec3::Y * self.half_length + } + } + } else { + Vec3::ZERO + } + } + + fn sample_boundary(&self, rng: &mut R) -> Vec3 { + let cylinder_surface = TAU * self.radius * 2.0 * self.half_length; + let capsule_surface = cylinder_surface + 4.0 * PI * self.radius * self.radius; + if capsule_surface > 0.0 { + if rng.gen_bool((cylinder_surface / capsule_surface) as f64) { + let Vec2 { x, y: z } = Circle::new(self.radius).sample_boundary(rng); + let y = rng.gen_range(-self.half_length..=self.half_length); + Vec3::new(x, y, z) + } else { + let sphere = Sphere::new(self.radius); + let point = sphere.sample_boundary(rng); + // Add half length if it is the top semi-sphere, otherwise subtract half + if point.y > 0.0 { + point + Vec3::Y * self.half_length + } else { + point - Vec3::Y * self.half_length + } + } + } else { + Vec3::ZERO + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rand::SeedableRng; + use rand_chacha::ChaCha8Rng; + + #[test] + fn circle_interior_sampling() { + let mut rng = ChaCha8Rng::from_seed(Default::default()); + let circle = Circle::new(8.0); + + let boxes = [ + (-3.0, 3.0), + (1.0, 2.0), + (-1.0, -2.0), + (3.0, -2.0), + (1.0, -6.0), + (-3.0, -7.0), + (-7.0, -3.0), + (-6.0, 1.0), + ]; + let mut box_hits = [0; 8]; + + // Checks which boxes (if any) the sampled points are in + for _ in 0..5000 { + let point = circle.sample_interior(&mut rng); + + for (i, box_) in boxes.iter().enumerate() { + if (point.x > box_.0 && point.x < box_.0 + 4.0) + && (point.y > box_.1 && point.y < box_.1 + 4.0) + { + box_hits[i] += 1; + } + } + } + + assert_eq!( + box_hits, + [396, 377, 415, 404, 366, 408, 408, 430], + "samples will occur across all array items at statistically equal chance" + ); + } + + #[test] + fn circle_boundary_sampling() { + let mut rng = ChaCha8Rng::from_seed(Default::default()); + let circle = Circle::new(1.0); + + let mut wedge_hits = [0; 8]; + + // Checks in which eighth of the circle each sampled point is in + for _ in 0..5000 { + let point = circle.sample_boundary(&mut rng); + + let angle = f32::atan(point.y / point.x) + PI / 2.0; + let wedge = (angle * 8.0 / PI).floor() as usize; + wedge_hits[wedge] += 1; + } + + assert_eq!( + wedge_hits, + [636, 608, 639, 603, 614, 650, 640, 610], + "samples will occur across all array items at statistically equal chance" + ); + } +} diff --git a/crates/bevy_panic_handler/Cargo.toml b/crates/bevy_panic_handler/Cargo.toml new file mode 100644 index 0000000000000..24b96de3c2a4a --- /dev/null +++ b/crates/bevy_panic_handler/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "bevy_panic_handler" +version = "0.14.0-dev" +edition = "2021" +description = "Provides panic handlers for Bevy Engine" +homepage = "https://bevyengine.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT OR Apache-2.0" +keywords = ["bevy"] + +[features] + +[dependencies] +bevy_app = { path = "../bevy_app", version = "0.14.0-dev" } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +console_error_panic_hook = "0.1.6" + +[lints] +workspace = true + +[package.metadata.docs.rs] +all-features = true diff --git a/crates/bevy_panic_handler/src/lib.rs b/crates/bevy_panic_handler/src/lib.rs new file mode 100644 index 0000000000000..8e3705980de62 --- /dev/null +++ b/crates/bevy_panic_handler/src/lib.rs @@ -0,0 +1,54 @@ +//! This crate provides panic handlers for [Bevy](https://bevyengine.org) +//! apps, and automatically configures platform specifics (i.e. WASM or Android). +//! +//! By default, the [`PanicHandlerPlugin`] from this crate is included in Bevy's `DefaultPlugins`. +//! +//! For more fine-tuned control over panic behavior, disable the [`PanicHandlerPlugin`] or +//! `DefaultPlugins` during app initialization. +#![cfg_attr(docsrs, feature(doc_auto_cfg))] + +use bevy_app::{App, Plugin}; + +/// Adds sensible panic handlers to Apps. This plugin is part of the `DefaultPlugins`. Adding +/// this plugin will setup a panic hook appropriate to your target platform: +/// * On WASM, uses [`console_error_panic_hook`](https://crates.io/crates/console_error_panic_hook), logging +/// to the browser console. +/// * Other platforms are currently not setup. +/// +/// ```no_run +/// # use bevy_app::{App, NoopPluginGroup as MinimalPlugins, PluginGroup}; +/// # use bevy_panic_handler::PanicHandlerPlugin; +/// fn main() { +/// App::new() +/// .add_plugins(MinimalPlugins) +/// .add_plugins(PanicHandlerPlugin) +/// .run(); +/// } +/// ``` +/// +/// If you want to setup your own panic handler, you should disable this +/// plugin from `DefaultPlugins`: +/// ```no_run +/// # use bevy_app::{App, NoopPluginGroup as DefaultPlugins, PluginGroup}; +/// # use bevy_panic_handler::PanicHandlerPlugin; +/// fn main() { +/// App::new() +/// .add_plugins(DefaultPlugins.build().disable::()) +/// .run(); +/// } +/// ``` +#[derive(Default)] +pub struct PanicHandlerPlugin; + +impl Plugin for PanicHandlerPlugin { + fn build(&self, _app: &mut App) { + #[cfg(target_arch = "wasm32")] + { + console_error_panic_hook::set_once(); + } + #[cfg(not(target_arch = "wasm32"))] + { + // Use the default target panic hook - Do nothing. + } + } +} diff --git a/crates/bevy_pbr/Cargo.toml b/crates/bevy_pbr/Cargo.toml index b1f45d16ca673..0b148fc2729bd 100644 --- a/crates/bevy_pbr/Cargo.toml +++ b/crates/bevy_pbr/Cargo.toml @@ -35,7 +35,7 @@ bevy_derive = { path = "../bevy_derive", version = "0.14.0-dev" } # other bitflags = "2.3" -fixedbitset = "0.4" +fixedbitset = "0.5" # direct dependency required for derive macro bytemuck = { version = "1", features = ["derive"] } radsort = "0.1" diff --git a/crates/bevy_pbr/src/light/ambient_light.rs b/crates/bevy_pbr/src/light/ambient_light.rs new file mode 100644 index 0000000000000..d3f7744bd4f5c --- /dev/null +++ b/crates/bevy_pbr/src/light/ambient_light.rs @@ -0,0 +1,39 @@ +use super::*; + +/// An ambient light, which lights the entire scene equally. +/// +/// This resource is inserted by the [`PbrPlugin`] and by default it is set to a low ambient light. +/// +/// # Examples +/// +/// Make ambient light slightly brighter: +/// +/// ``` +/// # use bevy_ecs::system::ResMut; +/// # use bevy_pbr::AmbientLight; +/// fn setup_ambient_light(mut ambient_light: ResMut) { +/// ambient_light.brightness = 100.0; +/// } +/// ``` +#[derive(Resource, Clone, Debug, ExtractResource, Reflect)] +#[reflect(Resource)] +pub struct AmbientLight { + pub color: Color, + /// A direct scale factor multiplied with `color` before being passed to the shader. + pub brightness: f32, +} + +impl Default for AmbientLight { + fn default() -> Self { + Self { + color: Color::WHITE, + brightness: 80.0, + } + } +} +impl AmbientLight { + pub const NONE: AmbientLight = AmbientLight { + color: Color::WHITE, + brightness: 0.0, + }; +} diff --git a/crates/bevy_pbr/src/light.rs b/crates/bevy_pbr/src/light/mod.rs similarity index 99% rename from crates/bevy_pbr/src/light.rs rename to crates/bevy_pbr/src/light/mod.rs index e131d92d2c3ca..5d6be4fcf48d3 100644 --- a/crates/bevy_pbr/src/light.rs +++ b/crates/bevy_pbr/src/light/mod.rs @@ -20,6 +20,9 @@ use bevy_utils::tracing::warn; use crate::*; +mod ambient_light; +pub use ambient_light::AmbientLight; + /// Constants for operating with the light units: lumens, and lux. pub mod light_consts { /// Approximations for converting the wattage of lamps to lumens. @@ -616,45 +619,6 @@ fn calculate_cascade( texel_size: cascade_texel_size, } } - -/// An ambient light, which lights the entire scene equally. -/// -/// This resource is inserted by the [`PbrPlugin`] and by default it is set to a low ambient light. -/// -/// # Examples -/// -/// Make ambient light slightly brighter: -/// -/// ``` -/// # use bevy_ecs::system::ResMut; -/// # use bevy_pbr::AmbientLight; -/// fn setup_ambient_light(mut ambient_light: ResMut) { -/// ambient_light.brightness = 100.0; -/// } -/// ``` -#[derive(Resource, Clone, Debug, ExtractResource, Reflect)] -#[reflect(Resource)] -pub struct AmbientLight { - pub color: Color, - /// A direct scale factor multiplied with `color` before being passed to the shader. - pub brightness: f32, -} - -impl Default for AmbientLight { - fn default() -> Self { - Self { - color: Color::WHITE, - brightness: 80.0, - } - } -} -impl AmbientLight { - pub const NONE: AmbientLight = AmbientLight { - color: Color::WHITE, - brightness: 0.0, - }; -} - /// Add this component to make a [`Mesh`](bevy_render::mesh::Mesh) not cast shadows. #[derive(Component, Reflect, Default)] #[reflect(Component, Default)] diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index 6939153c7fcb4..96c32bfd6834c 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -193,6 +193,8 @@ pub struct MaterialPlugin { /// When it is enabled, it will automatically add the [`PrepassPlugin`] /// required to make the prepass work on this Material. pub prepass_enabled: bool, + /// Controls if shadows are enabled for the Material. + pub shadows_enabled: bool, pub _marker: PhantomData, } @@ -200,6 +202,7 @@ impl Default for MaterialPlugin { fn default() -> Self { Self { prepass_enabled: true, + shadows_enabled: true, _marker: Default::default(), } } @@ -231,18 +234,26 @@ where prepare_materials:: .in_set(RenderSet::PrepareAssets) .after(prepare_assets::), - queue_shadows:: - .in_set(RenderSet::QueueMeshes) - .after(prepare_materials::), queue_material_meshes:: .in_set(RenderSet::QueueMeshes) .after(prepare_materials::), ), ); + + if self.shadows_enabled { + render_app.add_systems( + Render, + (queue_shadows:: + .in_set(RenderSet::QueueMeshes) + .after(prepare_materials::),), + ); + } } - // PrepassPipelinePlugin is required for shadow mapping and the optional PrepassPlugin - app.add_plugins(PrepassPipelinePlugin::::default()); + if self.shadows_enabled || self.prepass_enabled { + // PrepassPipelinePlugin is required for shadow mapping and the optional PrepassPlugin + app.add_plugins(PrepassPipelinePlugin::::default()); + } if self.prepass_enabled { app.add_plugins(PrepassPlugin::::default()); @@ -934,6 +945,10 @@ pub fn prepare_materials( ) { let queued_assets = std::mem::take(&mut prepare_next_frame.assets); for (id, material) in queued_assets.into_iter() { + if extracted_assets.removed.contains(&id) { + continue; + } + match prepare_material( &material, &render_device, diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 061c34a8b2f3b..12d8961f988a4 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -4,6 +4,7 @@ use bevy_ecs::prelude::*; use bevy_math::{Mat4, UVec3, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; use bevy_render::{ camera::Camera, + diagnostic::RecordDiagnostics, mesh::Mesh, primitives::{CascadesFrusta, CubemapFrusta, Frustum, HalfSpace}, render_asset::RenderAssets, @@ -1809,6 +1810,9 @@ impl Node for ShadowPassNode { render_context: &mut RenderContext<'w>, world: &'w World, ) -> Result<(), NodeRunError> { + let diagnostics = render_context.diagnostic_recorder(); + let time_span = diagnostics.time_span(render_context.command_encoder(), "shadows"); + let view_entity = graph.view_entity(); if let Ok(view_lights) = self.main_view_query.get_manual(world, view_entity) { for view_light_entity in view_lights.lights.iter().copied() { @@ -1820,6 +1824,7 @@ impl Node for ShadowPassNode { let depth_stencil_attachment = Some(view_light.depth_attachment.get_attachment(StoreOp::Store)); + let diagnostics = render_context.diagnostic_recorder(); render_context.add_command_buffer_generation_task(move |render_device| { #[cfg(feature = "trace")] let _shadow_pass_span = info_span!("shadow_pass").entered(); @@ -1836,16 +1841,22 @@ impl Node for ShadowPassNode { timestamp_writes: None, occlusion_query_set: None, }); + let mut render_pass = TrackedRenderPass::new(&render_device, render_pass); + let pass_span = + diagnostics.pass_span(&mut render_pass, view_light.pass_name.clone()); shadow_phase.render(&mut render_pass, world, view_light_entity); + pass_span.end(&mut render_pass); drop(render_pass); command_encoder.finish() }); } } + time_span.end(render_context.command_encoder()); + Ok(()) } } diff --git a/crates/bevy_reflect/src/impls/std.rs b/crates/bevy_reflect/src/impls/std.rs index 578879091bfcc..3b50abb0d9fdf 100644 --- a/crates/bevy_reflect/src/impls/std.rs +++ b/crates/bevy_reflect/src/impls/std.rs @@ -96,6 +96,7 @@ impl_reflect_value!(::std::path::PathBuf( Deserialize, Default )); +impl_reflect_value!(::std::any::TypeId(Debug, Hash, PartialEq,)); impl_reflect_value!( ::core::result::Result < T: Clone + Reflect + TypePath, E: Clone + Reflect + TypePath > () @@ -218,6 +219,7 @@ impl_reflect_value!(::std::ffi::OsString( )); #[cfg(not(any(unix, windows)))] impl_reflect_value!(::std::ffi::OsString(Debug, Hash, PartialEq)); +impl_reflect_value!(::alloc::collections::BinaryHeap); macro_rules! impl_reflect_for_veclike { ($ty:path, $insert:expr, $remove:expr, $push:expr, $pop:expr, $sub:ty) => { @@ -2104,6 +2106,13 @@ mod tests { assert_eq!(path, output); } + #[test] + fn type_id_should_from_reflect() { + let type_id = std::any::TypeId::of::(); + let output = ::from_reflect(&type_id).unwrap(); + assert_eq!(type_id, output); + } + #[test] fn static_str_should_from_reflect() { let expected = "Hello, World!"; diff --git a/crates/bevy_reflect/src/serde/de.rs b/crates/bevy_reflect/src/serde/de.rs index 0949085bc4976..a121a7831c188 100644 --- a/crates/bevy_reflect/src/serde/de.rs +++ b/crates/bevy_reflect/src/serde/de.rs @@ -753,8 +753,12 @@ impl<'a, 'de> Visitor<'de> for EnumVisitor<'a> { )? .into(), }; - - dynamic_enum.set_variant(variant_info.name(), value); + let variant_name = variant_info.name(); + let variant_index = self + .enum_info + .index_of(variant_name) + .expect("variant should exist"); + dynamic_enum.set_variant_with_index(variant_index, variant_name, value); Ok(dynamic_enum) } } @@ -1058,7 +1062,7 @@ mod tests { use bevy_utils::HashMap; use crate as bevy_reflect; - use crate::serde::{TypedReflectDeserializer, UntypedReflectDeserializer}; + use crate::serde::{ReflectSerializer, TypedReflectDeserializer, UntypedReflectDeserializer}; use crate::{DynamicEnum, FromReflect, Reflect, ReflectDeserialize, TypeRegistry}; #[derive(Reflect, Debug, PartialEq)] @@ -1116,7 +1120,7 @@ mod tests { #[reflect(Deserialize)] struct CustomDeserialize { value: usize, - #[serde(rename = "renamed")] + #[serde(alias = "renamed")] inner_struct: SomeDeserializableStruct, } @@ -1166,12 +1170,11 @@ mod tests { registry } - #[test] - fn should_deserialize() { + fn get_my_struct() -> MyStruct { let mut map = HashMap::new(); map.insert(64, 32); - let expected = MyStruct { + MyStruct { primitive_value: 123, option_value: Some(String::from("Hello world!")), option_value_complex: Some(SomeStruct { foo: 123 }), @@ -1198,7 +1201,13 @@ mod tests { value: 100, inner_struct: SomeDeserializableStruct { foo: 101 }, }, - }; + } + } + + #[test] + fn should_deserialize() { + let expected = get_my_struct(); + let registry = get_registry(); let input = r#"{ "bevy_reflect::serde::de::tests::MyStruct": ( @@ -1243,7 +1252,6 @@ mod tests { ), }"#; - let registry = get_registry(); let reflect_deserializer = UntypedReflectDeserializer::new(®istry); let mut ron_deserializer = ron::de::Deserializer::from_str(input).unwrap(); let dynamic_output = reflect_deserializer @@ -1425,40 +1433,28 @@ mod tests { assert!(expected.reflect_partial_eq(output.as_ref()).unwrap()); } + // Regression test for https://github.com/bevyengine/bevy/issues/12462 #[test] - fn should_deserialize_non_self_describing_binary() { - let mut map = HashMap::new(); - map.insert(64, 32); + fn should_reserialize() { + let registry = get_registry(); + let input1 = get_my_struct(); - let expected = MyStruct { - primitive_value: 123, - option_value: Some(String::from("Hello world!")), - option_value_complex: Some(SomeStruct { foo: 123 }), - tuple_value: (PI, 1337), - list_value: vec![-2, -1, 0, 1, 2], - array_value: [-2, -1, 0, 1, 2], - map_value: map, - struct_value: SomeStruct { foo: 999999999 }, - tuple_struct_value: SomeTupleStruct(String::from("Tuple Struct")), - unit_struct: SomeUnitStruct, - unit_enum: SomeEnum::Unit, - newtype_enum: SomeEnum::NewType(123), - tuple_enum: SomeEnum::Tuple(1.23, 3.21), - struct_enum: SomeEnum::Struct { - foo: String::from("Struct variant value"), - }, - ignored_struct: SomeIgnoredStruct { ignored: 0 }, - ignored_tuple_struct: SomeIgnoredTupleStruct(0), - ignored_struct_variant: SomeIgnoredEnum::Struct { - foo: String::default(), - }, - ignored_tuple_variant: SomeIgnoredEnum::Tuple(0.0, 0.0), - custom_deserialize: CustomDeserialize { - value: 100, - inner_struct: SomeDeserializableStruct { foo: 101 }, - }, - }; + let serializer1 = ReflectSerializer::new(&input1, ®istry); + let serialized1 = ron::ser::to_string(&serializer1).unwrap(); + + let mut deserializer = ron::de::Deserializer::from_str(&serialized1).unwrap(); + let reflect_deserializer = UntypedReflectDeserializer::new(®istry); + let input2 = reflect_deserializer.deserialize(&mut deserializer).unwrap(); + + let serializer2 = ReflectSerializer::new(&*input2, ®istry); + let serialized2 = ron::ser::to_string(&serializer2).unwrap(); + assert_eq!(serialized1, serialized2); + } + + #[test] + fn should_deserialize_non_self_describing_binary() { + let expected = get_my_struct(); let registry = get_registry(); let input = vec![ @@ -1490,38 +1486,7 @@ mod tests { #[test] fn should_deserialize_self_describing_binary() { - let mut map = HashMap::new(); - map.insert(64, 32); - - let expected = MyStruct { - primitive_value: 123, - option_value: Some(String::from("Hello world!")), - option_value_complex: Some(SomeStruct { foo: 123 }), - tuple_value: (PI, 1337), - list_value: vec![-2, -1, 0, 1, 2], - array_value: [-2, -1, 0, 1, 2], - map_value: map, - struct_value: SomeStruct { foo: 999999999 }, - tuple_struct_value: SomeTupleStruct(String::from("Tuple Struct")), - unit_struct: SomeUnitStruct, - unit_enum: SomeEnum::Unit, - newtype_enum: SomeEnum::NewType(123), - tuple_enum: SomeEnum::Tuple(1.23, 3.21), - struct_enum: SomeEnum::Struct { - foo: String::from("Struct variant value"), - }, - ignored_struct: SomeIgnoredStruct { ignored: 0 }, - ignored_tuple_struct: SomeIgnoredTupleStruct(0), - ignored_struct_variant: SomeIgnoredEnum::Struct { - foo: String::default(), - }, - ignored_tuple_variant: SomeIgnoredEnum::Tuple(0.0, 0.0), - custom_deserialize: CustomDeserialize { - value: 100, - inner_struct: SomeDeserializableStruct { foo: 101 }, - }, - }; - + let expected = get_my_struct(); let registry = get_registry(); let input = vec![ diff --git a/crates/bevy_reflect/src/serde/ser.rs b/crates/bevy_reflect/src/serde/ser.rs index 2823f2f811617..c67b81e8cc2e2 100644 --- a/crates/bevy_reflect/src/serde/ser.rs +++ b/crates/bevy_reflect/src/serde/ser.rs @@ -574,12 +574,10 @@ mod tests { registry } - #[test] - fn should_serialize() { + fn get_my_struct() -> MyStruct { let mut map = HashMap::new(); map.insert(64, 32); - - let input = MyStruct { + MyStruct { primitive_value: 123, option_value: Some(String::from("Hello world!")), option_value_complex: Some(SomeStruct { foo: 123 }), @@ -606,9 +604,14 @@ mod tests { value: 100, inner_struct: SomeSerializableStruct { foo: 101 }, }, - }; + } + } + #[test] + fn should_serialize() { + let input = get_my_struct(); let registry = get_registry(); + let serializer = ReflectSerializer::new(&input, ®istry); let config = PrettyConfig::default() @@ -776,38 +779,7 @@ mod tests { #[test] fn should_serialize_non_self_describing_binary() { - let mut map = HashMap::new(); - map.insert(64, 32); - - let input = MyStruct { - primitive_value: 123, - option_value: Some(String::from("Hello world!")), - option_value_complex: Some(SomeStruct { foo: 123 }), - tuple_value: (PI, 1337), - list_value: vec![-2, -1, 0, 1, 2], - array_value: [-2, -1, 0, 1, 2], - map_value: map, - struct_value: SomeStruct { foo: 999999999 }, - tuple_struct_value: SomeTupleStruct(String::from("Tuple Struct")), - unit_struct: SomeUnitStruct, - unit_enum: SomeEnum::Unit, - newtype_enum: SomeEnum::NewType(123), - tuple_enum: SomeEnum::Tuple(1.23, 3.21), - struct_enum: SomeEnum::Struct { - foo: String::from("Struct variant value"), - }, - ignored_struct: SomeIgnoredStruct { ignored: 123 }, - ignored_tuple_struct: SomeIgnoredTupleStruct(123), - ignored_struct_variant: SomeIgnoredEnum::Struct { - foo: String::from("Struct Variant"), - }, - ignored_tuple_variant: SomeIgnoredEnum::Tuple(1.23, 3.45), - custom_serialize: CustomSerialize { - value: 100, - inner_struct: SomeSerializableStruct { foo: 101 }, - }, - }; - + let input = get_my_struct(); let registry = get_registry(); let serializer = ReflectSerializer::new(&input, ®istry); @@ -834,38 +806,7 @@ mod tests { #[test] fn should_serialize_self_describing_binary() { - let mut map = HashMap::new(); - map.insert(64, 32); - - let input = MyStruct { - primitive_value: 123, - option_value: Some(String::from("Hello world!")), - option_value_complex: Some(SomeStruct { foo: 123 }), - tuple_value: (PI, 1337), - list_value: vec![-2, -1, 0, 1, 2], - array_value: [-2, -1, 0, 1, 2], - map_value: map, - struct_value: SomeStruct { foo: 999999999 }, - tuple_struct_value: SomeTupleStruct(String::from("Tuple Struct")), - unit_struct: SomeUnitStruct, - unit_enum: SomeEnum::Unit, - newtype_enum: SomeEnum::NewType(123), - tuple_enum: SomeEnum::Tuple(1.23, 3.21), - struct_enum: SomeEnum::Struct { - foo: String::from("Struct variant value"), - }, - ignored_struct: SomeIgnoredStruct { ignored: 123 }, - ignored_tuple_struct: SomeIgnoredTupleStruct(123), - ignored_struct_variant: SomeIgnoredEnum::Struct { - foo: String::from("Struct Variant"), - }, - ignored_tuple_variant: SomeIgnoredEnum::Tuple(1.23, 3.45), - custom_serialize: CustomSerialize { - value: 100, - inner_struct: SomeSerializableStruct { foo: 101 }, - }, - }; - + let input = get_my_struct(); let registry = get_registry(); let serializer = ReflectSerializer::new(&input, ®istry); diff --git a/crates/bevy_reflect_compile_fail_tests/tests/reflect_derive/generics.fail.stderr b/crates/bevy_reflect_compile_fail_tests/tests/reflect_derive/generics.fail.stderr index 22a6cc8d53ccf..3f1dca9e6a20f 100644 --- a/crates/bevy_reflect_compile_fail_tests/tests/reflect_derive/generics.fail.stderr +++ b/crates/bevy_reflect_compile_fail_tests/tests/reflect_derive/generics.fail.stderr @@ -26,7 +26,7 @@ error[E0277]: the trait bound `NoReflect: GetTypeRegistration` is not satisfied --> tests/reflect_derive/generics.fail.rs:14:36 | 14 | let mut foo: Box = Box::new(Foo:: { a: NoReflect(42.0) }); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `GetTypeRegistration` is not implemented for `NoReflect` + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `GetTypeRegistration` is not implemented for `NoReflect`, which is required by `Foo: bevy_reflect::Struct` | = help: the following other types implement trait `GetTypeRegistration`: bool diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index e981506a9d3fc..49079ed55802b 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -42,6 +42,7 @@ bevy_asset = { path = "../bevy_asset", version = "0.14.0-dev" } bevy_color = { path = "../bevy_color", version = "0.14.0-dev" } bevy_core = { path = "../bevy_core", version = "0.14.0-dev" } bevy_derive = { path = "../bevy_derive", version = "0.14.0-dev" } +bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.14.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" } bevy_encase_derive = { path = "../bevy_encase_derive", version = "0.14.0-dev" } bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.14.0-dev" } diff --git a/crates/bevy_render/macros/src/as_bind_group.rs b/crates/bevy_render/macros/src/as_bind_group.rs index b1c7a12e0b0bc..06ce6f7982810 100644 --- a/crates/bevy_render/macros/src/as_bind_group.rs +++ b/crates/bevy_render/macros/src/as_bind_group.rs @@ -272,7 +272,8 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { let fallback_image = get_fallback_image(&render_path, dimension); - binding_impls.push(quote! { + // insert fallible texture-based entries at 0 so that if we fail here, we exit before allocating any buffers + binding_impls.insert(0, quote! { ( #binding_index, #render_path::render_resource::OwnedBindingResource::TextureView({ let handle: Option<&#asset_path::Handle<#render_path::texture::Image>> = (&self.#field_name).into(); @@ -311,7 +312,8 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { let fallback_image = get_fallback_image(&render_path, *dimension); - binding_impls.push(quote! { + // insert fallible texture-based entries at 0 so that if we fail here, we exit before allocating any buffers + binding_impls.insert(0, quote! { ( #binding_index, #render_path::render_resource::OwnedBindingResource::TextureView({ @@ -353,7 +355,8 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { let fallback_image = get_fallback_image(&render_path, *dimension); - binding_impls.push(quote! { + // insert fallible texture-based entries at 0 so that if we fail here, we exit before allocating any buffers + binding_impls.insert(0, quote! { ( #binding_index, #render_path::render_resource::OwnedBindingResource::Sampler({ diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index ca22a773dc767..728a863b703cf 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -88,7 +88,7 @@ pub struct ComputedCameraValues { /// /// #[derive(Component, Clone, Copy, Reflect)] -#[reflect_value(Component)] +#[reflect_value(Component, Default)] pub struct Exposure { /// pub ev100: f32, @@ -184,7 +184,7 @@ impl Default for PhysicalCameraParameters { /// Adding a camera is typically done by adding a bundle, either the `Camera2dBundle` or the /// `Camera3dBundle`. #[derive(Component, Debug, Reflect, Clone)] -#[reflect(Component)] +#[reflect(Component, Default)] pub struct Camera { /// If set, this camera will render to the given [`Viewport`] rectangle within the configured [`RenderTarget`]. pub viewport: Option, @@ -771,7 +771,7 @@ pub fn camera_system( /// This component lets you control the [`TextureUsages`] field of the main texture generated for the camera #[derive(Component, ExtractComponent, Clone, Copy, Reflect)] -#[reflect_value(Component)] +#[reflect_value(Component, Default)] pub struct CameraMainTextureUsages(pub TextureUsages); impl Default for CameraMainTextureUsages { fn default() -> Self { diff --git a/crates/bevy_render/src/camera/clear_color.rs b/crates/bevy_render/src/camera/clear_color.rs index 02b74ce46c1a3..e986b3d30b29a 100644 --- a/crates/bevy_render/src/camera/clear_color.rs +++ b/crates/bevy_render/src/camera/clear_color.rs @@ -2,12 +2,12 @@ use crate::extract_resource::ExtractResource; use bevy_color::Color; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::prelude::*; -use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; +use bevy_reflect::prelude::*; use serde::{Deserialize, Serialize}; /// For a camera, specifies the color used to clear the viewport before rendering. #[derive(Reflect, Serialize, Deserialize, Clone, Debug, Default)] -#[reflect(Serialize, Deserialize)] +#[reflect(Serialize, Deserialize, Default)] pub enum ClearColorConfig { /// The clear color is taken from the world's [`ClearColor`] resource. #[default] @@ -31,7 +31,7 @@ impl From for ClearColorConfig { /// This color appears as the "background" color for simple apps, /// when there are portions of the screen with nothing rendered. #[derive(Resource, Clone, Debug, Deref, DerefMut, ExtractResource, Reflect)] -#[reflect(Resource)] +#[reflect(Resource, Default)] pub struct ClearColor(pub Color); /// Match the dark gray bevy website code block color by default. diff --git a/crates/bevy_render/src/diagnostic/internal.rs b/crates/bevy_render/src/diagnostic/internal.rs new file mode 100644 index 0000000000000..85cefccf5d9c2 --- /dev/null +++ b/crates/bevy_render/src/diagnostic/internal.rs @@ -0,0 +1,650 @@ +use std::{ + borrow::Cow, + ops::{DerefMut, Range}, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, + thread::{self, ThreadId}, +}; + +use bevy_diagnostic::{Diagnostic, DiagnosticMeasurement, DiagnosticPath, DiagnosticsStore}; +use bevy_ecs::system::{Res, ResMut, Resource}; +use bevy_utils::{tracing, Instant}; +use std::sync::Mutex; +use wgpu::{ + Buffer, BufferDescriptor, BufferUsages, CommandEncoder, ComputePass, Features, MapMode, + PipelineStatisticsTypes, QuerySet, QuerySetDescriptor, QueryType, Queue, RenderPass, +}; + +use crate::renderer::RenderDevice; + +use super::RecordDiagnostics; + +// buffer offset must be divisible by 256, so this constant must be divisible by 32 (=256/8) +const MAX_TIMESTAMP_QUERIES: u32 = 256; +const MAX_PIPELINE_STATISTICS: u32 = 128; + +const TIMESTAMP_SIZE: u64 = 8; +const PIPELINE_STATISTICS_SIZE: u64 = 40; + +/// Records diagnostics into [`QuerySet`]'s keeping track of the mapping between +/// spans and indices to the corresponding entries in the [`QuerySet`]. +#[derive(Resource)] +pub struct DiagnosticsRecorder { + timestamp_period_ns: f32, + features: Features, + current_frame: Mutex, + submitted_frames: Vec, + finished_frames: Vec, +} + +impl DiagnosticsRecorder { + /// Creates the new `DiagnosticsRecorder`. + pub fn new(device: &RenderDevice, queue: &Queue) -> DiagnosticsRecorder { + let features = device.features(); + + let timestamp_period_ns = if features.contains(Features::TIMESTAMP_QUERY) { + queue.get_timestamp_period() + } else { + 0.0 + }; + + DiagnosticsRecorder { + timestamp_period_ns, + features, + current_frame: Mutex::new(FrameData::new(device, features)), + submitted_frames: Vec::new(), + finished_frames: Vec::new(), + } + } + + fn current_frame_mut(&mut self) -> &mut FrameData { + self.current_frame.get_mut().expect("lock poisoned") + } + + fn current_frame_lock(&self) -> impl DerefMut + '_ { + self.current_frame.lock().expect("lock poisoned") + } + + /// Begins recording diagnostics for a new frame. + pub fn begin_frame(&mut self) { + let mut idx = 0; + while idx < self.submitted_frames.len() { + if self.submitted_frames[idx].run_mapped_callback(self.timestamp_period_ns) { + self.finished_frames + .push(self.submitted_frames.swap_remove(idx)); + } else { + idx += 1; + } + } + + self.current_frame_mut().begin(); + } + + /// Copies data from [`QuerySet`]'s to a [`Buffer`], after which it can be downloaded to CPU. + /// + /// Should be called before [`DiagnosticsRecorder::finish_frame`] + pub fn resolve(&mut self, encoder: &mut CommandEncoder) { + self.current_frame_mut().resolve(encoder); + } + + /// Finishes recording diagnostics for the current frame. + /// + /// The specified `callback` will be invoked when diagnostics become available. + /// + /// Should be called after [`DiagnosticsRecorder::resolve`], + /// and **after** all commands buffers have been queued. + pub fn finish_frame( + &mut self, + device: &RenderDevice, + callback: impl FnOnce(RenderDiagnostics) + Send + Sync + 'static, + ) { + self.current_frame_mut().finish(callback); + + // reuse one of the finished frames, if we can + let new_frame = match self.finished_frames.pop() { + Some(frame) => frame, + None => FrameData::new(device, self.features), + }; + + let old_frame = std::mem::replace(self.current_frame_mut(), new_frame); + self.submitted_frames.push(old_frame); + } +} + +impl RecordDiagnostics for DiagnosticsRecorder { + fn begin_time_span(&self, encoder: &mut E, span_name: Cow<'static, str>) { + self.current_frame_lock() + .begin_time_span(encoder, span_name); + } + + fn end_time_span(&self, encoder: &mut E) { + self.current_frame_lock().end_time_span(encoder); + } + + fn begin_pass_span(&self, pass: &mut P, span_name: Cow<'static, str>) { + self.current_frame_lock().begin_pass(pass, span_name); + } + + fn end_pass_span(&self, pass: &mut P) { + self.current_frame_lock().end_pass(pass); + } +} + +struct SpanRecord { + thread_id: ThreadId, + path_range: Range, + pass_kind: Option, + begin_timestamp_index: Option, + end_timestamp_index: Option, + begin_instant: Option, + end_instant: Option, + pipeline_statistics_index: Option, +} + +struct FrameData { + timestamps_query_set: Option, + num_timestamps: u32, + supports_timestamps_inside_passes: bool, + pipeline_statistics_query_set: Option, + num_pipeline_statistics: u32, + buffer_size: u64, + pipeline_statistics_buffer_offset: u64, + resolve_buffer: Option, + read_buffer: Option, + path_components: Vec>, + open_spans: Vec, + closed_spans: Vec, + is_mapped: Arc, + callback: Option>, +} + +impl FrameData { + fn new(device: &RenderDevice, features: Features) -> FrameData { + let wgpu_device = device.wgpu_device(); + let mut buffer_size = 0; + + let timestamps_query_set = if features.contains(Features::TIMESTAMP_QUERY) { + buffer_size += u64::from(MAX_TIMESTAMP_QUERIES) * TIMESTAMP_SIZE; + Some(wgpu_device.create_query_set(&QuerySetDescriptor { + label: Some("timestamps_query_set"), + ty: QueryType::Timestamp, + count: MAX_TIMESTAMP_QUERIES, + })) + } else { + None + }; + + let pipeline_statistics_buffer_offset = buffer_size; + + let pipeline_statistics_query_set = + if features.contains(Features::PIPELINE_STATISTICS_QUERY) { + buffer_size += u64::from(MAX_PIPELINE_STATISTICS) * PIPELINE_STATISTICS_SIZE; + Some(wgpu_device.create_query_set(&QuerySetDescriptor { + label: Some("pipeline_statistics_query_set"), + ty: QueryType::PipelineStatistics(PipelineStatisticsTypes::all()), + count: MAX_PIPELINE_STATISTICS, + })) + } else { + None + }; + + let (resolve_buffer, read_buffer) = if buffer_size > 0 { + let resolve_buffer = wgpu_device.create_buffer(&BufferDescriptor { + label: Some("render_statistics_resolve_buffer"), + size: buffer_size, + usage: BufferUsages::QUERY_RESOLVE | BufferUsages::COPY_SRC, + mapped_at_creation: false, + }); + let read_buffer = wgpu_device.create_buffer(&BufferDescriptor { + label: Some("render_statistics_read_buffer"), + size: buffer_size, + usage: BufferUsages::COPY_DST | BufferUsages::MAP_READ, + mapped_at_creation: false, + }); + (Some(resolve_buffer), Some(read_buffer)) + } else { + (None, None) + }; + + FrameData { + timestamps_query_set, + num_timestamps: 0, + supports_timestamps_inside_passes: features + .contains(Features::TIMESTAMP_QUERY_INSIDE_PASSES), + pipeline_statistics_query_set, + num_pipeline_statistics: 0, + buffer_size, + pipeline_statistics_buffer_offset, + resolve_buffer, + read_buffer, + path_components: Vec::new(), + open_spans: Vec::new(), + closed_spans: Vec::new(), + is_mapped: Arc::new(AtomicBool::new(false)), + callback: None, + } + } + + fn begin(&mut self) { + self.num_timestamps = 0; + self.num_pipeline_statistics = 0; + self.path_components.clear(); + self.open_spans.clear(); + self.closed_spans.clear(); + } + + fn write_timestamp( + &mut self, + encoder: &mut impl WriteTimestamp, + is_inside_pass: bool, + ) -> Option { + if is_inside_pass && !self.supports_timestamps_inside_passes { + return None; + } + + if self.num_timestamps >= MAX_TIMESTAMP_QUERIES { + return None; + } + + let set = self.timestamps_query_set.as_ref()?; + let index = self.num_timestamps; + encoder.write_timestamp(set, index); + self.num_timestamps += 1; + Some(index) + } + + fn write_pipeline_statistics( + &mut self, + encoder: &mut impl WritePipelineStatistics, + ) -> Option { + if self.num_pipeline_statistics >= MAX_PIPELINE_STATISTICS { + return None; + } + + let set = self.pipeline_statistics_query_set.as_ref()?; + let index = self.num_pipeline_statistics; + encoder.begin_pipeline_statistics_query(set, index); + self.num_pipeline_statistics += 1; + Some(index) + } + + fn open_span( + &mut self, + pass_kind: Option, + name: Cow<'static, str>, + ) -> &mut SpanRecord { + let thread_id = thread::current().id(); + + let parent = self + .open_spans + .iter() + .filter(|v| v.thread_id == thread_id) + .last(); + + let path_range = match &parent { + Some(parent) if parent.path_range.end == self.path_components.len() => { + parent.path_range.start..parent.path_range.end + 1 + } + Some(parent) => { + self.path_components + .extend_from_within(parent.path_range.clone()); + self.path_components.len() - parent.path_range.len()..self.path_components.len() + 1 + } + None => self.path_components.len()..self.path_components.len() + 1, + }; + + self.path_components.push(name); + + self.open_spans.push(SpanRecord { + thread_id, + path_range, + pass_kind, + begin_timestamp_index: None, + end_timestamp_index: None, + begin_instant: None, + end_instant: None, + pipeline_statistics_index: None, + }); + + self.open_spans.last_mut().unwrap() + } + + fn close_span(&mut self) -> &mut SpanRecord { + let thread_id = thread::current().id(); + + let iter = self.open_spans.iter(); + let (index, _) = iter + .enumerate() + .filter(|(_, v)| v.thread_id == thread_id) + .last() + .unwrap(); + + let span = self.open_spans.swap_remove(index); + self.closed_spans.push(span); + self.closed_spans.last_mut().unwrap() + } + + fn begin_time_span(&mut self, encoder: &mut impl WriteTimestamp, name: Cow<'static, str>) { + let begin_instant = Instant::now(); + let begin_timestamp_index = self.write_timestamp(encoder, false); + + let span = self.open_span(None, name); + span.begin_instant = Some(begin_instant); + span.begin_timestamp_index = begin_timestamp_index; + } + + fn end_time_span(&mut self, encoder: &mut impl WriteTimestamp) { + let end_timestamp_index = self.write_timestamp(encoder, false); + + let span = self.close_span(); + span.end_timestamp_index = end_timestamp_index; + span.end_instant = Some(Instant::now()); + } + + fn begin_pass(&mut self, pass: &mut P, name: Cow<'static, str>) { + let begin_instant = Instant::now(); + + let begin_timestamp_index = self.write_timestamp(pass, true); + let pipeline_statistics_index = self.write_pipeline_statistics(pass); + + let span = self.open_span(Some(P::KIND), name); + span.begin_instant = Some(begin_instant); + span.begin_timestamp_index = begin_timestamp_index; + span.pipeline_statistics_index = pipeline_statistics_index; + } + + fn end_pass(&mut self, pass: &mut impl Pass) { + let end_timestamp_index = self.write_timestamp(pass, true); + + let span = self.close_span(); + span.end_timestamp_index = end_timestamp_index; + + if span.pipeline_statistics_index.is_some() { + pass.end_pipeline_statistics_query(); + } + + span.end_instant = Some(Instant::now()); + } + + fn resolve(&mut self, encoder: &mut CommandEncoder) { + let Some(resolve_buffer) = &self.resolve_buffer else { + return; + }; + + match &self.timestamps_query_set { + Some(set) if self.num_timestamps > 0 => { + encoder.resolve_query_set(set, 0..self.num_timestamps, resolve_buffer, 0); + } + _ => {} + } + + match &self.pipeline_statistics_query_set { + Some(set) if self.num_pipeline_statistics > 0 => { + encoder.resolve_query_set( + set, + 0..self.num_pipeline_statistics, + resolve_buffer, + self.pipeline_statistics_buffer_offset, + ); + } + _ => {} + } + + let Some(read_buffer) = &self.read_buffer else { + return; + }; + + encoder.copy_buffer_to_buffer(resolve_buffer, 0, read_buffer, 0, self.buffer_size); + } + + fn diagnostic_path(&self, range: &Range, field: &str) -> DiagnosticPath { + DiagnosticPath::from_components( + std::iter::once("render") + .chain(self.path_components[range.clone()].iter().map(|v| &**v)) + .chain(std::iter::once(field)), + ) + } + + fn finish(&mut self, callback: impl FnOnce(RenderDiagnostics) + Send + Sync + 'static) { + let Some(read_buffer) = &self.read_buffer else { + // we still have cpu timings, so let's use them + + let mut diagnostics = Vec::new(); + + for span in &self.closed_spans { + if let (Some(begin), Some(end)) = (span.begin_instant, span.end_instant) { + diagnostics.push(RenderDiagnostic { + path: self.diagnostic_path(&span.path_range, "elapsed_cpu"), + suffix: "ms", + value: (end - begin).as_secs_f64() * 1000.0, + }); + } + } + + callback(RenderDiagnostics(diagnostics)); + return; + }; + + self.callback = Some(Box::new(callback)); + + let is_mapped = self.is_mapped.clone(); + read_buffer.slice(..).map_async(MapMode::Read, move |res| { + if let Err(e) = res { + tracing::warn!("Failed to download render statistics buffer: {e}"); + return; + } + + is_mapped.store(true, Ordering::Release); + }); + } + + // returns true if the frame is considered finished, false otherwise + fn run_mapped_callback(&mut self, timestamp_period_ns: f32) -> bool { + let Some(read_buffer) = &self.read_buffer else { + return true; + }; + if !self.is_mapped.load(Ordering::Acquire) { + // need to wait more + return false; + } + let Some(callback) = self.callback.take() else { + return true; + }; + + let data = read_buffer.slice(..).get_mapped_range(); + + let timestamps = data[..(self.num_timestamps * 8) as usize] + .chunks(8) + .map(|v| u64::from_ne_bytes(v.try_into().unwrap())) + .collect::>(); + + let start = self.pipeline_statistics_buffer_offset as usize; + let len = (self.num_pipeline_statistics as usize) * 40; + let pipeline_statistics = data[start..start + len] + .chunks(8) + .map(|v| u64::from_ne_bytes(v.try_into().unwrap())) + .collect::>(); + + let mut diagnostics = Vec::new(); + + for span in &self.closed_spans { + if let (Some(begin), Some(end)) = (span.begin_instant, span.end_instant) { + diagnostics.push(RenderDiagnostic { + path: self.diagnostic_path(&span.path_range, "elapsed_cpu"), + suffix: "ms", + value: (end - begin).as_secs_f64() * 1000.0, + }); + } + + if let (Some(begin), Some(end)) = (span.begin_timestamp_index, span.end_timestamp_index) + { + let begin = timestamps[begin as usize] as f64; + let end = timestamps[end as usize] as f64; + let value = (end - begin) * (timestamp_period_ns as f64) / 1e6; + + diagnostics.push(RenderDiagnostic { + path: self.diagnostic_path(&span.path_range, "elapsed_gpu"), + suffix: "ms", + value, + }); + } + + if let Some(index) = span.pipeline_statistics_index { + let index = (index as usize) * 5; + + if span.pass_kind == Some(PassKind::Render) { + diagnostics.push(RenderDiagnostic { + path: self.diagnostic_path(&span.path_range, "vertex_shader_invocations"), + suffix: "", + value: pipeline_statistics[index] as f64, + }); + + diagnostics.push(RenderDiagnostic { + path: self.diagnostic_path(&span.path_range, "clipper_invocations"), + suffix: "", + value: pipeline_statistics[index + 1] as f64, + }); + + diagnostics.push(RenderDiagnostic { + path: self.diagnostic_path(&span.path_range, "clipper_primitives_out"), + suffix: "", + value: pipeline_statistics[index + 2] as f64, + }); + + diagnostics.push(RenderDiagnostic { + path: self.diagnostic_path(&span.path_range, "fragment_shader_invocations"), + suffix: "", + value: pipeline_statistics[index + 3] as f64, + }); + } + + if span.pass_kind == Some(PassKind::Compute) { + diagnostics.push(RenderDiagnostic { + path: self.diagnostic_path(&span.path_range, "compute_shader_invocations"), + suffix: "", + value: pipeline_statistics[index + 4] as f64, + }); + } + } + } + + callback(RenderDiagnostics(diagnostics)); + + drop(data); + read_buffer.unmap(); + self.is_mapped.store(false, Ordering::Release); + + true + } +} + +/// Resource which stores render diagnostics of the most recent frame. +#[derive(Debug, Default, Clone, Resource)] +pub struct RenderDiagnostics(Vec); + +/// A render diagnostic which has been recorded, but not yet stored in [`DiagnosticsStore`]. +#[derive(Debug, Clone, Resource)] +pub struct RenderDiagnostic { + pub path: DiagnosticPath, + pub suffix: &'static str, + pub value: f64, +} + +/// Stores render diagnostics before they can be synced with the main app. +/// +/// This mutex is locked twice per frame: +/// 1. in `PreUpdate`, during [`sync_diagnostics`], +/// 2. after rendering has finished and statistics have been downloaded from GPU. +#[derive(Debug, Default, Clone, Resource)] +pub struct RenderDiagnosticsMutex(pub(crate) Arc>>); + +/// Updates render diagnostics measurements. +pub fn sync_diagnostics(mutex: Res, mut store: ResMut) { + let Some(diagnostics) = mutex.0.lock().ok().and_then(|mut v| v.take()) else { + return; + }; + + let time = Instant::now(); + + for diagnostic in &diagnostics.0 { + if store.get(&diagnostic.path).is_none() { + store.add(Diagnostic::new(diagnostic.path.clone()).with_suffix(diagnostic.suffix)); + } + + store + .get_mut(&diagnostic.path) + .unwrap() + .add_measurement(DiagnosticMeasurement { + time, + value: diagnostic.value, + }); + } +} + +pub trait WriteTimestamp { + fn write_timestamp(&mut self, query_set: &QuerySet, index: u32); +} + +impl WriteTimestamp for CommandEncoder { + fn write_timestamp(&mut self, query_set: &QuerySet, index: u32) { + CommandEncoder::write_timestamp(self, query_set, index); + } +} + +impl WriteTimestamp for RenderPass<'_> { + fn write_timestamp(&mut self, query_set: &QuerySet, index: u32) { + RenderPass::write_timestamp(self, query_set, index); + } +} + +impl WriteTimestamp for ComputePass<'_> { + fn write_timestamp(&mut self, query_set: &QuerySet, index: u32) { + ComputePass::write_timestamp(self, query_set, index); + } +} + +pub trait WritePipelineStatistics { + fn begin_pipeline_statistics_query(&mut self, query_set: &QuerySet, index: u32); + + fn end_pipeline_statistics_query(&mut self); +} + +impl WritePipelineStatistics for RenderPass<'_> { + fn begin_pipeline_statistics_query(&mut self, query_set: &QuerySet, index: u32) { + RenderPass::begin_pipeline_statistics_query(self, query_set, index); + } + + fn end_pipeline_statistics_query(&mut self) { + RenderPass::end_pipeline_statistics_query(self); + } +} + +impl WritePipelineStatistics for ComputePass<'_> { + fn begin_pipeline_statistics_query(&mut self, query_set: &QuerySet, index: u32) { + ComputePass::begin_pipeline_statistics_query(self, query_set, index); + } + + fn end_pipeline_statistics_query(&mut self) { + ComputePass::end_pipeline_statistics_query(self); + } +} + +pub trait Pass: WritePipelineStatistics + WriteTimestamp { + const KIND: PassKind; +} + +impl Pass for RenderPass<'_> { + const KIND: PassKind = PassKind::Render; +} + +impl Pass for ComputePass<'_> { + const KIND: PassKind = PassKind::Compute; +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub enum PassKind { + Render, + Compute, +} diff --git a/crates/bevy_render/src/diagnostic/mod.rs b/crates/bevy_render/src/diagnostic/mod.rs new file mode 100644 index 0000000000000..af6e4a1c3784c --- /dev/null +++ b/crates/bevy_render/src/diagnostic/mod.rs @@ -0,0 +1,185 @@ +//! Infrastructure for recording render diagnostics. +//! +//! For more info, see [`RenderDiagnosticsPlugin`]. + +pub(crate) mod internal; + +use std::{borrow::Cow, marker::PhantomData, sync::Arc}; + +use bevy_app::{App, Plugin, PreUpdate}; + +use crate::RenderApp; + +use self::internal::{ + sync_diagnostics, DiagnosticsRecorder, Pass, RenderDiagnosticsMutex, WriteTimestamp, +}; + +use super::{RenderDevice, RenderQueue}; + +/// Enables collecting render diagnostics, such as CPU/GPU elapsed time per render pass, +/// as well as pipeline statistics (number of primitives, number of shader invocations, etc). +/// +/// To access the diagnostics, you can use [`DiagnosticsStore`](bevy_diagnostic::DiagnosticsStore) resource, +/// or add [`LogDiagnosticsPlugin`](bevy_diagnostic::LogDiagnosticsPlugin). +/// +/// To record diagnostics in your own passes: +/// 1. First, obtain the diagnostic recorder using [`RenderContext::diagnostic_recorder`](crate::renderer::RenderContext::diagnostic_recorder). +/// +/// It won't do anything unless [`RenderDiagnosticsPlugin`] is present, +/// so you're free to omit `#[cfg]` clauses. +/// ```ignore +/// let diagnostics = render_context.diagnostic_recorder(); +/// ``` +/// 2. Begin the span inside a command encoder, or a render/compute pass encoder. +/// ```ignore +/// let time_span = diagnostics.time_span(render_context.command_encoder(), "shadows"); +/// ``` +/// 3. End the span, providing the same encoder. +/// ```ignore +/// time_span.end(render_context.command_encoder()); +/// ``` +/// +/// # Supported platforms +/// Timestamp queries and pipeline statistics are currently supported only on Vulkan and DX12. +/// On other platforms (Metal, WebGPU, WebGL2) only CPU time will be recorded. +#[allow(clippy::doc_markdown)] +#[derive(Default)] +pub struct RenderDiagnosticsPlugin; + +impl Plugin for RenderDiagnosticsPlugin { + fn build(&self, app: &mut App) { + let render_diagnostics_mutex = RenderDiagnosticsMutex::default(); + app.insert_resource(render_diagnostics_mutex.clone()) + .add_systems(PreUpdate, sync_diagnostics); + + if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { + render_app.insert_resource(render_diagnostics_mutex); + } + } + + fn finish(&self, app: &mut App) { + let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + let device = render_app.world.resource::(); + let queue = render_app.world.resource::(); + render_app.insert_resource(DiagnosticsRecorder::new(device, queue)); + } +} + +/// Allows recording diagnostic spans. +pub trait RecordDiagnostics: Send + Sync { + /// Begin a time span, which will record elapsed CPU and GPU time. + /// + /// Returns a guard, which will panic on drop unless you end the span. + fn time_span(&self, encoder: &mut E, name: N) -> TimeSpanGuard<'_, Self, E> + where + E: WriteTimestamp, + N: Into>, + { + self.begin_time_span(encoder, name.into()); + TimeSpanGuard { + recorder: self, + marker: PhantomData, + } + } + + /// Begin a pass span, which will record elapsed CPU and GPU time, + /// as well as pipeline statistics on supported platforms. + /// + /// Returns a guard, which will panic on drop unless you end the span. + fn pass_span(&self, pass: &mut P, name: N) -> PassSpanGuard<'_, Self, P> + where + P: Pass, + N: Into>, + { + self.begin_pass_span(pass, name.into()); + PassSpanGuard { + recorder: self, + marker: PhantomData, + } + } + + #[doc(hidden)] + fn begin_time_span(&self, encoder: &mut E, name: Cow<'static, str>); + + #[doc(hidden)] + fn end_time_span(&self, encoder: &mut E); + + #[doc(hidden)] + fn begin_pass_span(&self, pass: &mut P, name: Cow<'static, str>); + + #[doc(hidden)] + fn end_pass_span(&self, pass: &mut P); +} + +/// Guard returned by [`RecordDiagnostics::time_span`]. +/// +/// Will panic on drop unless [`TimeSpanGuard::end`] is called. +pub struct TimeSpanGuard<'a, R: ?Sized, E> { + recorder: &'a R, + marker: PhantomData, +} + +impl TimeSpanGuard<'_, R, E> { + /// End the span. You have to provide the same encoder which was used to begin the span. + pub fn end(self, encoder: &mut E) { + self.recorder.end_time_span(encoder); + std::mem::forget(self); + } +} + +impl Drop for TimeSpanGuard<'_, R, E> { + fn drop(&mut self) { + panic!("TimeSpanScope::end was never called") + } +} + +/// Guard returned by [`RecordDiagnostics::pass_span`]. +/// +/// Will panic on drop unless [`PassSpanGuard::end`] is called. +pub struct PassSpanGuard<'a, R: ?Sized, P> { + recorder: &'a R, + marker: PhantomData

, +} + +impl PassSpanGuard<'_, R, P> { + /// End the span. You have to provide the same encoder which was used to begin the span. + pub fn end(self, pass: &mut P) { + self.recorder.end_pass_span(pass); + std::mem::forget(self); + } +} + +impl Drop for PassSpanGuard<'_, R, P> { + fn drop(&mut self) { + panic!("PassSpanScope::end was never called") + } +} + +impl RecordDiagnostics for Option> { + fn begin_time_span(&self, encoder: &mut E, name: Cow<'static, str>) { + if let Some(recorder) = &self { + recorder.begin_time_span(encoder, name); + } + } + + fn end_time_span(&self, encoder: &mut E) { + if let Some(recorder) = &self { + recorder.end_time_span(encoder); + } + } + + fn begin_pass_span(&self, pass: &mut P, name: Cow<'static, str>) { + if let Some(recorder) = &self { + recorder.begin_pass_span(pass, name); + } + } + + fn end_pass_span(&self, pass: &mut P) { + if let Some(recorder) = &self { + recorder.end_pass_span(pass); + } + } +} diff --git a/crates/bevy_render/src/globals.rs b/crates/bevy_render/src/globals.rs index d1a7df0b5e31a..11f0c5295efff 100644 --- a/crates/bevy_render/src/globals.rs +++ b/crates/bevy_render/src/globals.rs @@ -9,7 +9,7 @@ use bevy_app::{App, Plugin}; use bevy_asset::{load_internal_asset, Handle}; use bevy_core::FrameCount; use bevy_ecs::prelude::*; -use bevy_reflect::Reflect; +use bevy_reflect::prelude::*; use bevy_time::Time; pub const GLOBALS_TYPE_HANDLE: Handle = Handle::weak_from_u128(17924628719070609599); @@ -45,7 +45,7 @@ fn extract_time(mut commands: Commands, time: Extract>) { /// Contains global values useful when writing shaders. /// Currently only contains values related to time. #[derive(Default, Clone, Resource, ExtractResource, Reflect, ShaderType)] -#[reflect(Resource)] +#[reflect(Resource, Default)] pub struct GlobalsUniform { /// The time since startup in seconds. /// Wraps to 0 after 1 hour. diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index dd0a04b0882f2..290b2cabcea73 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -11,6 +11,7 @@ pub mod alpha; pub mod batching; pub mod camera; pub mod deterministic; +pub mod diagnostic; pub mod extract_component; pub mod extract_instances; mod extract_param; diff --git a/crates/bevy_render/src/mesh/mesh/skinning.rs b/crates/bevy_render/src/mesh/mesh/skinning.rs index 616f2a5472abf..f1605ec74b735 100644 --- a/crates/bevy_render/src/mesh/mesh/skinning.rs +++ b/crates/bevy_render/src/mesh/mesh/skinning.rs @@ -6,11 +6,11 @@ use bevy_ecs::{ reflect::ReflectMapEntities, }; use bevy_math::Mat4; -use bevy_reflect::{Reflect, TypePath}; +use bevy_reflect::prelude::*; use std::ops::Deref; #[derive(Component, Debug, Default, Clone, Reflect)] -#[reflect(Component, MapEntities)] +#[reflect(Component, MapEntities, Default)] pub struct SkinnedMesh { pub inverse_bindposes: Handle, pub joints: Vec, diff --git a/crates/bevy_render/src/mesh/morph.rs b/crates/bevy_render/src/mesh/morph.rs index 74430beffc7c8..b5eac7cdfec63 100644 --- a/crates/bevy_render/src/mesh/morph.rs +++ b/crates/bevy_render/src/mesh/morph.rs @@ -9,7 +9,7 @@ use bevy_asset::Handle; use bevy_ecs::prelude::*; use bevy_hierarchy::Children; use bevy_math::Vec3; -use bevy_reflect::Reflect; +use bevy_reflect::prelude::*; use bytemuck::{Pod, Zeroable}; use std::{iter, mem}; use thiserror::Error; @@ -128,7 +128,7 @@ impl MorphTargetImage { /// /// [morph targets]: https://en.wikipedia.org/wiki/Morph_target_animation #[derive(Reflect, Default, Debug, Clone, Component)] -#[reflect(Debug, Component)] +#[reflect(Debug, Component, Default)] pub struct MorphWeights { weights: Vec, /// The first mesh primitive assigned to these weights @@ -173,7 +173,7 @@ impl MorphWeights { /// /// [morph targets]: https://en.wikipedia.org/wiki/Morph_target_animation #[derive(Reflect, Default, Debug, Clone, Component)] -#[reflect(Debug, Component)] +#[reflect(Debug, Component, Default)] pub struct MeshMorphWeights { weights: Vec, } diff --git a/crates/bevy_render/src/mesh/primitives/dim3/sphere.rs b/crates/bevy_render/src/mesh/primitives/dim3/sphere.rs index 463f77025c169..3c3c5755ad8dd 100644 --- a/crates/bevy_render/src/mesh/primitives/dim3/sphere.rs +++ b/crates/bevy_render/src/mesh/primitives/dim3/sphere.rs @@ -25,7 +25,7 @@ pub enum IcosphereError { /// A type of sphere mesh. #[derive(Clone, Copy, Debug)] pub enum SphereKind { - /// An icosphere, a spherical mesh that consists of equally sized triangles. + /// An icosphere, a spherical mesh that consists of similar sized triangles. Ico { /// The number of subdivisions applied. /// The number of faces quadruples with each subdivision. diff --git a/crates/bevy_render/src/primitives/mod.rs b/crates/bevy_render/src/primitives/mod.rs index 25fef4abd2c09..d5796b4a7ff70 100644 --- a/crates/bevy_render/src/primitives/mod.rs +++ b/crates/bevy_render/src/primitives/mod.rs @@ -2,7 +2,7 @@ use std::borrow::Borrow; use bevy_ecs::{component::Component, entity::EntityHashMap, reflect::ReflectComponent}; use bevy_math::{Affine3A, Mat3A, Mat4, Vec3, Vec3A, Vec4, Vec4Swizzles}; -use bevy_reflect::Reflect; +use bevy_reflect::prelude::*; /// An axis-aligned bounding box, defined by: /// - a center, @@ -31,7 +31,7 @@ use bevy_reflect::Reflect; /// [`Mesh`]: crate::mesh::Mesh /// [`Handle`]: crate::mesh::Mesh #[derive(Component, Clone, Copy, Debug, Default, Reflect, PartialEq)] -#[reflect(Component)] +#[reflect(Component, Default)] pub struct Aabb { pub center: Vec3A, pub half_extents: Vec3A, @@ -212,7 +212,7 @@ impl HalfSpace { /// [`CameraProjection`]: crate::camera::CameraProjection /// [`GlobalTransform`]: bevy_transform::components::GlobalTransform #[derive(Component, Clone, Copy, Debug, Default, Reflect)] -#[reflect(Component)] +#[reflect(Component, Default)] pub struct Frustum { #[reflect(ignore)] pub half_spaces: [HalfSpace; 6], @@ -303,7 +303,7 @@ impl Frustum { } #[derive(Component, Clone, Debug, Default, Reflect)] -#[reflect(Component)] +#[reflect(Component, Default)] pub struct CubemapFrusta { #[reflect(ignore)] pub frusta: [Frustum; 6], @@ -319,7 +319,7 @@ impl CubemapFrusta { } #[derive(Component, Debug, Default, Reflect)] -#[reflect(Component)] +#[reflect(Component, Default)] pub struct CascadesFrusta { #[reflect(ignore)] pub frusta: EntityHashMap>, diff --git a/crates/bevy_render/src/render_asset.rs b/crates/bevy_render/src/render_asset.rs index 0b1686fbce641..0af7b877854dc 100644 --- a/crates/bevy_render/src/render_asset.rs +++ b/crates/bevy_render/src/render_asset.rs @@ -402,6 +402,10 @@ pub fn prepare_assets( let mut param = param.into_inner(); let queued_assets = std::mem::take(&mut prepare_next_frame.assets); for (id, extracted_asset) in queued_assets { + if extracted_assets.removed.contains(&id) { + continue; + } + match extracted_asset.prepare_asset(&mut param) { Ok(prepared_asset) => { render_assets.insert(id, prepared_asset); diff --git a/crates/bevy_render/src/render_phase/draw_state.rs b/crates/bevy_render/src/render_phase/draw_state.rs index 0c09b6aba63b9..41482de6a8e00 100644 --- a/crates/bevy_render/src/render_phase/draw_state.rs +++ b/crates/bevy_render/src/render_phase/draw_state.rs @@ -1,5 +1,6 @@ use crate::{ camera::Viewport, + diagnostic::internal::{Pass, PassKind, WritePipelineStatistics, WriteTimestamp}, render_resource::{ BindGroup, BindGroupId, Buffer, BufferId, BufferSlice, RenderPipeline, RenderPipelineId, ShaderStages, @@ -9,7 +10,7 @@ use crate::{ use bevy_color::LinearRgba; use bevy_utils::{default, detailed_trace}; use std::ops::Range; -use wgpu::{IndexFormat, RenderPass}; +use wgpu::{IndexFormat, QuerySet, RenderPass}; /// Tracks the state of a [`TrackedRenderPass`]. /// @@ -179,7 +180,7 @@ impl<'a> TrackedRenderPass<'a> { /// Assign a vertex buffer to a slot. /// /// Subsequent calls to [`draw`] and [`draw_indexed`] on this - /// [`RenderPass`] will use `buffer` as one of the source vertex buffers. + /// [`TrackedRenderPass`] will use `buffer` as one of the source vertex buffers. /// /// The `slot_index` refers to the index of the matching descriptor in /// [`VertexState::buffers`](crate::render_resource::VertexState::buffers). @@ -603,3 +604,23 @@ impl<'a> TrackedRenderPass<'a> { self.pass.set_blend_constant(wgpu::Color::from(color)); } } + +impl WriteTimestamp for TrackedRenderPass<'_> { + fn write_timestamp(&mut self, query_set: &wgpu::QuerySet, index: u32) { + self.pass.write_timestamp(query_set, index); + } +} + +impl WritePipelineStatistics for TrackedRenderPass<'_> { + fn begin_pipeline_statistics_query(&mut self, query_set: &QuerySet, index: u32) { + self.pass.begin_pipeline_statistics_query(query_set, index); + } + + fn end_pipeline_statistics_query(&mut self) { + self.pass.end_pipeline_statistics_query(); + } +} + +impl Pass for TrackedRenderPass<'_> { + const KIND: PassKind = PassKind::Render; +} diff --git a/crates/bevy_render/src/render_resource/shader.rs b/crates/bevy_render/src/render_resource/shader.rs index 677378cc90f16..49d61533eaeeb 100644 --- a/crates/bevy_render/src/render_resource/shader.rs +++ b/crates/bevy_render/src/render_resource/shader.rs @@ -2,7 +2,7 @@ use super::ShaderDefVal; use crate::define_atomic_id; use bevy_asset::{io::Reader, Asset, AssetLoader, AssetPath, Handle, LoadContext}; use bevy_reflect::TypePath; -use bevy_utils::{tracing::error, BoxedFuture}; +use bevy_utils::tracing::error; use futures_lite::AsyncReadExt; use std::{borrow::Cow, marker::Copy}; use thiserror::Error; @@ -259,43 +259,39 @@ impl AssetLoader for ShaderLoader { type Asset = Shader; type Settings = (); type Error = ShaderLoaderError; - fn load<'a>( + async fn load<'a>( &'a self, - reader: &'a mut Reader, + reader: &'a mut Reader<'_>, _settings: &'a Self::Settings, - load_context: &'a mut LoadContext, - ) -> BoxedFuture<'a, Result> { - Box::pin(async move { - let ext = load_context.path().extension().unwrap().to_str().unwrap(); - let path = load_context.asset_path().to_string(); - // On windows, the path will inconsistently use \ or /. - // TODO: remove this once AssetPath forces cross-platform "slash" consistency. See #10511 - let path = path.replace(std::path::MAIN_SEPARATOR, "/"); - let mut bytes = Vec::new(); - reader.read_to_end(&mut bytes).await?; - let mut shader = match ext { - "spv" => Shader::from_spirv(bytes, load_context.path().to_string_lossy()), - "wgsl" => Shader::from_wgsl(String::from_utf8(bytes)?, path), - "vert" => { - Shader::from_glsl(String::from_utf8(bytes)?, naga::ShaderStage::Vertex, path) - } - "frag" => { - Shader::from_glsl(String::from_utf8(bytes)?, naga::ShaderStage::Fragment, path) - } - "comp" => { - Shader::from_glsl(String::from_utf8(bytes)?, naga::ShaderStage::Compute, path) - } - _ => panic!("unhandled extension: {ext}"), - }; + load_context: &'a mut LoadContext<'_>, + ) -> Result { + let ext = load_context.path().extension().unwrap().to_str().unwrap(); + let path = load_context.asset_path().to_string(); + // On windows, the path will inconsistently use \ or /. + // TODO: remove this once AssetPath forces cross-platform "slash" consistency. See #10511 + let path = path.replace(std::path::MAIN_SEPARATOR, "/"); + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).await?; + let mut shader = match ext { + "spv" => Shader::from_spirv(bytes, load_context.path().to_string_lossy()), + "wgsl" => Shader::from_wgsl(String::from_utf8(bytes)?, path), + "vert" => Shader::from_glsl(String::from_utf8(bytes)?, naga::ShaderStage::Vertex, path), + "frag" => { + Shader::from_glsl(String::from_utf8(bytes)?, naga::ShaderStage::Fragment, path) + } + "comp" => { + Shader::from_glsl(String::from_utf8(bytes)?, naga::ShaderStage::Compute, path) + } + _ => panic!("unhandled extension: {ext}"), + }; - // collect and store file dependencies - for import in &shader.imports { - if let ShaderImport::AssetPath(asset_path) = import { - shader.file_dependencies.push(load_context.load(asset_path)); - } + // collect and store file dependencies + for import in &shader.imports { + if let ShaderImport::AssetPath(asset_path) = import { + shader.file_dependencies.push(load_context.load(asset_path)); } - Ok(shader) - }) + } + Ok(shader) } fn extensions(&self) -> &[&str] { diff --git a/crates/bevy_render/src/renderer/graph_runner.rs b/crates/bevy_render/src/renderer/graph_runner.rs index c155f4027da46..2023ab326eead 100644 --- a/crates/bevy_render/src/renderer/graph_runner.rs +++ b/crates/bevy_render/src/renderer/graph_runner.rs @@ -8,6 +8,7 @@ use std::{borrow::Cow, collections::VecDeque}; use thiserror::Error; use crate::{ + diagnostic::internal::{DiagnosticsRecorder, RenderDiagnosticsMutex}, render_graph::{ Edge, InternedRenderLabel, InternedRenderSubGraph, NodeRunError, NodeState, RenderGraph, RenderGraphContext, SlotLabel, SlotType, SlotValue, @@ -54,21 +55,39 @@ impl RenderGraphRunner { pub fn run( graph: &RenderGraph, render_device: RenderDevice, + mut diagnostics_recorder: Option, queue: &wgpu::Queue, adapter: &wgpu::Adapter, world: &World, finalizer: impl FnOnce(&mut wgpu::CommandEncoder), - ) -> Result<(), RenderGraphRunnerError> { - let mut render_context = RenderContext::new(render_device, adapter.get_info()); + ) -> Result, RenderGraphRunnerError> { + if let Some(recorder) = &mut diagnostics_recorder { + recorder.begin_frame(); + } + + let mut render_context = + RenderContext::new(render_device, adapter.get_info(), diagnostics_recorder); Self::run_graph(graph, None, &mut render_context, world, &[], None)?; finalizer(render_context.command_encoder()); - { + let (render_device, mut diagnostics_recorder) = { #[cfg(feature = "trace")] let _span = info_span!("submit_graph_commands").entered(); - queue.submit(render_context.finish()); + + let (commands, render_device, diagnostics_recorder) = render_context.finish(); + queue.submit(commands); + + (render_device, diagnostics_recorder) + }; + + if let Some(recorder) = &mut diagnostics_recorder { + let render_diagnostics_mutex = world.resource::().0.clone(); + recorder.finish_frame(&render_device, move |diagnostics| { + *render_diagnostics_mutex.lock().expect("lock poisoned") = Some(diagnostics); + }); } - Ok(()) + + Ok(diagnostics_recorder) } fn run_graph<'w>( diff --git a/crates/bevy_render/src/renderer/mod.rs b/crates/bevy_render/src/renderer/mod.rs index fa4377a405c47..92eada8f238d4 100644 --- a/crates/bevy_render/src/renderer/mod.rs +++ b/crates/bevy_render/src/renderer/mod.rs @@ -8,6 +8,7 @@ pub use graph_runner::*; pub use render_device::*; use crate::{ + diagnostic::{internal::DiagnosticsRecorder, RecordDiagnostics}, render_graph::RenderGraph, render_phase::TrackedRenderPass, render_resource::RenderPassDescriptor, @@ -27,34 +28,46 @@ pub fn render_system(world: &mut World, state: &mut SystemState| { graph.update(world); }); + + let diagnostics_recorder = world.remove_resource::(); + let graph = world.resource::(); let render_device = world.resource::(); let render_queue = world.resource::(); let render_adapter = world.resource::(); - if let Err(e) = RenderGraphRunner::run( + let res = RenderGraphRunner::run( graph, render_device.clone(), // TODO: is this clone really necessary? + diagnostics_recorder, &render_queue.0, &render_adapter.0, world, |encoder| { crate::view::screenshot::submit_screenshot_commands(world, encoder); }, - ) { - error!("Error running render graph:"); - { - let mut src: &dyn std::error::Error = &e; - loop { - error!("> {}", src); - match src.source() { - Some(s) => src = s, - None => break, + ); + + match res { + Ok(Some(diagnostics_recorder)) => { + world.insert_resource(diagnostics_recorder); + } + Ok(None) => {} + Err(e) => { + error!("Error running render graph:"); + { + let mut src: &dyn std::error::Error = &e; + loop { + error!("> {}", src); + match src.source() { + Some(s) => src = s, + None => break, + } } } - } - panic!("Error running render graph: {e}"); + panic!("Error running render graph: {e}"); + } } { @@ -306,11 +319,16 @@ pub struct RenderContext<'w> { command_encoder: Option, command_buffer_queue: Vec>, force_serial: bool, + diagnostics_recorder: Option>, } impl<'w> RenderContext<'w> { /// Creates a new [`RenderContext`] from a [`RenderDevice`]. - pub fn new(render_device: RenderDevice, adapter_info: AdapterInfo) -> Self { + pub fn new( + render_device: RenderDevice, + adapter_info: AdapterInfo, + diagnostics_recorder: Option, + ) -> Self { // HACK: Parallel command encoding is currently bugged on AMD + Windows + Vulkan with wgpu 0.19.1 #[cfg(target_os = "windows")] let force_serial = @@ -326,6 +344,7 @@ impl<'w> RenderContext<'w> { command_encoder: None, command_buffer_queue: Vec::new(), force_serial, + diagnostics_recorder: diagnostics_recorder.map(Arc::new), } } @@ -334,6 +353,12 @@ impl<'w> RenderContext<'w> { &self.render_device } + /// Gets the diagnostics recorder, used to track elapsed time and pipeline statistics + /// of various render and compute passes. + pub fn diagnostic_recorder(&self) -> impl RecordDiagnostics { + self.diagnostics_recorder.clone() + } + /// Gets the current [`CommandEncoder`]. pub fn command_encoder(&mut self) -> &mut CommandEncoder { self.command_encoder.get_or_insert_with(|| { @@ -353,6 +378,7 @@ impl<'w> RenderContext<'w> { self.render_device .create_command_encoder(&wgpu::CommandEncoderDescriptor::default()) }); + let render_pass = command_encoder.begin_render_pass(&descriptor); TrackedRenderPass::new(&self.render_device, render_pass) } @@ -389,7 +415,13 @@ impl<'w> RenderContext<'w> { /// /// This function will wait until all command buffer generation tasks are complete /// by running them in parallel (where supported). - pub fn finish(mut self) -> Vec { + pub fn finish( + mut self, + ) -> ( + Vec, + RenderDevice, + Option, + ) { self.flush_encoder(); let mut command_buffers = Vec::with_capacity(self.command_buffer_queue.len()); @@ -413,9 +445,30 @@ impl<'w> RenderContext<'w> { } } }); + command_buffers.append(&mut task_based_command_buffers); command_buffers.sort_unstable_by_key(|(i, _)| *i); - command_buffers.into_iter().map(|(_, cb)| cb).collect() + + let mut command_buffers = command_buffers + .into_iter() + .map(|(_, cb)| cb) + .collect::>(); + + let mut diagnostics_recorder = self.diagnostics_recorder.take().map(|v| { + Arc::try_unwrap(v) + .ok() + .expect("diagnostic recorder shouldn't be held longer than necessary") + }); + + if let Some(recorder) = &mut diagnostics_recorder { + let mut command_encoder = self + .render_device + .create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); + recorder.resolve(&mut command_encoder); + command_buffers.push(command_encoder.finish()); + } + + (command_buffers, self.render_device, diagnostics_recorder) } fn flush_encoder(&mut self) { diff --git a/crates/bevy_render/src/texture/compressed_image_saver.rs b/crates/bevy_render/src/texture/compressed_image_saver.rs index dde2a900b4a21..0ab053df331f6 100644 --- a/crates/bevy_render/src/texture/compressed_image_saver.rs +++ b/crates/bevy_render/src/texture/compressed_image_saver.rs @@ -1,6 +1,6 @@ use crate::texture::{Image, ImageFormat, ImageFormatSetting, ImageLoader, ImageLoaderSettings}; use bevy_asset::saver::{AssetSaver, SavedAsset}; -use futures_lite::{AsyncWriteExt, FutureExt}; +use futures_lite::AsyncWriteExt; use thiserror::Error; pub struct CompressedImageSaver; @@ -19,46 +19,46 @@ impl AssetSaver for CompressedImageSaver { type OutputLoader = ImageLoader; type Error = CompressedImageSaverError; - fn save<'a>( + async fn save<'a>( &'a self, writer: &'a mut bevy_asset::io::Writer, image: SavedAsset<'a, Self::Asset>, _settings: &'a Self::Settings, - ) -> bevy_utils::BoxedFuture<'a, Result> { - // PERF: this should live inside the future, but CompressorParams and Compressor are not Send / can't be owned by the BoxedFuture (which _is_ Send) - let mut compressor_params = basis_universal::CompressorParams::new(); - compressor_params.set_basis_format(basis_universal::BasisTextureFormat::UASTC4x4); - compressor_params.set_generate_mipmaps(true); + ) -> Result { let is_srgb = image.texture_descriptor.format.is_srgb(); - let color_space = if is_srgb { - basis_universal::ColorSpace::Srgb - } else { - basis_universal::ColorSpace::Linear - }; - compressor_params.set_color_space(color_space); - compressor_params.set_uastc_quality_level(basis_universal::UASTC_QUALITY_DEFAULT); - let mut source_image = compressor_params.source_image_mut(0); - let size = image.size(); - source_image.init(&image.data, size.x, size.y, 4); + let compressed_basis_data = { + let mut compressor_params = basis_universal::CompressorParams::new(); + compressor_params.set_basis_format(basis_universal::BasisTextureFormat::UASTC4x4); + compressor_params.set_generate_mipmaps(true); + let color_space = if is_srgb { + basis_universal::ColorSpace::Srgb + } else { + basis_universal::ColorSpace::Linear + }; + compressor_params.set_color_space(color_space); + compressor_params.set_uastc_quality_level(basis_universal::UASTC_QUALITY_DEFAULT); + + let mut source_image = compressor_params.source_image_mut(0); + let size = image.size(); + source_image.init(&image.data, size.x, size.y, 4); + + let mut compressor = basis_universal::Compressor::new(4); + // SAFETY: the CompressorParams are "valid" to the best of our knowledge. The basis-universal + // library bindings note that invalid params might produce undefined behavior. + unsafe { + compressor.init(&compressor_params); + compressor.process().unwrap(); + } + compressor.basis_file().to_vec() + }; - let mut compressor = basis_universal::Compressor::new(4); - // SAFETY: the CompressorParams are "valid" to the best of our knowledge. The basis-universal - // library bindings note that invalid params might produce undefined behavior. - unsafe { - compressor.init(&compressor_params); - compressor.process().unwrap(); - } - let compressed_basis_data = compressor.basis_file().to_vec(); - async move { - writer.write_all(&compressed_basis_data).await?; - Ok(ImageLoaderSettings { - format: ImageFormatSetting::Format(ImageFormat::Basis), - is_srgb, - sampler: image.sampler.clone(), - asset_usage: image.asset_usage, - }) - } - .boxed() + writer.write_all(&compressed_basis_data).await?; + Ok(ImageLoaderSettings { + format: ImageFormatSetting::Format(ImageFormat::Basis), + is_srgb, + sampler: image.sampler.clone(), + asset_usage: image.asset_usage, + }) } } diff --git a/crates/bevy_render/src/texture/exr_texture_loader.rs b/crates/bevy_render/src/texture/exr_texture_loader.rs index 9f0d670658048..d5f39aa3c0542 100644 --- a/crates/bevy_render/src/texture/exr_texture_loader.rs +++ b/crates/bevy_render/src/texture/exr_texture_loader.rs @@ -6,7 +6,6 @@ use bevy_asset::{ io::{AsyncReadExt, Reader}, AssetLoader, LoadContext, }; -use bevy_utils::BoxedFuture; use image::ImageDecoder; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -36,45 +35,43 @@ impl AssetLoader for ExrTextureLoader { type Settings = ExrTextureLoaderSettings; type Error = ExrTextureLoaderError; - fn load<'a>( + async fn load<'a>( &'a self, - reader: &'a mut Reader, + reader: &'a mut Reader<'_>, settings: &'a Self::Settings, - _load_context: &'a mut LoadContext, - ) -> BoxedFuture<'a, Result> { - Box::pin(async move { - let format = TextureFormat::Rgba32Float; - debug_assert_eq!( - format.pixel_size(), - 4 * 4, - "Format should have 32bit x 4 size" - ); + _load_context: &'a mut LoadContext<'_>, + ) -> Result { + let format = TextureFormat::Rgba32Float; + debug_assert_eq!( + format.pixel_size(), + 4 * 4, + "Format should have 32bit x 4 size" + ); - let mut bytes = Vec::new(); - reader.read_to_end(&mut bytes).await?; - let decoder = image::codecs::openexr::OpenExrDecoder::with_alpha_preference( - std::io::Cursor::new(bytes), - Some(true), - )?; - let (width, height) = decoder.dimensions(); + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).await?; + let decoder = image::codecs::openexr::OpenExrDecoder::with_alpha_preference( + std::io::Cursor::new(bytes), + Some(true), + )?; + let (width, height) = decoder.dimensions(); - let total_bytes = decoder.total_bytes() as usize; + let total_bytes = decoder.total_bytes() as usize; - let mut buf = vec![0u8; total_bytes]; - decoder.read_image(buf.as_mut_slice())?; + let mut buf = vec![0u8; total_bytes]; + decoder.read_image(buf.as_mut_slice())?; - Ok(Image::new( - Extent3d { - width, - height, - depth_or_array_layers: 1, - }, - TextureDimension::D2, - buf, - format, - settings.asset_usage, - )) - }) + Ok(Image::new( + Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + TextureDimension::D2, + buf, + format, + settings.asset_usage, + )) } fn extensions(&self) -> &[&str] { diff --git a/crates/bevy_render/src/texture/hdr_texture_loader.rs b/crates/bevy_render/src/texture/hdr_texture_loader.rs index 1fd021863943c..641055690fd58 100644 --- a/crates/bevy_render/src/texture/hdr_texture_loader.rs +++ b/crates/bevy_render/src/texture/hdr_texture_loader.rs @@ -29,48 +29,46 @@ impl AssetLoader for HdrTextureLoader { type Asset = Image; type Settings = HdrTextureLoaderSettings; type Error = HdrTextureLoaderError; - fn load<'a>( + async fn load<'a>( &'a self, - reader: &'a mut Reader, + reader: &'a mut Reader<'_>, settings: &'a Self::Settings, - _load_context: &'a mut LoadContext, - ) -> bevy_utils::BoxedFuture<'a, Result> { - Box::pin(async move { - let format = TextureFormat::Rgba32Float; - debug_assert_eq!( - format.pixel_size(), - 4 * 4, - "Format should have 32bit x 4 size" - ); + _load_context: &'a mut LoadContext<'_>, + ) -> Result { + let format = TextureFormat::Rgba32Float; + debug_assert_eq!( + format.pixel_size(), + 4 * 4, + "Format should have 32bit x 4 size" + ); - let mut bytes = Vec::new(); - reader.read_to_end(&mut bytes).await?; - let decoder = image::codecs::hdr::HdrDecoder::new(bytes.as_slice())?; - let info = decoder.metadata(); - let rgb_data = decoder.read_image_hdr()?; - let mut rgba_data = Vec::with_capacity(rgb_data.len() * format.pixel_size()); + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).await?; + let decoder = image::codecs::hdr::HdrDecoder::new(bytes.as_slice())?; + let info = decoder.metadata(); + let rgb_data = decoder.read_image_hdr()?; + let mut rgba_data = Vec::with_capacity(rgb_data.len() * format.pixel_size()); - for rgb in rgb_data { - let alpha = 1.0f32; + for rgb in rgb_data { + let alpha = 1.0f32; - rgba_data.extend_from_slice(&rgb.0[0].to_ne_bytes()); - rgba_data.extend_from_slice(&rgb.0[1].to_ne_bytes()); - rgba_data.extend_from_slice(&rgb.0[2].to_ne_bytes()); - rgba_data.extend_from_slice(&alpha.to_ne_bytes()); - } + rgba_data.extend_from_slice(&rgb.0[0].to_ne_bytes()); + rgba_data.extend_from_slice(&rgb.0[1].to_ne_bytes()); + rgba_data.extend_from_slice(&rgb.0[2].to_ne_bytes()); + rgba_data.extend_from_slice(&alpha.to_ne_bytes()); + } - Ok(Image::new( - Extent3d { - width: info.width, - height: info.height, - depth_or_array_layers: 1, - }, - TextureDimension::D2, - rgba_data, - format, - settings.asset_usage, - )) - }) + Ok(Image::new( + Extent3d { + width: info.width, + height: info.height, + depth_or_array_layers: 1, + }, + TextureDimension::D2, + rgba_data, + format, + settings.asset_usage, + )) } fn extensions(&self) -> &[&str] { diff --git a/crates/bevy_render/src/texture/image.rs b/crates/bevy_render/src/texture/image.rs index b052519b056f6..d163841837c03 100644 --- a/crates/bevy_render/src/texture/image.rs +++ b/crates/bevy_render/src/texture/image.rs @@ -15,7 +15,7 @@ use bevy_asset::Asset; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::system::{lifetimeless::SRes, Resource, SystemParamItem}; use bevy_math::{AspectRatio, UVec2, Vec2}; -use bevy_reflect::Reflect; +use bevy_reflect::prelude::*; use serde::{Deserialize, Serialize}; use std::hash::Hash; use thiserror::Error; @@ -112,7 +112,7 @@ impl ImageFormat { } #[derive(Asset, Reflect, Debug, Clone)] -#[reflect_value] +#[reflect_value(Default)] pub struct Image { pub data: Vec, // TODO: this nesting makes accessing Image metadata verbose. Either flatten out descriptor or add accessors diff --git a/crates/bevy_render/src/texture/image_loader.rs b/crates/bevy_render/src/texture/image_loader.rs index 44a4fdb9251cf..534d064409b3f 100644 --- a/crates/bevy_render/src/texture/image_loader.rs +++ b/crates/bevy_render/src/texture/image_loader.rs @@ -85,37 +85,35 @@ impl AssetLoader for ImageLoader { type Asset = Image; type Settings = ImageLoaderSettings; type Error = ImageLoaderError; - fn load<'a>( + async fn load<'a>( &'a self, - reader: &'a mut Reader, + reader: &'a mut Reader<'_>, settings: &'a ImageLoaderSettings, - load_context: &'a mut LoadContext, - ) -> bevy_utils::BoxedFuture<'a, Result> { - Box::pin(async move { - // use the file extension for the image type - let ext = load_context.path().extension().unwrap().to_str().unwrap(); + load_context: &'a mut LoadContext<'_>, + ) -> Result { + // use the file extension for the image type + let ext = load_context.path().extension().unwrap().to_str().unwrap(); - let mut bytes = Vec::new(); - reader.read_to_end(&mut bytes).await?; - let image_type = match settings.format { - ImageFormatSetting::FromExtension => ImageType::Extension(ext), - ImageFormatSetting::Format(format) => ImageType::Format(format), - }; - Ok(Image::from_buffer( - #[cfg(all(debug_assertions, feature = "dds"))] - load_context.path().display().to_string(), - &bytes, - image_type, - self.supported_compressed_formats, - settings.is_srgb, - settings.sampler.clone(), - settings.asset_usage, - ) - .map_err(|err| FileTextureError { - error: err, - path: format!("{}", load_context.path().display()), - })?) - }) + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).await?; + let image_type = match settings.format { + ImageFormatSetting::FromExtension => ImageType::Extension(ext), + ImageFormatSetting::Format(format) => ImageType::Format(format), + }; + Ok(Image::from_buffer( + #[cfg(all(debug_assertions, feature = "dds"))] + load_context.path().display().to_string(), + &bytes, + image_type, + self.supported_compressed_formats, + settings.is_srgb, + settings.sampler.clone(), + settings.asset_usage, + ) + .map_err(|err| FileTextureError { + error: err, + path: format!("{}", load_context.path().display()), + })?) } fn extensions(&self) -> &[&str] { diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index 444888b569ad0..3b888e33e69dd 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -90,7 +90,7 @@ impl Plugin for ViewPlugin { #[derive( Resource, Default, Clone, Copy, ExtractResource, Reflect, PartialEq, PartialOrd, Debug, )] -#[reflect(Resource)] +#[reflect(Resource, Default)] pub enum Msaa { Off = 1, Sample2 = 2, diff --git a/crates/bevy_render/src/view/visibility/mod.rs b/crates/bevy_render/src/view/visibility/mod.rs index c674a36829e05..50ea668f14c7c 100644 --- a/crates/bevy_render/src/view/visibility/mod.rs +++ b/crates/bevy_render/src/view/visibility/mod.rs @@ -168,7 +168,7 @@ pub struct NoFrustumCulling; /// This component is intended to be attached to the same entity as the [`Camera`] and /// the [`Frustum`] defining the view. #[derive(Clone, Component, Default, Debug, Reflect)] -#[reflect(Component)] +#[reflect(Component, Default)] pub struct VisibleEntities { #[reflect(ignore)] pub entities: Vec, diff --git a/crates/bevy_scene/src/scene_loader.rs b/crates/bevy_scene/src/scene_loader.rs index f4dce7c66a3d8..107d014b2bb09 100644 --- a/crates/bevy_scene/src/scene_loader.rs +++ b/crates/bevy_scene/src/scene_loader.rs @@ -6,7 +6,6 @@ use bevy_asset::{io::Reader, AssetLoader, AsyncReadExt, LoadContext}; use bevy_ecs::reflect::AppTypeRegistry; use bevy_ecs::world::{FromWorld, World}; use bevy_reflect::TypeRegistryArc; -use bevy_utils::BoxedFuture; #[cfg(feature = "serialize")] use serde::de::DeserializeSeed; use thiserror::Error; @@ -44,23 +43,21 @@ impl AssetLoader for SceneLoader { type Settings = (); type Error = SceneLoaderError; - fn load<'a>( + async fn load<'a>( &'a self, - reader: &'a mut Reader, + reader: &'a mut Reader<'_>, _settings: &'a (), - _load_context: &'a mut LoadContext, - ) -> BoxedFuture<'a, Result> { - Box::pin(async move { - let mut bytes = Vec::new(); - reader.read_to_end(&mut bytes).await?; - let mut deserializer = ron::de::Deserializer::from_bytes(&bytes)?; - let scene_deserializer = SceneDeserializer { - type_registry: &self.type_registry.read(), - }; - Ok(scene_deserializer - .deserialize(&mut deserializer) - .map_err(|e| deserializer.span_error(e))?) - }) + _load_context: &'a mut LoadContext<'_>, + ) -> Result { + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).await?; + let mut deserializer = ron::de::Deserializer::from_bytes(&bytes)?; + let scene_deserializer = SceneDeserializer { + type_registry: &self.type_registry.read(), + }; + Ok(scene_deserializer + .deserialize(&mut deserializer) + .map_err(|e| deserializer.span_error(e))?) } fn extensions(&self) -> &[&str] { diff --git a/crates/bevy_sprite/Cargo.toml b/crates/bevy_sprite/Cargo.toml index 01fea40918265..ba2d5d0ed979f 100644 --- a/crates/bevy_sprite/Cargo.toml +++ b/crates/bevy_sprite/Cargo.toml @@ -30,7 +30,7 @@ bevy_derive = { path = "../bevy_derive", version = "0.14.0-dev" } # other bytemuck = { version = "1.5", features = ["derive"] } -fixedbitset = "0.4" +fixedbitset = "0.5" guillotiere = "0.6.0" thiserror = "1.0" rectangle-pack = "0.4" diff --git a/crates/bevy_sprite/src/mesh2d/material.rs b/crates/bevy_sprite/src/mesh2d/material.rs index 2b5772c686559..0b4a7be28c669 100644 --- a/crates/bevy_sprite/src/mesh2d/material.rs +++ b/crates/bevy_sprite/src/mesh2d/material.rs @@ -572,6 +572,10 @@ pub fn prepare_materials_2d( ) { let queued_assets = std::mem::take(&mut prepare_next_frame.assets); for (id, material) in queued_assets { + if extracted_assets.removed.contains(&id) { + continue; + } + match prepare_material2d( &material, &render_device, diff --git a/crates/bevy_text/src/font_loader.rs b/crates/bevy_text/src/font_loader.rs index a47abbd9619a0..45f3e9701e11b 100644 --- a/crates/bevy_text/src/font_loader.rs +++ b/crates/bevy_text/src/font_loader.rs @@ -21,17 +21,15 @@ impl AssetLoader for FontLoader { type Asset = Font; type Settings = (); type Error = FontLoaderError; - fn load<'a>( + async fn load<'a>( &'a self, - reader: &'a mut Reader, + reader: &'a mut Reader<'_>, _settings: &'a (), - _load_context: &'a mut LoadContext, - ) -> bevy_utils::BoxedFuture<'a, Result> { - Box::pin(async move { - let mut bytes = Vec::new(); - reader.read_to_end(&mut bytes).await?; - Ok(Font::try_from_bytes(bytes)?) - }) + _load_context: &'a mut LoadContext<'_>, + ) -> Result { + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).await?; + Ok(Font::try_from_bytes(bytes)?) } fn extensions(&self) -> &[&str] { diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index b0a3dc7604662..34384e312bf4c 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use crate::Font; -#[derive(Component, Debug, Clone, Reflect)] +#[derive(Component, Debug, Clone, Default, Reflect)] #[reflect(Component, Default)] pub struct Text { pub sections: Vec, @@ -18,16 +18,6 @@ pub struct Text { pub linebreak_behavior: BreakLineOn, } -impl Default for Text { - fn default() -> Self { - Self { - sections: Default::default(), - justify: JustifyText::Left, - linebreak_behavior: BreakLineOn::WordBoundary, - } - } -} - impl Text { /// Constructs a [`Text`] with a single section. /// @@ -219,12 +209,13 @@ impl Default for TextStyle { } /// Determines how lines will be broken when preventing text from running out of bounds. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Reflect, Serialize, Deserialize)] #[reflect(Serialize, Deserialize)] pub enum BreakLineOn { /// Uses the [Unicode Line Breaking Algorithm](https://www.unicode.org/reports/tr14/). /// Lines will be broken up at the nearest suitable word boundary, usually a space. /// This behavior suits most cases, as it keeps words intact across linebreaks. + #[default] WordBoundary, /// Lines will be broken without discrimination on any character that would leave bounds. /// This is closer to the behavior one might expect from text in a terminal. diff --git a/crates/bevy_transform/src/components/transform.rs b/crates/bevy_transform/src/components/transform.rs index ed8b004074f9a..6dd190eb93551 100644 --- a/crates/bevy_transform/src/components/transform.rs +++ b/crates/bevy_transform/src/components/transform.rs @@ -30,9 +30,7 @@ use std::ops::Mul; /// # Examples /// /// - [`transform`] -/// - [`global_vs_local_translation`] /// -/// [`global_vs_local_translation`]: https://github.com/bevyengine/bevy/blob/latest/examples/transforms/global_vs_local_translation.rs /// [`transform`]: https://github.com/bevyengine/bevy/blob/latest/examples/transforms/transform.rs #[derive(Component, Debug, PartialEq, Clone, Copy, Reflect)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -122,11 +120,11 @@ impl Transform { /// /// In some cases it's not possible to construct a rotation. Another axis will be picked in those cases: /// * if `target` is the same as the transform translation, `Vec3::Z` is used instead - /// * if `up` is zero, `Vec3::Y` is used instead + /// * if `up` fails converting to `Dir3` (e.g if it is `Vec3::ZERO`), `Dir3::Y` is used instead /// * if the resulting forward direction is parallel with `up`, an orthogonal vector is used as the "right" direction #[inline] #[must_use] - pub fn looking_at(mut self, target: Vec3, up: Vec3) -> Self { + pub fn looking_at(mut self, target: Vec3, up: impl TryInto) -> Self { self.look_at(target, up); self } @@ -135,16 +133,49 @@ impl Transform { /// points in the given `direction` and [`Transform::up`] points towards `up`. /// /// In some cases it's not possible to construct a rotation. Another axis will be picked in those cases: - /// * if `direction` is zero, `Vec3::Z` is used instead - /// * if `up` is zero, `Vec3::Y` is used instead + /// * if `direction` fails converting to `Dir3` (e.g if it is `Vec3::ZERO`), `Dir3::Z` is used instead + /// * if `up` fails converting to `Dir3`, `Dir3::Y` is used instead /// * if `direction` is parallel with `up`, an orthogonal vector is used as the "right" direction #[inline] #[must_use] - pub fn looking_to(mut self, direction: Vec3, up: Vec3) -> Self { + pub fn looking_to(mut self, direction: impl TryInto, up: impl TryInto) -> Self { self.look_to(direction, up); self } + /// Rotates this [`Transform`] so that the `main_axis` vector, reinterpreted in local coordinates, points + /// in the given `main_direction`, while `secondary_axis` points towards `secondary_direction`. + /// For example, if a spaceship model has its nose pointing in the X-direction in its own local coordinates + /// and its dorsal fin pointing in the Y-direction, then `align(Dir3::X, v, Dir3::Y, w)` will make the spaceship's + /// nose point in the direction of `v`, while the dorsal fin does its best to point in the direction `w`. + /// + /// + /// In some cases a rotation cannot be constructed. Another axis will be picked in those cases: + /// * if `main_axis` or `main_direction` fail converting to `Dir3` (e.g are zero), `Dir3::X` takes their place + /// * if `secondary_axis` or `secondary_direction` fail converting, `Dir3::Y` takes their place + /// * if `main_axis` is parallel with `secondary_axis` or `main_direction` is parallel with `secondary_direction`, + /// a rotation is constructed which takes `main_axis` to `main_direction` along a great circle, ignoring the secondary + /// counterparts + /// + /// See [`Transform::align`] for additional details. + #[inline] + #[must_use] + pub fn aligned_by( + mut self, + main_axis: impl TryInto, + main_direction: impl TryInto, + secondary_axis: impl TryInto, + secondary_direction: impl TryInto, + ) -> Self { + self.align( + main_axis, + main_direction, + secondary_axis, + secondary_direction, + ); + self + } + /// Returns this [`Transform`] with a new translation. #[inline] #[must_use] @@ -340,10 +371,10 @@ impl Transform { /// /// In some cases it's not possible to construct a rotation. Another axis will be picked in those cases: /// * if `target` is the same as the transform translation, `Vec3::Z` is used instead - /// * if `up` is zero, `Vec3::Y` is used instead + /// * if `up` fails converting to `Dir3` (e.g if it is `Vec3::ZERO`), `Dir3::Y` is used instead /// * if the resulting forward direction is parallel with `up`, an orthogonal vector is used as the "right" direction #[inline] - pub fn look_at(&mut self, target: Vec3, up: Vec3) { + pub fn look_at(&mut self, target: Vec3, up: impl TryInto) { self.look_to(target - self.translation, up); } @@ -351,19 +382,100 @@ impl Transform { /// and [`Transform::up`] points towards `up`. /// /// In some cases it's not possible to construct a rotation. Another axis will be picked in those cases: - /// * if `direction` is zero, `Vec3::NEG_Z` is used instead - /// * if `up` is zero, `Vec3::Y` is used instead + /// * if `direction` fails converting to `Dir3` (e.g if it is `Vec3::ZERO`), `Dir3::NEG_Z` is used instead + /// * if `up` fails converting to `Dir3`, `Dir3::Y` is used instead /// * if `direction` is parallel with `up`, an orthogonal vector is used as the "right" direction #[inline] - pub fn look_to(&mut self, direction: Vec3, up: Vec3) { - let back = -direction.try_normalize().unwrap_or(Vec3::NEG_Z); - let up = up.try_normalize().unwrap_or(Vec3::Y); + pub fn look_to(&mut self, direction: impl TryInto, up: impl TryInto) { + let back = -direction.try_into().unwrap_or(Dir3::NEG_Z); + let up = up.try_into().unwrap_or(Dir3::Y); let right = up - .cross(back) + .cross(back.into()) .try_normalize() .unwrap_or_else(|| up.any_orthonormal_vector()); let up = back.cross(right); - self.rotation = Quat::from_mat3(&Mat3::from_cols(right, up, back)); + self.rotation = Quat::from_mat3(&Mat3::from_cols(right, up, back.into())); + } + + /// Rotates this [`Transform`] so that the `main_axis` vector, reinterpreted in local coordinates, points + /// in the given `main_direction`, while `secondary_axis` points towards `secondary_direction`. + /// + /// For example, if a spaceship model has its nose pointing in the X-direction in its own local coordinates + /// and its dorsal fin pointing in the Y-direction, then `align(Dir3::X, v, Dir3::Y, w)` will make the spaceship's + /// nose point in the direction of `v`, while the dorsal fin does its best to point in the direction `w`. + /// + /// More precisely, the [`Transform::rotation`] produced will be such that: + /// * applying it to `main_axis` results in `main_direction` + /// * applying it to `secondary_axis` produces a vector that lies in the half-plane generated by `main_direction` and + /// `secondary_direction` (with positive contribution by `secondary_direction`) + /// + /// [`Transform::look_to`] is recovered, for instance, when `main_axis` is `Dir3::NEG_Z` (the [`Transform::forward`] + /// direction in the default orientation) and `secondary_axis` is `Dir3::Y` (the [`Transform::up`] direction in the default + /// orientation). (Failure cases may differ somewhat.) + /// + /// In some cases a rotation cannot be constructed. Another axis will be picked in those cases: + /// * if `main_axis` or `main_direction` fail converting to `Dir3` (e.g are zero), `Dir3::X` takes their place + /// * if `secondary_axis` or `secondary_direction` fail converting, `Dir3::Y` takes their place + /// * if `main_axis` is parallel with `secondary_axis` or `main_direction` is parallel with `secondary_direction`, + /// a rotation is constructed which takes `main_axis` to `main_direction` along a great circle, ignoring the secondary + /// counterparts + /// + /// Example + /// ``` + /// # use bevy_math::{Dir3, Vec3, Quat}; + /// # use bevy_transform::components::Transform; + /// # let mut t1 = Transform::IDENTITY; + /// # let mut t2 = Transform::IDENTITY; + /// t1.align(Dir3::X, Dir3::Y, Vec3::new(1., 1., 0.), Dir3::Z); + /// let main_axis_image = t1.rotation * Dir3::X; + /// let secondary_axis_image = t1.rotation * Vec3::new(1., 1., 0.); + /// assert!(main_axis_image.abs_diff_eq(Vec3::Y, 1e-5)); + /// assert!(secondary_axis_image.abs_diff_eq(Vec3::new(0., 1., 1.), 1e-5)); + /// + /// t1.align(Vec3::ZERO, Dir3::Z, Vec3::ZERO, Dir3::X); + /// t2.align(Dir3::X, Dir3::Z, Dir3::Y, Dir3::X); + /// assert_eq!(t1.rotation, t2.rotation); + /// + /// t1.align(Dir3::X, Dir3::Z, Dir3::X, Dir3::Y); + /// assert_eq!(t1.rotation, Quat::from_rotation_arc(Vec3::X, Vec3::Z)); + /// ``` + #[inline] + pub fn align( + &mut self, + main_axis: impl TryInto, + main_direction: impl TryInto, + secondary_axis: impl TryInto, + secondary_direction: impl TryInto, + ) { + let main_axis = main_axis.try_into().unwrap_or(Dir3::X); + let main_direction = main_direction.try_into().unwrap_or(Dir3::X); + let secondary_axis = secondary_axis.try_into().unwrap_or(Dir3::Y); + let secondary_direction = secondary_direction.try_into().unwrap_or(Dir3::Y); + + // The solution quaternion will be constructed in two steps. + // First, we start with a rotation that takes `main_axis` to `main_direction`. + let first_rotation = Quat::from_rotation_arc(main_axis.into(), main_direction.into()); + + // Let's follow by rotating about the `main_direction` axis so that the image of `secondary_axis` + // is taken to something that lies in the plane of `main_direction` and `secondary_direction`. Since + // `main_direction` is fixed by this rotation, the first criterion is still satisfied. + let secondary_image = first_rotation * secondary_axis; + let secondary_image_ortho = secondary_image + .reject_from_normalized(main_direction.into()) + .try_normalize(); + let secondary_direction_ortho = secondary_direction + .reject_from_normalized(main_direction.into()) + .try_normalize(); + + // If one of the two weak vectors was parallel to `main_direction`, then we just do the first part + self.rotation = match (secondary_image_ortho, secondary_direction_ortho) { + (Some(secondary_img_ortho), Some(secondary_dir_ortho)) => { + let second_rotation = + Quat::from_rotation_arc(secondary_img_ortho, secondary_dir_ortho); + second_rotation * first_rotation + } + _ => first_rotation, + }; } /// Multiplies `self` with `transform` component by component, returning the diff --git a/crates/bevy_ui/src/geometry.rs b/crates/bevy_ui/src/geometry.rs index c4cf096bc687d..b217bfa1b44dc 100644 --- a/crates/bevy_ui/src/geometry.rs +++ b/crates/bevy_ui/src/geometry.rs @@ -536,6 +536,82 @@ impl UiRect { ..Default::default() } } + + /// Returns the [`UiRect`] with its `left` field set to the given value. + /// + /// # Example + /// + /// ``` + /// # use bevy_ui::{UiRect, Val}; + /// # + /// let ui_rect = UiRect::all(Val::Px(20.0)).with_left(Val::Px(10.0)); + /// assert_eq!(ui_rect.left, Val::Px(10.0)); + /// assert_eq!(ui_rect.right, Val::Px(20.0)); + /// assert_eq!(ui_rect.top, Val::Px(20.0)); + /// assert_eq!(ui_rect.bottom, Val::Px(20.0)); + /// ``` + #[inline] + pub fn with_left(mut self, left: Val) -> Self { + self.left = left; + self + } + + /// Returns the [`UiRect`] with its `right` field set to the given value. + /// + /// # Example + /// + /// ``` + /// # use bevy_ui::{UiRect, Val}; + /// # + /// let ui_rect = UiRect::all(Val::Px(20.0)).with_right(Val::Px(10.0)); + /// assert_eq!(ui_rect.left, Val::Px(20.0)); + /// assert_eq!(ui_rect.right, Val::Px(10.0)); + /// assert_eq!(ui_rect.top, Val::Px(20.0)); + /// assert_eq!(ui_rect.bottom, Val::Px(20.0)); + /// ``` + #[inline] + pub fn with_right(mut self, right: Val) -> Self { + self.right = right; + self + } + + /// Returns the [`UiRect`] with its `top` field set to the given value. + /// + /// # Example + /// + /// ``` + /// # use bevy_ui::{UiRect, Val}; + /// # + /// let ui_rect = UiRect::all(Val::Px(20.0)).with_top(Val::Px(10.0)); + /// assert_eq!(ui_rect.left, Val::Px(20.0)); + /// assert_eq!(ui_rect.right, Val::Px(20.0)); + /// assert_eq!(ui_rect.top, Val::Px(10.0)); + /// assert_eq!(ui_rect.bottom, Val::Px(20.0)); + /// ``` + #[inline] + pub fn with_top(mut self, top: Val) -> Self { + self.top = top; + self + } + + /// Returns the [`UiRect`] with its `bottom` field set to the given value. + /// + /// # Example + /// + /// ``` + /// # use bevy_ui::{UiRect, Val}; + /// # + /// let ui_rect = UiRect::all(Val::Px(20.0)).with_bottom(Val::Px(10.0)); + /// assert_eq!(ui_rect.left, Val::Px(20.0)); + /// assert_eq!(ui_rect.right, Val::Px(20.0)); + /// assert_eq!(ui_rect.top, Val::Px(20.0)); + /// assert_eq!(ui_rect.bottom, Val::Px(10.0)); + /// ``` + #[inline] + pub fn with_bottom(mut self, bottom: Val) -> Self { + self.bottom = bottom; + self + } } impl Default for UiRect { diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 486919a8b2d5d..2ec6b130350c6 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -112,6 +112,7 @@ impl Plugin for UiPlugin { .register_type::() .register_type::() .register_type::() + .register_type::() .register_type::() .register_type::() .register_type::() @@ -149,17 +150,15 @@ impl Plugin for UiPlugin { // They run independently since `widget::image_node_system` will only ever observe // its own UiImage, and `widget::text_system` & `bevy_text::update_text2d_layout` // will never modify a pre-existing `Image` asset. + widget::update_image_content_size_system + .before(UiSystem::Layout) + .in_set(AmbiguousWithTextSystem) + .in_set(AmbiguousWithUpdateText2DLayout), ( - widget::update_image_content_size_system - .before(UiSystem::Layout) - .in_set(AmbiguousWithTextSystem) - .in_set(AmbiguousWithUpdateText2DLayout), - ( - texture_slice::compute_slices_on_asset_event, - texture_slice::compute_slices_on_image_change, - ), + texture_slice::compute_slices_on_asset_event, + texture_slice::compute_slices_on_image_change, ) - .chain(), + .after(UiSystem::Layout), ), ); diff --git a/crates/bevy_ui/src/node_bundles.rs b/crates/bevy_ui/src/node_bundles.rs index 57b2bb4857d22..17dddacc87253 100644 --- a/crates/bevy_ui/src/node_bundles.rs +++ b/crates/bevy_ui/src/node_bundles.rs @@ -6,8 +6,8 @@ use crate::widget::TextFlags; use crate::{ widget::{Button, UiImageSize}, - BackgroundColor, BorderColor, ContentSize, FocusPolicy, Interaction, Node, Style, UiImage, - UiMaterial, ZIndex, + BackgroundColor, BorderColor, BorderRadius, ContentSize, FocusPolicy, Interaction, Node, Style, + UiImage, UiMaterial, ZIndex, }; use bevy_asset::Handle; use bevy_color::Color; @@ -34,6 +34,8 @@ pub struct NodeBundle { pub background_color: BackgroundColor, /// The color of the Node's border pub border_color: BorderColor, + /// The border radius of the node + pub border_radius: BorderRadius, /// Whether this node should block interaction with lower nodes pub focus_policy: FocusPolicy, /// The transform of the node @@ -62,6 +64,7 @@ impl Default for NodeBundle { // Transparent background background_color: Color::NONE.into(), border_color: Color::NONE.into(), + border_radius: BorderRadius::default(), node: Default::default(), style: Default::default(), focus_policy: Default::default(), @@ -314,6 +317,8 @@ pub struct ButtonBundle { pub focus_policy: FocusPolicy, /// The color of the Node's border pub border_color: BorderColor, + /// The border radius of the node + pub border_radius: BorderRadius, /// The image of the node pub image: UiImage, /// The transform of the node @@ -344,6 +349,7 @@ impl Default for ButtonBundle { interaction: Default::default(), focus_policy: FocusPolicy::Block, border_color: BorderColor(Color::NONE), + border_radius: BorderRadius::default(), image: Default::default(), transform: Default::default(), global_transform: Default::default(), diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index f2fa85a71f3d5..49b3fb1ad979a 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -15,15 +15,16 @@ pub use ui_material_pipeline::*; use crate::graph::{NodeUi, SubGraphUi}; use crate::{ - texture_slice::ComputedTextureSlices, BackgroundColor, BorderColor, CalculatedClip, - ContentSize, DefaultUiCamera, Node, Outline, Style, TargetCamera, UiImage, UiScale, Val, + texture_slice::ComputedTextureSlices, BackgroundColor, BorderColor, BorderRadius, + CalculatedClip, ContentSize, DefaultUiCamera, Node, Outline, Style, TargetCamera, UiImage, + UiScale, Val, }; use bevy_app::prelude::*; use bevy_asset::{load_internal_asset, AssetEvent, AssetId, Assets, Handle}; use bevy_ecs::entity::EntityHashMap; use bevy_ecs::prelude::*; -use bevy_math::{Mat4, Rect, URect, UVec4, Vec2, Vec3, Vec4Swizzles}; +use bevy_math::{Mat4, Rect, URect, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; use bevy_render::{ camera::Camera, render_asset::RenderAssets, @@ -141,6 +142,14 @@ fn get_ui_graph(render_app: &mut App) -> RenderGraph { ui_graph } +/// The type of UI node. +/// This is used to determine how to render the UI node. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum NodeType { + Rect, + Border, +} + pub struct ExtractedUiNode { pub stack_index: u32, pub transform: Mat4, @@ -155,6 +164,13 @@ pub struct ExtractedUiNode { // it is defaulted to a single camera if only one exists. // Nodes with ambiguous camera will be ignored. pub camera_entity: Entity, + /// Border radius of the UI node. + /// Ordering: top left, top right, bottom right, bottom left. + pub border_radius: [f32; 4], + /// Border thickness of the UI node. + /// Ordering: left, top, right, bottom. + pub border: [f32; 4], + pub node_type: NodeType, } #[derive(Resource, Default)] @@ -164,7 +180,9 @@ pub struct ExtractedUiNodes { pub fn extract_uinode_background_colors( mut extracted_uinodes: ResMut, + camera_query: Extract>, default_ui_camera: Extract, + ui_scale: Extract>, uinode_query: Extract< Query<( Entity, @@ -174,11 +192,20 @@ pub fn extract_uinode_background_colors( Option<&CalculatedClip>, Option<&TargetCamera>, &BackgroundColor, + Option<&BorderRadius>, )>, >, ) { - for (entity, uinode, transform, view_visibility, clip, camera, background_color) in - &uinode_query + for ( + entity, + uinode, + transform, + view_visibility, + clip, + camera, + background_color, + border_radius, + ) in &uinode_query { let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) else { @@ -190,6 +217,26 @@ pub fn extract_uinode_background_colors( continue; } + let ui_logical_viewport_size = camera_query + .get(camera_entity) + .ok() + .and_then(|(_, c)| c.logical_viewport_size()) + .unwrap_or(Vec2::ZERO) + // The logical window resolution returned by `Window` only takes into account the window scale factor and not `UiScale`, + // so we have to divide by `UiScale` to get the size of the UI viewport. + / ui_scale.0; + + let border_radius = if let Some(border_radius) = border_radius { + resolve_border_radius( + border_radius, + uinode.size(), + ui_logical_viewport_size, + ui_scale.0, + ) + } else { + [0.; 4] + }; + extracted_uinodes.uinodes.insert( entity, ExtractedUiNode { @@ -206,6 +253,9 @@ pub fn extract_uinode_background_colors( flip_x: false, flip_y: false, camera_entity, + border: [0.; 4], + border_radius, + node_type: NodeType::Rect, }, ); } @@ -214,7 +264,9 @@ pub fn extract_uinode_background_colors( pub fn extract_uinode_images( mut commands: Commands, mut extracted_uinodes: ResMut, + camera_query: Extract>, texture_atlases: Extract>>, + ui_scale: Extract>, default_ui_camera: Extract, uinode_query: Extract< Query<( @@ -226,10 +278,13 @@ pub fn extract_uinode_images( &UiImage, Option<&TextureAtlas>, Option<&ComputedTextureSlices>, + Option<&BorderRadius>, )>, >, ) { - for (uinode, transform, view_visibility, clip, camera, image, atlas, slices) in &uinode_query { + for (uinode, transform, view_visibility, clip, camera, image, atlas, slices, border_radius) in + &uinode_query + { let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) else { continue; @@ -272,6 +327,26 @@ pub fn extract_uinode_images( ), }; + let ui_logical_viewport_size = camera_query + .get(camera_entity) + .ok() + .and_then(|(_, c)| c.logical_viewport_size()) + .unwrap_or(Vec2::ZERO) + // The logical window resolution returned by `Window` only takes into account the window scale factor and not `UiScale`, + // so we have to divide by `UiScale` to get the size of the UI viewport. + / ui_scale.0; + + let border_radius = if let Some(border_radius) = border_radius { + resolve_border_radius( + border_radius, + uinode.size(), + ui_logical_viewport_size, + ui_scale.0, + ) + } else { + [0.; 4] + }; + extracted_uinodes.uinodes.insert( commands.spawn_empty().id(), ExtractedUiNode { @@ -285,6 +360,9 @@ pub fn extract_uinode_images( flip_x: image.flip_x, flip_y: image.flip_y, camera_entity, + border: [0.; 4], + border_radius, + node_type: NodeType::Rect, }, ); } @@ -302,6 +380,55 @@ pub(crate) fn resolve_border_thickness(value: Val, parent_width: f32, viewport_s } } +pub(crate) fn resolve_border_radius( + &values: &BorderRadius, + node_size: Vec2, + viewport_size: Vec2, + ui_scale: f32, +) -> [f32; 4] { + let max_radius = 0.5 * node_size.min_element() * ui_scale; + [ + values.top_left, + values.top_right, + values.bottom_right, + values.bottom_left, + ] + .map(|value| { + match value { + Val::Auto => 0., + Val::Px(px) => ui_scale * px, + Val::Percent(percent) => node_size.min_element() * percent / 100., + Val::Vw(percent) => viewport_size.x * percent / 100., + Val::Vh(percent) => viewport_size.y * percent / 100., + Val::VMin(percent) => viewport_size.min_element() * percent / 100., + Val::VMax(percent) => viewport_size.max_element() * percent / 100., + } + .clamp(0., max_radius) + }) +} + +#[inline] +fn clamp_corner(r: f32, size: Vec2, offset: Vec2) -> f32 { + let s = 0.5 * size + offset; + let sm = s.x.min(s.y); + r.min(sm) +} + +#[inline] +fn clamp_radius( + [top_left, top_right, bottom_right, bottom_left]: [f32; 4], + size: Vec2, + border: Vec4, +) -> [f32; 4] { + let s = size - border.xy() - border.zw(); + [ + clamp_corner(top_left, s, border.xy()), + clamp_corner(top_right, s, border.zy()), + clamp_corner(bottom_right, s, border.zw()), + clamp_corner(bottom_left, s, border.xw()), + ] +} + pub fn extract_uinode_borders( mut commands: Commands, mut extracted_uinodes: ResMut, @@ -319,6 +446,7 @@ pub fn extract_uinode_borders( Option<&Parent>, &Style, &BorderColor, + &BorderRadius, ), Without, >, @@ -327,8 +455,17 @@ pub fn extract_uinode_borders( ) { let image = AssetId::::default(); - for (node, global_transform, view_visibility, clip, camera, parent, style, border_color) in - &uinode_query + for ( + node, + global_transform, + view_visibility, + clip, + camera, + parent, + style, + border_color, + border_radius, + ) in &uinode_query { let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) else { @@ -368,60 +505,40 @@ pub fn extract_uinode_borders( let bottom = resolve_border_thickness(style.border.bottom, parent_width, ui_logical_viewport_size); - // Calculate the border rects, ensuring no overlap. - // The border occupies the space between the node's bounding rect and the node's bounding rect inset in each direction by the node's corresponding border value. - let max = 0.5 * node.size(); - let min = -max; - let inner_min = min + Vec2::new(left, top); - let inner_max = (max - Vec2::new(right, bottom)).max(inner_min); - let border_rects = [ - // Left border - Rect { - min, - max: Vec2::new(inner_min.x, max.y), - }, - // Right border - Rect { - min: Vec2::new(inner_max.x, min.y), - max, - }, - // Top border - Rect { - min: Vec2::new(inner_min.x, min.y), - max: Vec2::new(inner_max.x, inner_min.y), - }, - // Bottom border - Rect { - min: Vec2::new(inner_min.x, inner_max.y), - max: Vec2::new(inner_max.x, max.y), - }, - ]; + let border = [left, top, right, bottom]; + let border_radius = resolve_border_radius( + border_radius, + node.size(), + ui_logical_viewport_size, + ui_scale.0, + ); + + let border_radius = clamp_radius(border_radius, node.size(), border.into()); let transform = global_transform.compute_matrix(); - for edge in border_rects { - if edge.min.x < edge.max.x && edge.min.y < edge.max.y { - extracted_uinodes.uinodes.insert( - commands.spawn_empty().id(), - ExtractedUiNode { - stack_index: node.stack_index, - // This translates the uinode's transform to the center of the current border rectangle - transform: transform * Mat4::from_translation(edge.center().extend(0.)), - color: border_color.0.into(), - rect: Rect { - max: edge.size(), - ..Default::default() - }, - image, - atlas_size: None, - clip: clip.map(|clip| clip.clip), - flip_x: false, - flip_y: false, - camera_entity, - }, - ); - } - } + extracted_uinodes.uinodes.insert( + commands.spawn_empty().id(), + ExtractedUiNode { + stack_index: node.stack_index, + // This translates the uinode's transform to the center of the current border rectangle + transform, + color: border_color.0.into(), + rect: Rect { + max: node.size(), + ..Default::default() + }, + image, + atlas_size: None, + clip: clip.map(|clip| clip.clip), + flip_x: false, + flip_y: false, + camera_entity, + border_radius, + border, + node_type: NodeType::Border, + }, + ); } } @@ -490,7 +607,6 @@ pub fn extract_uinode_outlines( ]; let transform = global_transform.compute_matrix(); - for edge in outline_edges { if edge.min.x < edge.max.x && edge.min.y < edge.max.y { extracted_uinodes.uinodes.insert( @@ -510,6 +626,9 @@ pub fn extract_uinode_outlines( flip_x: false, flip_y: false, camera_entity, + border: [0.; 4], + border_radius: [0.; 4], + node_type: NodeType::Rect, }, ); } @@ -640,10 +759,13 @@ pub fn extract_uinode_text( // * Multiply by the rounded physical position by the inverse scale factor to return to logical coordinates let logical_top_left = -0.5 * uinode.size(); - let physical_nearest_pixel = (logical_top_left * scale_factor).round(); - let logical_top_left_nearest_pixel = physical_nearest_pixel * inverse_scale_factor; - let transform = Mat4::from(global_transform.affine()) - * Mat4::from_translation(logical_top_left_nearest_pixel.extend(0.)); + + let mut transform = global_transform.affine() + * bevy_math::Affine3A::from_translation(logical_top_left.extend(0.)); + + transform.translation *= scale_factor; + transform.translation = transform.translation.round(); + transform.translation *= inverse_scale_factor; let mut color = LinearRgba::WHITE; let mut current_section = usize::MAX; @@ -677,6 +799,9 @@ pub fn extract_uinode_text( flip_x: false, flip_y: false, camera_entity, + border: [0.; 4], + border_radius: [0.; 4], + node_type: NodeType::Rect, }, ); } @@ -689,12 +814,23 @@ struct UiVertex { pub position: [f32; 3], pub uv: [f32; 2], pub color: [f32; 4], - pub mode: u32, + /// Shader flags to determine how to render the UI node. + /// See [`shader_flags`] for possible values. + pub flags: u32, + /// Border radius of the UI node. + /// Ordering: top left, top right, bottom right, bottom left. + pub radius: [f32; 4], + /// Border thickness of the UI node. + /// Ordering: left, top, right, bottom. + pub border: [f32; 4], + /// Size of the UI node. + pub size: [f32; 2], } #[derive(Resource)] pub struct UiMeta { vertices: BufferVec, + indices: BufferVec, view_bind_group: Option, } @@ -702,6 +838,7 @@ impl Default for UiMeta { fn default() -> Self { Self { vertices: BufferVec::new(BufferUsages::VERTEX), + indices: BufferVec::new(BufferUsages::INDEX), view_bind_group: None, } } @@ -723,8 +860,14 @@ pub struct UiBatch { pub camera: Entity, } -const TEXTURED_QUAD: u32 = 0; -const UNTEXTURED_QUAD: u32 = 1; +/// The values here should match the values for the constants in `ui.wgsl` +pub mod shader_flags { + pub const UNTEXTURED: u32 = 0; + pub const TEXTURED: u32 = 1; + /// Ordering: top left, top right, bottom right, bottom left. + pub const CORNERS: [u32; 4] = [0, 2, 2 | 4, 4]; + pub const BORDER: u32 = 8; +} #[allow(clippy::too_many_arguments)] pub fn queue_uinodes( @@ -799,14 +942,17 @@ pub fn prepare_uinodes( let mut batches: Vec<(Entity, UiBatch)> = Vec::with_capacity(*previous_len); ui_meta.vertices.clear(); + ui_meta.indices.clear(); ui_meta.view_bind_group = Some(render_device.create_bind_group( "ui_view_bind_group", &ui_pipeline.view_layout, &BindGroupEntries::single(view_binding), )); - // Vertex buffer index - let mut index = 0; + // Buffer indexes + let mut vertices_index = 0; + let mut indices_index = 0; + for mut ui_phase in &mut phases { let mut batch_item_index = 0; let mut batch_image_handle = AssetId::invalid(); @@ -829,7 +975,7 @@ pub fn prepare_uinodes( batch_image_handle = extracted_uinode.image; let new_batch = UiBatch { - range: index..index, + range: vertices_index..vertices_index, image: extracted_uinode.image, camera: extracted_uinode.camera_entity, }; @@ -879,10 +1025,10 @@ pub fn prepare_uinodes( } } - let mode = if extracted_uinode.image != AssetId::default() { - TEXTURED_QUAD + let mut flags = if extracted_uinode.image != AssetId::default() { + shader_flags::TEXTURED } else { - UNTEXTURED_QUAD + shader_flags::UNTEXTURED }; let mut uinode_rect = extracted_uinode.rect; @@ -943,7 +1089,7 @@ pub fn prepare_uinodes( continue; } } - let uvs = if mode == UNTEXTURED_QUAD { + let uvs = if flags == shader_flags::UNTEXTURED { [Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y] } else { let atlas_extent = extracted_uinode.atlas_size.unwrap_or(uinode_rect.max); @@ -983,16 +1129,30 @@ pub fn prepare_uinodes( }; let color = extracted_uinode.color.to_f32_array(); - for i in QUAD_INDICES { + if extracted_uinode.node_type == NodeType::Border { + flags |= shader_flags::BORDER; + } + + for i in 0..4 { ui_meta.vertices.push(UiVertex { position: positions_clipped[i].into(), uv: uvs[i].into(), color, - mode, + flags: flags | shader_flags::CORNERS[i], + radius: extracted_uinode.border_radius, + border: extracted_uinode.border, + size: transformed_rect_size.xy().into(), }); } - index += QUAD_INDICES.len() as u32; - existing_batch.unwrap().1.range.end = index; + + for &i in &QUAD_INDICES { + ui_meta.indices.push(indices_index + i as u32); + } + + vertices_index += 6; + indices_index += 4; + + existing_batch.unwrap().1.range.end = vertices_index; ui_phase.items[batch_item_index].batch_range_mut().end += 1; } else { batch_image_handle = AssetId::invalid(); @@ -1000,6 +1160,7 @@ pub fn prepare_uinodes( } } ui_meta.vertices.write_buffer(&render_device, &render_queue); + ui_meta.indices.write_buffer(&render_device, &render_queue); *previous_len = batches.len(); commands.insert_or_spawn_batch(batches); } diff --git a/crates/bevy_ui/src/render/pipeline.rs b/crates/bevy_ui/src/render/pipeline.rs index 6dad2b104c3bb..e31dc3bcfbba9 100644 --- a/crates/bevy_ui/src/render/pipeline.rs +++ b/crates/bevy_ui/src/render/pipeline.rs @@ -65,6 +65,12 @@ impl SpecializedRenderPipeline for UiPipeline { VertexFormat::Float32x4, // mode VertexFormat::Uint32, + // border radius + VertexFormat::Float32x4, + // border thickness + VertexFormat::Float32x4, + // border size + VertexFormat::Float32x2, ], ); let shader_defs = Vec::new(); diff --git a/crates/bevy_ui/src/render/render_pass.rs b/crates/bevy_ui/src/render/render_pass.rs index 1f3ffb0d20dea..892186478e678 100644 --- a/crates/bevy_ui/src/render/render_pass.rs +++ b/crates/bevy_ui/src/render/render_pass.rs @@ -215,8 +215,17 @@ impl RenderCommand

for DrawUiNode { return RenderCommandResult::Failure; }; - pass.set_vertex_buffer(0, ui_meta.into_inner().vertices.buffer().unwrap().slice(..)); - pass.draw(batch.range.clone(), 0..1); + let ui_meta = ui_meta.into_inner(); + // Store the vertices + pass.set_vertex_buffer(0, ui_meta.vertices.buffer().unwrap().slice(..)); + // Define how to "connect" the vertices + pass.set_index_buffer( + ui_meta.indices.buffer().unwrap().slice(..), + 0, + bevy_render::render_resource::IndexFormat::Uint32, + ); + // Draw the vertices + pass.draw_indexed(batch.range.clone(), 0, 0..1); RenderCommandResult::Success } } diff --git a/crates/bevy_ui/src/render/ui.wgsl b/crates/bevy_ui/src/render/ui.wgsl index aeb57aad81358..245f75eda6340 100644 --- a/crates/bevy_ui/src/render/ui.wgsl +++ b/crates/bevy_ui/src/render/ui.wgsl @@ -1,13 +1,27 @@ #import bevy_render::view::View -const TEXTURED_QUAD: u32 = 0u; +const TEXTURED = 1u; +const RIGHT_VERTEX = 2u; +const BOTTOM_VERTEX = 4u; +const BORDER: u32 = 8u; + +fn enabled(flags: u32, mask: u32) -> bool { + return (flags & mask) != 0u; +} @group(0) @binding(0) var view: View; struct VertexOutput { @location(0) uv: vec2, @location(1) color: vec4, - @location(3) @interpolate(flat) mode: u32, + + @location(2) @interpolate(flat) size: vec2, + @location(3) @interpolate(flat) flags: u32, + @location(4) @interpolate(flat) radius: vec4, + @location(5) @interpolate(flat) border: vec4, + + // Position relative to the center of the rectangle. + @location(6) point: vec2, @builtin(position) position: vec4, }; @@ -16,27 +30,144 @@ fn vertex( @location(0) vertex_position: vec3, @location(1) vertex_uv: vec2, @location(2) vertex_color: vec4, - @location(3) mode: u32, + @location(3) flags: u32, + + // x: top left, y: top right, z: bottom right, w: bottom left. + @location(4) radius: vec4, + + // x: left, y: top, z: right, w: bottom. + @location(5) border: vec4, + @location(6) size: vec2, ) -> VertexOutput { var out: VertexOutput; out.uv = vertex_uv; - out.position = view.view_proj * vec4(vertex_position, 1.0); + out.position = view.view_proj * vec4(vertex_position, 1.0); out.color = vertex_color; - out.mode = mode; + out.flags = flags; + out.radius = radius; + out.size = size; + out.border = border; + var point = 0.49999 * size; + if (flags & RIGHT_VERTEX) == 0u { + point.x *= -1.; + } + if (flags & BOTTOM_VERTEX) == 0u { + point.y *= -1.; + } + out.point = point; + return out; } @group(1) @binding(0) var sprite_texture: texture_2d; @group(1) @binding(1) var sprite_sampler: sampler; +// The returned value is the shortest distance from the given point to the boundary of the rounded +// box. +// +// Negative values indicate that the point is inside the rounded box, positive values that the point +// is outside, and zero is exactly on the boundary. +// +// Arguments: +// - `point` -> The function will return the distance from this point to the closest point on +// the boundary. +// - `size` -> The maximum width and height of the box. +// - `corner_radii` -> The radius of each rounded corner. Ordered counter clockwise starting +// top left: +// x: top left, y: top right, z: bottom right, w: bottom left. +fn sd_rounded_box(point: vec2, size: vec2, corner_radii: vec4) -> f32 { + // If 0.0 < y then select bottom left (w) and bottom right corner radius (z). + // Else select top left (x) and top right corner radius (y). + let rs = select(corner_radii.xy, corner_radii.wz, 0.0 < point.y); + // w and z are swapped so that both pairs are in left to right order, otherwise this second + // select statement would return the incorrect value for the bottom pair. + let radius = select(rs.x, rs.y, 0.0 < point.x); + // Vector from the corner closest to the point, to the point. + let corner_to_point = abs(point) - 0.5 * size; + // Vector from the center of the radius circle to the point. + let q = corner_to_point + radius; + // Length from center of the radius circle to the point, zeros a component if the point is not + // within the quadrant of the radius circle that is part of the curved corner. + let l = length(max(q, vec2(0.0))); + let m = min(max(q.x, q.y), 0.0); + return l + m - radius; +} + +fn sd_inset_rounded_box(point: vec2, size: vec2, radius: vec4, inset: vec4) -> f32 { + let inner_size = size - inset.xy - inset.zw; + let inner_center = inset.xy + 0.5 * inner_size - 0.5 * size; + let inner_point = point - inner_center; + + var r = radius; + + // Top left corner. + r.x = r.x - max(inset.x, inset.y); + + // Top right corner. + r.y = r.y - max(inset.z, inset.y); + + // Bottom right corner. + r.z = r.z - max(inset.z, inset.w); + + // Bottom left corner. + r.w = r.w - max(inset.x, inset.w); + + let half_size = inner_size * 0.5; + let min = min(half_size.x, half_size.y); + + r = min(max(r, vec4(0.0)), vec4(min)); + + return sd_rounded_box(inner_point, inner_size, r); +} + +fn draw(in: VertexOutput) -> vec4 { + let texture_color = textureSample(sprite_texture, sprite_sampler, in.uv); + + // Only use the color sampled from the texture if the `TEXTURED` flag is enabled. + // This allows us to draw both textured and untextured shapes together in the same batch. + let color = select(in.color, in.color * texture_color, enabled(in.flags, TEXTURED)); + + // Signed distances. The magnitude is the distance of the point from the edge of the shape. + // * Negative values indicate that the point is inside the shape. + // * Zero values indicate the point is on on the edge of the shape. + // * Positive values indicate the point is outside the shape. + + // Signed distance from the exterior boundary. + let external_distance = sd_rounded_box(in.point, in.size, in.radius); + + // Signed distance from the border's internal edge (the signed distance is negative if the point + // is inside the rect but not on the border). + // If the border size is set to zero, this is the same as as the external distance. + let internal_distance = sd_inset_rounded_box(in.point, in.size, in.radius, in.border); + + // Signed distance from the border (the intersection of the rect with its border). + // Points inside the border have negative signed distance. Any point outside the border, whether + // outside the outside edge, or inside the inner edge have positive signed distance. + let border_distance = max(external_distance, -internal_distance); + + // The `fwidth` function returns an approximation of the rate of change of the signed distance + // value that is used to ensure that the smooth alpha transition created by smoothstep occurs + // over a range of distance values that is proportional to how quickly the distance is changing. + let fborder = fwidth(border_distance); + let fexternal = fwidth(external_distance); + + if enabled(in.flags, BORDER) { + // The item is a border + + // At external edges with no border, `border_distance` is equal to zero. + // This select statement ensures we only perform anti-aliasing where a non-zero width border + // is present, otherwise an outline about the external boundary would be drawn even without + // a border. + let t = 1. - select(step(0.0, border_distance), smoothstep(0.0, fborder, border_distance), external_distance < internal_distance); + return color.rgba * t; + } + + // The item is a rectangle, draw normally with anti-aliasing at the edges. + let t = 1. - smoothstep(0.0, fexternal, external_distance); + return color.rgba * t; +} + @fragment fn fragment(in: VertexOutput) -> @location(0) vec4 { - // textureSample can only be called in unform control flow, not inside an if branch. - var color = textureSample(sprite_texture, sprite_sampler, in.uv); - if in.mode == TEXTURED_QUAD { - color = in.color * color; - } else { - color = in.color; - } - return color; + return draw(in); } diff --git a/crates/bevy_ui/src/render/ui_material_pipeline.rs b/crates/bevy_ui/src/render/ui_material_pipeline.rs index 171cd04d958ea..1245cf3012f7a 100644 --- a/crates/bevy_ui/src/render/ui_material_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_material_pipeline.rs @@ -691,6 +691,10 @@ pub fn prepare_ui_materials( ) { let queued_assets = std::mem::take(&mut prepare_next_frame.assets); for (id, material) in queued_assets { + if extracted_assets.removed.contains(&id) { + continue; + } + match prepare_ui_material( &material, &render_device, diff --git a/crates/bevy_ui/src/texture_slice.rs b/crates/bevy_ui/src/texture_slice.rs index c0836a3b870bb..16cfacff76670 100644 --- a/crates/bevy_ui/src/texture_slice.rs +++ b/crates/bevy_ui/src/texture_slice.rs @@ -10,7 +10,7 @@ use bevy_sprite::{ImageScaleMode, TextureAtlas, TextureAtlasLayout, TextureSlice use bevy_transform::prelude::*; use bevy_utils::HashSet; -use crate::{CalculatedClip, ExtractedUiNode, Node, UiImage}; +use crate::{CalculatedClip, ExtractedUiNode, Node, NodeType, UiImage}; /// Component storing texture slices for image nodes entities with a tiled or sliced [`ImageScaleMode`] /// @@ -68,6 +68,9 @@ impl ComputedTextureSlices { atlas_size, clip: clip.map(|clip| clip.clip), camera_entity, + border: [0.; 4], + border_radius: [0.; 4], + node_type: NodeType::Rect, } }) } diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 36db16ca60614..272220f3d52ae 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -23,7 +23,7 @@ use thiserror::Error; /// - [`RelativeCursorPosition`](crate::RelativeCursorPosition) /// to obtain the cursor position relative to this node /// - [`Interaction`](crate::Interaction) to obtain the interaction state of this node -#[derive(Component, Debug, Copy, Clone, Reflect)] +#[derive(Component, Debug, Copy, Clone, PartialEq, Reflect)] #[reflect(Component, Default)] pub struct Node { /// The order of the node in the UI layout. @@ -1590,7 +1590,7 @@ pub enum GridPlacementError { /// The background color of the node /// /// This serves as the "fill" color. -#[derive(Component, Copy, Clone, Debug, Reflect)] +#[derive(Component, Copy, Clone, Debug, PartialEq, Reflect)] #[reflect(Component, Default)] #[cfg_attr( feature = "serialize", @@ -1616,7 +1616,7 @@ impl> From for BackgroundColor { } /// The border color of the UI node. -#[derive(Component, Copy, Clone, Debug, Reflect)] +#[derive(Component, Copy, Clone, Debug, PartialEq, Reflect)] #[reflect(Component, Default)] #[cfg_attr( feature = "serialize", @@ -1796,7 +1796,7 @@ pub struct CalculatedClip { /// `ZIndex::Local(n)` and `ZIndex::Global(n)` for root nodes. /// /// Nodes without this component will be treated as if they had a value of `ZIndex::Local(0)`. -#[derive(Component, Copy, Clone, Debug, Reflect)] +#[derive(Component, Copy, Clone, Debug, PartialEq, Eq, Reflect)] #[reflect(Component, Default)] pub enum ZIndex { /// Indicates the order in which this node should be rendered relative to its siblings. @@ -1812,6 +1812,268 @@ impl Default for ZIndex { } } +/// Used to add rounded corners to a UI node. You can set a UI node to have uniformly +/// rounded corners or specify different radii for each corner. If a given radius exceeds half +/// the length of the smallest dimension between the node's height or width, the radius will +/// calculated as half the smallest dimension. +/// +/// Elliptical nodes are not supported yet. Percentage values are based on the node's smallest +/// dimension, either width or height. +/// +/// # Example +/// ``` +/// # use bevy_ecs::prelude::*; +/// # use bevy_ui::prelude::*; +/// # use bevy_color::palettes::basic::{BLUE}; +/// fn setup_ui(mut commands: Commands) { +/// commands.spawn(( +/// NodeBundle { +/// style: Style { +/// width: Val::Px(100.), +/// height: Val::Px(100.), +/// border: UiRect::all(Val::Px(2.)), +/// ..Default::default() +/// }, +/// background_color: BLUE.into(), +/// border_radius: BorderRadius::new( +/// // top left +/// Val::Px(10.), +/// // top right +/// Val::Px(20.), +/// // bottom right +/// Val::Px(30.), +/// // bottom left +/// Val::Px(40.), +/// ), +/// ..Default::default() +/// }, +/// )); +/// } +/// ``` +/// +/// +#[derive(Component, Copy, Clone, Debug, PartialEq, Reflect)] +#[reflect(PartialEq, Default)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub struct BorderRadius { + pub top_left: Val, + pub top_right: Val, + pub bottom_left: Val, + pub bottom_right: Val, +} + +impl Default for BorderRadius { + fn default() -> Self { + Self::DEFAULT + } +} + +impl BorderRadius { + pub const DEFAULT: Self = Self::ZERO; + + /// Zero curvature. All the corners will be right-angled. + pub const ZERO: Self = Self::all(Val::Px(0.)); + + /// Maximum curvature. The UI Node will take a capsule shape or circular if width and height are equal. + pub const MAX: Self = Self::all(Val::Px(f32::MAX)); + + #[inline] + /// Set all four corners to the same curvature. + pub const fn all(radius: Val) -> Self { + Self { + top_left: radius, + top_right: radius, + bottom_left: radius, + bottom_right: radius, + } + } + + #[inline] + pub const fn new(top_left: Val, top_right: Val, bottom_right: Val, bottom_left: Val) -> Self { + Self { + top_left, + top_right, + bottom_right, + bottom_left, + } + } + + #[inline] + /// Sets the radii to logical pixel values. + pub const fn px(top_left: f32, top_right: f32, bottom_right: f32, bottom_left: f32) -> Self { + Self { + top_left: Val::Px(top_left), + top_right: Val::Px(top_right), + bottom_right: Val::Px(bottom_right), + bottom_left: Val::Px(bottom_left), + } + } + + #[inline] + /// Sets the radii to percentage values. + pub const fn percent( + top_left: f32, + top_right: f32, + bottom_right: f32, + bottom_left: f32, + ) -> Self { + Self { + top_left: Val::Px(top_left), + top_right: Val::Px(top_right), + bottom_right: Val::Px(bottom_right), + bottom_left: Val::Px(bottom_left), + } + } + + #[inline] + /// Sets the radius for the top left corner. + /// Remaining corners will be right-angled. + pub const fn top_left(radius: Val) -> Self { + Self { + top_left: radius, + ..Self::DEFAULT + } + } + + #[inline] + /// Sets the radius for the top right corner. + /// Remaining corners will be right-angled. + pub const fn top_right(radius: Val) -> Self { + Self { + top_right: radius, + ..Self::DEFAULT + } + } + + #[inline] + /// Sets the radius for the bottom right corner. + /// Remaining corners will be right-angled. + pub const fn bottom_right(radius: Val) -> Self { + Self { + bottom_right: radius, + ..Self::DEFAULT + } + } + + #[inline] + /// Sets the radius for the bottom left corner. + /// Remaining corners will be right-angled. + pub const fn bottom_left(radius: Val) -> Self { + Self { + bottom_left: radius, + ..Self::DEFAULT + } + } + + #[inline] + /// Sets the radii for the top left and bottom left corners. + /// Remaining corners will be right-angled. + pub const fn left(radius: Val) -> Self { + Self { + top_left: radius, + bottom_left: radius, + ..Self::DEFAULT + } + } + + #[inline] + /// Sets the radii for the top right and bottom right corners. + /// Remaining corners will be right-angled. + pub const fn right(radius: Val) -> Self { + Self { + top_right: radius, + bottom_right: radius, + ..Self::DEFAULT + } + } + + #[inline] + /// Sets the radii for the top left and top right corners. + /// Remaining corners will be right-angled. + pub const fn top(radius: Val) -> Self { + Self { + top_left: radius, + top_right: radius, + ..Self::DEFAULT + } + } + + #[inline] + /// Sets the radii for the bottom left and bottom right corners. + /// Remaining corners will be right-angled. + pub const fn bottom(radius: Val) -> Self { + Self { + bottom_left: radius, + bottom_right: radius, + ..Self::DEFAULT + } + } + + /// Returns the [`BorderRadius`] with its `top_left` field set to the given value. + #[inline] + pub const fn with_top_left(mut self, radius: Val) -> Self { + self.top_left = radius; + self + } + + /// Returns the [`BorderRadius`] with its `top_right` field set to the given value. + #[inline] + pub const fn with_top_right(mut self, radius: Val) -> Self { + self.top_right = radius; + self + } + + /// Returns the [`BorderRadius`] with its `bottom_right` field set to the given value. + #[inline] + pub const fn with_bottom_right(mut self, radius: Val) -> Self { + self.bottom_right = radius; + self + } + + /// Returns the [`BorderRadius`] with its `bottom_left` field set to the given value. + #[inline] + pub const fn with_bottom_left(mut self, radius: Val) -> Self { + self.bottom_left = radius; + self + } + + /// Returns the [`BorderRadius`] with its `top_left` and `bottom_left` fields set to the given value. + #[inline] + pub const fn with_left(mut self, radius: Val) -> Self { + self.top_left = radius; + self.bottom_left = radius; + self + } + + /// Returns the [`BorderRadius`] with its `top_right` and `bottom_right` fields set to the given value. + #[inline] + pub const fn with_right(mut self, radius: Val) -> Self { + self.top_right = radius; + self.bottom_right = radius; + self + } + + /// Returns the [`BorderRadius`] with its `top_left` and `top_right` fields set to the given value. + #[inline] + pub const fn with_top(mut self, radius: Val) -> Self { + self.top_left = radius; + self.top_right = radius; + self + } + + /// Returns the [`BorderRadius`] with its `bottom_left` and `bottom_right` fields set to the given value. + #[inline] + pub const fn with_bottom(mut self, radius: Val) -> Self { + self.bottom_left = radius; + self.bottom_right = radius; + self + } +} + #[cfg(test)] mod tests { use crate::GridPlacement; diff --git a/crates/bevy_ui/src/widget/button.rs b/crates/bevy_ui/src/widget/button.rs index 6c7dced0f3bb0..d28c9ce5cdff9 100644 --- a/crates/bevy_ui/src/widget/button.rs +++ b/crates/bevy_ui/src/widget/button.rs @@ -4,6 +4,6 @@ use bevy_reflect::std_traits::ReflectDefault; use bevy_reflect::Reflect; /// Marker struct for buttons -#[derive(Component, Debug, Default, Clone, Copy, Reflect)] +#[derive(Component, Debug, Default, Clone, Copy, PartialEq, Eq, Reflect)] #[reflect(Component, Default)] pub struct Button; diff --git a/crates/bevy_utils/src/lib.rs b/crates/bevy_utils/src/lib.rs index 33f5312171041..d142f43fbc9ef 100644 --- a/crates/bevy_utils/src/lib.rs +++ b/crates/bevy_utils/src/lib.rs @@ -36,21 +36,36 @@ use hashbrown::hash_map::RawEntryMut; use std::{ any::TypeId, fmt::Debug, - future::Future, hash::{BuildHasher, BuildHasherDefault, Hash, Hasher}, marker::PhantomData, mem::ManuallyDrop, ops::Deref, - pin::Pin, }; -/// An owned and dynamically typed Future used when you can't statically type your result or need to add some indirection. #[cfg(not(target_arch = "wasm32"))] -pub type BoxedFuture<'a, T> = Pin + Send + 'a>>; +mod conditional_send { + /// Use [`ConditionalSend`] to mark an optional Send trait bound. Useful as on certain platforms (eg. WASM), + /// futures aren't Send. + pub trait ConditionalSend: Send {} + impl ConditionalSend for T {} +} -#[allow(missing_docs)] #[cfg(target_arch = "wasm32")] -pub type BoxedFuture<'a, T> = Pin + 'a>>; +#[allow(missing_docs)] +mod conditional_send { + pub trait ConditionalSend {} + impl ConditionalSend for T {} +} + +pub use conditional_send::*; + +/// Use [`ConditionalSendFuture`] for a future with an optional Send trait bound, as on certain platforms (eg. WASM), +/// futures aren't Send. +pub trait ConditionalSendFuture: std::future::Future + ConditionalSend {} +impl ConditionalSendFuture for T {} + +/// An owned and dynamically typed Future used when you can't statically type your result or need to add some indirection. +pub type BoxedFuture<'a, T> = std::pin::Pin + 'a>>; /// A shortcut alias for [`hashbrown::hash_map::Entry`]. pub type Entry<'a, K, V, S = BuildHasherDefault> = hashbrown::hash_map::Entry<'a, K, V, S>; diff --git a/crates/bevy_window/src/window.rs b/crates/bevy_window/src/window.rs index c241c9b71330a..ccc861a78d23b 100644 --- a/crates/bevy_window/src/window.rs +++ b/crates/bevy_window/src/window.rs @@ -259,6 +259,17 @@ pub struct Window { /// /// - **Android / Wayland / Web:** Unsupported. pub visible: bool, + /// Sets whether the window should be shown in the taskbar. + /// + /// If `true`, the window will not appear in the taskbar. + /// If `false`, the window will appear in the taskbar. + /// + /// Note that this will only take effect on window creation. + /// + /// ## Platform-specific + /// + /// - Only supported on Windows. + pub skip_taskbar: bool, } impl Default for Window { @@ -287,6 +298,7 @@ impl Default for Window { canvas: None, window_theme: None, visible: true, + skip_taskbar: false, } } } diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 9992178a78114..a50ecdf0c55e8 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -770,8 +770,8 @@ fn run_app_update_if_should( UpdateMode::Reactive { wait } | UpdateMode::ReactiveLowPower { wait } => { // TODO(bug): this is unexpected behavior. // When Reactive, user expects bevy to actually wait that amount of time, - // and not potentially infinitely depending on plateform specifics (which this does) - // Need to verify the plateform specifics (whether this can occur in + // and not potentially infinitely depending on platform specifics (which this does) + // Need to verify the platform specifics (whether this can occur in // rare-but-possible cases) and replace this with a panic or a log warn! if let Some(next) = runner_state.last_update.checked_add(*wait) { event_loop.set_control_flow(ControlFlow::WaitUntil(next)); diff --git a/crates/bevy_winit/src/winit_windows.rs b/crates/bevy_winit/src/winit_windows.rs index 4375e7c277744..de589a74543a4 100644 --- a/crates/bevy_winit/src/winit_windows.rs +++ b/crates/bevy_winit/src/winit_windows.rs @@ -104,6 +104,12 @@ impl WinitWindows { .with_transparent(window.transparent) .with_visible(window.visible); + #[cfg(target_os = "windows")] + { + use winit::platform::windows::WindowBuilderExtWindows; + winit_window_builder = winit_window_builder.with_skip_taskbar(window.skip_taskbar); + } + #[cfg(any( target_os = "linux", target_os = "dragonfly", diff --git a/examples/2d/bounding_2d.rs b/examples/2d/bounding_2d.rs index a360dfb6c0bf4..57ad9738e425b 100644 --- a/examples/2d/bounding_2d.rs +++ b/examples/2d/bounding_2d.rs @@ -284,15 +284,19 @@ fn setup(mut commands: Commands, loader: Res) { ); } +fn draw_filled_circle(gizmos: &mut Gizmos, position: Vec2, color: Srgba) { + for r in [1., 2., 3.] { + gizmos.circle_2d(position, r, color); + } +} + fn draw_ray(gizmos: &mut Gizmos, ray: &RayCast2d) { gizmos.line_2d( ray.ray.origin, ray.ray.origin + *ray.ray.direction * ray.max, WHITE, ); - for r in [1., 2., 3.] { - gizmos.circle_2d(ray.ray.origin, r, FUCHSIA); - } + draw_filled_circle(gizmos, ray.ray.origin, FUCHSIA); } fn get_and_draw_ray(gizmos: &mut Gizmos, time: &Time) -> RayCast2d { @@ -323,9 +327,11 @@ fn ray_cast_system( }; **intersects = toi.is_some(); if let Some(toi) = toi { - for r in [1., 2., 3.] { - gizmos.circle_2d(ray_cast.ray.origin + *ray_cast.ray.direction * toi, r, LIME); - } + draw_filled_circle( + &mut gizmos, + ray_cast.ray.origin + *ray_cast.ray.direction * toi, + LIME, + ); } } } @@ -350,9 +356,7 @@ 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 - + aabb_cast.aabb.center(), + aabb_cast.ray.ray.origin + *aabb_cast.ray.ray.direction * toi, 0., aabb_cast.aabb.half_size() * 2., LIME, @@ -381,9 +385,7 @@ 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 - + circle_cast.circle.center(), + circle_cast.ray.ray.origin + *circle_cast.ray.ray.direction * toi, circle_cast.circle.radius(), LIME, ); diff --git a/examples/3d/animated_material.rs b/examples/3d/animated_material.rs index f588295ec3a38..ecf18cea04a03 100644 --- a/examples/3d/animated_material.rs +++ b/examples/3d/animated_material.rs @@ -30,14 +30,19 @@ fn setup( )); let cube = meshes.add(Cuboid::new(0.5, 0.5, 0.5)); + + const GOLDEN_ANGLE: f32 = 137.507_77; + + let mut hsla = Hsla::hsl(0.0, 1.0, 0.5); for x in -1..2 { for z in -1..2 { commands.spawn(PbrBundle { mesh: cube.clone(), - material: materials.add(Color::WHITE), + material: materials.add(Color::from(hsla)), transform: Transform::from_translation(Vec3::new(x as f32, 0.0, z as f32)), ..default() }); + hsla = hsla.rotate_hue(GOLDEN_ANGLE); } } } @@ -47,13 +52,11 @@ fn animate_materials( time: Res