diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml index 7e6e7ceef2..4fd4c37e8a 100644 --- a/.github/workflows/daily.yml +++ b/.github/workflows/daily.yml @@ -99,6 +99,8 @@ jobs: os_version: "12.0" - device: "Samsung Galaxy S23" os_version: "13.0" + - device: "Google Pixel 8" + os_version: "14.0" steps: - uses: actions/checkout@v4 diff --git a/Cargo.toml b/Cargo.toml index 8619dc17e6..b32fb0cb6f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1411,6 +1411,16 @@ description = "Illustrates creating custom system parameters with `SystemParam`" category = "ECS (Entity Component System)" wasm = false +[[example]] +name = "time" +path = "examples/ecs/time.rs" + +[package.metadata.example.time] +name = "Time handling" +description = "Explains how Time is handled in ECS" +category = "ECS (Entity Component System)" +wasm = false + [[example]] name = "timers" path = "examples/ecs/timers.rs" @@ -1723,6 +1733,16 @@ description = "A shader and a material that uses it" category = "Shaders" wasm = true +[[example]] +name = "extended_material" +path = "examples/shader/extended_material.rs" + +[package.metadata.example.extended_material] +name = "Extended Material" +description = "A custom shader that builds on the standard material" +category = "Shaders" +wasm = true + [[example]] name = "shader_prepass" path = "examples/shader/shader_prepass.rs" diff --git a/assets/shaders/array_texture.wgsl b/assets/shaders/array_texture.wgsl index 05b0b85531..dd55487a69 100644 --- a/assets/shaders/array_texture.wgsl +++ b/assets/shaders/array_texture.wgsl @@ -46,5 +46,5 @@ fn fragment( ); pbr_input.V = fns::calculate_view(mesh.world_position, pbr_input.is_orthographic); - return tone_mapping(fns::pbr(pbr_input), view.color_grading); + return tone_mapping(fns::apply_pbr_lighting(pbr_input), view.color_grading); } diff --git a/assets/shaders/extended_material.wgsl b/assets/shaders/extended_material.wgsl new file mode 100644 index 0000000000..c39848c77d --- /dev/null +++ b/assets/shaders/extended_material.wgsl @@ -0,0 +1,53 @@ +#import bevy_pbr::pbr_fragment pbr_input_from_standard_material +#import bevy_pbr::pbr_functions alpha_discard + +#ifdef PREPASS_PIPELINE +#import bevy_pbr::prepass_io VertexOutput, FragmentOutput +#import bevy_pbr::pbr_deferred_functions deferred_output +#else +#import bevy_pbr::forward_io VertexOutput, FragmentOutput +#import bevy_pbr::pbr_functions apply_pbr_lighting, main_pass_post_lighting_processing +#endif + +struct MyExtendedMaterial { + quantize_steps: u32, +} + +@group(1) @binding(100) +var my_extended_material: MyExtendedMaterial; + +@fragment +fn fragment( + in: VertexOutput, + @builtin(front_facing) is_front: bool, +) -> FragmentOutput { + // generate a PbrInput struct from the StandardMaterial bindings + var pbr_input = pbr_input_from_standard_material(in, is_front); + + // we can optionally modify the input before lighting and alpha_discard is applied + pbr_input.material.base_color.b = pbr_input.material.base_color.r; + + // alpha discard + pbr_input.material.base_color = alpha_discard(pbr_input.material, pbr_input.material.base_color); + +#ifdef PREPASS_PIPELINE + // in deferred mode we can't modify anything after that, as lighting is run in a separate fullscreen shader. + let out = deferred_output(in, pbr_input); +#else + var out: FragmentOutput; + // apply lighting + out.color = apply_pbr_lighting(pbr_input); + + // we can optionally modify the lit color before post-processing is applied + out.color = vec4(vec4(out.color * f32(my_extended_material.quantize_steps))) / f32(my_extended_material.quantize_steps); + + // apply in-shader post processing (fog, alpha-premultiply, and also tonemapping, debanding if the camera is non-hdr) + // note this does not include fullscreen postprocessing effects like bloom. + out.color = main_pass_post_lighting_processing(pbr_input, out.color); + + // we can optionally modify the final result here + out.color = out.color * 2.0; +#endif + + return out; +} diff --git a/crates/bevy_asset/src/io/source.rs b/crates/bevy_asset/src/io/source.rs index ea07f8d39a..8ee192462a 100644 --- a/crates/bevy_asset/src/io/source.rs +++ b/crates/bevy_asset/src/io/source.rs @@ -241,20 +241,25 @@ impl AssetSourceBuilder { /// Returns a builder containing the "platform default source" for the given `path` and `processed_path`. /// For most platforms, this will use [`FileAssetReader`](crate::io::file::FileAssetReader) / [`FileAssetWriter`](crate::io::file::FileAssetWriter), /// but some platforms (such as Android) have their own default readers / writers / watchers. - pub fn platform_default(path: &str, processed_path: &str) -> Self { - Self::default() + pub fn platform_default(path: &str, processed_path: Option<&str>) -> Self { + let default = Self::default() .with_reader(AssetSource::get_default_reader(path.to_string())) .with_writer(AssetSource::get_default_writer(path.to_string())) .with_watcher(AssetSource::get_default_watcher( path.to_string(), Duration::from_millis(300), - )) - .with_processed_reader(AssetSource::get_default_reader(processed_path.to_string())) - .with_processed_writer(AssetSource::get_default_writer(processed_path.to_string())) - .with_processed_watcher(AssetSource::get_default_watcher( - processed_path.to_string(), - Duration::from_millis(300), - )) + )); + if let Some(processed_path) = processed_path { + default + .with_processed_reader(AssetSource::get_default_reader(processed_path.to_string())) + .with_processed_writer(AssetSource::get_default_writer(processed_path.to_string())) + .with_processed_watcher(AssetSource::get_default_watcher( + processed_path.to_string(), + Duration::from_millis(300), + )) + } else { + default + } } } @@ -315,7 +320,7 @@ impl AssetSourceBuilders { } /// Initializes the default [`AssetSourceBuilder`] if it has not already been set. - pub fn init_default_source(&mut self, path: &str, processed_path: &str) { + pub fn init_default_source(&mut self, path: &str, processed_path: Option<&str>) { self.default .get_or_insert_with(|| AssetSourceBuilder::platform_default(path, processed_path)); } diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index 148dc40e56..66af7f0216 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -122,7 +122,11 @@ impl Plugin for AssetPlugin { let mut sources = app .world .get_resource_or_insert_with::(Default::default); - sources.init_default_source(&self.file_path, &self.processed_file_path); + sources.init_default_source( + &self.file_path, + (!matches!(self.mode, AssetMode::Unprocessed)) + .then_some(self.processed_file_path.as_str()), + ); embedded.register_source(&mut sources); } { diff --git a/crates/bevy_asset/src/path.rs b/crates/bevy_asset/src/path.rs index 11168ca245..43b7d2cab5 100644 --- a/crates/bevy_asset/src/path.rs +++ b/crates/bevy_asset/src/path.rs @@ -203,6 +203,12 @@ impl<'a> AssetPath<'a> { self.label.as_deref() } + /// Gets the "sub-asset label". + #[inline] + pub fn label_cow(&self) -> Option> { + self.label.clone() + } + /// Gets the path to the asset in the "virtual filesystem". #[inline] pub fn path(&self) -> &Path { diff --git a/crates/bevy_asset/src/server/info.rs b/crates/bevy_asset/src/server/info.rs index baa4007001..f41057f622 100644 --- a/crates/bevy_asset/src/server/info.rs +++ b/crates/bevy_asset/src/server/info.rs @@ -69,6 +69,9 @@ pub(crate) struct AssetInfos { /// Tracks assets that depend on the "key" asset path inside their asset loaders ("loader dependencies") /// This should only be set when watching for changes to avoid unnecessary work. pub(crate) loader_dependants: HashMap, HashSet>>, + /// Tracks living labeled assets for a given source asset. + /// This should only be set when watching for changes to avoid unnecessary work. + pub(crate) living_labeled_assets: HashMap, HashSet>, pub(crate) handle_providers: HashMap, pub(crate) dependency_loaded_event_sender: HashMap, } @@ -88,6 +91,8 @@ impl AssetInfos { Self::create_handle_internal( &mut self.infos, &self.handle_providers, + &mut self.living_labeled_assets, + self.watching_for_changes, TypeId::of::(), None, None, @@ -107,6 +112,8 @@ impl AssetInfos { Self::create_handle_internal( &mut self.infos, &self.handle_providers, + &mut self.living_labeled_assets, + self.watching_for_changes, type_id, None, None, @@ -116,9 +123,12 @@ impl AssetInfos { ) } + #[allow(clippy::too_many_arguments)] fn create_handle_internal( infos: &mut HashMap, handle_providers: &HashMap, + living_labeled_assets: &mut HashMap, HashSet>, + watching_for_changes: bool, type_id: TypeId, path: Option>, meta_transform: Option, @@ -128,6 +138,16 @@ impl AssetInfos { .get(&type_id) .ok_or(MissingHandleProviderError(type_id))?; + if watching_for_changes { + if let Some(path) = &path { + let mut without_label = path.to_owned(); + if let Some(label) = without_label.take_label() { + let labels = living_labeled_assets.entry(without_label).or_default(); + labels.insert(label.to_string()); + } + } + } + let handle = provider.reserve_handle_internal(true, path.clone(), meta_transform); let mut info = AssetInfo::new(Arc::downgrade(&handle), path); if loading { @@ -136,6 +156,7 @@ impl AssetInfos { info.rec_dep_load_state = RecursiveDependencyLoadState::Loading; } infos.insert(handle.id, info); + Ok(UntypedHandle::Strong(handle)) } @@ -226,6 +247,8 @@ impl AssetInfos { let handle = Self::create_handle_internal( &mut self.infos, &self.handle_providers, + &mut self.living_labeled_assets, + self.watching_for_changes, type_id, Some(path), meta_transform, @@ -256,7 +279,7 @@ impl AssetInfos { Some(UntypedHandle::Strong(strong_handle)) } - /// Returns `true` if this path has + /// Returns `true` if the asset this path points to is still alive pub(crate) fn is_path_alive<'a>(&self, path: impl Into>) -> bool { let path = path.into(); if let Some(id) = self.path_to_id.get(&path) { @@ -267,12 +290,26 @@ impl AssetInfos { false } + /// Returns `true` if the asset at this path should be reloaded + pub(crate) fn should_reload(&self, path: &AssetPath) -> bool { + if self.is_path_alive(path) { + return true; + } + + if let Some(living) = self.living_labeled_assets.get(path) { + !living.is_empty() + } else { + false + } + } + // Returns `true` if the asset should be removed from the collection pub(crate) fn process_handle_drop(&mut self, id: UntypedAssetId) -> bool { Self::process_handle_drop_internal( &mut self.infos, &mut self.path_to_id, &mut self.loader_dependants, + &mut self.living_labeled_assets, self.watching_for_changes, id, ) @@ -521,6 +558,7 @@ impl AssetInfos { infos: &mut HashMap, path_to_id: &mut HashMap, UntypedAssetId>, loader_dependants: &mut HashMap, HashSet>>, + living_labeled_assets: &mut HashMap, HashSet>, watching_for_changes: bool, id: UntypedAssetId, ) -> bool { @@ -540,6 +578,18 @@ impl AssetInfos { dependants.remove(&path); } } + if let Some(label) = path.label() { + let mut without_label = path.to_owned(); + without_label.remove_label(); + if let Entry::Occupied(mut entry) = + living_labeled_assets.entry(without_label) + { + entry.get_mut().remove(label); + if entry.get().is_empty() { + entry.remove(); + } + }; + } } path_to_id.remove(&path); } @@ -566,6 +616,7 @@ impl AssetInfos { &mut self.infos, &mut self.path_to_id, &mut self.loader_dependants, + &mut self.living_labeled_assets, self.watching_for_changes, id.untyped(provider.type_id), ); diff --git a/crates/bevy_asset/src/server/mod.rs b/crates/bevy_asset/src/server/mod.rs index 257813acd6..160a3e4d32 100644 --- a/crates/bevy_asset/src/server/mod.rs +++ b/crates/bevy_asset/src/server/mod.rs @@ -257,7 +257,7 @@ impl AssetServer { path: impl Into>, meta_transform: Option, ) -> Handle { - let mut path = path.into().into_owned(); + let path = path.into().into_owned(); let (handle, should_load) = self.data.infos.write().get_or_create_path_handle::( path.clone(), HandleLoadingMode::Request, @@ -265,13 +265,10 @@ impl AssetServer { ); if should_load { - let mut owned_handle = Some(handle.clone().untyped()); + let owned_handle = Some(handle.clone().untyped()); let server = self.clone(); IoTaskPool::get() .spawn(async move { - if path.take_label().is_some() { - owned_handle = None; - } if let Err(err) = server.load_internal(owned_handle, path, false, None).await { error!("{}", err); } @@ -291,6 +288,10 @@ impl AssetServer { self.load_internal(None, path, false, None).await } + /// Performs an async asset load. + /// + /// `input_handle` must only be [`Some`] if `should_load` was true when retrieving `input_handle`. This is an optimization to + /// avoid looking up `should_load` twice, but it means you _must_ be sure a load is necessary when calling this function with [`Some`]. async fn load_internal<'a>( &self, input_handle: Option, @@ -298,7 +299,7 @@ impl AssetServer { force: bool, meta_transform: Option, ) -> Result { - let mut path = path.into_owned(); + let path = path.into_owned(); let path_clone = path.clone(); let (mut meta, loader, mut reader) = self .get_meta_loader_and_reader(&path_clone) @@ -312,18 +313,8 @@ impl AssetServer { e })?; - let has_label = path.label().is_some(); - let (handle, should_load) = match input_handle { Some(handle) => { - if !has_label && handle.type_id() != loader.asset_type_id() { - return Err(AssetLoadError::RequestedHandleTypeMismatch { - path: path.into_owned(), - requested: handle.type_id(), - actual_asset_name: loader.asset_type_name(), - loader_name: loader.type_name(), - }); - } // if a handle was passed in, the "should load" check was already done (handle, true) } @@ -339,37 +330,51 @@ impl AssetServer { } }; + if path.label().is_none() && handle.type_id() != loader.asset_type_id() { + return Err(AssetLoadError::RequestedHandleTypeMismatch { + path: path.into_owned(), + requested: handle.type_id(), + actual_asset_name: loader.asset_type_name(), + loader_name: loader.type_name(), + }); + } + if !should_load && !force { return Ok(handle); } - let base_asset_id = if has_label { - path.remove_label(); - // If the path has a label, the current id does not match the asset root type. - // We need to get the actual asset id + + let (base_handle, base_path) = if path.label().is_some() { let mut infos = self.data.infos.write(); - let (actual_handle, _) = infos.get_or_create_path_handle_untyped( - path.clone(), + let base_path = path.without_label().into_owned(); + let (base_handle, _) = infos.get_or_create_path_handle_untyped( + base_path.clone(), loader.asset_type_id(), loader.asset_type_name(), - // ignore current load state ... we kicked off this sub asset load because it needed to be loaded but - // does not currently exist HandleLoadingMode::Force, None, ); - actual_handle.id() + (base_handle, base_path) } else { - handle.id() + (handle.clone(), path.clone()) }; - if let Some(meta_transform) = handle.meta_transform() { + if let Some(meta_transform) = base_handle.meta_transform() { (*meta_transform)(&mut *meta); } match self - .load_with_meta_loader_and_reader(&path, meta, &*loader, &mut *reader, true, false) + .load_with_meta_loader_and_reader(&base_path, meta, &*loader, &mut *reader, true, false) .await { Ok(mut loaded_asset) => { + if let Some(label) = path.label_cow() { + if !loaded_asset.labeled_assets.contains_key(&label) { + return Err(AssetLoadError::MissingLabel { + base_path, + label: label.to_string(), + }); + } + } for (_, labeled_asset) in loaded_asset.labeled_assets.drain() { self.send_asset_event(InternalAssetEvent::Loaded { id: labeled_asset.handle.id(), @@ -377,13 +382,15 @@ impl AssetServer { }); } self.send_asset_event(InternalAssetEvent::Loaded { - id: base_asset_id, + id: base_handle.id(), loaded_asset, }); Ok(handle) } Err(err) => { - self.send_asset_event(InternalAssetEvent::Failed { id: base_asset_id }); + self.send_asset_event(InternalAssetEvent::Failed { + id: base_handle.id(), + }); Err(err) } } @@ -395,7 +402,7 @@ impl AssetServer { let path = path.into().into_owned(); IoTaskPool::get() .spawn(async move { - if server.data.infos.read().is_path_alive(&path) { + if server.data.infos.read().should_reload(&path) { info!("Reloading {path} because it has changed"); if let Err(err) = server.load_internal(None, path, true, None).await { error!("{}", err); @@ -935,6 +942,11 @@ pub enum AssetLoadError { loader_name: &'static str, error: Box, }, + #[error("The file at '{base_path}' does not contain the labeled asset '{label}'.")] + MissingLabel { + base_path: AssetPath<'static>, + label: String, + }, } /// An error that occurs when an [`AssetLoader`] is not registered for a given extension. diff --git a/crates/bevy_core/src/lib.rs b/crates/bevy_core/src/lib.rs index e5682d3a92..c222af6a9d 100644 --- a/crates/bevy_core/src/lib.rs +++ b/crates/bevy_core/src/lib.rs @@ -154,7 +154,10 @@ impl Plugin for FrameCountPlugin { } } -fn update_frame_count(mut frame_count: ResMut) { +/// A system used to increment [`FrameCount`] with wrapping addition. +/// +/// See [`FrameCount`] for more details. +pub fn update_frame_count(mut frame_count: ResMut) { frame_count.0 = frame_count.0.wrapping_add(1); } diff --git a/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs b/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs index a17a6a19d4..7d7360a3b7 100644 --- a/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs +++ b/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs @@ -2,7 +2,7 @@ use crate::{Diagnostic, DiagnosticId, Diagnostics, RegisterDiagnostic}; use bevy_app::prelude::*; use bevy_core::FrameCount; use bevy_ecs::prelude::*; -use bevy_time::Time; +use bevy_time::{Real, Time}; /// Adds "frame time" diagnostic to an App, specifically "frame time", "fps" and "frame count" #[derive(Default)] @@ -30,12 +30,12 @@ impl FrameTimeDiagnosticsPlugin { pub fn diagnostic_system( mut diagnostics: Diagnostics, - time: Res