diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d1d0909cac009..fc4f2beeea01e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -145,6 +145,31 @@ jobs: - name: Check wasm run: cargo check --target wasm32-unknown-unknown + build-wasm-atomics: + runs-on: ubuntu-latest + timeout-minutes: 30 + needs: build + steps: + - uses: actions/checkout@v4 + - uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ubuntu-assets-cargo-build-wasm-nightly-${{ hashFiles('**/Cargo.toml') }} + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.NIGHTLY_TOOLCHAIN }} + targets: wasm32-unknown-unknown + components: rust-src + - name: Check wasm + run: cargo check --target wasm32-unknown-unknown -Z build-std=std,panic_abort + env: + RUSTFLAGS: "-C target-feature=+atomics,+bulk-memory" + markdownlint: runs-on: ubuntu-latest timeout-minutes: 30 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index ee3c6d848c5af..5f163f757d5e9 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -49,6 +49,9 @@ jobs: echo "" > header.html - name: Build docs + env: + # needs to be in sync with [package.metadata.docs.rs] + RUSTDOCFLAGS: -Zunstable-options --cfg=docsrs run: cargo doc --all-features --no-deps -p bevy -Zunstable-options -Zrustdoc-scrape-examples # This adds the following: 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/CREDITS.md b/CREDITS.md index 8da7203793f52..cc8b15083ac69 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -28,6 +28,8 @@ * FiraMono by The Mozilla Foundation and Telefonica S.A (SIL Open Font License, Version 1.1: assets/fonts/FiraMono-LICENSE) * Barycentric from [mk_bary_gltf](https://github.com/komadori/mk_bary_gltf) (MIT OR Apache-2.0) * `MorphStressTest.gltf`, [MorphStressTest] ([CC-BY 4.0] by Analytical Graphics, Inc, Model and textures by Ed Mackey) +* Mysterious acoustic guitar music sample from [florianreichelt](https://freesound.org/people/florianreichelt/sounds/412429/) (CC0 license) +* Epic orchestra music sample, modified to loop, from [Migfus20](https://freesound.org/people/Migfus20/sounds/560449/) ([CC BY 4.0 DEED](https://creativecommons.org/licenses/by/4.0/)) [MorphStressTest]: https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/MorphStressTest [fox]: https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/Fox diff --git a/Cargo.toml b/Cargo.toml index 9c43da5a15bd1..019ce0c6e9059 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ license = "MIT OR Apache-2.0" readme = "README.md" repository = "https://github.com/bevyengine/bevy" documentation = "https://docs.rs/bevy" -rust-version = "1.76.0" +rust-version = "1.77.0" [workspace] exclude = [ @@ -47,6 +47,7 @@ ptr_cast_constness = "warn" [workspace.lints.rust] unsafe_op_in_unsafe_fn = "warn" missing_docs = "warn" +unsafe_code = "deny" [lints] workspace = true @@ -321,6 +322,12 @@ embedded_watcher = ["bevy_internal/embedded_watcher"] # Enable stepping-based debugging of Bevy systems bevy_debug_stepping = ["bevy_internal/bevy_debug_stepping"] +# Enables the meshlet renderer for dense high-poly scenes (experimental) +meshlet = ["bevy_internal/meshlet"] + +# Enables processing meshes into meshlet meshes for bevy_pbr +meshlet_processor = ["bevy_internal/meshlet_processor"] + # Enable support for the ios_simulator by downgrading some rendering capabilities ios_simulator = ["bevy_internal/ios_simulator"] @@ -330,6 +337,7 @@ bevy_internal = { path = "crates/bevy_internal", version = "0.14.0-dev", default [dev-dependencies] rand = "0.8.0" +rand_chacha = "0.3.1" ron = "0.8.0" flate2 = "1.0" serde = { version = "1", features = ["derive"] } @@ -554,6 +562,17 @@ description = "Showcases bounding volumes and intersection tests" category = "2D Rendering" wasm = true +[[example]] +name = "wireframe_2d" +path = "examples/2d/wireframe_2d.rs" +doc-scrape-examples = true + +[package.metadata.example.wireframe_2d] +name = "2D Wireframe" +description = "Showcases wireframes for 2d meshes" +category = "2D Rendering" +wasm = false + # 3D Rendering [[example]] name = "3d_scene" @@ -949,6 +968,18 @@ description = "Demonstrates irradiance volumes" category = "3D Rendering" wasm = false +[[example]] +name = "meshlet" +path = "examples/3d/meshlet.rs" +doc-scrape-examples = true +required-features = ["meshlet"] + +[package.metadata.example.meshlet] +name = "Meshlet" +description = "Meshlet rendering for dense high-poly scenes (experimental)" +category = "3D Rendering" +wasm = false + [[example]] name = "lightmaps" path = "examples/3d/lightmaps.rs" @@ -1013,6 +1044,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" @@ -1347,6 +1389,17 @@ description = "Shows how to create and register a custom audio source by impleme category = "Audio" wasm = true +[[example]] +name = "soundtrack" +path = "examples/audio/soundtrack.rs" +doc-scrape-examples = true + +[package.metadata.example.soundtrack] +name = "Soundtrack" +description = "Shows how to play different soundtracks based on game state" +category = "Audio" +wasm = true + [[example]] name = "spatial_audio_2d" path = "examples/audio/spatial_audio_2d.rs" @@ -2340,6 +2393,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" @@ -2798,5 +2862,6 @@ lto = "fat" panic = "abort" [package.metadata.docs.rs] -cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] all-features = true +cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] diff --git a/assets/models/bunny.meshlet_mesh b/assets/models/bunny.meshlet_mesh new file mode 100644 index 0000000000000..735d002601293 Binary files /dev/null and b/assets/models/bunny.meshlet_mesh differ diff --git a/assets/sounds/Epic orchestra music.ogg b/assets/sounds/Epic orchestra music.ogg new file mode 100644 index 0000000000000..c88b99a23762b Binary files /dev/null and b/assets/sounds/Epic orchestra music.ogg differ diff --git a/assets/sounds/Mysterious acoustic guitar.ogg b/assets/sounds/Mysterious acoustic guitar.ogg new file mode 100644 index 0000000000000..5dae3e7080fb7 Binary files /dev/null and b/assets/sounds/Mysterious acoustic guitar.ogg differ diff --git a/benches/Cargo.toml b/benches/Cargo.toml index e64a79d2b389a..ee9d45953a6a3 100644 --- a/benches/Cargo.toml +++ b/benches/Cargo.toml @@ -17,6 +17,7 @@ bevy_reflect = { path = "../crates/bevy_reflect" } bevy_tasks = { path = "../crates/bevy_tasks" } bevy_utils = { path = "../crates/bevy_utils" } bevy_math = { path = "../crates/bevy_math" } +bevy_render = { path = "../crates/bevy_render" } [profile.release] opt-level = 3 @@ -62,6 +63,11 @@ name = "bezier" path = "benches/bevy_math/bezier.rs" harness = false +[[bench]] +name = "torus" +path = "benches/bevy_render/torus.rs" +harness = false + [[bench]] name = "entity_hash" path = "benches/bevy_ecs/world/entity_hash.rs" diff --git a/benches/benches/bevy_render/torus.rs b/benches/benches/bevy_render/torus.rs new file mode 100644 index 0000000000000..8ec81c80409d8 --- /dev/null +++ b/benches/benches/bevy_render/torus.rs @@ -0,0 +1,15 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; + +use bevy_render::mesh::TorusMeshBuilder; + +fn torus(c: &mut Criterion) { + c.bench_function("build_torus", |b| { + b.iter(|| black_box(TorusMeshBuilder::new(black_box(0.5),black_box(1.0)))); + }); +} + +criterion_group!( + benches, + torus, +); +criterion_main!(benches); diff --git a/crates/bevy_a11y/Cargo.toml b/crates/bevy_a11y/Cargo.toml index 1f13d6e8a52d8..4ee262fa22974 100644 --- a/crates/bevy_a11y/Cargo.toml +++ b/crates/bevy_a11y/Cargo.toml @@ -18,3 +18,7 @@ accesskit = "0.12" [lints] workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +all-features = true diff --git a/crates/bevy_a11y/src/lib.rs b/crates/bevy_a11y/src/lib.rs index 53a515c7eeb6d..aab4c23f2c860 100644 --- a/crates/bevy_a11y/src/lib.rs +++ b/crates/bevy_a11y/src/lib.rs @@ -1,6 +1,11 @@ -//! Accessibility for Bevy - #![forbid(unsafe_code)] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] + +//! Accessibility for Bevy use std::sync::{ atomic::{AtomicBool, Ordering}, diff --git a/crates/bevy_animation/Cargo.toml b/crates/bevy_animation/Cargo.toml index c6cc5275f5255..52c2786b849c7 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" @@ -39,3 +40,7 @@ uuid = { version = "1.7", features = ["v4"] } [lints] workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +all-features = true diff --git a/crates/bevy_animation/src/animatable.rs b/crates/bevy_animation/src/animatable.rs index 17e11bfe5fd2c..666485a305d30 100644 --- a/crates/bevy_animation/src/animatable.rs +++ b/crates/bevy_animation/src/animatable.rs @@ -1,9 +1,9 @@ use crate::util; +use bevy_color::{ClampColor, Laba, LinearRgba, Oklaba, Srgba, Xyza}; use bevy_ecs::world::World; use bevy_math::*; use bevy_reflect::Reflect; use bevy_transform::prelude::Transform; -use bevy_utils::FloatOrd; /// An individual input for [`Animatable::blend`]. pub struct BlendInput { @@ -57,6 +57,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 +92,12 @@ 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!(Srgba); +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_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index d3dbed8406193..c4f8bc9248e6b 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -1,3 +1,10 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![forbid(unsafe_code)] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] + //! Animation for the game engine Bevy mod animatable; diff --git a/crates/bevy_app/Cargo.toml b/crates/bevy_app/Cargo.toml index 28b272571edd3..15f511ceb30a2 100644 --- a/crates/bevy_app/Cargo.toml +++ b/crates/bevy_app/Cargo.toml @@ -31,9 +31,11 @@ thiserror = "1.0" [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen = { version = "0.2" } web-sys = { version = "0.3", features = ["Window"] } +console_error_panic_hook = "0.1.6" [lints] workspace = true [package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] all-features = true diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index 10f567f2f211c..7b57f321e07e1 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -6,6 +6,7 @@ use bevy_ecs::{ common_conditions::run_once as run_once_condition, run_enter_schedule, InternedScheduleLabel, ScheduleBuildSettings, ScheduleLabel, }, + system::SystemId, }; use bevy_utils::{intern::Interned, tracing::debug, HashMap, HashSet}; use std::{ @@ -453,6 +454,22 @@ impl App { self } + /// Registers a system and returns a [`SystemId`] so it can later be called by [`World::run_system`]. + /// + /// It's possible to register the same systems more than once, they'll be stored separately. + /// + /// This is different from adding systems to a [`Schedule`] with [`App::add_systems`], + /// because the [`SystemId`] that is returned can be used anywhere in the [`World`] to run the associated system. + /// This allows for running systems in a push-based fashion. + /// Using a [`Schedule`] is still preferred for most cases + /// due to its better performance and ability to run non-conflicting systems simultaneously. + pub fn register_system + 'static>( + &mut self, + system: S, + ) -> SystemId { + self.world.register_system(system) + } + /// Configures a collection of system sets in the provided schedule, adding any sets that do not exist. #[track_caller] pub fn configure_sets( diff --git a/crates/bevy_app/src/lib.rs b/crates/bevy_app/src/lib.rs index d87bcbde130a6..232c3e801e73a 100644 --- a/crates/bevy_app/src/lib.rs +++ b/crates/bevy_app/src/lib.rs @@ -1,8 +1,15 @@ -//! This crate is about everything concerning the highest-level, application layer of a Bevy app. #![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![forbid(unsafe_code)] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] + +//! This crate is about everything concerning the highest-level, application layer of a Bevy app. mod app; mod main_schedule; +mod panic_handler; mod plugin; mod plugin_group; mod schedule_runner; @@ -10,6 +17,7 @@ mod schedule_runner; pub use app::*; pub use bevy_derive::DynamicPlugin; pub use main_schedule::*; +pub use panic_handler::*; pub use plugin::*; pub use plugin_group::*; pub use schedule_runner::*; diff --git a/crates/bevy_app/src/panic_handler.rs b/crates/bevy_app/src/panic_handler.rs new file mode 100644 index 0000000000000..d66062e104f41 --- /dev/null +++ b/crates/bevy_app/src/panic_handler.rs @@ -0,0 +1,52 @@ +//! This module 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. + +use crate::App; +use crate::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, 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, 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_asset/Cargo.toml b/crates/bevy_asset/Cargo.toml index 62f499857d925..04398df8ec254 100644 --- a/crates/bevy_asset/Cargo.toml +++ b/crates/bevy_asset/Cargo.toml @@ -61,4 +61,5 @@ bevy_log = { path = "../bevy_log", version = "0.14.0-dev" } workspace = true [package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] all-features = true diff --git a/crates/bevy_asset/macros/Cargo.toml b/crates/bevy_asset/macros/Cargo.toml index 21d6b1fdcda1a..f178067cc19f3 100644 --- a/crates/bevy_asset/macros/Cargo.toml +++ b/crates/bevy_asset/macros/Cargo.toml @@ -20,3 +20,7 @@ quote = "1.0" [lints] workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +all-features = true diff --git a/crates/bevy_asset/macros/src/lib.rs b/crates/bevy_asset/macros/src/lib.rs index 8dc8975f2352b..6c290367e64f2 100644 --- a/crates/bevy_asset/macros/src/lib.rs +++ b/crates/bevy_asset/macros/src/lib.rs @@ -1,5 +1,6 @@ // FIXME(3492): remove once docs are ready #![allow(missing_docs)] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] use bevy_macro_utils::BevyManifest; use proc_macro::{Span, TokenStream}; 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/handle.rs b/crates/bevy_asset/src/handle.rs index d14c325973c77..b2ebea2af9024 100644 --- a/crates/bevy_asset/src/handle.rs +++ b/crates/bevy_asset/src/handle.rs @@ -249,30 +249,30 @@ impl PartialEq for Handle { impl Eq for Handle {} -impl From> for AssetId { +impl From<&Handle> for AssetId { #[inline] - fn from(value: Handle) -> Self { + fn from(value: &Handle) -> Self { value.id() } } -impl From<&Handle> for AssetId { +impl From<&Handle> for UntypedAssetId { #[inline] fn from(value: &Handle) -> Self { - value.id() + value.id().into() } } -impl From> for UntypedAssetId { +impl From<&mut Handle> for AssetId { #[inline] - fn from(value: Handle) -> Self { - value.id().into() + fn from(value: &mut Handle) -> Self { + value.id() } } -impl From<&Handle> for UntypedAssetId { +impl From<&mut Handle> for UntypedAssetId { #[inline] - fn from(value: &Handle) -> Self { + fn from(value: &mut Handle) -> Self { value.id().into() } } @@ -429,13 +429,6 @@ impl PartialOrd for UntypedHandle { } } -impl From for UntypedAssetId { - #[inline] - fn from(value: UntypedHandle) -> Self { - value.id() - } -} - impl From<&UntypedHandle> for UntypedAssetId { #[inline] fn from(value: &UntypedHandle) -> Self { 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/embedded/mod.rs b/crates/bevy_asset/src/io/embedded/mod.rs index b953e1e669f08..d8f85c315cbe6 100644 --- a/crates/bevy_asset/src/io/embedded/mod.rs +++ b/crates/bevy_asset/src/io/embedded/mod.rs @@ -254,7 +254,7 @@ pub fn watched_path(_source_file_path: &'static str, _asset_path: &'static str) macro_rules! load_internal_asset { ($app: ident, $handle: expr, $path_str: expr, $loader: expr) => {{ let mut assets = $app.world.resource_mut::<$crate::Assets<_>>(); - assets.insert($handle, ($loader)( + assets.insert($handle.id(), ($loader)( include_str!($path_str), std::path::Path::new(file!()) .parent() @@ -266,7 +266,7 @@ macro_rules! load_internal_asset { // we can't support params without variadic arguments, so internal assets with additional params can't be hot-reloaded ($app: ident, $handle: ident, $path_str: expr, $loader: expr $(, $param:expr)+) => {{ let mut assets = $app.world.resource_mut::<$crate::Assets<_>>(); - assets.insert($handle, ($loader)( + assets.insert($handle.id(), ($loader)( include_str!($path_str), std::path::Path::new(file!()) .parent() @@ -284,7 +284,7 @@ macro_rules! load_internal_binary_asset { ($app: ident, $handle: expr, $path_str: expr, $loader: expr) => {{ let mut assets = $app.world.resource_mut::<$crate::Assets<_>>(); assets.insert( - $handle, + $handle.id(), ($loader)( include_bytes!($path_str).as_ref(), std::path::Path::new(file!()) 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..2b7136e5d33b1 100644 --- a/crates/bevy_asset/src/io/wasm.rs +++ b/crates/bevy_asset/src/io/wasm.rs @@ -2,13 +2,27 @@ 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}; +use wasm_bindgen::{prelude::wasm_bindgen, JsCast, JsValue}; use wasm_bindgen_futures::JsFuture; use web_sys::Response; +/// Represents the global object in the JavaScript context +#[wasm_bindgen] +extern "C" { + /// The [Global](https://developer.mozilla.org/en-US/docs/Glossary/Global_object) object. + type Global; + + /// The [window](https://developer.mozilla.org/en-US/docs/Web/API/Window) global object. + #[wasm_bindgen(method, getter, js_name = Window)] + fn window(this: &Global) -> JsValue; + + /// The [WorkerGlobalScope](https://developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope) global object. + #[wasm_bindgen(method, getter, js_name = WorkerGlobalScope)] + fn worker(this: &Global) -> JsValue; +} + /// Reader implementation for loading assets via HTTP in WASM. pub struct HttpWasmAssetReader { root_path: PathBuf, @@ -38,8 +52,22 @@ fn js_value_to_err<'a>(context: &'a str) -> impl FnOnce(JsValue) -> std::io::Err impl HttpWasmAssetReader { async fn fetch_bytes<'a>(&self, path: PathBuf) -> Result>, AssetReaderError> { - let window = web_sys::window().unwrap(); - let resp_value = JsFuture::from(window.fetch_with_str(path.to_str().unwrap())) + // The JS global scope includes a self-reference via a specialising name, which can be used to determine the type of global context available. + let global: Global = js_sys::global().unchecked_into(); + let promise = if !global.window().is_undefined() { + let window: web_sys::Window = global.unchecked_into(); + window.fetch_with_str(path.to_str().unwrap()) + } else if !global.worker().is_undefined() { + let worker: web_sys::WorkerGlobalScope = global.unchecked_into(); + worker.fetch_with_str(path.to_str().unwrap()) + } else { + let error = std::io::Error::new( + std::io::ErrorKind::Other, + "Unsupported JavaScript global context", + ); + return Err(AssetReaderError::Io(error.into())); + }; + let resp_value = JsFuture::from(promise) .await .map_err(js_value_to_err("fetch path"))?; let resp = resp_value @@ -59,40 +87,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..4c525524b01b4 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -1,6 +1,10 @@ // FIXME(3492): remove once docs are ready #![allow(missing_docs)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] pub mod io; pub mod meta; @@ -40,8 +44,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 +450,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 +499,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 +560,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 +599,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/path.rs b/crates/bevy_asset/src/path.rs index e49db629362f0..e61e27e5dced1 100644 --- a/crates/bevy_asset/src/path.rs +++ b/crates/bevy_asset/src/path.rs @@ -1,15 +1,10 @@ use crate::io::AssetSourceId; -use bevy_reflect::{ - std_traits::ReflectDefault, utility::NonGenericTypeInfoCell, FromReflect, FromType, - GetTypeRegistration, Reflect, ReflectDeserialize, ReflectFromPtr, ReflectFromReflect, - ReflectKind, ReflectMut, ReflectOwned, ReflectRef, ReflectSerialize, TypeInfo, TypePath, - TypeRegistration, Typed, ValueInfo, -}; +use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; use bevy_utils::CowArc; use serde::{de::Visitor, Deserialize, Serialize}; use std::{ fmt::{Debug, Display}, - hash::{Hash, Hasher}, + hash::Hash, ops::Deref, path::{Path, PathBuf}, }; @@ -52,7 +47,8 @@ use thiserror::Error; /// This means that the common case of `asset_server.load("my_scene.scn")` when it creates and /// clones internal owned [`AssetPaths`](AssetPath). /// This also means that you should use [`AssetPath::parse`] in cases where `&str` is the explicit type. -#[derive(Eq, PartialEq, Hash, Clone, Default)] +#[derive(Eq, PartialEq, Hash, Clone, Default, Reflect)] +#[reflect_value(Debug, PartialEq, Hash, Serialize, Deserialize)] pub struct AssetPath<'a> { source: AssetSourceId<'a>, path: CowArc<'a, Path>, @@ -572,136 +568,6 @@ impl<'de> Visitor<'de> for AssetPathVisitor { } } -// NOTE: We manually implement "reflect value" because deriving Reflect on `AssetPath` breaks dynamic linking -// See https://github.com/bevyengine/bevy/issues/9747 -// NOTE: This could use `impl_reflect_value` if it supported static lifetimes. - -impl GetTypeRegistration for AssetPath<'static> { - fn get_type_registration() -> TypeRegistration { - let mut registration = TypeRegistration::of::(); - registration.insert::(FromType::::from_type()); - registration.insert::(FromType::::from_type()); - registration.insert::(FromType::::from_type()); - registration.insert::(FromType::::from_type()); - registration.insert::(FromType::::from_type()); - registration - } -} - -impl TypePath for AssetPath<'static> { - fn type_path() -> &'static str { - "bevy_asset::path::AssetPath<'static>" - } - fn short_type_path() -> &'static str { - "AssetPath<'static>" - } - fn type_ident() -> Option<&'static str> { - Some("AssetPath<'static>") - } - fn crate_name() -> Option<&'static str> { - None - } - fn module_path() -> Option<&'static str> { - None - } -} -impl Typed for AssetPath<'static> { - fn type_info() -> &'static TypeInfo { - static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); - CELL.get_or_set(|| { - let info = ValueInfo::new::(); - TypeInfo::Value(info) - }) - } -} -impl Reflect for AssetPath<'static> { - #[inline] - fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { - Some(::type_info()) - } - #[inline] - fn into_any(self: Box) -> Box { - self - } - #[inline] - fn as_any(&self) -> &dyn core::any::Any { - self - } - #[inline] - fn as_any_mut(&mut self) -> &mut dyn core::any::Any { - self - } - #[inline] - fn into_reflect(self: Box) -> Box { - self - } - #[inline] - fn as_reflect(&self) -> &dyn Reflect { - self - } - #[inline] - fn as_reflect_mut(&mut self) -> &mut dyn Reflect { - self - } - #[inline] - fn apply(&mut self, value: &dyn Reflect) { - let value = Reflect::as_any(value); - if let Some(value) = value.downcast_ref::() { - *self = value.clone(); - } else { - panic!("Value is not {}.", std::any::type_name::()); - } - } - #[inline] - fn set( - &mut self, - value: Box, - ) -> Result<(), Box> { - *self = ::take(value)?; - Ok(()) - } - fn reflect_kind(&self) -> ReflectKind { - ReflectKind::Value - } - fn reflect_ref(&self) -> ReflectRef { - ReflectRef::Value(self) - } - fn reflect_mut(&mut self) -> ReflectMut { - ReflectMut::Value(self) - } - fn reflect_owned(self: Box) -> ReflectOwned { - ReflectOwned::Value(self) - } - #[inline] - fn clone_value(&self) -> Box { - Box::new(self.clone()) - } - fn reflect_hash(&self) -> Option { - let mut hasher = bevy_reflect::utility::reflect_hasher(); - Hash::hash(&::core::any::Any::type_id(self), &mut hasher); - Hash::hash(self, &mut hasher); - Some(Hasher::finish(&hasher)) - } - fn reflect_partial_eq(&self, value: &dyn Reflect) -> Option { - let value = ::as_any(value); - if let Some(value) = ::downcast_ref::(value) { - Some(PartialEq::eq(self, value)) - } else { - Some(false) - } - } - fn debug(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - ::core::fmt::Debug::fmt(self, f) - } -} -impl FromReflect for AssetPath<'static> { - fn from_reflect(reflect: &dyn Reflect) -> Option { - Some(Clone::clone(::downcast_ref::< - AssetPath<'static>, - >(::as_any(reflect))?)) - } -} - /// Normalizes the path by collapsing all occurrences of '.' and '..' dot-segments where possible /// as per [RFC 1808](https://datatracker.ietf.org/doc/html/rfc1808) pub(crate) fn normalize_path(path: &Path) -> PathBuf { diff --git a/crates/bevy_asset/src/processor/mod.rs b/crates/bevy_asset/src/processor/mod.rs index ace12c8f7301b..f3d373da063f9 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, @@ -19,7 +20,7 @@ use crate::{ use bevy_ecs::prelude::*; use bevy_tasks::IoTaskPool; use bevy_utils::tracing::{debug, error, trace, warn}; -use bevy_utils::{BoxedFuture, HashMap, HashSet}; +use bevy_utils::{HashMap, HashSet}; use futures_io::ErrorKind; use futures_lite::{AsyncReadExt, AsyncWriteExt, StreamExt}; use parking_lot::RwLock; @@ -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`]. /// @@ -430,27 +435,25 @@ impl AssetProcessor { #[allow(unused)] #[cfg(all(not(target_arch = "wasm32"), feature = "multi-threaded"))] - fn process_assets_internal<'scope>( + async fn process_assets_internal<'scope>( &'scope self, scope: &'scope bevy_tasks::Scope<'scope, '_, ()>, source: &'scope AssetSource, path: PathBuf, - ) -> BoxedFuture<'scope, Result<(), AssetReaderError>> { - Box::pin(async move { - if source.reader().is_directory(&path).await? { - let mut path_stream = source.reader().read_directory(&path).await?; - while let Some(path) = path_stream.next().await { - self.process_assets_internal(scope, source, path).await?; - } - } else { - // Files without extensions are skipped - let processor = self.clone(); - scope.spawn(async move { - processor.process_asset(source, path).await; - }); + ) -> Result<(), AssetReaderError> { + if source.reader().is_directory(&path).await? { + let mut path_stream = source.reader().read_directory(&path).await?; + while let Some(path) = path_stream.next().await { + Box::pin(self.process_assets_internal(scope, source, path)).await?; } - Ok(()) - }) + } else { + // Files without extensions are skipped + let processor = self.clone(); + scope.spawn(async move { + processor.process_asset(source, path).await; + }); + } + Ok(()) } async fn try_reprocessing_queued(&self) { @@ -509,34 +512,36 @@ 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>, + async fn get_asset_paths<'a>( + reader: &'a dyn ErasedAssetReader, + clean_empty_folders_writer: Option<&'a dyn ErasedAssetWriter>, path: PathBuf, paths: &'a mut Vec, - ) -> BoxedFuture<'a, Result> { - Box::pin(async move { - if reader.is_directory(&path).await? { - let mut path_stream = reader.read_directory(&path).await?; - let mut contains_files = false; - while let Some(child_path) = path_stream.next().await { - contains_files = - get_asset_paths(reader, clean_empty_folders_writer, child_path, paths) - .await? - && contains_files; - } - if !contains_files && path.parent().is_some() { - if let Some(writer) = clean_empty_folders_writer { - // it is ok for this to fail as it is just a cleanup job. - let _ = writer.remove_empty_directory(&path).await; - } + ) -> Result { + if reader.is_directory(&path).await? { + let mut path_stream = reader.read_directory(&path).await?; + let mut contains_files = false; + + while let Some(child_path) = path_stream.next().await { + contains_files |= Box::pin(get_asset_paths( + reader, + clean_empty_folders_writer, + child_path, + paths, + )) + .await?; + } + if !contains_files && path.parent().is_some() { + if let Some(writer) = clean_empty_folders_writer { + // it is ok for this to fail as it is just a cleanup job. + let _ = writer.remove_empty_directory(&path).await; } - Ok(contains_files) - } else { - paths.push(path); - Ok(true) } - }) + Ok(contains_files) + } else { + paths.push(path); + Ok(true) + } } for source in self.sources().iter_processed() { @@ -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/reflect.rs b/crates/bevy_asset/src/reflect.rs index dd95df8525bd6..02e8cd85278b1 100644 --- a/crates/bevy_asset/src/reflect.rs +++ b/crates/bevy_asset/src/reflect.rs @@ -46,6 +46,7 @@ impl ReflectAsset { } /// Equivalent of [`Assets::get_mut`] + #[allow(unsafe_code)] pub fn get_mut<'w>( &self, world: &'w mut World, @@ -82,6 +83,7 @@ impl ReflectAsset { /// violating Rust's aliasing rules. To avoid this: /// * Only call this method if you know that the [`UnsafeWorldCell`] may be used to access the corresponding `Assets` /// * Don't call this method more than once in the same scope. + #[allow(unsafe_code)] pub unsafe fn get_unchecked_mut<'w>( &self, world: UnsafeWorldCell<'w>, @@ -135,6 +137,7 @@ impl FromType for ReflectAsset { get_unchecked_mut: |world, handle| { // SAFETY: `get_unchecked_mut` must be called with `UnsafeWorldCell` having access to `Assets`, // and must ensure to only have at most one reference to it live at all times. + #[allow(unsafe_code)] let assets = unsafe { world.get_resource_mut::>().unwrap().into_inner() }; let asset = assets.get_mut(&handle.typed_debug_checked()); asset.map(|asset| asset as &mut dyn Reflect) @@ -149,7 +152,7 @@ impl FromType for ReflectAsset { let mut assets = world.resource_mut::>(); let value: A = FromReflect::from_reflect(value) .expect("could not call `FromReflect::from_reflect` in `ReflectAsset::set`"); - assets.insert(handle.typed_debug_checked(), value); + assets.insert(&handle.typed_debug_checked(), value); }, len: |world| { let assets = world.resource::>(); @@ -161,7 +164,7 @@ impl FromType for ReflectAsset { }, remove: |world, handle| { let mut assets = world.resource_mut::>(); - let value = assets.remove(handle.typed_debug_checked()); + let value = assets.remove(&handle.typed_debug_checked()); value.map(|value| Box::new(value) as Box) }, } 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..26b057bddf243 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. /// @@ -654,38 +658,42 @@ impl AssetServer { } pub(crate) fn load_folder_internal(&self, id: UntypedAssetId, path: AssetPath) { - fn load_folder<'a>( + async 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>> { - Box::pin(async move { - let is_dir = reader.is_directory(path).await?; - if is_dir { - let mut path_stream = reader.read_directory(path.as_ref()).await?; - while let Some(child_path) = path_stream.next().await { - if reader.is_directory(&child_path).await? { - load_folder(source.clone(), &child_path, reader, server, handles) - .await?; - } else { - let path = child_path.to_str().expect("Path should be a valid string."); - let asset_path = AssetPath::parse(path).with_source(source.clone()); - match server.load_untyped_async(asset_path).await { - Ok(handle) => handles.push(handle), - // skip assets that cannot be loaded - Err( - AssetLoadError::MissingAssetLoaderForTypeName(_) - | AssetLoadError::MissingAssetLoaderForExtension(_), - ) => {} - Err(err) => return Err(err), - } + ) -> Result<(), AssetLoadError> { + let is_dir = reader.is_directory(path).await?; + if is_dir { + let mut path_stream = reader.read_directory(path.as_ref()).await?; + while let Some(child_path) = path_stream.next().await { + if reader.is_directory(&child_path).await? { + Box::pin(load_folder( + source.clone(), + &child_path, + reader, + server, + handles, + )) + .await?; + } else { + let path = child_path.to_str().expect("Path should be a valid string."); + let asset_path = AssetPath::parse(path).with_source(source.clone()); + match server.load_untyped_async(asset_path).await { + Ok(handle) => handles.push(handle), + // skip assets that cannot be loaded + Err( + AssetLoadError::MissingAssetLoaderForTypeName(_) + | AssetLoadError::MissingAssetLoaderForExtension(_), + ) => {} + Err(err) => return Err(err), } } } - Ok(()) - }) + } + Ok(()) } let path = path.into_owned(); 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/Cargo.toml b/crates/bevy_audio/Cargo.toml index 3f831ce3e4a2e..fc0bb3aaec3f5 100644 --- a/crates/bevy_audio/Cargo.toml +++ b/crates/bevy_audio/Cargo.toml @@ -52,4 +52,5 @@ android_shared_stdcxx = ["cpal/oboe-shared-stdcxx"] workspace = true [package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] all-features = true diff --git a/crates/bevy_audio/src/audio_source.rs b/crates/bevy_audio/src/audio_source.rs index 8b0c7090eac14..b501eaeea09af 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 @@ -12,8 +11,10 @@ pub struct AudioSource { /// Raw data of the audio source. /// /// The data must be one of the file formats supported by Bevy (`wav`, `ogg`, `flac`, or `mp3`). - /// It is decoded using [`rodio::decoder::Decoder`](https://docs.rs/rodio/latest/rodio/decoder/struct.Decoder.html). + /// However, support for these file formats is not part of Bevy's [`default feature set`](https://docs.rs/bevy/latest/bevy/index.html#default-features). + /// In order to be able to use these file formats, you will have to enable the appropriate [`optional features`](https://docs.rs/bevy/latest/bevy/index.html#optional-features). /// + /// It is decoded using [`rodio::decoder::Decoder`](https://docs.rs/rodio/latest/rodio/decoder/struct.Decoder.html). /// The decoder has conditionally compiled methods /// depending on the features enabled. /// If the format used is not enabled, @@ -43,18 +44,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_audio/src/lib.rs b/crates/bevy_audio/src/lib.rs index 3f42acf3b6e22..1d5df733a00e5 100644 --- a/crates/bevy_audio/src/lib.rs +++ b/crates/bevy_audio/src/lib.rs @@ -1,3 +1,10 @@ +#![forbid(unsafe_code)] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] + //! Audio support for the game engine Bevy //! //! ```no_run @@ -19,8 +26,6 @@ //! }); //! } //! ``` -#![forbid(unsafe_code)] -#![cfg_attr(docsrs, feature(doc_auto_cfg))] mod audio; mod audio_output; diff --git a/crates/bevy_color/Cargo.toml b/crates/bevy_color/Cargo.toml index 29bd0218ad1f3..733a0ad3cceb7 100644 --- a/crates/bevy_color/Cargo.toml +++ b/crates/bevy_color/Cargo.toml @@ -14,10 +14,17 @@ bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", features = [ "bevy", ] } bytemuck = "1" -serde = "1.0" +serde = { version = "1.0", features = ["derive"], optional = true } thiserror = "1.0" wgpu = { version = "0.19.3", default-features = false } encase = { version = "0.7", default-features = false } +[features] +serialize = ["serde"] + [lints] workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +all-features = true diff --git a/crates/bevy_color/src/color.rs b/crates/bevy_color/src/color.rs index bfb5118712289..25ed54809ec77 100644 --- a/crates/bevy_color/src/color.rs +++ b/crates/bevy_color/src/color.rs @@ -1,8 +1,7 @@ use crate::{ Alpha, Hsla, Hsva, Hwba, Laba, Lcha, LinearRgba, Oklaba, Oklcha, Srgba, StandardColor, Xyza, }; -use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; -use serde::{Deserialize, Serialize}; +use bevy_reflect::prelude::*; /// An enumerated type that can represent any of the color types in this crate. /// @@ -12,8 +11,13 @@ use serde::{Deserialize, Serialize}; ///
#[doc = include_str!("../docs/diagrams/model_graph.svg")] ///
-#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)] -#[reflect(PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Reflect)] +#[reflect(PartialEq, Default)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] pub enum Color { /// A color in the sRGB color space with alpha. Srgba(Srgba), @@ -266,14 +270,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..1f39f3254c489 100644 --- a/crates/bevy_color/src/color_ops.rs +++ b/crates/bevy_color/src/color_ops.rs @@ -61,3 +61,83 @@ 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; +} + +/// Utility function for interpolating hue values. This ensures that the interpolation +/// takes the shortest path around the color wheel, and that the result is always between +/// 0 and 360. +pub(crate) fn lerp_hue(a: f32, b: f32, t: f32) -> f32 { + let diff = (b - a + 180.0).rem_euclid(360.) - 180.; + (a + diff * t).rem_euclid(360.0) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{testing::assert_approx_eq, 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); + } + + #[test] + fn test_hue_wrap() { + assert_approx_eq!(lerp_hue(10., 20., 0.25), 12.5, 0.001); + assert_approx_eq!(lerp_hue(10., 20., 0.5), 15., 0.001); + assert_approx_eq!(lerp_hue(10., 20., 0.75), 17.5, 0.001); + + assert_approx_eq!(lerp_hue(20., 10., 0.25), 17.5, 0.001); + assert_approx_eq!(lerp_hue(20., 10., 0.5), 15., 0.001); + assert_approx_eq!(lerp_hue(20., 10., 0.75), 12.5, 0.001); + + assert_approx_eq!(lerp_hue(10., 350., 0.25), 5., 0.001); + assert_approx_eq!(lerp_hue(10., 350., 0.5), 0., 0.001); + assert_approx_eq!(lerp_hue(10., 350., 0.75), 355., 0.001); + + assert_approx_eq!(lerp_hue(350., 10., 0.25), 355., 0.001); + assert_approx_eq!(lerp_hue(350., 10., 0.5), 0., 0.001); + assert_approx_eq!(lerp_hue(350., 10., 0.75), 5., 0.001); + } +} diff --git a/crates/bevy_color/src/hsla.rs b/crates/bevy_color/src/hsla.rs index 2e31d5f9b1176..ac7cdf93dabb6 100644 --- a/crates/bevy_color/src/hsla.rs +++ b/crates/bevy_color/src/hsla.rs @@ -1,6 +1,8 @@ -use crate::{Alpha, Hsva, Hwba, Lcha, LinearRgba, Luminance, Mix, Srgba, StandardColor, Xyza}; -use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; -use serde::{Deserialize, Serialize}; +use crate::{ + Alpha, ClampColor, Hsva, Hue, Hwba, Lcha, LinearRgba, Luminance, Mix, Srgba, StandardColor, + Xyza, +}; +use bevy_reflect::prelude::*; /// Color in Hue-Saturation-Lightness (HSL) color space with alpha. /// Further information on this color model can be found on [Wikipedia](https://en.wikipedia.org/wiki/HSL_and_HSV). @@ -8,8 +10,13 @@ use serde::{Deserialize, Serialize}; ///
#[doc = include_str!("../docs/diagrams/model_graph.svg")] ///
-#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)] -#[reflect(PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Reflect)] +#[reflect(PartialEq, Default)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] pub struct Hsla { /// The hue channel. [0.0, 360.0] pub hue: f32, @@ -52,11 +59,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 } @@ -107,16 +109,8 @@ impl Mix for Hsla { #[inline] fn mix(&self, other: &Self, factor: f32) -> Self { let n_factor = 1.0 - factor; - // TODO: Refactor this into EuclideanModulo::lerp_modulo - let shortest_angle = ((((other.hue - self.hue) % 360.) + 540.) % 360.) - 180.; - let mut hue = self.hue + shortest_angle * factor; - if hue < 0. { - hue += 360.; - } else if hue >= 360. { - hue -= 360.; - } Self { - hue, + hue: crate::color_ops::lerp_hue(self.hue, other.hue, factor), saturation: self.saturation * n_factor + other.saturation * factor, lightness: self.lightness * n_factor + other.lightness * factor, alpha: self.alpha * n_factor + other.alpha * factor, @@ -141,6 +135,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 +177,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 +386,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..014b447cba980 100644 --- a/crates/bevy_color/src/hsva.rs +++ b/crates/bevy_color/src/hsva.rs @@ -1,6 +1,5 @@ -use crate::{Alpha, Hwba, Lcha, LinearRgba, Srgba, StandardColor, Xyza}; -use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; -use serde::{Deserialize, Serialize}; +use crate::{Alpha, ClampColor, Hue, Hwba, Lcha, LinearRgba, Mix, Srgba, StandardColor, Xyza}; +use bevy_reflect::prelude::*; /// Color in Hue-Saturation-Value (HSV) color space with alpha. /// Further information on this color model can be found on [Wikipedia](https://en.wikipedia.org/wiki/HSL_and_HSV). @@ -8,8 +7,13 @@ use serde::{Deserialize, Serialize}; ///
#[doc = include_str!("../docs/diagrams/model_graph.svg")] ///
-#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)] -#[reflect(PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Reflect)] +#[reflect(PartialEq, Default)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] pub struct Hsva { /// The hue channel. [0.0, 360.0] pub hue: f32, @@ -52,11 +56,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 } @@ -74,6 +73,19 @@ impl Default for Hsva { } } +impl Mix for Hsva { + #[inline] + fn mix(&self, other: &Self, factor: f32) -> Self { + let n_factor = 1.0 - factor; + Self { + hue: crate::color_ops::lerp_hue(self.hue, other.hue, factor), + saturation: self.saturation * n_factor + other.saturation * factor, + value: self.value * n_factor + other.value * factor, + alpha: self.alpha * n_factor + other.alpha * factor, + } + } +} + impl Alpha for Hsva { #[inline] fn with_alpha(&self, alpha: f32) -> Self { @@ -91,6 +103,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 +259,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..41b4bd06be591 100644 --- a/crates/bevy_color/src/hwba.rs +++ b/crates/bevy_color/src/hwba.rs @@ -2,9 +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 serde::{Deserialize, Serialize}; +use crate::{Alpha, ClampColor, Hue, Lcha, LinearRgba, Mix, Srgba, StandardColor, Xyza}; +use bevy_reflect::prelude::*; /// Color in Hue-Whiteness-Blackness (HWB) color space with alpha. /// Further information on this color model can be found on [Wikipedia](https://en.wikipedia.org/wiki/HWB_color_model). @@ -12,8 +11,13 @@ use serde::{Deserialize, Serialize}; ///
#[doc = include_str!("../docs/diagrams/model_graph.svg")] ///
-#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)] -#[reflect(PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Reflect)] +#[reflect(PartialEq, Default)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] pub struct Hwba { /// The hue channel. [0.0, 360.0] pub hue: f32, @@ -56,11 +60,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 } @@ -78,6 +77,19 @@ impl Default for Hwba { } } +impl Mix for Hwba { + #[inline] + fn mix(&self, other: &Self, factor: f32) -> Self { + let n_factor = 1.0 - factor; + Self { + hue: crate::color_ops::lerp_hue(self.hue, other.hue, factor), + whiteness: self.whiteness * n_factor + other.whiteness * factor, + blackness: self.blackness * n_factor + other.blackness * factor, + alpha: self.alpha * n_factor + other.alpha * factor, + } + } +} + impl Alpha for Hwba { #[inline] fn with_alpha(&self, alpha: f32) -> Self { @@ -95,6 +107,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 +292,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..5c95472932420 100644 --- a/crates/bevy_color/src/laba.rs +++ b/crates/bevy_color/src/laba.rs @@ -1,16 +1,21 @@ use crate::{ - Alpha, Hsla, Hsva, Hwba, LinearRgba, Luminance, Mix, Oklaba, Srgba, StandardColor, Xyza, + impl_componentwise_vector_space, Alpha, ClampColor, Hsla, Hsva, Hwba, LinearRgba, Luminance, + Mix, Oklaba, Srgba, StandardColor, Xyza, }; -use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; -use serde::{Deserialize, Serialize}; +use bevy_reflect::prelude::*; /// Color in LAB color space, with alpha #[doc = include_str!("../docs/conversion.md")] ///
#[doc = include_str!("../docs/diagrams/model_graph.svg")] ///
-#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)] -#[reflect(PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Reflect)] +#[reflect(PartialEq, Default)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] pub struct Laba { /// The lightness channel. [0.0, 1.5] pub lightness: f32, @@ -24,6 +29,8 @@ pub struct Laba { impl StandardColor for Laba {} +impl_componentwise_vector_space!(Laba, [lightness, a, b, alpha]); + impl Laba { /// Construct a new [`Laba`] color from components. /// @@ -110,6 +117,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 +378,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..75051a3ffe59e 100644 --- a/crates/bevy_color/src/lcha.rs +++ b/crates/bevy_color/src/lcha.rs @@ -1,14 +1,18 @@ -use crate::{Alpha, Laba, LinearRgba, Luminance, Mix, Srgba, StandardColor, Xyza}; -use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; -use serde::{Deserialize, Serialize}; +use crate::{Alpha, ClampColor, Hue, Laba, LinearRgba, Luminance, Mix, Srgba, StandardColor, Xyza}; +use bevy_reflect::prelude::*; /// Color in LCH color space, with alpha #[doc = include_str!("../docs/conversion.md")] ///
#[doc = include_str!("../docs/diagrams/model_graph.svg")] ///
-#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)] -#[reflect(PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Reflect)] +#[reflect(PartialEq, Default)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] pub struct Lcha { /// The lightness channel. [0.0, 1.5] pub lightness: f32, @@ -56,11 +60,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 +136,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 +182,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 +347,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..923a603c0c596 100644 --- a/crates/bevy_color/src/lib.rs +++ b/crates/bevy_color/src/lib.rs @@ -1,3 +1,10 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![forbid(unsafe_code)] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] + //! Representations of colors in various color spaces. //! //! This crate provides a number of color representations, including: @@ -65,6 +72,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 //! //! ``` @@ -134,7 +147,6 @@ where Self: core::fmt::Debug, Self: Clone + Copy, Self: PartialEq, - Self: serde::Serialize + for<'a> serde::Deserialize<'a>, Self: bevy_reflect::Reflect, Self: Default, Self: From + Into, @@ -151,3 +163,99 @@ where Self: Alpha, { } + +macro_rules! impl_componentwise_vector_space { + ($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::Neg for $ty { + type Output = Self; + + fn neg(self) -> Self::Output { + Self::Output { + $($element: -self.$element,)+ + } + } + } + + 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::VectorSpace for $ty { + const ZERO: Self = Self { + $($element: 0.0,)+ + }; + } + }; +} + +pub(crate) use impl_componentwise_vector_space; diff --git a/crates/bevy_color/src/linear_rgba.rs b/crates/bevy_color/src/linear_rgba.rs index b07efa951e8ce..946e461694547 100644 --- a/crates/bevy_color/src/linear_rgba.rs +++ b/crates/bevy_color/src/linear_rgba.rs @@ -1,18 +1,23 @@ -use std::ops::{Div, Mul}; - -use crate::{color_difference::EuclideanDistance, Alpha, Luminance, Mix, StandardColor}; +use crate::{ + color_difference::EuclideanDistance, impl_componentwise_vector_space, 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}; /// Linear RGB color with alpha. #[doc = include_str!("../docs/conversion.md")] ///
#[doc = include_str!("../docs/diagrams/model_graph.svg")] ///
-#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)] -#[reflect(PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Reflect, Pod, Zeroable)] +#[reflect(PartialEq, Default)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] #[repr(C)] pub struct LinearRgba { /// The red channel. [0.0, 1.0] @@ -27,6 +32,8 @@ pub struct LinearRgba { impl StandardColor for LinearRgba {} +impl_componentwise_vector_space!(LinearRgba, [red, green, blue, alpha]); + impl LinearRgba { /// A fully black color with full alpha. pub const BLACK: Self = Self { @@ -256,6 +263,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 +304,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 { @@ -390,33 +373,6 @@ impl encase::private::CreateFrom for LinearRgba { } } -/// A [`Zeroable`] type is one whose bytes can be filled with zeroes while remaining valid. -/// -/// SAFETY: [`LinearRgba`] is inhabited -/// SAFETY: [`LinearRgba`]'s all-zero bit pattern is a valid value -unsafe impl Zeroable for LinearRgba { - fn zeroed() -> Self { - LinearRgba { - red: 0.0, - green: 0.0, - blue: 0.0, - alpha: 0.0, - } - } -} - -/// The [`Pod`] trait is [`bytemuck`]'s marker for types that can be safely transmuted from a byte array. -/// -/// It is intended to only be implemented for types which are "Plain Old Data". -/// -/// SAFETY: [`LinearRgba`] is inhabited. -/// SAFETY: [`LinearRgba`] permits any bit value. -/// SAFETY: [`LinearRgba`] does not have padding bytes. -/// SAFETY: all of the fields of [`LinearRgba`] are [`Pod`], as f32 is [`Pod`]. -/// SAFETY: [`LinearRgba`] is `repr(C)` -/// SAFETY: [`LinearRgba`] does not permit interior mutability. -unsafe impl Pod for LinearRgba {} - impl encase::ShaderSize for LinearRgba {} #[cfg(test)] @@ -455,4 +411,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..65cd88f7758b7 100644 --- a/crates/bevy_color/src/oklaba.rs +++ b/crates/bevy_color/src/oklaba.rs @@ -1,20 +1,24 @@ use crate::{ - color_difference::EuclideanDistance, Alpha, Hsla, Hsva, Hwba, Lcha, LinearRgba, Luminance, Mix, - Srgba, StandardColor, Xyza, + color_difference::EuclideanDistance, impl_componentwise_vector_space, Alpha, ClampColor, Hsla, + Hsva, Hwba, Lcha, LinearRgba, Luminance, Mix, Srgba, StandardColor, Xyza, }; -use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; -use serde::{Deserialize, Serialize}; +use bevy_reflect::prelude::*; /// Color in Oklab color space, with alpha #[doc = include_str!("../docs/conversion.md")] ///
#[doc = include_str!("../docs/diagrams/model_graph.svg")] ///
-#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)] -#[reflect(PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Reflect)] +#[reflect(PartialEq, Default)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] 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 +29,45 @@ pub struct Oklaba { impl StandardColor for Oklaba {} +impl_componentwise_vector_space!(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 +92,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 +119,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 +199,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 +312,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 +345,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..949b80eb08450 100644 --- a/crates/bevy_color/src/oklcha.rs +++ b/crates/bevy_color/src/oklcha.rs @@ -1,17 +1,21 @@ 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 serde::{Deserialize, Serialize}; +use bevy_reflect::prelude::*; /// Color in Oklch color space, with alpha #[doc = include_str!("../docs/conversion.md")] ///
#[doc = include_str!("../docs/diagrams/model_graph.svg")] ///
-#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)] -#[reflect(PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Reflect)] +#[reflect(PartialEq, Default)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] pub struct Oklcha { /// The 'lightness' channel. [0.0, 1.0] pub lightness: f32, @@ -56,20 +60,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 +135,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 +191,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 +225,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 +389,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..43c82fda25d68 100644 --- a/crates/bevy_color/src/srgba.rs +++ b/crates/bevy_color/src/srgba.rs @@ -1,10 +1,10 @@ -use std::ops::{Div, Mul}; - use crate::color_difference::EuclideanDistance; -use crate::{Alpha, LinearRgba, Luminance, Mix, StandardColor, Xyza}; +use crate::{ + impl_componentwise_vector_space, Alpha, ClampColor, LinearRgba, Luminance, Mix, StandardColor, + Xyza, +}; use bevy_math::Vec4; -use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; -use serde::{Deserialize, Serialize}; +use bevy_reflect::prelude::*; use thiserror::Error; /// Non-linear standard RGB with alpha. @@ -12,8 +12,13 @@ use thiserror::Error; ///
#[doc = include_str!("../docs/diagrams/model_graph.svg")] ///
-#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)] -#[reflect(PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Reflect)] +#[reflect(PartialEq, Default)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] pub struct Srgba { /// The red channel. [0.0, 1.0] pub red: f32, @@ -27,6 +32,8 @@ pub struct Srgba { impl StandardColor for Srgba {} +impl_componentwise_vector_space!(Srgba, [red, green, blue, alpha]); + impl Srgba { // The standard VGA colors, with alpha set to 1.0. // https://en.wikipedia.org/wiki/Web_colors#Basic_colors @@ -307,6 +314,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 { @@ -371,48 +396,6 @@ pub enum HexColorError { Char(char), } -/// All color channels are scaled directly, -/// but alpha is unchanged. -/// -/// Values are not clamped. -impl Mul for Srgba { - 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 = Srgba; - - fn mul(self, rhs: Srgba) -> Srgba { - rhs * self - } -} - -/// All color channels are scaled directly, -/// but alpha is unchanged. -/// -/// Values are not clamped. -impl Div for Srgba { - 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, - } - } -} - #[cfg(test)] mod tests { use crate::testing::assert_approx_eq; @@ -490,4 +473,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..d3baf464f472e 100644 --- a/crates/bevy_color/src/xyza.rs +++ b/crates/bevy_color/src/xyza.rs @@ -1,14 +1,20 @@ -use crate::{Alpha, LinearRgba, Luminance, Mix, StandardColor}; -use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; -use serde::{Deserialize, Serialize}; +use crate::{ + impl_componentwise_vector_space, Alpha, ClampColor, LinearRgba, Luminance, Mix, StandardColor, +}; +use bevy_reflect::prelude::*; /// [CIE 1931](https://en.wikipedia.org/wiki/CIE_1931_color_space) color space, also known as XYZ, with an alpha channel. #[doc = include_str!("../docs/conversion.md")] ///
#[doc = include_str!("../docs/diagrams/model_graph.svg")] ///
-#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)] -#[reflect(PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Reflect)] +#[reflect(PartialEq, Default)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] pub struct Xyza { /// The x-axis. [0.0, 1.0] pub x: f32, @@ -22,6 +28,8 @@ pub struct Xyza { impl StandardColor for Xyza {} +impl_componentwise_vector_space!(Xyza, [x, y, z, alpha]); + impl Xyza { /// Construct a new [`Xyza`] color from components. /// @@ -134,6 +142,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 +234,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/Cargo.toml b/crates/bevy_core/Cargo.toml index 75ccb157ea0ae..d149581ebb93e 100644 --- a/crates/bevy_core/Cargo.toml +++ b/crates/bevy_core/Cargo.toml @@ -37,4 +37,5 @@ crossbeam-channel = "0.5.0" workspace = true [package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] all-features = true diff --git a/crates/bevy_core/src/lib.rs b/crates/bevy_core/src/lib.rs index 8ccd3ee4d9408..b636122c17b23 100644 --- a/crates/bevy_core/src/lib.rs +++ b/crates/bevy_core/src/lib.rs @@ -1,5 +1,11 @@ -//! This crate provides core functionality for Bevy Engine. #![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![forbid(unsafe_code)] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] + +//! This crate provides core functionality for Bevy Engine. mod name; #[cfg(feature = "serialize")] diff --git a/crates/bevy_core_pipeline/Cargo.toml b/crates/bevy_core_pipeline/Cargo.toml index e03b93e9d90ae..e6846d0ec161d 100644 --- a/crates/bevy_core_pipeline/Cargo.toml +++ b/crates/bevy_core_pipeline/Cargo.toml @@ -42,4 +42,5 @@ nonmax = "0.5" workspace = true [package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] all-features = true 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..320141818af16 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,8 +2,9 @@ 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_phase::SortedRenderPhase, render_resource::RenderPassDescriptor, renderer::RenderContext, view::{ExtractedView, ViewTarget}, @@ -15,7 +16,7 @@ pub struct MainPass2dNode { query: QueryState< ( &'static ExtractedCamera, - &'static RenderPhase, + &'static SortedRenderPhase, &'static ViewTarget, ), With, @@ -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_2d/mod.rs b/crates/bevy_core_pipeline/src/core_2d/mod.rs index 5dc0a5558662b..bc872950701fd 100644 --- a/crates/bevy_core_pipeline/src/core_2d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_2d/mod.rs @@ -31,18 +31,18 @@ pub use main_pass_2d_node::*; use bevy_app::{App, Plugin}; use bevy_ecs::prelude::*; +use bevy_math::FloatOrd; use bevy_render::{ camera::Camera, extract_component::ExtractComponentPlugin, render_graph::{EmptyNode, RenderGraphApp, ViewNodeRunner}, render_phase::{ sort_phase_system, CachedRenderPipelinePhaseItem, DrawFunctionId, DrawFunctions, PhaseItem, - RenderPhase, + SortedPhaseItem, SortedRenderPhase, }, render_resource::CachedRenderPipelineId, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; -use bevy_utils::FloatOrd; use nonmax::NonMaxU32; use crate::{tonemapping::TonemappingNode, upscaling::UpscalingNode}; @@ -96,29 +96,16 @@ pub struct Transparent2d { } impl PhaseItem for Transparent2d { - type SortKey = FloatOrd; - #[inline] fn entity(&self) -> Entity { self.entity } - #[inline] - fn sort_key(&self) -> Self::SortKey { - self.sort_key - } - #[inline] fn draw_function(&self) -> DrawFunctionId { self.draw_function } - #[inline] - fn sort(items: &mut [Self]) { - // radsort is a stable radix sort that performed better than `slice::sort_by_key` or `slice::sort_unstable_by_key`. - radsort::sort_by_key(items, |item| item.sort_key().0); - } - #[inline] fn batch_range(&self) -> &Range { &self.batch_range @@ -140,6 +127,21 @@ impl PhaseItem for Transparent2d { } } +impl SortedPhaseItem for Transparent2d { + type SortKey = FloatOrd; + + #[inline] + fn sort_key(&self) -> Self::SortKey { + self.sort_key + } + + #[inline] + fn sort(items: &mut [Self]) { + // radsort is a stable radix sort that performed better than `slice::sort_by_key` or `slice::sort_unstable_by_key`. + radsort::sort_by_key(items, |item| item.sort_key().0); + } +} + impl CachedRenderPipelinePhaseItem for Transparent2d { #[inline] fn cached_pipeline(&self) -> CachedRenderPipelineId { @@ -155,7 +157,7 @@ pub fn extract_core_2d_camera_phases( if camera.is_active { commands .get_or_spawn(entity) - .insert(RenderPhase::::default()); + .insert(SortedRenderPhase::::default()); } } } 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..2d490ac9bfc25 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,8 +5,9 @@ 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_phase::{BinnedRenderPhase, TrackedRenderPass}, render_resource::{CommandEncoderDescriptor, PipelineCache, RenderPassDescriptor, StoreOp}, renderer::RenderContext, view::{ViewDepthTexture, ViewTarget, ViewUniformOffset}, @@ -16,14 +17,16 @@ use bevy_utils::tracing::info_span; use super::AlphaMask3d; -/// A [`bevy_render::render_graph::Node`] that runs the [`Opaque3d`] and [`AlphaMask3d`] [`RenderPhase`]. +/// A [`bevy_render::render_graph::Node`] that runs the [`Opaque3d`] +/// [`BinnedRenderPhase`] and [`AlphaMask3d`] +/// [`bevy_render::render_phase::SortedRenderPhase`]s. #[derive(Default)] pub struct MainOpaquePass3dNode; impl ViewNode for MainOpaquePass3dNode { type ViewQuery = ( &'static ExtractedCamera, - &'static RenderPhase, - &'static RenderPhase, + &'static BinnedRenderPhase, + &'static BinnedRenderPhase, &'static ViewTarget, &'static ViewDepthTexture, Option<&'static SkyboxPipelineId>, @@ -47,6 +50,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,19 +75,21 @@ 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); } // Opaque draws - if !opaque_phase.items.is_empty() { + if !opaque_phase.is_empty() { #[cfg(feature = "trace")] let _opaque_main_pass_3d_span = info_span!("opaque_main_pass_3d").entered(); opaque_phase.render(&mut render_pass, world, view_entity); } // Alpha draws - if !alpha_mask_phase.items.is_empty() { + if !alpha_mask_phase.is_empty() { #[cfg(feature = "trace")] let _alpha_mask_main_pass_3d_span = info_span!("alpha_mask_main_pass_3d").entered(); alpha_mask_phase.render(&mut render_pass, world, view_entity); @@ -104,6 +111,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_transmissive_pass_3d_node.rs b/crates/bevy_core_pipeline/src/core_3d/main_transmissive_pass_3d_node.rs index 73a679ba047eb..54c6c623f1656 100644 --- a/crates/bevy_core_pipeline/src/core_3d/main_transmissive_pass_3d_node.rs +++ b/crates/bevy_core_pipeline/src/core_3d/main_transmissive_pass_3d_node.rs @@ -4,7 +4,7 @@ use bevy_ecs::{prelude::*, query::QueryItem}; use bevy_render::{ camera::ExtractedCamera, render_graph::{NodeRunError, RenderGraphContext, ViewNode}, - render_phase::RenderPhase, + render_phase::SortedRenderPhase, render_resource::{Extent3d, RenderPassDescriptor, StoreOp}, renderer::RenderContext, view::{ViewDepthTexture, ViewTarget}, @@ -13,7 +13,8 @@ use bevy_render::{ use bevy_utils::tracing::info_span; use std::ops::Range; -/// A [`bevy_render::render_graph::Node`] that runs the [`Transmissive3d`] [`RenderPhase`]. +/// A [`bevy_render::render_graph::Node`] that runs the [`Transmissive3d`] +/// [`SortedRenderPhase`]. #[derive(Default)] pub struct MainTransmissivePass3dNode; @@ -21,7 +22,7 @@ impl ViewNode for MainTransmissivePass3dNode { type ViewQuery = ( &'static ExtractedCamera, &'static Camera3d, - &'static RenderPhase, + &'static SortedRenderPhase, &'static ViewTarget, Option<&'static ViewTransmissionTexture>, &'static ViewDepthTexture, 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..3c42434330655 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,8 +2,9 @@ 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_phase::SortedRenderPhase, render_resource::{RenderPassDescriptor, StoreOp}, renderer::RenderContext, view::{ViewDepthTexture, ViewTarget}, @@ -11,14 +12,15 @@ use bevy_render::{ #[cfg(feature = "trace")] use bevy_utils::tracing::info_span; -/// A [`bevy_render::render_graph::Node`] that runs the [`Transparent3d`] [`RenderPhase`]. +/// A [`bevy_render::render_graph::Node`] that runs the [`Transparent3d`] +/// [`SortedRenderPhase`]. #[derive(Default)] pub struct MainTransparentPass3dNode; impl ViewNode for MainTransparentPass3dNode { type ViewQuery = ( &'static ExtractedCamera, - &'static RenderPhase, + &'static SortedRenderPhase, &'static ViewTarget, &'static ViewDepthTexture, ); @@ -37,6 +39,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 +55,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/core_3d/mod.rs b/crates/bevy_core_pipeline/src/core_3d/mod.rs index 1e6dafa4ccb7e..7c622141a6464 100644 --- a/crates/bevy_core_pipeline/src/core_3d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_3d/mod.rs @@ -48,6 +48,7 @@ pub use main_transparent_pass_3d_node::*; use bevy_app::{App, Plugin, PostUpdate}; use bevy_ecs::prelude::*; +use bevy_math::FloatOrd; use bevy_render::{ camera::{Camera, ExtractedCamera}, extract_component::ExtractComponentPlugin, @@ -55,19 +56,19 @@ use bevy_render::{ prelude::Msaa, render_graph::{EmptyNode, RenderGraphApp, ViewNodeRunner}, render_phase::{ - sort_phase_system, CachedRenderPipelinePhaseItem, DrawFunctionId, DrawFunctions, PhaseItem, - RenderPhase, + sort_phase_system, BinnedPhaseItem, BinnedRenderPhase, CachedRenderPipelinePhaseItem, + DrawFunctionId, DrawFunctions, PhaseItem, SortedPhaseItem, SortedRenderPhase, }, render_resource::{ - CachedRenderPipelineId, Extent3d, FilterMode, Sampler, SamplerDescriptor, Texture, - TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, TextureView, + BindGroupId, CachedRenderPipelineId, Extent3d, FilterMode, Sampler, SamplerDescriptor, + Texture, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, TextureView, }, renderer::RenderDevice, - texture::{BevyDefault, ColorAttachment, TextureCache}, + texture::{BevyDefault, ColorAttachment, Image, TextureCache}, view::{ExtractedView, ViewDepthTexture, ViewTarget}, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; -use bevy_utils::{tracing::warn, FloatOrd, HashMap}; +use bevy_utils::{tracing::warn, HashMap}; use nonmax::NonMaxU32; use crate::{ @@ -79,8 +80,8 @@ use crate::{ }, prepass::{ node::PrepassNode, AlphaMask3dPrepass, DeferredPrepass, DepthPrepass, MotionVectorPrepass, - NormalPrepass, Opaque3dPrepass, ViewPrepassTextures, MOTION_VECTOR_PREPASS_FORMAT, - NORMAL_PREPASS_FORMAT, + NormalPrepass, Opaque3dPrepass, OpaqueNoLightmap3dBinKey, ViewPrepassTextures, + MOTION_VECTOR_PREPASS_FORMAT, NORMAL_PREPASS_FORMAT, }, skybox::SkyboxPlugin, tonemapping::TonemappingNode, @@ -116,14 +117,8 @@ impl Plugin for Core3dPlugin { .add_systems( Render, ( - sort_phase_system::.in_set(RenderSet::PhaseSort), - sort_phase_system::.in_set(RenderSet::PhaseSort), sort_phase_system::.in_set(RenderSet::PhaseSort), sort_phase_system::.in_set(RenderSet::PhaseSort), - sort_phase_system::.in_set(RenderSet::PhaseSort), - sort_phase_system::.in_set(RenderSet::PhaseSort), - sort_phase_system::.in_set(RenderSet::PhaseSort), - sort_phase_system::.in_set(RenderSet::PhaseSort), prepare_core_3d_depth_textures.in_set(RenderSet::PrepareResources), prepare_core_3d_transmission_textures.in_set(RenderSet::PrepareResources), prepare_prepass_textures.in_set(RenderSet::PrepareResources), @@ -179,37 +174,49 @@ impl Plugin for Core3dPlugin { } } +/// Opaque 3D [`BinnedPhaseItem`]s. pub struct Opaque3d { - pub asset_id: AssetId, - pub pipeline: CachedRenderPipelineId, - pub entity: Entity, - pub draw_function: DrawFunctionId, + /// The key, which determines which can be batched. + pub key: Opaque3dBinKey, + /// An entity from which data will be fetched, including the mesh if + /// applicable. + pub representative_entity: Entity, + /// The ranges of instances. pub batch_range: Range, + /// The dynamic offset. pub dynamic_offset: Option, } -impl PhaseItem for Opaque3d { - type SortKey = (usize, AssetId); +/// Data that must be identical in order to batch meshes together. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Opaque3dBinKey { + /// The identifier of the render pipeline. + pub pipeline: CachedRenderPipelineId, - #[inline] - fn entity(&self) -> Entity { - self.entity - } + /// The function used to draw. + pub draw_function: DrawFunctionId, - #[inline] - fn sort_key(&self) -> Self::SortKey { - // Sort by pipeline, then by mesh to massively decrease drawcall counts in real scenes. - (self.pipeline.id(), self.asset_id) - } + /// The mesh. + pub asset_id: AssetId, + + /// The ID of a bind group specific to the material. + /// + /// In the case of PBR, this is the `MaterialBindGroupId`. + pub material_bind_group_id: Option, + /// The lightmap, if present. + pub lightmap_image: Option>, +} + +impl PhaseItem for Opaque3d { #[inline] - fn draw_function(&self) -> DrawFunctionId { - self.draw_function + fn entity(&self) -> Entity { + self.representative_entity } #[inline] - fn sort(items: &mut [Self]) { - items.sort_unstable_by_key(Self::sort_key); + fn draw_function(&self) -> DrawFunctionId { + self.key.draw_function } #[inline] @@ -233,44 +240,48 @@ impl PhaseItem for Opaque3d { } } +impl BinnedPhaseItem for Opaque3d { + type BinKey = Opaque3dBinKey; + + #[inline] + fn new( + key: Self::BinKey, + representative_entity: Entity, + batch_range: Range, + dynamic_offset: Option, + ) -> Self { + Opaque3d { + key, + representative_entity, + batch_range, + dynamic_offset, + } + } +} + impl CachedRenderPipelinePhaseItem for Opaque3d { #[inline] fn cached_pipeline(&self) -> CachedRenderPipelineId { - self.pipeline + self.key.pipeline } } pub struct AlphaMask3d { - pub asset_id: AssetId, - pub pipeline: CachedRenderPipelineId, - pub entity: Entity, - pub draw_function: DrawFunctionId, + pub key: OpaqueNoLightmap3dBinKey, + pub representative_entity: Entity, pub batch_range: Range, pub dynamic_offset: Option, } impl PhaseItem for AlphaMask3d { - type SortKey = (usize, AssetId); - #[inline] fn entity(&self) -> Entity { - self.entity - } - - #[inline] - fn sort_key(&self) -> Self::SortKey { - // Sort by pipeline, then by mesh to massively decrease drawcall counts in real scenes. - (self.pipeline.id(), self.asset_id) + self.representative_entity } #[inline] fn draw_function(&self) -> DrawFunctionId { - self.draw_function - } - - #[inline] - fn sort(items: &mut [Self]) { - items.sort_unstable_by_key(Self::sort_key); + self.key.draw_function } #[inline] @@ -294,10 +305,29 @@ impl PhaseItem for AlphaMask3d { } } +impl BinnedPhaseItem for AlphaMask3d { + type BinKey = OpaqueNoLightmap3dBinKey; + + #[inline] + fn new( + key: Self::BinKey, + representative_entity: Entity, + batch_range: Range, + dynamic_offset: Option, + ) -> Self { + Self { + key, + representative_entity, + batch_range, + dynamic_offset, + } + } +} + impl CachedRenderPipelinePhaseItem for AlphaMask3d { #[inline] fn cached_pipeline(&self) -> CachedRenderPipelineId { - self.pipeline + self.key.pipeline } } @@ -311,9 +341,6 @@ pub struct Transmissive3d { } impl PhaseItem for Transmissive3d { - // NOTE: Values increase towards the camera. Back-to-front ordering for transmissive means we need an ascending sort. - type SortKey = FloatOrd; - /// For now, automatic batching is disabled for transmissive items because their rendering is /// split into multiple steps depending on [`Camera3d::screen_space_specular_transmission_steps`], /// which the batching system doesn't currently know about. @@ -330,21 +357,11 @@ impl PhaseItem for Transmissive3d { self.entity } - #[inline] - fn sort_key(&self) -> Self::SortKey { - FloatOrd(self.distance) - } - #[inline] fn draw_function(&self) -> DrawFunctionId { self.draw_function } - #[inline] - fn sort(items: &mut [Self]) { - radsort::sort_by_key(items, |item| item.distance); - } - #[inline] fn batch_range(&self) -> &Range { &self.batch_range @@ -366,6 +383,21 @@ impl PhaseItem for Transmissive3d { } } +impl SortedPhaseItem for Transmissive3d { + // NOTE: Values increase towards the camera. Back-to-front ordering for transmissive means we need an ascending sort. + type SortKey = FloatOrd; + + #[inline] + fn sort_key(&self) -> Self::SortKey { + FloatOrd(self.distance) + } + + #[inline] + fn sort(items: &mut [Self]) { + radsort::sort_by_key(items, |item| item.distance); + } +} + impl CachedRenderPipelinePhaseItem for Transmissive3d { #[inline] fn cached_pipeline(&self) -> CachedRenderPipelineId { @@ -383,29 +415,16 @@ pub struct Transparent3d { } impl PhaseItem for Transparent3d { - // NOTE: Values increase towards the camera. Back-to-front ordering for transparent means we need an ascending sort. - type SortKey = FloatOrd; - #[inline] fn entity(&self) -> Entity { self.entity } - #[inline] - fn sort_key(&self) -> Self::SortKey { - FloatOrd(self.distance) - } - #[inline] fn draw_function(&self) -> DrawFunctionId { self.draw_function } - #[inline] - fn sort(items: &mut [Self]) { - radsort::sort_by_key(items, |item| item.distance); - } - #[inline] fn batch_range(&self) -> &Range { &self.batch_range @@ -427,6 +446,21 @@ impl PhaseItem for Transparent3d { } } +impl SortedPhaseItem for Transparent3d { + // NOTE: Values increase towards the camera. Back-to-front ordering for transparent means we need an ascending sort. + type SortKey = FloatOrd; + + #[inline] + fn sort_key(&self) -> Self::SortKey { + FloatOrd(self.distance) + } + + #[inline] + fn sort(items: &mut [Self]) { + radsort::sort_by_key(items, |item| item.distance); + } +} + impl CachedRenderPipelinePhaseItem for Transparent3d { #[inline] fn cached_pipeline(&self) -> CachedRenderPipelineId { @@ -441,10 +475,10 @@ pub fn extract_core_3d_camera_phases( for (entity, camera) in &cameras_3d { if camera.is_active { commands.get_or_spawn(entity).insert(( - RenderPhase::::default(), - RenderPhase::::default(), - RenderPhase::::default(), - RenderPhase::::default(), + BinnedRenderPhase::::default(), + BinnedRenderPhase::::default(), + SortedRenderPhase::::default(), + SortedRenderPhase::::default(), )); } } @@ -475,15 +509,15 @@ pub fn extract_camera_prepass_phase( if depth_prepass || normal_prepass || motion_vector_prepass { entity.insert(( - RenderPhase::::default(), - RenderPhase::::default(), + BinnedRenderPhase::::default(), + BinnedRenderPhase::::default(), )); } if deferred_prepass { entity.insert(( - RenderPhase::::default(), - RenderPhase::::default(), + BinnedRenderPhase::::default(), + BinnedRenderPhase::::default(), )); } @@ -511,10 +545,10 @@ pub fn prepare_core_3d_depth_textures( views_3d: Query< (Entity, &ExtractedCamera, Option<&DepthPrepass>, &Camera3d), ( - With>, - With>, - With>, - With>, + With>, + With>, + With>, + With>, ), >, ) { @@ -594,12 +628,12 @@ pub fn prepare_core_3d_transmission_textures( &ExtractedCamera, &Camera3d, &ExtractedView, - &RenderPhase, + &SortedRenderPhase, ), ( - With>, - With>, - With>, + With>, + With>, + With>, ), >, ) { @@ -699,10 +733,10 @@ pub fn prepare_prepass_textures( Has, ), Or<( - With>, - With>, - With>, - With>, + With>, + With>, + With>, + With>, )>, >, ) { diff --git a/crates/bevy_core_pipeline/src/deferred/mod.rs b/crates/bevy_core_pipeline/src/deferred/mod.rs index a6b659fdbe391..3ccd8caad0e12 100644 --- a/crates/bevy_core_pipeline/src/deferred/mod.rs +++ b/crates/bevy_core_pipeline/src/deferred/mod.rs @@ -3,15 +3,15 @@ pub mod node; use std::ops::Range; -use bevy_asset::AssetId; use bevy_ecs::prelude::*; use bevy_render::{ - mesh::Mesh, - render_phase::{CachedRenderPipelinePhaseItem, DrawFunctionId, PhaseItem}, + render_phase::{BinnedPhaseItem, CachedRenderPipelinePhaseItem, DrawFunctionId, PhaseItem}, render_resource::{CachedRenderPipelineId, TextureFormat}, }; use nonmax::NonMaxU32; +use crate::prepass::OpaqueNoLightmap3dBinKey; + pub const DEFERRED_PREPASS_FORMAT: TextureFormat = TextureFormat::Rgba32Uint; pub const DEFERRED_LIGHTING_PASS_ID_FORMAT: TextureFormat = TextureFormat::R8Uint; pub const DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT: TextureFormat = TextureFormat::Depth16Unorm; @@ -21,37 +21,23 @@ pub const DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT: TextureFormat = TextureFormat: /// Sorted by pipeline, then by mesh to improve batching. /// /// Used to render all 3D meshes with materials that have no transparency. +#[derive(PartialEq, Eq, Hash)] pub struct Opaque3dDeferred { - pub entity: Entity, - pub asset_id: AssetId, - pub pipeline_id: CachedRenderPipelineId, - pub draw_function: DrawFunctionId, + pub key: OpaqueNoLightmap3dBinKey, + pub representative_entity: Entity, pub batch_range: Range, pub dynamic_offset: Option, } impl PhaseItem for Opaque3dDeferred { - type SortKey = (usize, AssetId); - #[inline] fn entity(&self) -> Entity { - self.entity - } - - #[inline] - fn sort_key(&self) -> Self::SortKey { - // Sort by pipeline, then by mesh to massively decrease drawcall counts in real scenes. - (self.pipeline_id.id(), self.asset_id) + self.representative_entity } #[inline] fn draw_function(&self) -> DrawFunctionId { - self.draw_function - } - - #[inline] - fn sort(items: &mut [Self]) { - items.sort_unstable_by_key(Self::sort_key); + self.key.draw_function } #[inline] @@ -75,10 +61,29 @@ impl PhaseItem for Opaque3dDeferred { } } +impl BinnedPhaseItem for Opaque3dDeferred { + type BinKey = OpaqueNoLightmap3dBinKey; + + #[inline] + fn new( + key: Self::BinKey, + representative_entity: Entity, + batch_range: Range, + dynamic_offset: Option, + ) -> Self { + Self { + key, + representative_entity, + batch_range, + dynamic_offset, + } + } +} + impl CachedRenderPipelinePhaseItem for Opaque3dDeferred { #[inline] fn cached_pipeline(&self) -> CachedRenderPipelineId { - self.pipeline_id + self.key.pipeline } } @@ -88,36 +93,21 @@ impl CachedRenderPipelinePhaseItem for Opaque3dDeferred { /// /// Used to render all meshes with a material with an alpha mask. pub struct AlphaMask3dDeferred { - pub asset_id: AssetId, - pub entity: Entity, - pub pipeline_id: CachedRenderPipelineId, - pub draw_function: DrawFunctionId, + pub key: OpaqueNoLightmap3dBinKey, + pub representative_entity: Entity, pub batch_range: Range, pub dynamic_offset: Option, } impl PhaseItem for AlphaMask3dDeferred { - type SortKey = (usize, AssetId); - #[inline] fn entity(&self) -> Entity { - self.entity - } - - #[inline] - fn sort_key(&self) -> Self::SortKey { - // Sort by pipeline, then by mesh to massively decrease drawcall counts in real scenes. - (self.pipeline_id.id(), self.asset_id) + self.representative_entity } #[inline] fn draw_function(&self) -> DrawFunctionId { - self.draw_function - } - - #[inline] - fn sort(items: &mut [Self]) { - items.sort_unstable_by_key(Self::sort_key); + self.key.draw_function } #[inline] @@ -141,9 +131,27 @@ impl PhaseItem for AlphaMask3dDeferred { } } +impl BinnedPhaseItem for AlphaMask3dDeferred { + type BinKey = OpaqueNoLightmap3dBinKey; + + fn new( + key: Self::BinKey, + representative_entity: Entity, + batch_range: Range, + dynamic_offset: Option, + ) -> Self { + Self { + key, + representative_entity, + batch_range, + dynamic_offset, + } + } +} + impl CachedRenderPipelinePhaseItem for AlphaMask3dDeferred { #[inline] fn cached_pipeline(&self) -> CachedRenderPipelineId { - self.pipeline_id + self.key.pipeline } } diff --git a/crates/bevy_core_pipeline/src/deferred/node.rs b/crates/bevy_core_pipeline/src/deferred/node.rs index c2acfe53861cd..d599cb7c8bc03 100644 --- a/crates/bevy_core_pipeline/src/deferred/node.rs +++ b/crates/bevy_core_pipeline/src/deferred/node.rs @@ -2,12 +2,11 @@ use bevy_ecs::prelude::*; use bevy_ecs::query::QueryItem; use bevy_render::render_graph::ViewNode; -use bevy_render::render_phase::TrackedRenderPass; +use bevy_render::render_phase::{BinnedRenderPhase, TrackedRenderPass}; use bevy_render::render_resource::{CommandEncoderDescriptor, StoreOp}; use bevy_render::{ camera::ExtractedCamera, render_graph::{NodeRunError, RenderGraphContext}, - render_phase::RenderPhase, render_resource::RenderPassDescriptor, renderer::RenderContext, view::ViewDepthTexture, @@ -28,8 +27,8 @@ pub struct DeferredGBufferPrepassNode; impl ViewNode for DeferredGBufferPrepassNode { type ViewQuery = ( &'static ExtractedCamera, - &'static RenderPhase, - &'static RenderPhase, + &'static BinnedRenderPhase, + &'static BinnedRenderPhase, &'static ViewDepthTexture, &'static ViewPrepassTextures, ); @@ -138,14 +137,16 @@ impl ViewNode for DeferredGBufferPrepassNode { } // Opaque draws - if !opaque_deferred_phase.items.is_empty() { + if !opaque_deferred_phase.batchable_keys.is_empty() + || !opaque_deferred_phase.unbatchable_keys.is_empty() + { #[cfg(feature = "trace")] let _opaque_prepass_span = info_span!("opaque_deferred").entered(); opaque_deferred_phase.render(&mut render_pass, world, view_entity); } // Alpha masked draws - if !alpha_mask_deferred_phase.items.is_empty() { + if !alpha_mask_deferred_phase.is_empty() { #[cfg(feature = "trace")] let _alpha_mask_deferred_span = info_span!("alpha_mask_deferred").entered(); alpha_mask_deferred_phase.render(&mut render_pass, world, view_entity); diff --git a/crates/bevy_core_pipeline/src/lib.rs b/crates/bevy_core_pipeline/src/lib.rs index d6ce9af95dab8..9bb44c4e33116 100644 --- a/crates/bevy_core_pipeline/src/lib.rs +++ b/crates/bevy_core_pipeline/src/lib.rs @@ -1,6 +1,11 @@ // FIXME(3492): remove once docs are ready #![allow(missing_docs)] +#![forbid(unsafe_code)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] pub mod blit; pub mod bloom; @@ -20,6 +25,8 @@ pub mod upscaling; pub use skybox::Skybox; /// Experimental features that are not yet finished. Please report any issues you encounter! +/// +/// Expect bugs, missing features, compatibility issues, low performance, and/or future breaking changes. pub mod experimental { pub mod taa { pub use crate::taa::{ diff --git a/crates/bevy_core_pipeline/src/prepass/mod.rs b/crates/bevy_core_pipeline/src/prepass/mod.rs index 348419336ff59..01fca93ddc254 100644 --- a/crates/bevy_core_pipeline/src/prepass/mod.rs +++ b/crates/bevy_core_pipeline/src/prepass/mod.rs @@ -34,8 +34,8 @@ use bevy_ecs::prelude::*; use bevy_reflect::Reflect; use bevy_render::{ mesh::Mesh, - render_phase::{CachedRenderPipelinePhaseItem, DrawFunctionId, PhaseItem}, - render_resource::{CachedRenderPipelineId, Extent3d, TextureFormat, TextureView}, + render_phase::{BinnedPhaseItem, CachedRenderPipelinePhaseItem, DrawFunctionId, PhaseItem}, + render_resource::{BindGroupId, CachedRenderPipelineId, Extent3d, TextureFormat, TextureView}, texture::ColorAttachment, }; use nonmax::NonMaxU32; @@ -111,36 +111,45 @@ impl ViewPrepassTextures { /// /// Used to render all 3D meshes with materials that have no transparency. pub struct Opaque3dPrepass { - pub entity: Entity, - pub asset_id: AssetId, - pub pipeline_id: CachedRenderPipelineId, - pub draw_function: DrawFunctionId, + /// Information that separates items into bins. + pub key: OpaqueNoLightmap3dBinKey, + + /// An entity from which Bevy fetches data common to all instances in this + /// batch, such as the mesh. + pub representative_entity: Entity, + pub batch_range: Range, pub dynamic_offset: Option, } -impl PhaseItem for Opaque3dPrepass { - type SortKey = (usize, AssetId); +// TODO: Try interning these. +/// The data used to bin each opaque 3D mesh in the prepass and deferred pass. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct OpaqueNoLightmap3dBinKey { + /// The ID of the GPU pipeline. + pub pipeline: CachedRenderPipelineId, - #[inline] - fn entity(&self) -> Entity { - self.entity - } + /// The function used to draw the mesh. + pub draw_function: DrawFunctionId, - #[inline] - fn sort_key(&self) -> Self::SortKey { - // Sort by pipeline, then by mesh to massively decrease drawcall counts in real scenes. - (self.pipeline_id.id(), self.asset_id) - } + /// The ID of the mesh. + pub asset_id: AssetId, + /// The ID of a bind group specific to the material. + /// + /// In the case of PBR, this is the `MaterialBindGroupId`. + pub material_bind_group_id: Option, +} + +impl PhaseItem for Opaque3dPrepass { #[inline] - fn draw_function(&self) -> DrawFunctionId { - self.draw_function + fn entity(&self) -> Entity { + self.representative_entity } #[inline] - fn sort(items: &mut [Self]) { - items.sort_unstable_by_key(Self::sort_key); + fn draw_function(&self) -> DrawFunctionId { + self.key.draw_function } #[inline] @@ -164,10 +173,29 @@ impl PhaseItem for Opaque3dPrepass { } } +impl BinnedPhaseItem for Opaque3dPrepass { + type BinKey = OpaqueNoLightmap3dBinKey; + + #[inline] + fn new( + key: Self::BinKey, + representative_entity: Entity, + batch_range: Range, + dynamic_offset: Option, + ) -> Self { + Opaque3dPrepass { + key, + representative_entity, + batch_range, + dynamic_offset, + } + } +} + impl CachedRenderPipelinePhaseItem for Opaque3dPrepass { #[inline] fn cached_pipeline(&self) -> CachedRenderPipelineId { - self.pipeline_id + self.key.pipeline } } @@ -177,36 +205,21 @@ impl CachedRenderPipelinePhaseItem for Opaque3dPrepass { /// /// Used to render all meshes with a material with an alpha mask. pub struct AlphaMask3dPrepass { - pub asset_id: AssetId, - pub entity: Entity, - pub pipeline_id: CachedRenderPipelineId, - pub draw_function: DrawFunctionId, + pub key: OpaqueNoLightmap3dBinKey, + pub representative_entity: Entity, pub batch_range: Range, pub dynamic_offset: Option, } impl PhaseItem for AlphaMask3dPrepass { - type SortKey = (usize, AssetId); - #[inline] fn entity(&self) -> Entity { - self.entity - } - - #[inline] - fn sort_key(&self) -> Self::SortKey { - // Sort by pipeline, then by mesh to massively decrease drawcall counts in real scenes. - (self.pipeline_id.id(), self.asset_id) + self.representative_entity } #[inline] fn draw_function(&self) -> DrawFunctionId { - self.draw_function - } - - #[inline] - fn sort(items: &mut [Self]) { - items.sort_unstable_by_key(Self::sort_key); + self.key.draw_function } #[inline] @@ -230,9 +243,28 @@ impl PhaseItem for AlphaMask3dPrepass { } } +impl BinnedPhaseItem for AlphaMask3dPrepass { + type BinKey = OpaqueNoLightmap3dBinKey; + + #[inline] + fn new( + key: Self::BinKey, + representative_entity: Entity, + batch_range: Range, + dynamic_offset: Option, + ) -> Self { + Self { + key, + representative_entity, + batch_range, + dynamic_offset, + } + } +} + impl CachedRenderPipelinePhaseItem for AlphaMask3dPrepass { #[inline] fn cached_pipeline(&self) -> CachedRenderPipelineId { - self.pipeline_id + self.key.pipeline } } diff --git a/crates/bevy_core_pipeline/src/prepass/node.rs b/crates/bevy_core_pipeline/src/prepass/node.rs index 928183ad7d010..74de568e2bdfb 100644 --- a/crates/bevy_core_pipeline/src/prepass/node.rs +++ b/crates/bevy_core_pipeline/src/prepass/node.rs @@ -2,8 +2,9 @@ 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_phase::{BinnedRenderPhase, TrackedRenderPass}, render_resource::{CommandEncoderDescriptor, RenderPassDescriptor, StoreOp}, renderer::RenderContext, view::ViewDepthTexture, @@ -22,8 +23,8 @@ pub struct PrepassNode; impl ViewNode for PrepassNode { type ViewQuery = ( &'static ExtractedCamera, - &'static RenderPhase, - &'static RenderPhase, + &'static BinnedRenderPhase, + &'static BinnedRenderPhase, &'static ViewDepthTexture, &'static ViewPrepassTextures, Option<&'static DeferredPrepass>, @@ -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,25 +86,31 @@ 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); } // Opaque draws - if !opaque_prepass_phase.items.is_empty() { + if !opaque_prepass_phase.batchable_keys.is_empty() + || !opaque_prepass_phase.unbatchable_keys.is_empty() + { #[cfg(feature = "trace")] let _opaque_prepass_span = info_span!("opaque_prepass").entered(); opaque_prepass_phase.render(&mut render_pass, world, view_entity); } // Alpha masked draws - if !alpha_mask_prepass_phase.items.is_empty() { + if !alpha_mask_prepass_phase.is_empty() { #[cfg(feature = "trace")] let _alpha_mask_prepass_span = info_span!("alpha_mask_prepass").entered(); 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_derive/Cargo.toml b/crates/bevy_derive/Cargo.toml index abf5e47f4bb0e..a936ac773a88c 100644 --- a/crates/bevy_derive/Cargo.toml +++ b/crates/bevy_derive/Cargo.toml @@ -19,3 +19,7 @@ syn = { version = "2.0", features = ["full"] } [lints] workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +all-features = true diff --git a/crates/bevy_derive/src/lib.rs b/crates/bevy_derive/src/lib.rs index fda825dc161a6..576b26bf99c18 100644 --- a/crates/bevy_derive/src/lib.rs +++ b/crates/bevy_derive/src/lib.rs @@ -1,5 +1,11 @@ // FIXME(3492): remove once docs are ready #![allow(missing_docs)] +#![forbid(unsafe_code)] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] extern crate proc_macro; diff --git a/crates/bevy_dev_tools/Cargo.toml b/crates/bevy_dev_tools/Cargo.toml index 7be2541583148..727ba84f80691 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 } @@ -34,4 +43,5 @@ ron = { version = "0.8.0", optional = true } workspace = true [package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] all-features = true 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..d244873160d72 100644 --- a/crates/bevy_dev_tools/src/lib.rs +++ b/crates/bevy_dev_tools/src/lib.rs @@ -1,6 +1,12 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![forbid(unsafe_code)] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] + //! This crate provides additional utilities for the [Bevy game engine](https://bevyengine.org), //! focused on improving developer experience. -#![cfg_attr(docsrs, feature(doc_auto_cfg))] use bevy_app::prelude::*; @@ -8,6 +14,9 @@ use bevy_app::prelude::*; pub mod ci_testing; pub mod fps_overlay; +#[cfg(feature = "bevy_ui_debug")] +pub mod ui_debug_overlay; + /// Enables developer tools in an [`App`]. This plugin is added automatically with `bevy_dev_tools` /// feature. /// diff --git a/crates/bevy_dev_tools/src/ui_debug_overlay/inset.rs b/crates/bevy_dev_tools/src/ui_debug_overlay/inset.rs new file mode 100644 index 0000000000000..86be2146c73d7 --- /dev/null +++ b/crates/bevy_dev_tools/src/ui_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/ui_debug_overlay/mod.rs b/crates/bevy_dev_tools/src/ui_debug_overlay/mod.rs new file mode 100644 index 0000000000000..3029a05c9b6cc --- /dev/null +++ b/crates/bevy_dev_tools/src/ui_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 = 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_diagnostic/Cargo.toml b/crates/bevy_diagnostic/Cargo.toml index d82a5f2e52c75..78f1d0bb005fb 100644 --- a/crates/bevy_diagnostic/Cargo.toml +++ b/crates/bevy_diagnostic/Cargo.toml @@ -38,4 +38,5 @@ sysinfo = { version = "0.30.0", optional = true, default-features = false } workspace = true [package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] all-features = true 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_diagnostic/src/lib.rs b/crates/bevy_diagnostic/src/lib.rs index dc21ec143f1c5..bb0fb5d41d537 100644 --- a/crates/bevy_diagnostic/src/lib.rs +++ b/crates/bevy_diagnostic/src/lib.rs @@ -1,6 +1,11 @@ // FIXME(3492): remove once docs are ready #![allow(missing_docs)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![forbid(unsafe_code)] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] //! This crate provides a straightforward solution for integrating diagnostics in the [Bevy game engine](https://bevyengine.org/). //! It allows users to easily add diagnostic functionality to their Bevy applications, enhancing @@ -19,7 +24,7 @@ pub use entity_count_diagnostics_plugin::EntityCountDiagnosticsPlugin; pub use frame_time_diagnostics_plugin::FrameTimeDiagnosticsPlugin; pub use log_diagnostics_plugin::LogDiagnosticsPlugin; #[cfg(feature = "sysinfo_plugin")] -pub use system_information_diagnostics_plugin::SystemInformationDiagnosticsPlugin; +pub use system_information_diagnostics_plugin::{SystemInfo, SystemInformationDiagnosticsPlugin}; use bevy_app::prelude::*; @@ -28,12 +33,11 @@ use bevy_app::prelude::*; pub struct DiagnosticsPlugin; impl Plugin for DiagnosticsPlugin { - fn build(&self, _app: &mut App) { + fn build(&self, app: &mut App) { + app.init_resource::(); + #[cfg(feature = "sysinfo_plugin")] - _app.init_resource::().add_systems( - Startup, - system_information_diagnostics_plugin::internal::log_system_info, - ); + app.init_resource::(); } } diff --git a/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs b/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs index 0ffe3d479b2af..3709a6d05fd18 100644 --- a/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs +++ b/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs @@ -1,5 +1,6 @@ use crate::DiagnosticPath; use bevy_app::prelude::*; +use bevy_ecs::system::Resource; /// Adds a System Information Diagnostic, specifically `cpu_usage` (in %) and `mem_usage` (in %) /// @@ -24,10 +25,27 @@ impl Plugin for SystemInformationDiagnosticsPlugin { } impl SystemInformationDiagnosticsPlugin { + /// Total system cpu usage in % pub const CPU_USAGE: DiagnosticPath = DiagnosticPath::const_new("system/cpu_usage"); + /// Total system memory usage in % pub const MEM_USAGE: DiagnosticPath = DiagnosticPath::const_new("system/mem_usage"); } +/// A resource that stores diagnostic information about the system. +/// This information can be useful for debugging and profiling purposes. +/// +/// # See also +/// +/// [`SystemInformationDiagnosticsPlugin`] for more information. +#[derive(Debug, Resource)] +pub struct SystemInfo { + pub os: String, + pub kernel: String, + pub cpu: String, + pub core_count: String, + pub memory: String, +} + // NOTE: sysinfo fails to compile when using bevy dynamic or on iOS and does nothing on wasm #[cfg(all( any( @@ -45,7 +63,7 @@ pub mod internal { use crate::{Diagnostic, Diagnostics, DiagnosticsStore}; - use super::SystemInformationDiagnosticsPlugin; + use super::{SystemInfo, SystemInformationDiagnosticsPlugin}; const BYTES_TO_GIB: f64 = 1.0 / 1024.0 / 1024.0 / 1024.0; @@ -87,41 +105,33 @@ pub mod internal { }); } - #[derive(Debug)] - // This is required because the Debug trait doesn't detect it's used when it's only used in a print :( - #[allow(dead_code)] - struct SystemInfo { - os: String, - kernel: String, - cpu: String, - core_count: String, - memory: String, - } - - pub(crate) fn log_system_info() { - let sys = System::new_with_specifics( - RefreshKind::new() - .with_cpu(CpuRefreshKind::new()) - .with_memory(MemoryRefreshKind::new().with_ram()), - ); - - let info = SystemInfo { - os: System::long_os_version().unwrap_or_else(|| String::from("not available")), - kernel: System::kernel_version().unwrap_or_else(|| String::from("not available")), - cpu: sys - .cpus() - .first() - .map(|cpu| cpu.brand().trim().to_string()) - .unwrap_or_else(|| String::from("not available")), - core_count: sys - .physical_core_count() - .map(|x| x.to_string()) - .unwrap_or_else(|| String::from("not available")), - // Convert from Bytes to GibiBytes since it's probably what people expect most of the time - memory: format!("{:.1} GiB", sys.total_memory() as f64 * BYTES_TO_GIB), - }; - - info!("{:?}", info); + impl Default for SystemInfo { + fn default() -> Self { + let sys = System::new_with_specifics( + RefreshKind::new() + .with_cpu(CpuRefreshKind::new()) + .with_memory(MemoryRefreshKind::new().with_ram()), + ); + + let system_info = SystemInfo { + os: System::long_os_version().unwrap_or_else(|| String::from("not available")), + kernel: System::kernel_version().unwrap_or_else(|| String::from("not available")), + cpu: sys + .cpus() + .first() + .map(|cpu| cpu.brand().trim().to_string()) + .unwrap_or_else(|| String::from("not available")), + core_count: sys + .physical_core_count() + .map(|x| x.to_string()) + .unwrap_or_else(|| String::from("not available")), + // Convert from Bytes to GibiBytes since it's probably what people expect most of the time + memory: format!("{:.1} GiB", sys.total_memory() as f64 * BYTES_TO_GIB), + }; + + info!("{:?}", system_info); + system_info + } } } @@ -143,7 +153,16 @@ pub mod internal { // no-op } - pub(crate) fn log_system_info() { - // no-op + impl Default for super::SystemInfo { + fn default() -> Self { + let unknown = "Unknown".to_string(); + Self { + os: unknown.clone(), + kernel: unknown.clone(), + cpu: unknown.clone(), + core_count: unknown.clone(), + memory: unknown.clone(), + } + } } } diff --git a/crates/bevy_dylib/Cargo.toml b/crates/bevy_dylib/Cargo.toml index cd6c5c783659c..28535950c0b49 100644 --- a/crates/bevy_dylib/Cargo.toml +++ b/crates/bevy_dylib/Cargo.toml @@ -16,3 +16,7 @@ bevy_internal = { path = "../bevy_internal", version = "0.14.0-dev", default-fea [lints] workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +all-features = true diff --git a/crates/bevy_dylib/src/lib.rs b/crates/bevy_dylib/src/lib.rs index 9aefb17ace32f..c37cff70c36a1 100644 --- a/crates/bevy_dylib/src/lib.rs +++ b/crates/bevy_dylib/src/lib.rs @@ -1,4 +1,8 @@ -#![allow(clippy::single_component_path_imports)] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] //! Forces dynamic linking of Bevy. //! @@ -51,4 +55,5 @@ // Force linking of the main bevy crate #[allow(unused_imports)] +#[allow(clippy::single_component_path_imports)] use bevy_internal; diff --git a/crates/bevy_dynamic_plugin/Cargo.toml b/crates/bevy_dynamic_plugin/Cargo.toml index 67f22232c2f45..5e7d11754361b 100644 --- a/crates/bevy_dynamic_plugin/Cargo.toml +++ b/crates/bevy_dynamic_plugin/Cargo.toml @@ -18,3 +18,7 @@ thiserror = "1.0" [lints] workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +all-features = true diff --git a/crates/bevy_dynamic_plugin/src/lib.rs b/crates/bevy_dynamic_plugin/src/lib.rs index 3a620cee1f6be..9d84d049f037b 100644 --- a/crates/bevy_dynamic_plugin/src/lib.rs +++ b/crates/bevy_dynamic_plugin/src/lib.rs @@ -1,3 +1,9 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] + //! Bevy's dynamic plugin loading functionality. //! //! This crate allows loading dynamic libraries (`.dylib`, `.so`) that export a single diff --git a/crates/bevy_dynamic_plugin/src/loader.rs b/crates/bevy_dynamic_plugin/src/loader.rs index 94c283a886e78..8b6517b237244 100644 --- a/crates/bevy_dynamic_plugin/src/loader.rs +++ b/crates/bevy_dynamic_plugin/src/loader.rs @@ -1,3 +1,5 @@ +#![allow(unsafe_code)] + use libloading::{Library, Symbol}; use std::ffi::OsStr; use thiserror::Error; diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml index 11fd506a382ea..6e54aaf7359a2 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" @@ -51,4 +51,5 @@ path = "examples/change_detection.rs" workspace = true [package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] all-features = true diff --git a/crates/bevy_ecs/macros/Cargo.toml b/crates/bevy_ecs/macros/Cargo.toml index c9f3a5754c9ce..abc6647a8bc52 100644 --- a/crates/bevy_ecs/macros/Cargo.toml +++ b/crates/bevy_ecs/macros/Cargo.toml @@ -17,3 +17,7 @@ proc-macro2 = "1.0" [lints] workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +all-features = true diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index 732cd3fd37e08..267969a55ed6b 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -1,5 +1,6 @@ // FIXME(3492): remove once docs are ready #![allow(missing_docs)] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] extern crate proc_macro; diff --git a/crates/bevy_ecs/src/archetype.rs b/crates/bevy_ecs/src/archetype.rs index 1beb719944162..69e9444cabab3 100644 --- a/crates/bevy_ecs/src/archetype.rs +++ b/crates/bevy_ecs/src/archetype.rs @@ -115,7 +115,10 @@ pub(crate) enum ComponentStatus { } pub(crate) struct AddBundle { + /// The target archetype after the bundle is added to the source archetype pub archetype_id: ArchetypeId, + /// For each component iterated in the same order as the source [`Bundle`](crate::bundle::Bundle), + /// indicate if the component is newly added to the target archetype or if it already existed pub bundle_status: Vec, } @@ -286,8 +289,12 @@ impl ArchetypeEntity { } } +/// Internal metadata for an [`Entity`] getting removed from an [`Archetype`]. pub(crate) struct ArchetypeSwapRemoveResult { + /// If the [`Entity`] was not the last in the [`Archetype`], it gets removed by swapping it out + /// with the last entity in the archetype. In that case, this field contains the swapped entity. pub(crate) swapped_entity: Option, + /// The [`TableRow`] where the removed entity's components are stored. pub(crate) table_row: TableRow, } diff --git a/crates/bevy_ecs/src/bundle.rs b/crates/bevy_ecs/src/bundle.rs index 3cb5aae7cf231..898323a30d2be 100644 --- a/crates/bevy_ecs/src/bundle.rs +++ b/crates/bevy_ecs/src/bundle.rs @@ -996,6 +996,8 @@ impl Bundles { } /// Initializes a new [`BundleInfo`] for a statically known type. + /// + /// Also initializes all the components in the bundle. pub(crate) fn init_info( &mut self, components: &mut Components, @@ -1018,6 +1020,8 @@ impl Bundles { id } + /// # Safety + /// A `BundleInfo` with the given `BundleId` must have been initialized for this instance of `Bundles`. pub(crate) unsafe fn get_unchecked(&self, id: BundleId) -> &BundleInfo { self.bundle_infos.get_unchecked(id.0) } diff --git a/crates/bevy_ecs/src/event.rs b/crates/bevy_ecs/src/event.rs index 62cfce6329a61..af7b6df6b6a27 100644 --- a/crates/bevy_ecs/src/event.rs +++ b/crates/bevy_ecs/src/event.rs @@ -614,6 +614,15 @@ impl Default for ManualEventReader { } } +impl Clone for ManualEventReader { + fn clone(&self) -> Self { + ManualEventReader { + last_event_count: self.last_event_count, + _marker: PhantomData, + } + } +} + #[allow(clippy::len_without_is_empty)] // Check fails since the is_empty implementation has a signature other than `(&self) -> bool` impl ManualEventReader { /// See [`EventReader::read`] diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 17c5693d59269..4626fbd6c2d07 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -2,6 +2,11 @@ #![allow(unsafe_op_in_unsafe_fn)] #![doc = include_str!("../README.md")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![allow(unsafe_code)] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] #[cfg(target_pointer_width = "16")] compile_error!("bevy_ecs cannot safely compile for a 16-bit platform."); @@ -84,6 +89,7 @@ mod tests { #[derive(Component, Debug, PartialEq, Eq, Clone, Copy)] struct C; + #[allow(dead_code)] #[derive(Default)] struct NonSendA(usize, PhantomData<*mut ()>); @@ -102,6 +108,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 +1732,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/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 7f249f4a36660..545b15f8dad55 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -1,8 +1,8 @@ use crate::{ - archetype::Archetype, + archetype::{Archetype, Archetypes}, change_detection::{Ticks, TicksMut}, component::{Component, ComponentId, StorageType, Tick}, - entity::Entity, + entity::{Entities, Entity, EntityLocation}, query::{Access, DebugCheckedUnwrap, FilteredAccess, WorldQuery}, storage::{ComponentSparseSet, Table, TableRow}, world::{ @@ -18,7 +18,7 @@ use std::{cell::UnsafeCell, marker::PhantomData}; /// /// There are many types that natively implement this trait: /// -/// - **Component references.** +/// - **Component references. (&T and &mut T)** /// Fetches a component by reference (immutably or mutably). /// - **`QueryData` tuples.** /// If every element of a tuple implements `QueryData`, then the tuple itself also implements the same trait. @@ -27,6 +27,14 @@ use std::{cell::UnsafeCell, marker::PhantomData}; /// but nesting of tuples allows infinite `WorldQuery`s. /// - **[`Entity`].** /// Gets the identifier of the queried entity. +/// - **[`EntityLocation`].** +/// Gets the location metadata of the queried entity. +/// - **[`EntityRef`].** +/// Read-only access to arbitrary components on the queried entity. +/// - **[`EntityMut`].** +/// Mutable access to arbitrary components on the queried entity. +/// - **[`&Archetype`](Archetype).** +/// Read-only access to the archetype-level metadata of the queried entity. /// - **[`Option`].** /// By default, a world query only tests entities that have the matching component types. /// Wrapping it into an `Option` will increase the query search space, and it will return `None` if an entity doesn't satisfy the `WorldQuery`. @@ -345,6 +353,78 @@ unsafe impl QueryData for Entity { /// SAFETY: access is read only unsafe impl ReadOnlyQueryData for Entity {} +/// SAFETY: +/// `update_component_access` and `update_archetype_component_access` do nothing. +/// This is sound because `fetch` does not access components. +unsafe impl WorldQuery for EntityLocation { + type Item<'w> = EntityLocation; + type Fetch<'w> = &'w Entities; + type State = (); + + fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + item + } + + unsafe fn init_fetch<'w>( + world: UnsafeWorldCell<'w>, + _state: &Self::State, + _last_run: Tick, + _this_run: Tick, + ) -> Self::Fetch<'w> { + world.entities() + } + + // This is set to true to avoid forcing archetypal iteration in compound queries, is likely to be slower + // in most practical use case. + const IS_DENSE: bool = true; + + #[inline] + unsafe fn set_archetype<'w>( + _fetch: &mut Self::Fetch<'w>, + _state: &Self::State, + _archetype: &'w Archetype, + _table: &Table, + ) { + } + + #[inline] + unsafe fn set_table<'w>(_fetch: &mut Self::Fetch<'w>, _state: &Self::State, _table: &'w Table) { + } + + #[inline(always)] + unsafe fn fetch<'w>( + fetch: &mut Self::Fetch<'w>, + entity: Entity, + _table_row: TableRow, + ) -> Self::Item<'w> { + // SAFETY: `fetch` must be called with an entity that exists in the world + unsafe { fetch.get(entity).debug_checked_unwrap() } + } + + fn update_component_access(_state: &Self::State, _access: &mut FilteredAccess) {} + + fn init_state(_world: &mut World) {} + + fn get_state(_world: &World) -> Option<()> { + Some(()) + } + + fn matches_component_set( + _state: &Self::State, + _set_contains_id: &impl Fn(ComponentId) -> bool, + ) -> bool { + true + } +} + +/// SAFETY: `Self` is the same as `Self::ReadOnly` +unsafe impl QueryData for EntityLocation { + type ReadOnly = Self; +} + +/// SAFETY: access is read only +unsafe impl ReadOnlyQueryData for EntityLocation {} + /// SAFETY: /// `fetch` accesses all components in a readonly way. /// This is sound because `update_component_access` and `update_archetype_component_access` set read access for all components and panic when appropriate. @@ -709,6 +789,81 @@ unsafe impl<'a> QueryData for FilteredEntityMut<'a> { type ReadOnly = FilteredEntityRef<'a>; } +/// SAFETY: +/// `update_component_access` and `update_archetype_component_access` do nothing. +/// This is sound because `fetch` does not access components. +unsafe impl WorldQuery for &Archetype { + type Item<'w> = &'w Archetype; + type Fetch<'w> = (&'w Entities, &'w Archetypes); + type State = (); + + fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + item + } + + unsafe fn init_fetch<'w>( + world: UnsafeWorldCell<'w>, + _state: &Self::State, + _last_run: Tick, + _this_run: Tick, + ) -> Self::Fetch<'w> { + (world.entities(), world.archetypes()) + } + + // This could probably be a non-dense query and just set a Option<&Archetype> fetch value in + // set_archetypes, but forcing archetypal iteration is likely to be slower in any compound query. + const IS_DENSE: bool = true; + + #[inline] + unsafe fn set_archetype<'w>( + _fetch: &mut Self::Fetch<'w>, + _state: &Self::State, + _archetype: &'w Archetype, + _table: &Table, + ) { + } + + #[inline] + unsafe fn set_table<'w>(_fetch: &mut Self::Fetch<'w>, _state: &Self::State, _table: &'w Table) { + } + + #[inline(always)] + unsafe fn fetch<'w>( + fetch: &mut Self::Fetch<'w>, + entity: Entity, + _table_row: TableRow, + ) -> Self::Item<'w> { + let (entities, archetypes) = *fetch; + // SAFETY: `fetch` must be called with an entity that exists in the world + let location = unsafe { entities.get(entity).debug_checked_unwrap() }; + // SAFETY: The assigned archetype for a living entity must always be valid. + unsafe { archetypes.get(location.archetype_id).debug_checked_unwrap() } + } + + fn update_component_access(_state: &Self::State, _access: &mut FilteredAccess) {} + + fn init_state(_world: &mut World) {} + + fn get_state(_world: &World) -> Option<()> { + Some(()) + } + + fn matches_component_set( + _state: &Self::State, + _set_contains_id: &impl Fn(ComponentId) -> bool, + ) -> bool { + true + } +} + +/// SAFETY: `Self` is the same as `Self::ReadOnly` +unsafe impl QueryData for &Archetype { + type ReadOnly = Self; +} + +/// SAFETY: access is read only +unsafe impl ReadOnlyQueryData for &Archetype {} + #[doc(hidden)] pub struct ReadFetch<'w, T> { // T::STORAGE_TYPE = StorageType::Table @@ -1410,6 +1565,12 @@ unsafe impl ReadOnlyQueryData for Option {} /// ``` pub struct Has(PhantomData); +impl std::fmt::Debug for Has { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + write!(f, "Has<{}>", std::any::type_name::()) + } +} + /// SAFETY: /// `update_component_access` and `update_archetype_component_access` do nothing. /// This is sound because `fetch` does not access components. @@ -1648,7 +1809,7 @@ all_tuples!(impl_anytuple_fetch, 0, 15, F, S); /// [`WorldQuery`] used to nullify queries by turning `Query` into `Query>` /// /// This will rarely be useful to consumers of `bevy_ecs`. -pub struct NopWorldQuery(PhantomData); +pub(crate) struct NopWorldQuery(PhantomData); /// SAFETY: /// `update_component_access` and `update_archetype_component_access` do nothing. diff --git a/crates/bevy_ecs/src/query/filter.rs b/crates/bevy_ecs/src/query/filter.rs index cb581b58b82d6..d5a1c3839efd0 100644 --- a/crates/bevy_ecs/src/query/filter.rs +++ b/crates/bevy_ecs/src/query/filter.rs @@ -79,6 +79,12 @@ pub trait QueryFilter: WorldQuery { /// many elements are being iterated (such as `Iterator::collect()`). const IS_ARCHETYPAL: bool; + /// Returns true if the provided [`Entity`] and [`TableRow`] should be included in the query results. + /// If false, the entity will be skipped. + /// + /// Note that this is called after already restricting the matched [`Table`]s and [`Archetype`]s to the + /// ones that are compatible with the Filter's access. + /// /// # Safety /// /// Must always be called _after_ [`WorldQuery::set_table`] or [`WorldQuery::set_archetype`]. `entity` and @@ -357,7 +363,7 @@ impl Clone for OrFetch<'_, T> { } } -macro_rules! impl_query_filter_tuple { +macro_rules! impl_or_query_filter { ($(($filter: ident, $state: ident)),*) => { #[allow(unused_variables)] #[allow(non_snake_case)] @@ -506,7 +512,7 @@ macro_rules! impl_tuple_query_filter { } all_tuples!(impl_tuple_query_filter, 0, 15, F); -all_tuples!(impl_query_filter_tuple, 0, 15, F, S); +all_tuples!(impl_or_query_filter, 0, 15, F, S); /// A filter on a component that only retains results added after the system last ran. /// @@ -524,7 +530,7 @@ all_tuples!(impl_query_filter_tuple, 0, 15, F, S); /// # Time complexity /// /// `Added` is not [`ArchetypeFilter`], which practically means that -/// if query (with `T` component filter) matches million entities, +/// if the query (with `T` component filter) matches a million entities, /// `Added` filter will iterate over all of them even if none of them were just added. /// /// For example, these two systems are roughly equivalent in terms of performance: diff --git a/crates/bevy_ecs/src/query/iter.rs b/crates/bevy_ecs/src/query/iter.rs index 679910015beda..d22d1ec75b07d 100644 --- a/crates/bevy_ecs/src/query/iter.rs +++ b/crates/bevy_ecs/src/query/iter.rs @@ -1,9 +1,9 @@ use crate::{ - archetype::{Archetype, ArchetypeEntity, ArchetypeId, Archetypes}, + archetype::{Archetype, ArchetypeEntity, Archetypes}, component::Tick, entity::{Entities, Entity}, - query::{ArchetypeFilter, DebugCheckedUnwrap, QueryState}, - storage::{Table, TableId, TableRow, Tables}, + query::{ArchetypeFilter, DebugCheckedUnwrap, QueryState, StorageId}, + storage::{Table, TableRow, Tables}, world::unsafe_world_cell::UnsafeWorldCell, }; use std::{borrow::Borrow, iter::FusedIterator, mem::MaybeUninit, ops::Range}; @@ -42,7 +42,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { } /// Executes the equivalent of [`Iterator::for_each`] over a contiguous segment - /// from an table. + /// from a table. /// /// # Safety /// - all `rows` must be in `[0, table.entity_count)`. @@ -239,22 +239,20 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Iterator for QueryIter<'w, 's, D, F> let Some(item) = self.next() else { break }; accum = func(accum, item); } - if D::IS_DENSE && F::IS_DENSE { - for table_id in self.cursor.table_id_iter.clone() { + for id in self.cursor.storage_id_iter.clone() { + if D::IS_DENSE && F::IS_DENSE { // SAFETY: Matched table IDs are guaranteed to still exist. - let table = unsafe { self.tables.get(*table_id).debug_checked_unwrap() }; + let table = unsafe { self.tables.get(id.table_id).debug_checked_unwrap() }; accum = // SAFETY: // - The fetched table matches both D and F // - The provided range is equivalent to [0, table.entity_count) // - The if block ensures that D::IS_DENSE and F::IS_DENSE are both true unsafe { self.fold_over_table_range(accum, &mut func, table, 0..table.entity_count()) }; - } - } else { - for archetype_id in self.cursor.archetype_id_iter.clone() { + } else { let archetype = // SAFETY: Matched archetype IDs are guaranteed to still exist. - unsafe { self.archetypes.get(*archetype_id).debug_checked_unwrap() }; + unsafe { self.archetypes.get(id.archetype_id).debug_checked_unwrap() }; accum = // SAFETY: // - The fetched archetype matches both D and F @@ -650,13 +648,12 @@ impl<'w, 's, D: ReadOnlyQueryData, F: QueryFilter, const K: usize> FusedIterator } struct QueryIterationCursor<'w, 's, D: QueryData, F: QueryFilter> { - table_id_iter: std::slice::Iter<'s, TableId>, - archetype_id_iter: std::slice::Iter<'s, ArchetypeId>, + storage_id_iter: std::slice::Iter<'s, StorageId>, table_entities: &'w [Entity], archetype_entities: &'w [ArchetypeEntity], fetch: D::Fetch<'w>, filter: F::Fetch<'w>, - // length of the table table or length of the archetype, depending on whether both `D`'s and `F`'s fetches are dense + // length of the table or length of the archetype, depending on whether both `D`'s and `F`'s fetches are dense current_len: usize, // either table row or archetype index, depending on whether both `D`'s and `F`'s fetches are dense current_row: usize, @@ -665,8 +662,7 @@ struct QueryIterationCursor<'w, 's, D: QueryData, F: QueryFilter> { impl Clone for QueryIterationCursor<'_, '_, D, F> { fn clone(&self) -> Self { Self { - table_id_iter: self.table_id_iter.clone(), - archetype_id_iter: self.archetype_id_iter.clone(), + storage_id_iter: self.storage_id_iter.clone(), table_entities: self.table_entities, archetype_entities: self.archetype_entities, fetch: self.fetch.clone(), @@ -687,8 +683,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { this_run: Tick, ) -> Self { QueryIterationCursor { - table_id_iter: [].iter(), - archetype_id_iter: [].iter(), + storage_id_iter: [].iter(), ..Self::init(world, query_state, last_run, this_run) } } @@ -709,8 +704,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { filter, table_entities: &[], archetype_entities: &[], - table_id_iter: query_state.matched_table_ids.iter(), - archetype_id_iter: query_state.matched_archetype_ids.iter(), + storage_id_iter: query_state.matched_storage_ids.iter(), current_len: 0, current_row: 0, } @@ -743,21 +737,16 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { /// How many values will this cursor return at most? /// - /// Note that if `D::IS_ARCHETYPAL && F::IS_ARCHETYPAL`, the return value + /// Note that if `F::IS_ARCHETYPAL`, the return value /// will be **the exact count of remaining values**. fn max_remaining(&self, tables: &'w Tables, archetypes: &'w Archetypes) -> usize { + let ids = self.storage_id_iter.clone(); let remaining_matched: usize = if Self::IS_DENSE { - let ids = self.table_id_iter.clone(); - // SAFETY: The table was matched with the query, tables cannot be deleted, - // so table_id must be valid. - ids.map(|id| unsafe { tables.get(*id).debug_checked_unwrap().entity_count() }) - .sum() + // SAFETY: The if check ensures that storage_id_iter stores TableIds + unsafe { ids.map(|id| tables.get(id.table_id).debug_checked_unwrap().entity_count()).sum() } } else { - let ids = self.archetype_id_iter.clone(); - // SAFETY: The archetype was matched with the query, tables cannot be deleted, - // so archetype_id must be valid. - ids.map(|id| unsafe { archetypes.get(*id).debug_checked_unwrap().len() }) - .sum() + // SAFETY: The if check ensures that storage_id_iter stores ArchetypeIds + unsafe { ids.map(|id| archetypes.get(id.archetype_id).debug_checked_unwrap().len()).sum() } }; remaining_matched + self.current_len - self.current_row } @@ -779,8 +768,8 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { loop { // we are on the beginning of the query, or finished processing a table, so skip to the next if self.current_row == self.current_len { - let table_id = self.table_id_iter.next()?; - let table = tables.get(*table_id).debug_checked_unwrap(); + let table_id = self.storage_id_iter.next()?.table_id; + let table = tables.get(table_id).debug_checked_unwrap(); // SAFETY: `table` is from the world that `fetch/filter` were created for, // `fetch_state`/`filter_state` are the states that `fetch/filter` were initialized with unsafe { @@ -794,7 +783,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { } // SAFETY: set_table was called prior. - // `current_row` is a table row in range of the current table, because if it was not, then the if above would have been executed. + // `current_row` is a table row in range of the current table, because if it was not, then the above would have been executed. let entity = unsafe { self.table_entities.get_unchecked(self.current_row) }; let row = TableRow::from_usize(self.current_row); if !F::filter_fetch(&mut self.filter, *entity, row) { @@ -805,7 +794,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { // SAFETY: // - set_table was called prior. // - `current_row` must be a table row in range of the current table, - // because if it was not, then the if above would have been executed. + // because if it was not, then the above would have been executed. // - fetch is only called once for each `entity`. let item = unsafe { D::fetch(&mut self.fetch, *entity, row) }; @@ -815,8 +804,8 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { } else { loop { if self.current_row == self.current_len { - let archetype_id = self.archetype_id_iter.next()?; - let archetype = archetypes.get(*archetype_id).debug_checked_unwrap(); + let archetype_id = self.storage_id_iter.next()?.archetype_id; + let archetype = archetypes.get(archetype_id).debug_checked_unwrap(); let table = tables.get(archetype.table_id()).debug_checked_unwrap(); // SAFETY: `archetype` and `tables` are from the world that `fetch/filter` were created for, // `fetch_state`/`filter_state` are the states that `fetch/filter` were initialized with diff --git a/crates/bevy_ecs/src/query/par_iter.rs b/crates/bevy_ecs/src/query/par_iter.rs index ee7ae7ed165cf..6b59372c97713 100644 --- a/crates/bevy_ecs/src/query/par_iter.rs +++ b/crates/bevy_ecs/src/query/par_iter.rs @@ -162,28 +162,22 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryParIter<'w, 's, D, F> { thread_count > 0, "Attempted to run parallel iteration over a query with an empty TaskPool" ); + let id_iter = self.state.matched_storage_ids.iter(); let max_size = if D::IS_DENSE && F::IS_DENSE { // SAFETY: We only access table metadata. let tables = unsafe { &self.world.world_metadata().storages().tables }; - self.state - .matched_table_ids - .iter() - // SAFETY: The table was matched with the query, tables cannot be deleted, - // so table_id must be valid. - .map(|id| unsafe { tables.get(*id).debug_checked_unwrap() }.entity_count()) + id_iter + // SAFETY: The if check ensures that matched_storage_ids stores TableIds + .map(|id| unsafe { tables.get(id.table_id).debug_checked_unwrap().entity_count() }) .max() - .unwrap_or(0) } else { let archetypes = &self.world.archetypes(); - self.state - .matched_archetype_ids - .iter() - // SAFETY: The table was matched with the query, tables cannot be deleted, - // so table_id must be valid. - .map(|id| unsafe { archetypes.get(*id).debug_checked_unwrap().len() }) + id_iter + // SAFETY: The if check ensures that matched_storage_ids stores ArchetypeIds + .map(|id| unsafe { archetypes.get(id.archetype_id).debug_checked_unwrap().len() }) .max() - .unwrap_or(0) }; + let max_size = max_size.unwrap_or(0); let batches = thread_count * self.batching_strategy.batches_per_thread; // Round up to the nearest batch size. diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index 6048038f7a802..d99175257ef1d 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -21,7 +21,37 @@ use super::{ QuerySingleError, ROQueryItem, }; +/// An ID for either a table or an archetype. Used for Query iteration. +/// +/// Query iteration is exclusively dense (over tables) or archetypal (over archetypes) based on whether +/// both `D::IS_DENSE` and `F::IS_DENSE` are true or not. +/// +/// This is a union instead of an enum as the usage is determined at compile time, as all [`StorageId`]s for +/// a [`QueryState`] will be all [`TableId`]s or all [`ArchetypeId`]s, and not a mixture of both. This +/// removes the need for discriminator to minimize memory usage and branching during iteration, but requires +/// a safety invariant be verified when disambiguating them. +/// +/// # Safety +/// Must be initialized and accessed as a [`TableId`], if both generic parameters to the query are dense. +/// Must be initialized and accessed as an [`ArchetypeId`] otherwise. +#[derive(Clone, Copy)] +pub(super) union StorageId { + pub(super) table_id: TableId, + pub(super) archetype_id: ArchetypeId, +} + /// Provides scoped access to a [`World`] state according to a given [`QueryData`] and [`QueryFilter`]. +/// +/// This data is cached between system runs, and is used to: +/// - store metadata about which [`Table`] or [`Archetype`] are matched by the query. "Matched" means +/// that the query will iterate over the data in the matched table/archetype. +/// - cache the [`State`] needed to compute the [`Fetch`] struct used to retrieve data +/// from a specific [`Table`] or [`Archetype`] +/// - build iterators that can iterate over the query results +/// +/// [`State`]: crate::query::world_query::WorldQuery::State +/// [`Fetch`]: crate::query::world_query::WorldQuery::Fetch +/// [`Table`]: crate::storage::Table #[repr(C)] // SAFETY NOTE: // Do not add any new fields that use the `D` or `F` generic parameters as this may @@ -29,14 +59,15 @@ use super::{ pub struct QueryState { world_id: WorldId, pub(crate) archetype_generation: ArchetypeGeneration, + /// Metadata about the [`Table`](crate::storage::Table)s matched by this query. pub(crate) matched_tables: FixedBitSet, + /// Metadata about the [`Archetype`]s matched by this query. pub(crate) matched_archetypes: FixedBitSet, - pub(crate) archetype_component_access: Access, + /// [`FilteredAccess`] computed by combining the `D` and `F` access. Used to check which other queries + /// this query can run in parallel with. 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, - // NOTE: we maintain both a ArchetypeId bitset and a vec because iterating the vec is faster - pub(crate) matched_archetype_ids: Vec, + // NOTE: we maintain both a bitset and a vec because iterating the vec is faster + pub(super) matched_storage_ids: Vec, pub(crate) fetch_state: D::State, pub(crate) filter_state: F::State, #[cfg(feature = "trace")] @@ -47,8 +78,11 @@ impl fmt::Debug for QueryState { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("QueryState") .field("world_id", &self.world_id) - .field("matched_table_count", &self.matched_table_ids.len()) - .field("matched_archetype_count", &self.matched_archetype_ids.len()) + .field("matched_table_count", &self.matched_tables.count_ones(..)) + .field( + "matched_archetype_count", + &self.matched_archetypes.count_ones(..), + ) .finish_non_exhaustive() } } @@ -72,7 +106,7 @@ impl QueryState { /// /// This doesn't use `NopWorldQuery` as it loses filter functionality, for example /// `NopWorldQuery>` is functionally equivalent to `With`. - pub fn as_nop(&self) -> &QueryState, F> { + pub(crate) fn as_nop(&self) -> &QueryState, F> { // SAFETY: `NopWorldQuery` doesn't have any accesses and defers to // `D` for table/archetype matching unsafe { self.as_transmuted_state::, F>() } @@ -96,30 +130,50 @@ 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 } /// Returns the tables matched by this query. - pub fn matched_tables(&self) -> &[TableId] { - &self.matched_table_ids + pub fn matched_tables(&self) -> impl Iterator + '_ { + self.matched_tables.ones().map(TableId::from_usize) } /// Returns the archetypes matched by this query. - pub fn matched_archetypes(&self) -> &[ArchetypeId] { - &self.matched_archetype_ids + pub fn matched_archetypes(&self) -> impl Iterator + '_ { + self.matched_archetypes.ones().map(ArchetypeId::new) } } 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,26 +190,22 @@ 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(), - matched_archetype_ids: Vec::new(), + matched_storage_ids: Vec::new(), fetch_state, filter_state, 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`]. @@ -167,14 +217,12 @@ impl QueryState { let mut state = Self { world_id: builder.world().id(), archetype_generation: ArchetypeGeneration::initial(), - matched_table_ids: Vec::new(), - matched_archetype_ids: Vec::new(), + matched_storage_ids: Vec::new(), fetch_state, filter_state, 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", @@ -214,6 +262,24 @@ impl QueryState { } } + /// Returns `true` if the given [`Entity`] matches the query. + /// + /// This is always guaranteed to run in `O(1)` time. + #[inline] + pub fn contains(&self, entity: Entity, world: &World, last_run: Tick, this_run: Tick) -> bool { + // SAFETY: NopFetch does not access any members while &self ensures no one has exclusive access + unsafe { + self.as_nop() + .get_unchecked_manual( + world.as_unsafe_world_cell_readonly(), + entity, + last_run, + this_run, + ) + .is_ok() + } + } + /// Checks if the query is empty for the given [`UnsafeWorldCell`]. /// /// # Safety @@ -276,7 +342,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); } } @@ -303,25 +369,49 @@ 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); + } + } + + /// Process the given [`Archetype`] to update internal metadata about the [`Table`](crate::storage::Table)s + /// and [`Archetype`]s that are matched by this query. + /// + /// Returns `true` if the given `archetype` matches the query. Otherwise, returns `false`. + /// If there is no match, then there is no need to update the query's [`FilteredAccess`]. + 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_archetype_ids.push(archetype.id()); + self.matched_archetypes.grow_and_insert(archetype_index); + if !D::IS_DENSE || !F::IS_DENSE { + self.matched_storage_ids.push(StorageId { + archetype_id: 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_table_ids.push(archetype.table_id()); + self.matched_tables.grow_and_insert(table_index); + if D::IS_DENSE && F::IS_DENSE { + self.matched_storage_ids.push(StorageId { + table_id: archetype.table_id(), + }); + } } + true + } else { + false } } @@ -339,15 +429,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); } }); } @@ -392,14 +488,12 @@ impl QueryState { QueryState { world_id: self.world_id, archetype_generation: self.archetype_generation, - matched_table_ids: self.matched_table_ids.clone(), - matched_archetype_ids: self.matched_archetype_ids.clone(), + matched_storage_ids: self.matched_storage_ids.clone(), fetch_state, filter_state, 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", @@ -484,30 +578,35 @@ impl QueryState { } // take the intersection of the matched ids - let matched_tables: FixedBitSet = self - .matched_tables - .intersection(&other.matched_tables) - .collect(); - let matched_table_ids: Vec = - matched_tables.ones().map(TableId::from_usize).collect(); - let matched_archetypes: FixedBitSet = self - .matched_archetypes - .intersection(&other.matched_archetypes) - .collect(); - let matched_archetype_ids: Vec = - matched_archetypes.ones().map(ArchetypeId::new).collect(); + let mut matched_tables = self.matched_tables.clone(); + let mut matched_archetypes = self.matched_archetypes.clone(); + matched_tables.intersect_with(&other.matched_tables); + matched_archetypes.intersect_with(&other.matched_archetypes); + let matched_storage_ids = if NewD::IS_DENSE && NewF::IS_DENSE { + matched_tables + .ones() + .map(|id| StorageId { + table_id: TableId::from_usize(id), + }) + .collect() + } else { + matched_archetypes + .ones() + .map(|id| StorageId { + archetype_id: ArchetypeId::new(id), + }) + .collect() + }; QueryState { world_id: self.world_id, archetype_generation: self.archetype_generation, - matched_table_ids, - matched_archetype_ids, + matched_storage_ids, fetch_state: new_fetch_state, filter_state: new_filter_state, 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", @@ -520,6 +619,8 @@ impl QueryState { /// Gets the query result for the given [`World`] and [`Entity`]. /// /// This can only be called for read-only queries, see [`Self::get_mut`] for write-queries. + /// + /// This is always guaranteed to run in `O(1)` time. #[inline] pub fn get<'w>( &mut self, @@ -592,6 +693,8 @@ impl QueryState { } /// Gets the query result for the given [`World`] and [`Entity`]. + /// + /// This is always guaranteed to run in `O(1)` time. #[inline] pub fn get_mut<'w>( &mut self, @@ -683,6 +786,8 @@ impl QueryState { /// access to `self`. /// /// This can only be called for read-only queries, see [`Self::get_mut`] for mutable queries. + /// + /// This is always guaranteed to run in `O(1)` time. #[inline] pub fn get_manual<'w>( &self, @@ -703,6 +808,8 @@ impl QueryState { /// Gets the query result for the given [`World`] and [`Entity`]. /// + /// This is always guaranteed to run in `O(1)` time. + /// /// # Safety /// /// This does not check for mutable query correctness. To be safe, make sure mutable queries @@ -720,6 +827,8 @@ impl QueryState { /// Gets the query result for the given [`World`] and [`Entity`], where the last change and /// the current change tick are given. /// + /// This is always guaranteed to run in `O(1)` time. + /// /// # Safety /// /// This does not check for mutable query correctness. To be safe, make sure mutable queries @@ -800,6 +909,8 @@ impl QueryState { /// Gets the query results for the given [`World`] and array of [`Entity`], where the last change and /// the current change tick are given. /// + /// This is always guaranteed to run in `O(1)` time. + /// /// # Safety /// /// This does not check for unique access to subsets of the entity-component data. @@ -1276,11 +1387,14 @@ impl QueryState { ) { // NOTE: If you are changing query iteration code, remember to update the following places, where relevant: // QueryIter, QueryIterationCursor, QueryManyIter, QueryCombinationIter, QueryState::for_each_unchecked_manual, QueryState::par_for_each_unchecked_manual + bevy_tasks::ComputeTaskPool::get().scope(|scope| { - if D::IS_DENSE && F::IS_DENSE { - // SAFETY: We only access table data that has been registered in `self.archetype_component_access`. - let tables = unsafe { &world.storages().tables }; - for table_id in &self.matched_table_ids { + // SAFETY: We only access table data that has been registered in `self.archetype_component_access`. + let tables = unsafe { &world.storages().tables }; + let archetypes = world.archetypes(); + for storage_id in &self.matched_storage_ids { + if D::IS_DENSE && F::IS_DENSE { + let table_id = storage_id.table_id; // SAFETY: The table has been matched with the query and tables cannot be deleted, // and so table_id must be valid. let table = unsafe { tables.get(*table_id).debug_checked_unwrap() }; @@ -1292,25 +1406,19 @@ impl QueryState { while offset < table.entity_count() { let mut func = func.clone(); let len = batch_size.min(table.entity_count() - offset); + let batch = offset..offset + len; scope.spawn(async move { #[cfg(feature = "trace")] let _span = self.par_iter_span.enter(); - let table = &world - .storages() - .tables - .get(*table_id) - .debug_checked_unwrap(); - let batch = offset..offset + len; + let table = + &world.storages().tables.get(table_id).debug_checked_unwrap(); self.iter_unchecked_manual(world, last_run, this_run) .for_each_in_table_range(&mut func, table, batch); }); offset += batch_size; } - } - } else { - let archetypes = world.archetypes(); - for archetype_id in &self.matched_archetype_ids { - let mut offset = 0; + } else { + let archetype_id = storage_id.archetype_id; // SAFETY: The table has been matched with the query and tables cannot be deleted, // and so archetype_id must be valid. let archetype = unsafe { archetypes.get(*archetype_id).debug_checked_unwrap() }; @@ -1318,15 +1426,16 @@ impl QueryState { continue; } + let mut offset = 0; while offset < archetype.len() { let mut func = func.clone(); let len = batch_size.min(archetype.len() - offset); + let batch = offset..offset + len; scope.spawn(async move { #[cfg(feature = "trace")] let _span = self.par_iter_span.enter(); let archetype = - world.archetypes().get(*archetype_id).debug_checked_unwrap(); - let batch = offset..offset + len; + world.archetypes().get(archetype_id).debug_checked_unwrap(); self.iter_unchecked_manual(world, last_run, this_run) .for_each_in_archetype_range(&mut func, archetype, batch); }); diff --git a/crates/bevy_ecs/src/query/world_query.rs b/crates/bevy_ecs/src/query/world_query.rs index 9d0d0d829ef4b..19c6a3254b129 100644 --- a/crates/bevy_ecs/src/query/world_query.rs +++ b/crates/bevy_ecs/src/query/world_query.rs @@ -120,6 +120,8 @@ pub unsafe trait WorldQuery { ) -> Self::Item<'w>; /// Adds any component accesses used by this [`WorldQuery`] to `access`. + /// + /// Used to check which queries are disjoint and can run in parallel // This does not have a default body of `{}` because 99% of cases need to add accesses // and forgetting to do so would be unsound. fn update_component_access(state: &Self::State, access: &mut FilteredAccess); @@ -132,6 +134,9 @@ pub unsafe trait WorldQuery { fn get_state(world: &World) -> Option; /// Returns `true` if this query matches a set of components. Otherwise, returns `false`. + /// + /// Used to check which [`Archetype`]s can be skipped by the query + /// (if none of the [`Component`](crate::component::Component)s match) fn matches_component_set( state: &Self::State, set_contains_id: &impl Fn(ComponentId) -> bool, diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index a8d7fe9cc3116..06294b6fe1e24 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -1,6 +1,6 @@ mod parallel_scope; -use super::{Deferred, Resource}; +use super::{Deferred, IntoSystem, RegisterSystem, Resource}; use crate::{ self as bevy_ecs, bundle::Bundle, @@ -520,6 +520,72 @@ impl<'w, 's> Commands<'w, 's> { .push(RunSystemWithInput::new_with_input(id, input)); } + /// Registers a system and returns a [`SystemId`] so it can later be called by [`World::run_system`]. + /// + /// It's possible to register the same systems more than once, they'll be stored separately. + /// + /// This is different from adding systems to a [`Schedule`](crate::schedule::Schedule), + /// because the [`SystemId`] that is returned can be used anywhere in the [`World`] to run the associated system. + /// This allows for running systems in a push-based fashion. + /// Using a [`Schedule`](crate::schedule::Schedule) is still preferred for most cases + /// due to its better performance and ability to run non-conflicting systems simultaneously. + /// + /// If you want to prevent Commands from registering the same system multiple times, consider using [`Local`](crate::system::Local) + /// + /// # Example + /// + /// ``` + /// # use bevy_ecs::{prelude::*, world::CommandQueue, system::SystemId}; + /// + /// #[derive(Resource)] + /// struct Counter(i32); + /// + /// fn register_system(mut local_system: Local>, mut commands: Commands) { + /// if let Some(system) = *local_system { + /// commands.run_system(system); + /// } else { + /// *local_system = Some(commands.register_one_shot_system(increment_counter)); + /// } + /// } + /// + /// fn increment_counter(mut value: ResMut) { + /// value.0 += 1; + /// } + /// + /// # let mut world = World::default(); + /// # world.insert_resource(Counter(0)); + /// # let mut queue_1 = CommandQueue::default(); + /// # let systemid = { + /// # let mut commands = Commands::new(&mut queue_1, &world); + /// # commands.register_one_shot_system(increment_counter) + /// # }; + /// # let mut queue_2 = CommandQueue::default(); + /// # { + /// # let mut commands = Commands::new(&mut queue_2, &world); + /// # commands.run_system(systemid); + /// # } + /// # queue_1.append(&mut queue_2); + /// # queue_1.apply(&mut world); + /// # assert_eq!(1, world.resource::().0); + /// # bevy_ecs::system::assert_is_system(register_system); + /// ``` + pub fn register_one_shot_system< + I: 'static + Send, + O: 'static + Send, + M, + S: IntoSystem + 'static, + >( + &mut self, + system: S, + ) -> SystemId { + let entity = self.spawn_empty().id(); + self.queue.push(RegisterSystem::new(system, entity)); + SystemId { + entity, + marker: std::marker::PhantomData, + } + } + /// Pushes a generic [`Command`] to the command queue. /// /// `command` can be a built-in command, custom struct that implements [`Command`] or a closure @@ -764,13 +830,13 @@ impl EntityCommands<'_> { /// commands.entity(player.entity) /// // You can try_insert individual components: /// .try_insert(Defense(10)) - /// + /// /// // You can also insert tuples of components: /// .try_insert(CombatBundle { /// health: Health(100), /// strength: Strength(40), /// }); - /// + /// /// // Suppose this occurs in a parallel adjacent system or process /// commands.entity(player.entity) /// .despawn(); @@ -1092,6 +1158,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 fb87bb2e018ef..96747f89f9929 100644 --- a/crates/bevy_ecs/src/system/query.rs +++ b/crates/bevy_ecs/src/system/query.rs @@ -552,7 +552,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// Returns an [`Iterator`] over the read-only query items generated from an [`Entity`] list. /// /// Items are returned in the order of the list of entities, and may not be unique if the input - /// doesnn't guarantee uniqueness. Entities that don't match the query are skipped. + /// doesn't guarantee uniqueness. Entities that don't match the query are skipped. /// /// # Example /// @@ -813,6 +813,8 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// In case of a nonexisting entity or mismatched component, a [`QueryEntityError`] is returned instead. /// + /// This is always guaranteed to run in `O(1)` time. + /// /// # Example /// /// Here, `get` is used to retrieve the exact query item of the entity specified by the `SelectedCharacter` resource. @@ -931,6 +933,8 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// In case of a nonexisting entity or mismatched component, a [`QueryEntityError`] is returned instead. /// + /// This is always guaranteed to run in `O(1)` time. + /// /// # Example /// /// Here, `get_mut` is used to retrieve the exact query item of the entity specified by the `PoisonedCharacter` resource. @@ -1045,6 +1049,8 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// In case of a nonexisting entity or mismatched component, a [`QueryEntityError`] is returned instead. /// + /// This is always guaranteed to run in `O(1)` time. + /// /// # Safety /// /// This function makes it possible to violate Rust's aliasing guarantees. @@ -1248,6 +1254,8 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// Returns `true` if the given [`Entity`] matches the query. /// + /// This is always guaranteed to run in `O(1)` time. + /// /// # Example /// /// ``` @@ -1333,12 +1341,16 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// Besides removing parameters from the query, you can also /// make limited changes to the types of parameters. /// - /// * Can always add/remove `Entity` + /// * Can always add/remove [`Entity`] + /// * Can always add/remove [`EntityLocation`] + /// * Can always add/remove [`&Archetype`] /// * `Ref` <-> `&T` /// * `&mut T` -> `&T` /// * `&mut T` -> `Ref` /// * [`EntityMut`](crate::world::EntityMut) -> [`EntityRef`](crate::world::EntityRef) - /// + /// + /// [`EntityLocation`]: crate::entity::EntityLocation + /// [`&Archetype`]: crate::archetype::Archetype pub fn transmute_lens(&mut self) -> QueryLens<'_, NewD> { self.transmute_lens_filtered::() } 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 bb4c2fc2d8170..168b285aeb82f 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`]. -pub struct SystemId(Entity, std::marker::PhantomData O>); +pub struct SystemId { + pub(crate) entity: Entity, + pub(crate) marker: 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. @@ -56,22 +58,22 @@ impl Clone for SystemId { // A manual impl is used because the trait bounds should ignore the `I` and `O` phantom parameters. impl PartialEq for SystemId { fn eq(&self, other: &Self) -> bool { - self.0 == other.0 && self.1 == other.1 + self.entity == other.entity && self.marker == other.marker } } // A manual impl is used because the trait bounds should ignore the `I` and `O` phantom parameters. impl std::hash::Hash for SystemId { fn hash(&self, state: &mut H) { - self.0.hash(state); + self.entity.hash(state); } } impl std::fmt::Debug for SystemId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_tuple("SystemId") - .field(&self.0) - .field(&self.1) + .field(&self.entity) + .field(&self.entity) .finish() } } @@ -83,7 +85,7 @@ impl From> for Entity { /// is really an entity with associated handler function. /// /// For example, this is useful if you want to assign a name label to a system. - fn from(SystemId(entity, _): SystemId) -> Self { + fn from(SystemId { entity, .. }: SystemId) -> Self { entity } } @@ -113,14 +115,15 @@ impl World { &mut self, system: BoxedSystem, ) -> SystemId { - SystemId( - self.spawn(RegisteredSystem { - initialized: false, - system, - }) - .id(), - std::marker::PhantomData, - ) + SystemId { + entity: self + .spawn(RegisteredSystem { + initialized: false, + system, + }) + .id(), + marker: std::marker::PhantomData, + } } /// Removes a registered system and returns the system, if it exists. @@ -133,7 +136,7 @@ impl World { &mut self, id: SystemId, ) -> Result, RegisteredSystemError> { - match self.get_entity_mut(id.0) { + match self.get_entity_mut(id.entity) { Some(mut entity) => { let registered_system = entity .take::>() @@ -275,7 +278,7 @@ impl World { ) -> Result> { // lookup let mut entity = self - .get_entity_mut(id.0) + .get_entity_mut(id.entity) .ok_or(RegisteredSystemError::SystemIdNotRegistered(id))?; // take ownership of system trait object @@ -294,7 +297,7 @@ impl World { let result = system.run(input, self); // return ownership of system trait object (if entity still exists) - if let Some(mut entity) = self.get_entity_mut(id.0) { + if let Some(mut entity) = self.get_entity_mut(id.entity) { entity.insert::>(RegisteredSystem { initialized, system, @@ -356,6 +359,35 @@ impl Command for RunSystemWithInput { } } +/// The [`Command`] type for registering one shot systems from [Commands](crate::system::Commands). +/// +/// This command needs an already boxed system to register, and an already spawned entity +pub struct RegisterSystem { + system: BoxedSystem, + entity: Entity, +} + +impl RegisterSystem { + /// Creates a new [Command] struct, which can be added to [Commands](crate::system::Commands) + pub fn new + 'static>(system: S, entity: Entity) -> Self { + Self { + system: Box::new(IntoSystem::into_system(system)), + entity, + } + } +} + +impl Command for RegisterSystem { + fn apply(self, world: &mut World) { + let _ = world.get_entity_mut(self.entity).map(|mut entity| { + entity.insert(RegisteredSystem { + initialized: false, + system: self.system, + }); + }); + } +} + /// An operation with stored systems failed. #[derive(Error)] pub enum RegisteredSystemError { 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 66fad17d1c119..0587e4660769a 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> { @@ -601,6 +657,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`]. /// @@ -610,6 +675,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] @@ -618,6 +693,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] @@ -649,6 +733,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 @@ -665,6 +761,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. @@ -961,22 +1070,25 @@ impl<'w> EntityWorldMut<'w> { /// Remove the components of `bundle` from `entity`. /// - /// SAFETY: The components in `bundle_info` must exist. + /// SAFETY: + /// - A `BundleInfo` with the corresponding `BundleId` must have been initialized. #[allow(clippy::too_many_arguments)] unsafe fn remove_bundle(&mut self, bundle: BundleId) -> EntityLocation { let entity = self.entity; let world = &mut self.world; let location = self.location; + // SAFETY: the caller guarantees that the BundleInfo for this id has been initialized. let bundle_info = world.bundles.get_unchecked(bundle); // SAFETY: `archetype_id` exists because it is referenced in `location` which is valid - // and components in `bundle_info` must exist due to this functions safety invariants. + // and components in `bundle_info` must exist due to this function's safety invariants. let new_archetype_id = remove_bundle_from_archetype( &mut world.archetypes, &mut world.storages, &world.components, location.archetype_id, bundle_info, + // components from the bundle that are not present on the entity are ignored true, ) .expect("intersections should always return a result"); @@ -1059,8 +1171,7 @@ impl<'w> EntityWorldMut<'w> { let components = &mut self.world.components; let bundle_info = self.world.bundles.init_info::(components, storages); - // SAFETY: Components exist in `bundle_info` because `Bundles::init_info` - // initializes a: EntityLocation `BundleInfo` containing all components of the bundle type `T`. + // SAFETY: the `BundleInfo` is initialized above self.location = unsafe { self.remove_bundle(bundle_info) }; self @@ -1087,8 +1198,7 @@ impl<'w> EntityWorldMut<'w> { .collect::>(); let remove_bundle = self.world.bundles.init_dynamic_info(components, to_remove); - // SAFETY: Components exist in `remove_bundle` because `Bundles::init_dynamic_info` - // initializes a `BundleInfo` containing all components in the to_remove Bundle. + // SAFETY: the `BundleInfo` for the components to remove is initialized above self.location = unsafe { self.remove_bundle(remove_bundle) }; self } diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 032dc097b59c4..e5005f7e63d0e 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,11 +15,10 @@ pub use entity_ref::{ OccupiedEntry, VacantEntry, }; pub use spawn_batch::*; -pub use world_cell::*; use crate::{ archetype::{ArchetypeComponentId, ArchetypeId, ArchetypeRow, Archetypes}, - bundle::{Bundle, BundleInserter, BundleSpawner, Bundles}, + bundle::{Bundle, BundleInfo, BundleInserter, BundleSpawner, Bundles}, change_detection::{MutUntyped, TicksMut}, component::{ Component, ComponentDescriptor, ComponentHooks, ComponentId, ComponentInfo, ComponentTicks, @@ -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] @@ -1851,7 +1839,7 @@ impl World { } /// # Panics - /// panics if `component_id` is not registered in this world + /// Panics if `component_id` is not registered in this world #[inline] pub(crate) fn initialize_non_send_internal( &mut self, @@ -2100,6 +2088,20 @@ impl World { self.storages.resources.clear(); self.storages.non_send_resources.clear(); } + + /// Initializes all of the components in the given [`Bundle`] and returns both the component + /// ids and the bundle id. + /// + /// This is largely equivalent to calling [`init_component`](Self::init_component) on each + /// component in the bundle. + #[inline] + pub fn init_bundle(&mut self) -> &BundleInfo { + let id = self + .bundles + .init_info::(&mut self.components, &mut self.storages); + // SAFETY: We just initialised the bundle so its id should definitely be valid. + unsafe { self.bundles.get(id).debug_checked_unwrap() } + } } impl World { diff --git a/crates/bevy_ecs/src/world/unsafe_world_cell.rs b/crates/bevy_ecs/src/world/unsafe_world_cell.rs index 1b400ef4be6ae..d12cafc5fe9d5 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}, @@ -285,36 +285,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_encase_derive/Cargo.toml b/crates/bevy_encase_derive/Cargo.toml index d4b4b0a6ff4f3..ed50b8da959f1 100644 --- a/crates/bevy_encase_derive/Cargo.toml +++ b/crates/bevy_encase_derive/Cargo.toml @@ -17,3 +17,7 @@ encase_derive_impl = "0.7" [lints] workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +all-features = true diff --git a/crates/bevy_encase_derive/src/lib.rs b/crates/bevy_encase_derive/src/lib.rs index e09bc4b247d1a..a6d225dff1f8a 100644 --- a/crates/bevy_encase_derive/src/lib.rs +++ b/crates/bevy_encase_derive/src/lib.rs @@ -1,5 +1,11 @@ // FIXME(3492): remove once docs are ready #![allow(missing_docs)] +#![forbid(unsafe_code)] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] use bevy_macro_utils::BevyManifest; use encase_derive_impl::{implement, syn}; diff --git a/crates/bevy_gilrs/Cargo.toml b/crates/bevy_gilrs/Cargo.toml index bf0bd644abd26..ad98c34040016 100644 --- a/crates/bevy_gilrs/Cargo.toml +++ b/crates/bevy_gilrs/Cargo.toml @@ -22,3 +22,7 @@ thiserror = "1.0" [lints] workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +all-features = true diff --git a/crates/bevy_gilrs/src/lib.rs b/crates/bevy_gilrs/src/lib.rs index dad8efe744a05..0b03ea23df442 100644 --- a/crates/bevy_gilrs/src/lib.rs +++ b/crates/bevy_gilrs/src/lib.rs @@ -1,3 +1,10 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![forbid(unsafe_code)] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] + //! Systems and type definitions for gamepad handling in Bevy. //! //! This crate is built on top of [GilRs](gilrs), a library diff --git a/crates/bevy_gizmos/Cargo.toml b/crates/bevy_gizmos/Cargo.toml index 8b5bf14229eba..039151b1612c7 100644 --- a/crates/bevy_gizmos/Cargo.toml +++ b/crates/bevy_gizmos/Cargo.toml @@ -34,4 +34,5 @@ bytemuck = "1.0" workspace = true [package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] all-features = true diff --git a/crates/bevy_gizmos/macros/Cargo.toml b/crates/bevy_gizmos/macros/Cargo.toml index d4cf3e3ad0828..dad07b319cae2 100644 --- a/crates/bevy_gizmos/macros/Cargo.toml +++ b/crates/bevy_gizmos/macros/Cargo.toml @@ -11,8 +11,6 @@ keywords = ["bevy"] [lib] proc-macro = true -[lints] -workspace = true [dependencies] bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.14.0-dev" } @@ -20,3 +18,10 @@ bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.14.0-dev" } syn = "2.0" proc-macro2 = "1.0" quote = "1.0" + +[lints] +workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +all-features = true diff --git a/crates/bevy_gizmos/macros/src/lib.rs b/crates/bevy_gizmos/macros/src/lib.rs index eb2c598c90806..adce45a4d0e2a 100644 --- a/crates/bevy_gizmos/macros/src/lib.rs +++ b/crates/bevy_gizmos/macros/src/lib.rs @@ -1,3 +1,5 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] + //! Derive implementations for `bevy_gizmos`. use bevy_macro_utils::BevyManifest; diff --git a/crates/bevy_gizmos/src/config.rs b/crates/bevy_gizmos/src/config.rs index 04d11c8437d3d..92f6962c19384 100644 --- a/crates/bevy_gizmos/src/config.rs +++ b/crates/bevy_gizmos/src/config.rs @@ -30,6 +30,17 @@ pub enum GizmoLineJoint { Bevel, } +/// An enum used to configure the style of gizmo lines, similar to CSS line-style +#[derive(Copy, Clone, Debug, Default, Hash, PartialEq, Eq, Reflect)] +#[non_exhaustive] +pub enum GizmoLineStyle { + /// A solid line without any decorators + #[default] + Solid, + /// A dotted line + Dotted, +} + /// A trait used to create gizmo configs groups. /// /// Here you can store additional configuration for you gizmo group not covered by [`GizmoConfig`] @@ -135,6 +146,8 @@ pub struct GizmoConfig { /// /// Defaults to `false`. pub line_perspective: bool, + /// Determine the style of gizmo lines. + pub line_style: GizmoLineStyle, /// How closer to the camera than real geometry the line should be. /// /// In 2D this setting has no effect and is effectively always -1. @@ -163,6 +176,7 @@ impl Default for GizmoConfig { enabled: true, line_width: 2., line_perspective: false, + line_style: GizmoLineStyle::Solid, depth_bias: 0., render_layers: Default::default(), @@ -174,6 +188,7 @@ impl Default for GizmoConfig { #[derive(Component)] pub(crate) struct GizmoMeshConfig { pub line_perspective: bool, + pub line_style: GizmoLineStyle, pub render_layers: RenderLayers, } @@ -181,6 +196,7 @@ impl From<&GizmoConfig> for GizmoMeshConfig { fn from(item: &GizmoConfig) -> Self { GizmoMeshConfig { line_perspective: item.line_perspective, + line_style: item.line_style, render_layers: item.render_layers, } } diff --git a/crates/bevy_gizmos/src/gizmos.rs b/crates/bevy_gizmos/src/gizmos.rs index 5bf72159516f9..f161792bcaf2f 100644 --- a/crates/bevy_gizmos/src/gizmos.rs +++ b/crates/bevy_gizmos/src/gizmos.rs @@ -49,6 +49,8 @@ type GizmosState = ( pub struct GizmosFetchState { state: as SystemParam>::State, } + +#[allow(unsafe_code)] // SAFETY: All methods are delegated to existing `SystemParam` implementations unsafe impl SystemParam for Gizmos<'_, '_, T> { type State = GizmosFetchState; @@ -90,6 +92,8 @@ unsafe impl SystemParam for Gizmos<'_, '_, T> { } } } + +#[allow(unsafe_code)] // Safety: Each field is `ReadOnlySystemParam`, and Gizmos SystemParam does not mutate world unsafe impl<'w, 's, T: GizmoConfigGroup> ReadOnlySystemParam for Gizmos<'w, 's, T> where diff --git a/crates/bevy_gizmos/src/lib.rs b/crates/bevy_gizmos/src/lib.rs old mode 100755 new mode 100644 index 9163a12bf669a..3d03eac725cc8 --- a/crates/bevy_gizmos/src/lib.rs +++ b/crates/bevy_gizmos/src/lib.rs @@ -1,3 +1,9 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] + //! This crate adds an immediate mode drawing api to Bevy for visual debugging. //! //! # Example @@ -13,7 +19,6 @@ //! ``` //! //! See the documentation on [Gizmos](crate::gizmos::Gizmos) for more examples. -#![cfg_attr(docsrs, feature(doc_auto_cfg))] /// System set label for the systems handling the rendering of gizmos. #[derive(SystemSet, Clone, Debug, Hash, PartialEq, Eq)] @@ -48,7 +53,7 @@ pub mod prelude { aabb::{AabbGizmoConfigGroup, ShowAabbGizmo}, config::{ DefaultGizmoConfigGroup, GizmoConfig, GizmoConfigGroup, GizmoConfigStore, - GizmoLineJoint, + GizmoLineJoint, GizmoLineStyle, }, gizmos::Gizmos, light::{LightGizmoColor, LightGizmoConfigGroup, ShowLightGizmo}, @@ -100,6 +105,8 @@ const LINE_SHADER_HANDLE: Handle = Handle::weak_from_u128(74148126892380 const LINE_JOINT_SHADER_HANDLE: Handle = Handle::weak_from_u128(1162780797909187908); /// A [`Plugin`] that provides an immediate mode drawing api for visual debugging. +/// +/// Requires to be loaded after [`PbrPlugin`](bevy_pbr::PbrPlugin) or [`SpritePlugin`](bevy_sprite::SpritePlugin). pub struct GizmoPlugin; impl Plugin for GizmoPlugin { @@ -141,9 +148,17 @@ impl Plugin for GizmoPlugin { render_app.add_systems(ExtractSchedule, extract_gizmo_data); #[cfg(feature = "bevy_sprite")] - app.add_plugins(pipeline_2d::LineGizmo2dPlugin); + if app.is_plugin_added::() { + app.add_plugins(pipeline_2d::LineGizmo2dPlugin); + } else { + bevy_utils::tracing::warn!("bevy_sprite feature is enabled but bevy_sprite::SpritePlugin was not detected. Are you sure you loaded GizmoPlugin after SpritePlugin?"); + } #[cfg(feature = "bevy_pbr")] - app.add_plugins(pipeline_3d::LineGizmo3dPlugin); + if app.is_plugin_added::() { + app.add_plugins(pipeline_3d::LineGizmo3dPlugin); + } else { + bevy_utils::tracing::warn!("bevy_pbr feature is enabled but bevy_pbr::PbrPlugin was not detected. Are you sure you loaded GizmoPlugin after PbrPlugin?"); + } } fn finish(&self, app: &mut bevy_app::App) { diff --git a/crates/bevy_gizmos/src/lines.wgsl b/crates/bevy_gizmos/src/lines.wgsl index d5c9e1e1476eb..6edc3eca677ec 100644 --- a/crates/bevy_gizmos/src/lines.wgsl +++ b/crates/bevy_gizmos/src/lines.wgsl @@ -26,19 +26,20 @@ struct VertexInput { struct VertexOutput { @builtin(position) clip_position: vec4, @location(0) color: vec4, + @location(1) uv: f32, }; const EPSILON: f32 = 4.88e-04; @vertex fn vertex(vertex: VertexInput) -> VertexOutput { - var positions = array, 6>( - vec3(0., -0.5, 0.), - vec3(0., -0.5, 1.), - vec3(0., 0.5, 1.), - vec3(0., -0.5, 0.), - vec3(0., 0.5, 1.), - vec3(0., 0.5, 0.) + var positions = array, 6>( + vec2(-0.5, 0.), + vec2(-0.5, 1.), + vec2(0.5, 1.), + vec2(-0.5, 0.), + vec2(0.5, 1.), + vec2(0.5, 0.) ); let position = positions[vertex.index]; @@ -49,23 +50,52 @@ fn vertex(vertex: VertexInput) -> VertexOutput { // Manual near plane clipping to avoid errors when doing the perspective divide inside this shader. clip_a = clip_near_plane(clip_a, clip_b); clip_b = clip_near_plane(clip_b, clip_a); - - let clip = mix(clip_a, clip_b, position.z); + let clip = mix(clip_a, clip_b, position.y); let resolution = view.viewport.zw; let screen_a = resolution * (0.5 * clip_a.xy / clip_a.w + 0.5); let screen_b = resolution * (0.5 * clip_b.xy / clip_b.w + 0.5); - let x_basis = normalize(screen_a - screen_b); - let y_basis = vec2(-x_basis.y, x_basis.x); + let y_basis = normalize(screen_b - screen_a); + let x_basis = vec2(-y_basis.y, y_basis.x); - var color = mix(vertex.color_a, vertex.color_b, position.z); + var color = mix(vertex.color_a, vertex.color_b, position.y); var line_width = line_gizmo.line_width; var alpha = 1.; + var uv: f32; #ifdef PERSPECTIVE line_width /= clip.w; + + // get height of near clipping plane in world space + let pos0 = view.inverse_projection * vec4(0, -1, 0, 1); // Bottom of the screen + let pos1 = view.inverse_projection * vec4(0, 1, 0, 1); // Top of the screen + let near_clipping_plane_height = length(pos0.xyz - pos1.xyz); + + // We can't use vertex.position_X because we may have changed the clip positions with clip_near_plane + let position_a = view.inverse_view_proj * clip_a; + let position_b = view.inverse_view_proj * clip_b; + let world_distance = length(position_a.xyz - position_b.xyz); + + // Offset to compensate for moved clip positions. If removed dots on lines will slide when position a is ofscreen. + let clipped_offset = length(position_a.xyz - vertex.position_a); + + uv = (clipped_offset + position.y * world_distance) * resolution.y / near_clipping_plane_height / line_gizmo.line_width; +#else + // Get the distance of b to the camera along camera axes + let camera_b = view.inverse_projection * clip_b; + + // This differentiates between orthographic and perspective cameras. + // For orthographic cameras no depth adaptment (depth_adaptment = 1) is needed. + var depth_adaptment: f32; + if (clip_b.w == 1.0) { + depth_adaptment = 1.0; + } + else { + depth_adaptment = -camera_b.z; + } + uv = position.y * depth_adaptment * length(screen_b - screen_a) / line_gizmo.line_width; #endif // Line thinness fade from https://acegikmo.com/shapes/docs/#anti-aliasing @@ -74,8 +104,8 @@ fn vertex(vertex: VertexInput) -> VertexOutput { line_width = 1.; } - let offset = line_width * (position.x * x_basis + position.y * y_basis); - let screen = mix(screen_a, screen_b, position.z) + offset; + let x_offset = line_width * position.x * x_basis; + let screen = mix(screen_a, screen_b, position.y) + x_offset; var depth: f32; if line_gizmo.depth_bias >= 0. { @@ -93,7 +123,7 @@ fn vertex(vertex: VertexInput) -> VertexOutput { var clip_position = vec4(clip.w * ((2. * screen) / resolution - 1.), depth, clip.w); - return VertexOutput(clip_position, color); + return VertexOutput(clip_position, color, uv); } fn clip_near_plane(a: vec4, b: vec4) -> vec4 { @@ -111,7 +141,9 @@ fn clip_near_plane(a: vec4, b: vec4) -> vec4 { } struct FragmentInput { + @builtin(position) position: vec4, @location(0) color: vec4, + @location(1) uv: f32, }; struct FragmentOutput { @@ -119,6 +151,17 @@ struct FragmentOutput { }; @fragment -fn fragment(in: FragmentInput) -> FragmentOutput { +fn fragment_solid(in: FragmentInput) -> FragmentOutput { return FragmentOutput(in.color); } +@fragment +fn fragment_dotted(in: FragmentInput) -> FragmentOutput { + var alpha: f32; +#ifdef PERSPECTIVE + alpha = 1 - floor(in.uv % 2.0); +#else + alpha = 1 - floor((in.uv * in.position.w) % 2.0); +#endif + + return FragmentOutput(vec4(in.color.xyz, in.color.w * alpha)); +} diff --git a/crates/bevy_gizmos/src/pipeline_2d.rs b/crates/bevy_gizmos/src/pipeline_2d.rs index 8e44a6ddafc57..4a886485e8be1 100644 --- a/crates/bevy_gizmos/src/pipeline_2d.rs +++ b/crates/bevy_gizmos/src/pipeline_2d.rs @@ -1,5 +1,5 @@ use crate::{ - config::{GizmoLineJoint, GizmoMeshConfig}, + config::{GizmoLineJoint, GizmoLineStyle, GizmoMeshConfig}, line_gizmo_vertex_buffer_layouts, line_joint_gizmo_vertex_buffer_layouts, DrawLineGizmo, DrawLineJointGizmo, GizmoRenderSystem, LineGizmo, LineGizmoUniformBindgroupLayout, SetLineGizmoBindGroup, LINE_JOINT_SHADER_HANDLE, LINE_SHADER_HANDLE, @@ -14,9 +14,10 @@ use bevy_ecs::{ system::{Query, Res, ResMut, Resource}, world::{FromWorld, World}, }; +use bevy_math::FloatOrd; use bevy_render::{ render_asset::{prepare_assets, RenderAssets}, - render_phase::{AddRenderCommand, DrawFunctions, RenderPhase, SetItemPipeline}, + render_phase::{AddRenderCommand, DrawFunctions, SetItemPipeline, SortedRenderPhase}, render_resource::*, texture::BevyDefault, view::{ExtractedView, Msaa, RenderLayers, ViewTarget}, @@ -24,7 +25,6 @@ use bevy_render::{ }; use bevy_sprite::{Mesh2dPipeline, Mesh2dPipelineKey, SetMesh2dViewBindGroup}; use bevy_utils::tracing::error; -use bevy_utils::FloatOrd; pub struct LineGizmo2dPlugin; @@ -41,7 +41,12 @@ impl Plugin for LineGizmo2dPlugin { .init_resource::>() .configure_sets( Render, - GizmoRenderSystem::QueueLineGizmos2d.in_set(RenderSet::Queue), + GizmoRenderSystem::QueueLineGizmos2d + .in_set(RenderSet::Queue) + .ambiguous_with(bevy_sprite::queue_sprites) + .ambiguous_with( + bevy_sprite::queue_material2d_meshes::, + ), ) .add_systems( Render, @@ -83,6 +88,7 @@ impl FromWorld for LineGizmoPipeline { struct LineGizmoPipelineKey { mesh_key: Mesh2dPipelineKey, strip: bool, + line_style: GizmoLineStyle, } impl SpecializedRenderPipeline for LineGizmoPipeline { @@ -105,6 +111,11 @@ impl SpecializedRenderPipeline for LineGizmoPipeline { self.uniform_layout.clone(), ]; + let fragment_entry_point = match key.line_style { + GizmoLineStyle::Solid => "fragment_solid", + GizmoLineStyle::Dotted => "fragment_dotted", + }; + RenderPipelineDescriptor { vertex: VertexState { shader: LINE_SHADER_HANDLE, @@ -115,7 +126,7 @@ impl SpecializedRenderPipeline for LineGizmoPipeline { fragment: Some(FragmentState { shader: LINE_SHADER_HANDLE, shader_defs, - entry_point: "fragment".into(), + entry_point: fragment_entry_point.into(), targets: vec![Some(ColorTargetState { format, blend: Some(BlendState::ALPHA_BLENDING), @@ -245,7 +256,7 @@ fn queue_line_gizmos_2d( line_gizmo_assets: Res>, mut views: Query<( &ExtractedView, - &mut RenderPhase, + &mut SortedRenderPhase, Option<&RenderLayers>, )>, ) { @@ -271,6 +282,7 @@ fn queue_line_gizmos_2d( LineGizmoPipelineKey { mesh_key, strip: line_gizmo.strip, + line_style: config.line_style, }, ); @@ -297,7 +309,7 @@ fn queue_line_joint_gizmos_2d( line_gizmo_assets: Res>, mut views: Query<( &ExtractedView, - &mut RenderPhase, + &mut SortedRenderPhase, Option<&RenderLayers>, )>, ) { diff --git a/crates/bevy_gizmos/src/pipeline_3d.rs b/crates/bevy_gizmos/src/pipeline_3d.rs index ecefb13d510cf..1fe8d9977b2f1 100644 --- a/crates/bevy_gizmos/src/pipeline_3d.rs +++ b/crates/bevy_gizmos/src/pipeline_3d.rs @@ -1,6 +1,6 @@ use crate::{ - config::GizmoMeshConfig, line_gizmo_vertex_buffer_layouts, - line_joint_gizmo_vertex_buffer_layouts, prelude::GizmoLineJoint, DrawLineGizmo, + config::{GizmoLineJoint, GizmoLineStyle, GizmoMeshConfig}, + line_gizmo_vertex_buffer_layouts, line_joint_gizmo_vertex_buffer_layouts, DrawLineGizmo, DrawLineJointGizmo, GizmoRenderSystem, LineGizmo, LineGizmoUniformBindgroupLayout, SetLineGizmoBindGroup, LINE_JOINT_SHADER_HANDLE, LINE_SHADER_HANDLE, }; @@ -21,7 +21,7 @@ use bevy_ecs::{ use bevy_pbr::{MeshPipeline, MeshPipelineKey, SetMeshViewBindGroup}; use bevy_render::{ render_asset::{prepare_assets, RenderAssets}, - render_phase::{AddRenderCommand, DrawFunctions, RenderPhase, SetItemPipeline}, + render_phase::{AddRenderCommand, DrawFunctions, SetItemPipeline, SortedRenderPhase}, render_resource::*, texture::BevyDefault, view::{ExtractedView, Msaa, RenderLayers, ViewTarget}, @@ -43,7 +43,9 @@ impl Plugin for LineGizmo3dPlugin { .init_resource::>() .configure_sets( Render, - GizmoRenderSystem::QueueLineGizmos3d.in_set(RenderSet::Queue), + GizmoRenderSystem::QueueLineGizmos3d + .in_set(RenderSet::Queue) + .ambiguous_with(bevy_pbr::queue_material_meshes::), ) .add_systems( Render, @@ -86,6 +88,7 @@ struct LineGizmoPipelineKey { view_key: MeshPipelineKey, strip: bool, perspective: bool, + line_style: GizmoLineStyle, } impl SpecializedRenderPipeline for LineGizmoPipeline { @@ -114,6 +117,11 @@ impl SpecializedRenderPipeline for LineGizmoPipeline { let layout = vec![view_layout, self.uniform_layout.clone()]; + let fragment_entry_point = match key.line_style { + GizmoLineStyle::Solid => "fragment_solid", + GizmoLineStyle::Dotted => "fragment_dotted", + }; + RenderPipelineDescriptor { vertex: VertexState { shader: LINE_SHADER_HANDLE, @@ -124,7 +132,7 @@ impl SpecializedRenderPipeline for LineGizmoPipeline { fragment: Some(FragmentState { shader: LINE_SHADER_HANDLE, shader_defs, - entry_point: "fragment".into(), + entry_point: fragment_entry_point.into(), targets: vec![Some(ColorTargetState { format, blend: Some(BlendState::ALPHA_BLENDING), @@ -273,7 +281,7 @@ fn queue_line_gizmos_3d( line_gizmo_assets: Res>, mut views: Query<( &ExtractedView, - &mut RenderPhase, + &mut SortedRenderPhase, Option<&RenderLayers>, ( Has, @@ -329,6 +337,7 @@ fn queue_line_gizmos_3d( view_key, strip: line_gizmo.strip, perspective: config.line_perspective, + line_style: config.line_style, }, ); @@ -355,7 +364,7 @@ fn queue_line_joint_gizmos_3d( line_gizmo_assets: Res>, mut views: Query<( &ExtractedView, - &mut RenderPhase, + &mut SortedRenderPhase, Option<&RenderLayers>, ( Has, diff --git a/crates/bevy_gltf/Cargo.toml b/crates/bevy_gltf/Cargo.toml index 9cea2c7cb56bb..3fc387902f2c8 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" @@ -60,4 +60,5 @@ smallvec = "1.11" workspace = true [package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] all-features = true diff --git a/crates/bevy_gltf/src/lib.rs b/crates/bevy_gltf/src/lib.rs index de4751a69767d..9133f3128430b 100644 --- a/crates/bevy_gltf/src/lib.rs +++ b/crates/bevy_gltf/src/lib.rs @@ -1,8 +1,14 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![forbid(unsafe_code)] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] + //! Plugin providing an [`AssetLoader`](bevy_asset::AssetLoader) and type definitions //! for loading glTF 2.0 (a standard 3D scene definition format) files in Bevy. //! //! The [glTF 2.0 specification](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html) defines the format of the glTF files. -#![cfg_attr(docsrs, feature(doc_auto_cfg))] #[cfg(feature = "bevy_animation")] use bevy_animation::AnimationClip; diff --git a/crates/bevy_gltf/src/loader.rs b/crates/bevy_gltf/src/loader.rs index 6f2f27969805c..dc4714b276b69 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] { @@ -478,18 +476,10 @@ async fn load_gltf<'a, 'b, 'c>( if mesh.attribute(Mesh::ATTRIBUTE_NORMAL).is_none() && matches!(mesh.primitive_topology(), PrimitiveTopology::TriangleList) { - let vertex_count_before = mesh.count_vertices(); - mesh.duplicate_vertices(); + bevy_utils::tracing::debug!( + "Automatically calculating missing vertex normals for geometry." + ); mesh.compute_flat_normals(); - let vertex_count_after = mesh.count_vertices(); - - if vertex_count_before != vertex_count_after { - bevy_utils::tracing::debug!("Missing vertex normals in indexed geometry, computing them as flat. Vertex count increased from {} to {}", vertex_count_before, vertex_count_after); - } else { - bevy_utils::tracing::debug!( - "Missing vertex normals in indexed geometry, computing them as flat." - ); - } } if let Some(vertex_attribute) = reader diff --git a/crates/bevy_hierarchy/Cargo.toml b/crates/bevy_hierarchy/Cargo.toml index 9262ca8336dcc..74a9b434f3e3a 100644 --- a/crates/bevy_hierarchy/Cargo.toml +++ b/crates/bevy_hierarchy/Cargo.toml @@ -31,4 +31,5 @@ smallvec = { version = "1.11", features = ["union", "const_generics"] } workspace = true [package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] all-features = true 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_hierarchy/src/lib.rs b/crates/bevy_hierarchy/src/lib.rs index 464acada7ddf1..53e00f5a768cb 100644 --- a/crates/bevy_hierarchy/src/lib.rs +++ b/crates/bevy_hierarchy/src/lib.rs @@ -1,3 +1,10 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![forbid(unsafe_code)] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] + //! Parent-child relationships for Bevy entities. //! //! You should use the tools in this crate @@ -44,7 +51,6 @@ //! [plugin]: HierarchyPlugin //! [query extension methods]: HierarchyQueryExt //! [world]: BuildWorldChildren -#![cfg_attr(docsrs, feature(doc_auto_cfg))] mod components; pub use components::*; diff --git a/crates/bevy_hierarchy/src/valid_parent_check_plugin.rs b/crates/bevy_hierarchy/src/valid_parent_check_plugin.rs index c3d766bcde7d6..c61816e24d0db 100644 --- a/crates/bevy_hierarchy/src/valid_parent_check_plugin.rs +++ b/crates/bevy_hierarchy/src/valid_parent_check_plugin.rs @@ -68,7 +68,7 @@ pub fn check_hierarchy_component_has_valid_parent( "warning[B0004]: {name} with the {ty_name} component has a parent without {ty_name}.\n\ This will cause inconsistent behaviors! See: https://bevyengine.org/learn/errors/#b0004", ty_name = get_short_name(std::any::type_name::()), - name = name.map_or("An entity".to_owned(), |s| format!("The {s} entity")), + name = name.map_or_else(|| format!("Entity {}", entity), |s| format!("The {s} entity")), ); } } diff --git a/crates/bevy_input/Cargo.toml b/crates/bevy_input/Cargo.toml index fbad74b9d5f88..d696e208f1fad 100644 --- a/crates/bevy_input/Cargo.toml +++ b/crates/bevy_input/Cargo.toml @@ -32,4 +32,5 @@ smol_str = "0.2" workspace = true [package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] all-features = true diff --git a/crates/bevy_input/src/button_input.rs b/crates/bevy_input/src/button_input.rs index 821281293a07c..d4d4aca2f9b97 100644 --- a/crates/bevy_input/src/button_input.rs +++ b/crates/bevy_input/src/button_input.rs @@ -143,12 +143,14 @@ where self.just_released.extend(self.pressed.drain()); } - /// Returns `true` if the `input` has just been pressed. + /// Returns `true` if the `input` has been pressed during the current frame. + /// + /// Note: This function does not imply information regarding the current state of [`ButtonInput::pressed`] or [`ButtonInput::just_released`]. pub fn just_pressed(&self, input: T) -> bool { self.just_pressed.contains(&input) } - /// Returns `true` if any item in `inputs` has just been pressed. + /// Returns `true` if any item in `inputs` has been pressed during the current frame. pub fn any_just_pressed(&self, inputs: impl IntoIterator) -> bool { inputs.into_iter().any(|it| self.just_pressed(it)) } @@ -160,7 +162,9 @@ where self.just_pressed.remove(&input) } - /// Returns `true` if the `input` has just been released. + /// Returns `true` if the `input` has been released during the current frame. + /// + /// Note: This function does not imply information regarding the current state of [`ButtonInput::pressed`] or [`ButtonInput::just_pressed`]. pub fn just_released(&self, input: T) -> bool { self.just_released.contains(&input) } @@ -207,11 +211,15 @@ where } /// An iterator visiting every just pressed input in arbitrary order. + /// + /// Note: Returned elements do not imply information regarding the current state of [`ButtonInput::pressed`] or [`ButtonInput::just_released`]. pub fn get_just_pressed(&self) -> impl ExactSizeIterator { self.just_pressed.iter() } /// An iterator visiting every just released input in arbitrary order. + /// + /// Note: Returned elements do not imply information regarding the current state of [`ButtonInput::pressed`] or [`ButtonInput::just_pressed`]. pub fn get_just_released(&self) -> impl ExactSizeIterator { self.just_released.iter() } diff --git a/crates/bevy_input/src/lib.rs b/crates/bevy_input/src/lib.rs index 489e12b2f40bb..17d392583f27d 100644 --- a/crates/bevy_input/src/lib.rs +++ b/crates/bevy_input/src/lib.rs @@ -1,9 +1,15 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![forbid(unsafe_code)] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] + //! Input functionality for the [Bevy game engine](https://bevyengine.org/). //! //! # Supported input devices //! //! `bevy` currently supports keyboard, mouse, gamepad, and touch inputs. -#![cfg_attr(docsrs, feature(doc_auto_cfg))] mod axis; mod button_input; diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 68cb7421d3106..25476de016e50 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -73,6 +73,7 @@ serialize = [ "bevy_math/serialize", "bevy_scene?/serialize", "bevy_ui?/serialize", + "bevy_color?/serialize", ] multi-threaded = [ "bevy_asset?/multi-threaded", @@ -158,6 +159,12 @@ bevy_debug_stepping = [ "bevy_app/bevy_debug_stepping", ] +# Enables the meshlet renderer for dense high-poly scenes (experimental) +meshlet = ["bevy_pbr?/meshlet"] + +# Enables processing meshes into meshlet meshes for bevy_pbr +meshlet_processor = ["bevy_pbr?/meshlet_processor"] + # Provides a collection of developer tools bevy_dev_tools = ["dep:bevy_dev_tools"] @@ -202,7 +209,11 @@ 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 + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +all-features = true diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index dcea35be338e3..acfa89d0b659a 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::app::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_app::PanicHandlerPlugin) .add(bevy_log::LogPlugin::default()) .add(bevy_core::TaskPoolPlugin::default()) .add(bevy_core::TypeRegistrationPlugin) @@ -153,35 +155,14 @@ impl Plugin for IgnoreAmbiguitiesPlugin { fn build(&self, app: &mut bevy_app::App) { // bevy_ui owns the Transform and cannot be animated #[cfg(all(feature = "bevy_animation", feature = "bevy_ui"))] - app.ignore_ambiguity( - bevy_app::PostUpdate, - bevy_animation::advance_animations, - bevy_ui::ui_layout_system, - ); - - #[cfg(feature = "bevy_render")] - if let Ok(render_app) = app.get_sub_app_mut(bevy_render::RenderApp) { - #[cfg(all(feature = "bevy_gizmos", feature = "bevy_sprite"))] - { - render_app.ignore_ambiguity( - bevy_render::Render, - bevy_gizmos::GizmoRenderSystem::QueueLineGizmos2d, - bevy_sprite::queue_sprites, - ); - render_app.ignore_ambiguity( - bevy_render::Render, - bevy_gizmos::GizmoRenderSystem::QueueLineGizmos2d, - bevy_sprite::queue_material2d_meshes::, - ); - } - #[cfg(all(feature = "bevy_gizmos", feature = "bevy_pbr"))] - { - render_app.ignore_ambiguity( - bevy_render::Render, - bevy_gizmos::GizmoRenderSystem::QueueLineGizmos3d, - bevy_pbr::queue_material_meshes::, - ); - } + if app.is_plugin_added::() + && app.is_plugin_added::() + { + app.ignore_ambiguity( + bevy_app::PostUpdate, + bevy_animation::advance_animations, + bevy_ui::ui_layout_system, + ); } } } diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index 896091c404c98..944502e484163 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -1,3 +1,10 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![forbid(unsafe_code)] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] + //! This module is separated into its own crate to enable simple dynamic linking for Bevy, and should not be used directly /// `use bevy::prelude::*;` to import common components, bundles, and plugins. diff --git a/crates/bevy_log/Cargo.toml b/crates/bevy_log/Cargo.toml index a5229d61ac934..33cee7bea4ef5 100644 --- a/crates/bevy_log/Cargo.toml +++ b/crates/bevy_log/Cargo.toml @@ -34,11 +34,11 @@ 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] workspace = true [package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] all-features = true diff --git a/crates/bevy_log/src/android_tracing.rs b/crates/bevy_log/src/android_tracing.rs index a5e7bc8626b2e..fe55d6d46d845 100644 --- a/crates/bevy_log/src/android_tracing.rs +++ b/crates/bevy_log/src/android_tracing.rs @@ -3,7 +3,10 @@ use bevy_utils::tracing::{ span::{Attributes, Record}, Event, Id, Level, Subscriber, }; -use std::fmt::{Debug, Write}; +use std::{ + ffi::CString, + fmt::{Debug, Write}, +}; use tracing_subscriber::{field::Visit, layer::Context, registry::LookupSpan, Layer}; #[derive(Default)] @@ -37,16 +40,6 @@ impl Visit for StringRecorder { } } -impl core::fmt::Display for StringRecorder { - fn fmt(&self, mut f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - if !self.0.is_empty() { - write!(&mut f, " {}", self.0) - } else { - Ok(()) - } - } -} - impl core::default::Default for StringRecorder { fn default() -> Self { StringRecorder::new() @@ -73,25 +66,35 @@ impl LookupSpan<'a>> Layer for AndroidLayer { } } + #[allow(unsafe_code)] fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) { + fn sanitize(string: &str) -> CString { + let mut bytes: Vec = string + .as_bytes() + .into_iter() + .copied() + .filter(|byte| *byte != 0) + .collect(); + CString::new(bytes).unwrap() + } + let mut recorder = StringRecorder::new(); event.record(&mut recorder); let meta = event.metadata(); - let level = meta.level(); - let priority = match *level { + let priority = match *meta.level() { Level::TRACE => android_log_sys::LogPriority::VERBOSE, Level::DEBUG => android_log_sys::LogPriority::DEBUG, Level::INFO => android_log_sys::LogPriority::INFO, Level::WARN => android_log_sys::LogPriority::WARN, Level::ERROR => android_log_sys::LogPriority::ERROR, }; - let message = format!("{}\0", recorder); - let tag = format!("{}\0", meta.name()); + // SAFETY: Called only on Android platforms. priority is guaranteed to be in range of c_int. + // The provided tag and message are null terminated properly. unsafe { android_log_sys::__android_log_write( priority as android_log_sys::c_int, - tag.as_ptr() as *const android_log_sys::c_char, - message.as_ptr() as *const android_log_sys::c_char, + sanitize(meta.name()).as_ptr(), + sanitize(&recorder.0).as_ptr(), ); } } diff --git a/crates/bevy_log/src/lib.rs b/crates/bevy_log/src/lib.rs index eb3cef046a4ff..7391ffe039123 100644 --- a/crates/bevy_log/src/lib.rs +++ b/crates/bevy_log/src/lib.rs @@ -1,3 +1,9 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] + //! This crate provides logging functions and configuration for [Bevy](https://bevyengine.org) //! apps, and automatically configures platform specific log handlers (i.e. WASM or Android). //! @@ -9,7 +15,6 @@ //! //! For more fine-tuned control over logging behavior, set up the [`LogPlugin`] or //! `DefaultPlugins` during app initialization. -#![cfg_attr(docsrs, feature(doc_auto_cfg))] #[cfg(feature = "trace")] use std::panic; @@ -205,7 +210,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_macro_utils/Cargo.toml b/crates/bevy_macro_utils/Cargo.toml index 6dc67396bb18b..408716edd4f62 100644 --- a/crates/bevy_macro_utils/Cargo.toml +++ b/crates/bevy_macro_utils/Cargo.toml @@ -19,3 +19,7 @@ proc-macro2 = "1.0" [lints] workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +all-features = true diff --git a/crates/bevy_macro_utils/src/lib.rs b/crates/bevy_macro_utils/src/lib.rs index 443313f8e87bd..28de7e2227e26 100644 --- a/crates/bevy_macro_utils/src/lib.rs +++ b/crates/bevy_macro_utils/src/lib.rs @@ -1,4 +1,10 @@ -#![deny(unsafe_code)] +#![forbid(unsafe_code)] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] + //! A collection of helper types and functions for working on macros within the Bevy ecosystem. extern crate proc_macro; 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..31fd4653e8ee0 100644 --- a/crates/bevy_math/Cargo.toml +++ b/crates/bevy_math/Cargo.toml @@ -14,11 +14,20 @@ 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" +# Enable the approx feature when testing. +bevy_math = { path = ".", version = "0.14.0-dev", features = ["approx"] } [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,9 +40,12 @@ 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 [package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] all-features = true diff --git a/crates/bevy_math/src/aspect_ratio.rs b/crates/bevy_math/src/aspect_ratio.rs index 1d18c690b8f09..08e47e91ba4e1 100644 --- a/crates/bevy_math/src/aspect_ratio.rs +++ b/crates/bevy_math/src/aspect_ratio.rs @@ -1,5 +1,7 @@ //! Provides a simple aspect ratio struct to help with calculations. +use crate::Vec2; + /// An `AspectRatio` is the ratio of width to height. pub struct AspectRatio(f32); @@ -17,6 +19,13 @@ impl AspectRatio { } } +impl From for AspectRatio { + #[inline] + fn from(value: Vec2) -> Self { + Self::new(value.x, value.y) + } +} + impl From for f32 { #[inline] fn from(aspect_ratio: AspectRatio) -> Self { diff --git a/crates/bevy_math/src/common_traits.rs b/crates/bevy_math/src/common_traits.rs new file mode 100644 index 0000000000000..6074f2526607d --- /dev/null +++ b/crates/bevy_math/src/common_traits.rs @@ -0,0 +1,163 @@ +use glam::{Vec2, Vec3, Vec3A, Vec4}; +use std::fmt::Debug; +use std::ops::{Add, Div, Mul, Neg, Sub}; + +/// A type that supports the mathematical operations of a real vector space, irrespective of dimension. +/// In particular, this means that the implementing type supports: +/// - Scalar multiplication and division on the right by elements of `f32` +/// - Negation +/// - Addition and subtraction +/// - Zero +/// +/// Within the limitations of floating point arithmetic, all the following are required to hold: +/// - (Associativity of addition) For all `u, v, w: Self`, `(u + v) + w == u + (v + w)`. +/// - (Commutativity of addition) For all `u, v: Self`, `u + v == v + u`. +/// - (Additive identity) For all `v: Self`, `v + Self::ZERO == v`. +/// - (Additive inverse) For all `v: Self`, `v - v == v + (-v) == Self::ZERO`. +/// - (Compatibility of multiplication) For all `a, b: f32`, `v: Self`, `v * (a * b) == (v * a) * b`. +/// - (Multiplicative identity) For all `v: Self`, `v * 1.0 == v`. +/// - (Distributivity for vector addition) For all `a: f32`, `u, v: Self`, `(u + v) * a == u * a + v * a`. +/// - (Distributivity for scalar addition) For all `a, b: f32`, `v: Self`, `v * (a + b) == v * a + v * b`. +/// +/// Note that, because implementing types use floating point arithmetic, they are not required to actually +/// implement `PartialEq` or `Eq`. +pub trait VectorSpace: + Mul + + Div + + Add + + Sub + + Neg + + Default + + Debug + + Clone + + Copy +{ + /// The zero vector, which is the identity of addition for the vector space type. + const ZERO: Self; + + /// Perform vector space linear interpolation between this element and another, based + /// on the parameter `t`. When `t` is `0`, `self` is recovered. When `t` is `1`, `rhs` + /// is recovered. + /// + /// Note that the value of `t` is not clamped by this function, so interpolating outside + /// of the interval `[0,1]` is allowed. + #[inline] + fn lerp(&self, rhs: Self, t: f32) -> Self { + *self * (1. - t) + rhs * t + } +} + +impl VectorSpace for Vec4 { + const ZERO: Self = Vec4::ZERO; +} + +impl VectorSpace for Vec3 { + const ZERO: Self = Vec3::ZERO; +} + +impl VectorSpace for Vec3A { + const ZERO: Self = Vec3A::ZERO; +} + +impl VectorSpace for Vec2 { + const ZERO: Self = Vec2::ZERO; +} + +impl VectorSpace for f32 { + const ZERO: Self = 0.0; +} + +/// A type that supports the operations of a normed vector space; i.e. a norm operation in addition +/// to those of [`VectorSpace`]. Specifically, the implementor must guarantee that the following +/// relationships hold, within the limitations of floating point arithmetic: +/// - (Nonnegativity) For all `v: Self`, `v.norm() >= 0.0`. +/// - (Positive definiteness) For all `v: Self`, `v.norm() == 0.0` implies `v == Self::ZERO`. +/// - (Absolute homogeneity) For all `c: f32`, `v: Self`, `(v * c).norm() == v.norm() * c.abs()`. +/// - (Triangle inequality) For all `v, w: Self`, `(v + w).norm() <= v.norm() + w.norm()`. +/// +/// Note that, because implementing types use floating point arithmetic, they are not required to actually +/// implement `PartialEq` or `Eq`. +pub trait NormedVectorSpace: VectorSpace { + /// The size of this element. The return value should always be nonnegative. + fn norm(self) -> f32; + + /// The squared norm of this element. Computing this is often faster than computing + /// [`NormedVectorSpace::norm`]. + #[inline] + fn norm_squared(self) -> f32 { + self.norm() * self.norm() + } + + /// The distance between this element and another, as determined by the norm. + #[inline] + fn distance(self, rhs: Self) -> f32 { + (rhs - self).norm() + } + + /// The squared distance between this element and another, as determined by the norm. Note that + /// this is often faster to compute in practice than [`NormedVectorSpace::distance`]. + #[inline] + fn distance_squared(self, rhs: Self) -> f32 { + (rhs - self).norm_squared() + } +} + +impl NormedVectorSpace for Vec4 { + #[inline] + fn norm(self) -> f32 { + self.length() + } + + #[inline] + fn norm_squared(self) -> f32 { + self.length_squared() + } +} + +impl NormedVectorSpace for Vec3 { + #[inline] + fn norm(self) -> f32 { + self.length() + } + + #[inline] + fn norm_squared(self) -> f32 { + self.length_squared() + } +} + +impl NormedVectorSpace for Vec3A { + #[inline] + fn norm(self) -> f32 { + self.length() + } + + #[inline] + fn norm_squared(self) -> f32 { + self.length_squared() + } +} + +impl NormedVectorSpace for Vec2 { + #[inline] + fn norm(self) -> f32 { + self.length() + } + + #[inline] + fn norm_squared(self) -> f32 { + self.length_squared() + } +} + +impl NormedVectorSpace for f32 { + #[inline] + fn norm(self) -> f32 { + self.abs() + } + + #[inline] + fn norm_squared(self) -> f32 { + self * self + } +} diff --git a/crates/bevy_math/src/cubic_splines.rs b/crates/bevy_math/src/cubic_splines.rs index 422d5767e2b6a..c1da3184f7be8 100644 --- a/crates/bevy_math/src/cubic_splines.rs +++ b/crates/bevy_math/src/cubic_splines.rs @@ -1,33 +1,10 @@ //! Provides types for building cubic splines for rendering curves and use with animation easing. -use std::{ - fmt::Debug, - ops::{Add, Div, Mul, Sub}, -}; +use std::{fmt::Debug, iter::once}; -use glam::{Quat, Vec2, Vec3, Vec3A, Vec4}; -use thiserror::Error; - -/// A point in space of any dimension that supports the math ops needed for cubic spline -/// interpolation. -pub trait Point: - Mul - + Div - + Add - + Sub - + Default - + Debug - + Clone - + Copy -{ -} +use crate::{Vec2, VectorSpace}; -impl Point for Quat {} -impl Point for Vec4 {} -impl Point for Vec3 {} -impl Point for Vec3A {} -impl Point for Vec2 {} -impl Point for f32 {} +use thiserror::Error; /// A spline composed of a single cubic Bezier curve. /// @@ -63,11 +40,11 @@ impl Point for f32 {} /// let bezier = CubicBezier::new(points).to_curve(); /// let positions: Vec<_> = bezier.iter_positions(100).collect(); /// ``` -pub struct CubicBezier { +pub struct CubicBezier { control_points: Vec<[P; 4]>, } -impl CubicBezier

{ +impl CubicBezier

{ /// Create a new cubic Bezier curve from sets of control points. pub fn new(control_points: impl Into>) -> Self { Self { @@ -75,7 +52,7 @@ impl CubicBezier

{ } } } -impl CubicGenerator

for CubicBezier

{ +impl CubicGenerator

for CubicBezier

{ #[inline] fn to_curve(&self) -> CubicCurve

{ // A derivation for this matrix can be found in "General Matrix Representations for B-splines" by Kaihuai Qin. @@ -134,10 +111,10 @@ impl CubicGenerator

for CubicBezier

{ /// let hermite = CubicHermite::new(points, tangents).to_curve(); /// let positions: Vec<_> = hermite.iter_positions(100).collect(); /// ``` -pub struct CubicHermite { +pub struct CubicHermite { control_points: Vec<(P, P)>, } -impl CubicHermite

{ +impl CubicHermite

{ /// Create a new Hermite curve from sets of control points. pub fn new( control_points: impl IntoIterator, @@ -148,7 +125,7 @@ impl CubicHermite

{ } } } -impl CubicGenerator

for CubicHermite

{ +impl CubicGenerator

for CubicHermite

{ #[inline] fn to_curve(&self) -> CubicCurve

{ let char_matrix = [ @@ -172,7 +149,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 +161,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 /// @@ -199,12 +177,12 @@ impl CubicGenerator

for CubicHermite

{ /// let cardinal = CubicCardinalSpline::new(0.3, points).to_curve(); /// let positions: Vec<_> = cardinal.iter_positions(100).collect(); /// ``` -pub struct CubicCardinalSpline { +pub struct CubicCardinalSpline { tension: f32, control_points: Vec

, } -impl CubicCardinalSpline

{ +impl CubicCardinalSpline

{ /// Build a new Cardinal spline. pub fn new(tension: f32, control_points: impl Into>) -> Self { Self { @@ -221,7 +199,7 @@ impl CubicCardinalSpline

{ } } } -impl CubicGenerator

for CubicCardinalSpline

{ +impl CubicGenerator

for CubicCardinalSpline

{ #[inline] fn to_curve(&self) -> CubicCurve

{ let s = self.tension; @@ -232,10 +210,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 } @@ -268,10 +264,10 @@ impl CubicGenerator

for CubicCardinalSpline

{ /// let b_spline = CubicBSpline::new(points).to_curve(); /// let positions: Vec<_> = b_spline.iter_positions(100).collect(); /// ``` -pub struct CubicBSpline { +pub struct CubicBSpline { control_points: Vec

, } -impl CubicBSpline

{ +impl CubicBSpline

{ /// Build a new B-Spline. pub fn new(control_points: impl Into>) -> Self { Self { @@ -279,7 +275,7 @@ impl CubicBSpline

{ } } } -impl CubicGenerator

for CubicBSpline

{ +impl CubicGenerator

for CubicBSpline

{ #[inline] fn to_curve(&self) -> CubicCurve

{ // A derivation for this matrix can be found in "General Matrix Representations for B-splines" by Kaihuai Qin. @@ -385,12 +381,12 @@ pub enum CubicNurbsError { /// .to_curve(); /// let positions: Vec<_> = nurbs.iter_positions(100).collect(); /// ``` -pub struct CubicNurbs { +pub struct CubicNurbs { control_points: Vec

, weights: Vec, knots: Vec, } -impl CubicNurbs

{ +impl CubicNurbs

{ /// Build a Non-Uniform Rational B-Spline. /// /// If provided, weights must be the same length as the control points. Defaults to equal weights. @@ -550,7 +546,7 @@ impl CubicNurbs

{ ] } } -impl RationalGenerator

for CubicNurbs

{ +impl RationalGenerator

for CubicNurbs

{ #[inline] fn to_curve(&self) -> RationalCurve

{ let segments = self @@ -589,10 +585,10 @@ impl RationalGenerator

for CubicNurbs

{ /// /// ### Continuity /// The curve is C0 continuous, meaning it has no holes or jumps. -pub struct LinearSpline { +pub struct LinearSpline { points: Vec

, } -impl LinearSpline

{ +impl LinearSpline

{ /// Create a new linear spline pub fn new(points: impl Into>) -> Self { Self { @@ -600,7 +596,7 @@ impl LinearSpline

{ } } } -impl CubicGenerator

for LinearSpline

{ +impl CubicGenerator

for LinearSpline

{ #[inline] fn to_curve(&self) -> CubicCurve

{ let segments = self @@ -619,7 +615,7 @@ impl CubicGenerator

for LinearSpline

{ } /// Implement this on cubic splines that can generate a cubic curve from their spline parameters. -pub trait CubicGenerator { +pub trait CubicGenerator { /// Build a [`CubicCurve`] by computing the interpolation coefficients for each curve segment. fn to_curve(&self) -> CubicCurve

; } @@ -629,11 +625,11 @@ pub trait CubicGenerator { /// /// Segments can be chained together to form a longer compound curve. #[derive(Clone, Debug, Default, PartialEq)] -pub struct CubicSegment { +pub struct CubicSegment { coeff: [P; 4], } -impl CubicSegment

{ +impl CubicSegment

{ /// Instantaneous position of a point at parametric value `t`. #[inline] pub fn position(&self, t: f32) -> P { @@ -787,11 +783,11 @@ impl CubicSegment { /// Use any struct that implements the [`CubicGenerator`] trait to create a new curve, such as /// [`CubicBezier`]. #[derive(Clone, Debug, PartialEq)] -pub struct CubicCurve { +pub struct CubicCurve { segments: Vec>, } -impl CubicCurve

{ +impl CubicCurve

{ /// Compute the position of a point on the cubic curve at the parametric value `t`. /// /// Note that `t` varies from `0..=(n_points - 3)`. @@ -892,13 +888,13 @@ impl CubicCurve

{ } } -impl Extend> for CubicCurve

{ +impl Extend> for CubicCurve

{ fn extend>>(&mut self, iter: T) { self.segments.extend(iter); } } -impl IntoIterator for CubicCurve

{ +impl IntoIterator for CubicCurve

{ type IntoIter = > as IntoIterator>::IntoIter; type Item = CubicSegment

; @@ -909,7 +905,7 @@ impl IntoIterator for CubicCurve

{ } /// Implement this on cubic splines that can generate a rational cubic curve from their spline parameters. -pub trait RationalGenerator { +pub trait RationalGenerator { /// Build a [`RationalCurve`] by computing the interpolation coefficients for each curve segment. fn to_curve(&self) -> RationalCurve

; } @@ -919,7 +915,7 @@ pub trait RationalGenerator { /// /// Segments can be chained together to form a longer compound curve. #[derive(Clone, Debug, Default, PartialEq)] -pub struct RationalSegment { +pub struct RationalSegment { /// The coefficients matrix of the cubic curve. coeff: [P; 4], /// The homogeneous weight coefficients. @@ -928,7 +924,7 @@ pub struct RationalSegment { knot_span: f32, } -impl RationalSegment

{ +impl RationalSegment

{ /// Instantaneous position of a point at parametric value `t` in `[0, knot_span)`. #[inline] pub fn position(&self, t: f32) -> P { @@ -1046,11 +1042,11 @@ impl RationalSegment

{ /// Use any struct that implements the [`RationalGenerator`] trait to create a new curve, such as /// [`CubicNurbs`], or convert [`CubicCurve`] using `into/from`. #[derive(Clone, Debug, PartialEq)] -pub struct RationalCurve { +pub struct RationalCurve { segments: Vec>, } -impl RationalCurve

{ +impl RationalCurve

{ /// Compute the position of a point on the curve at the parametric value `t`. /// /// Note that `t` varies from `0..=(n_points - 3)`. @@ -1170,13 +1166,13 @@ impl RationalCurve

{ } } -impl Extend> for RationalCurve

{ +impl Extend> for RationalCurve

{ fn extend>>(&mut self, iter: T) { self.segments.extend(iter); } } -impl IntoIterator for RationalCurve

{ +impl IntoIterator for RationalCurve

{ type IntoIter = > as IntoIterator>::IntoIter; type Item = RationalSegment

; @@ -1186,7 +1182,7 @@ impl IntoIterator for RationalCurve

{ } } -impl From> for RationalSegment

{ +impl From> for RationalSegment

{ fn from(value: CubicSegment

) -> Self { Self { coeff: value.coeff, @@ -1196,7 +1192,7 @@ impl From> for RationalSegment

{ } } -impl From> for RationalCurve

{ +impl From> for RationalCurve

{ fn from(value: CubicCurve

, +} + +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..8cd9428048e23 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -1,6 +1,11 @@ // FIXME(3492): remove once docs are ready #![allow(missing_docs)] +#![allow(unsafe_code)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] #[cfg(target_pointer_width = "16")] compile_error!("bevy_render cannot compile for a 16-bit platform."); @@ -11,6 +16,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; @@ -57,6 +63,7 @@ use globals::GlobalsPlugin; use renderer::{RenderAdapter, RenderAdapterInfo, RenderDevice, RenderQueue}; use crate::deterministic::DeterministicRenderingConfig; +use crate::renderer::WgpuWrapper; use crate::{ camera::CameraPlugin, mesh::{morph::MorphPlugin, Mesh, MeshPlugin}, @@ -87,7 +94,7 @@ use std::{ pub struct RenderPlugin { pub render_creation: RenderCreation, /// If `true`, disables asynchronous pipeline compilation. - /// This has no effect on macOS, Wasm, or without the `multi-threaded` feature. + /// This has no effect on macOS, Wasm, iOS, or without the `multi-threaded` feature. pub synchronous_pipeline_compilation: bool, } @@ -103,13 +110,15 @@ pub enum RenderSet { PrepareAssets, /// Create any additional views such as those used for shadow mapping. ManageViews, - /// Queue drawable entities as phase items in [`RenderPhase`](crate::render_phase::RenderPhase)s - /// ready for sorting + /// Queue drawable entities as phase items in render phases ready for + /// sorting (if necessary) Queue, /// A sub-set within [`Queue`](RenderSet::Queue) where mesh entity queue systems are executed. Ensures `prepare_assets::` is completed. QueueMeshes, - // TODO: This could probably be moved in favor of a system ordering abstraction in `Render` or `Queue` - /// Sort the [`RenderPhases`](render_phase::RenderPhase) here. + // TODO: This could probably be moved in favor of a system ordering + // abstraction in `Render` or `Queue` + /// Sort the [`SortedRenderPhase`](render_phase::SortedRenderPhase)s and + /// [`BinKey`](render_phase::BinnedPhaseItem::BinKey)s here. PhaseSort, /// Prepare render resources from extracted data for the GPU based on their sorted order. /// Create [`BindGroups`](render_resource::BindGroup) that depend on those data. @@ -300,7 +309,7 @@ impl Plugin for RenderPlugin { queue, adapter_info, render_adapter, - RenderInstance(Arc::new(instance)), + RenderInstance(Arc::new(WgpuWrapper::new(instance))), )); }; // In wasm, spawn a task and detach it for execution diff --git a/crates/bevy_render/src/mesh/mesh/mod.rs b/crates/bevy_render/src/mesh/mesh/mod.rs index dc4549cd040b5..e5f355bc51578 100644 --- a/crates/bevy_render/src/mesh/mesh/mod.rs +++ b/crates/bevy_render/src/mesh/mesh/mod.rs @@ -369,6 +369,14 @@ impl Mesh { self } + /// Returns the size of a vertex in bytes. + pub fn get_vertex_size(&self) -> u64 { + self.attributes + .values() + .map(|data| data.attribute.format.get_size()) + .sum() + } + /// Computes and returns the index data of the mesh as bytes. /// This is used to transform the index data into a GPU friendly format. pub fn get_index_buffer_bytes(&self) -> Option<&[u8]> { @@ -536,12 +544,13 @@ impl Mesh { /// Calculates the [`Mesh::ATTRIBUTE_NORMAL`] of a mesh. /// /// # Panics - /// Panics if [`Indices`] are set or [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3` or - /// if the mesh has any other topology than [`PrimitiveTopology::TriangleList`]. - /// Consider calling [`Mesh::duplicate_vertices`] or export your mesh with normal attributes. + /// Panics if [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3`. + /// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`]. + /// + /// FIXME: The should handle more cases since this is called as a part of gltf + /// mesh loading where we can't really blame users for loading meshes that might + /// not conform to the limitations here! pub fn compute_flat_normals(&mut self) { - assert!(self.indices().is_none(), "`compute_flat_normals` can't work on indexed geometry. Consider calling `Mesh::duplicate_vertices`."); - assert!( matches!(self.primitive_topology, PrimitiveTopology::TriangleList), "`compute_flat_normals` can only work on `TriangleList`s" @@ -553,18 +562,56 @@ impl Mesh { .as_float3() .expect("`Mesh::ATTRIBUTE_POSITION` vertex attributes should be of type `float3`"); - let normals: Vec<_> = positions - .chunks_exact(3) - .map(|p| face_normal(p[0], p[1], p[2])) - .flat_map(|normal| [normal; 3]) - .collect(); + match self.indices() { + Some(indices) => { + let mut count: usize = 0; + let mut corners = [0_usize; 3]; + let mut normals = vec![[0.0f32; 3]; positions.len()]; + let mut adjacency_counts = vec![0_usize; positions.len()]; + + for i in indices.iter() { + corners[count % 3] = i; + count += 1; + if count % 3 == 0 { + let normal = face_normal( + positions[corners[0]], + positions[corners[1]], + positions[corners[2]], + ); + for corner in corners { + normals[corner] = + (Vec3::from(normal) + Vec3::from(normals[corner])).into(); + adjacency_counts[corner] += 1; + } + } + } + + // average (smooth) normals for shared vertices... + // TODO: support different methods of weighting the average + for i in 0..normals.len() { + let count = adjacency_counts[i]; + if count > 0 { + normals[i] = (Vec3::from(normals[i]) / (count as f32)).normalize().into(); + } + } - self.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals); + self.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals); + } + None => { + let normals: Vec<_> = positions + .chunks_exact(3) + .map(|p| face_normal(p[0], p[1], p[2])) + .flat_map(|normal| [normal; 3]) + .collect(); + + self.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals); + } + } } /// Consumes the mesh and returns a mesh with calculated [`Mesh::ATTRIBUTE_NORMAL`]. /// - /// (Alternatively, you can use [`Mesh::compute_flat_normals`] to mutate an existing mesh in-place) + /// (Alternatively, you can use [`Mesh::with_computed_flat_normals`] to mutate an existing mesh in-place) /// /// # Panics /// Panics if [`Indices`] are set or [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3` or 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/mod.rs b/crates/bevy_render/src/mesh/mod.rs index 7748a5a1eaeb5..2cb30bb2dc5ad 100644 --- a/crates/bevy_render/src/mesh/mod.rs +++ b/crates/bevy_render/src/mesh/mod.rs @@ -57,7 +57,7 @@ impl MeshVertexBufferLayouts { /// Inserts a new mesh vertex buffer layout in the store and returns a /// reference to it, reusing the existing reference if this mesh vertex /// buffer layout was already in the store. - pub(crate) fn insert(&mut self, layout: MeshVertexBufferLayout) -> MeshVertexBufferLayoutRef { + pub fn insert(&mut self, layout: MeshVertexBufferLayout) -> MeshVertexBufferLayoutRef { // Because the special `PartialEq` and `Hash` implementations that // compare by pointer are on `MeshVertexBufferLayoutRef`, not on // `Arc`, this compares the mesh vertex buffer 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/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 0af7b877854dc..6bf5ce9a6ea11 100644 --- a/crates/bevy_render/src/render_asset.rs +++ b/crates/bevy_render/src/render_asset.rs @@ -7,13 +7,7 @@ use bevy_ecs::{ system::{StaticSystemParam, SystemParam, SystemParamItem, SystemState}, world::{FromWorld, Mut}, }; -use bevy_reflect::std_traits::ReflectDefault; -use bevy_reflect::{ - utility::{reflect_hasher, NonGenericTypeInfoCell}, - FromReflect, FromType, GetTypeRegistration, Reflect, ReflectDeserialize, ReflectFromPtr, - ReflectFromReflect, ReflectKind, ReflectMut, ReflectOwned, ReflectRef, ReflectSerialize, - TypeInfo, TypePath, TypeRegistration, Typed, ValueInfo, -}; +use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; use bevy_utils::{HashMap, HashSet}; use serde::{Deserialize, Serialize}; use std::marker::PhantomData; @@ -77,7 +71,8 @@ bitflags::bitflags! { /// [discussion about memory management](https://github.com/WebAssembly/design/issues/1397) for more /// details. #[repr(transparent)] - #[derive(Serialize, TypePath, Deserialize, Hash, Clone, Copy, PartialEq, Eq, Debug)] + #[derive(Serialize, Deserialize, Hash, Clone, Copy, PartialEq, Eq, Debug, Reflect)] + #[reflect_value(Serialize, Deserialize, Hash, PartialEq, Debug)] pub struct RenderAssetUsages: u8 { const MAIN_WORLD = 1 << 0; const RENDER_WORLD = 1 << 1; @@ -98,99 +93,6 @@ impl Default for RenderAssetUsages { } } -impl Reflect for RenderAssetUsages { - fn get_represented_type_info(&self) -> Option<&'static bevy_reflect::TypeInfo> { - Some(::type_info()) - } - fn into_any(self: Box) -> Box { - self - } - fn as_any(&self) -> &dyn std::any::Any { - self - } - fn as_any_mut(&mut self) -> &mut dyn std::any::Any { - self - } - fn into_reflect(self: Box) -> Box { - self - } - fn as_reflect(&self) -> &dyn Reflect { - self - } - fn as_reflect_mut(&mut self) -> &mut dyn Reflect { - self - } - fn apply(&mut self, value: &dyn Reflect) { - let value = value.as_any(); - if let Some(&value) = value.downcast_ref::() { - *self = value; - } else { - panic!("Value is not a {}.", Self::type_path()); - } - } - fn set(&mut self, value: Box) -> Result<(), Box> { - *self = value.take()?; - Ok(()) - } - fn reflect_kind(&self) -> bevy_reflect::ReflectKind { - ReflectKind::Value - } - fn reflect_ref(&self) -> bevy_reflect::ReflectRef { - ReflectRef::Value(self) - } - fn reflect_mut(&mut self) -> bevy_reflect::ReflectMut { - ReflectMut::Value(self) - } - fn reflect_owned(self: Box) -> bevy_reflect::ReflectOwned { - ReflectOwned::Value(self) - } - fn clone_value(&self) -> Box { - Box::new(*self) - } - fn reflect_hash(&self) -> Option { - use std::hash::Hash; - use std::hash::Hasher; - let mut hasher = reflect_hasher(); - Hash::hash(&std::any::Any::type_id(self), &mut hasher); - Hash::hash(self, &mut hasher); - Some(hasher.finish()) - } - fn reflect_partial_eq(&self, value: &dyn Reflect) -> Option { - let value = value.as_any(); - if let Some(value) = value.downcast_ref::() { - Some(std::cmp::PartialEq::eq(self, value)) - } else { - Some(false) - } - } -} - -impl GetTypeRegistration for RenderAssetUsages { - fn get_type_registration() -> TypeRegistration { - let mut registration = TypeRegistration::of::(); - registration.insert::(FromType::::from_type()); - registration.insert::(FromType::::from_type()); - registration.insert::(FromType::::from_type()); - registration.insert::(FromType::::from_type()); - registration.insert::(FromType::::from_type()); - registration - } -} - -impl FromReflect for RenderAssetUsages { - fn from_reflect(reflect: &dyn Reflect) -> Option { - let raw_value = *reflect.as_any().downcast_ref::()?; - Self::from_bits(raw_value) - } -} - -impl Typed for RenderAssetUsages { - fn type_info() -> &'static TypeInfo { - static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); - CELL.get_or_set(|| TypeInfo::Value(ValueInfo::new::())) - } -} - /// This plugin extracts the changed assets from the "app world" into the "render world" /// and prepares them for the GPU. They can then be accessed from the [`RenderAssets`] resource. /// diff --git a/crates/bevy_render/src/render_phase/draw.rs b/crates/bevy_render/src/render_phase/draw.rs index aa05fa3e497f6..4dc6fc3e0822b 100644 --- a/crates/bevy_render/src/render_phase/draw.rs +++ b/crates/bevy_render/src/render_phase/draw.rs @@ -39,7 +39,7 @@ pub trait Draw: Send + Sync + 'static { // TODO: make this generic? /// An identifier for a [`Draw`] function stored in [`DrawFunctions`]. -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] pub struct DrawFunctionId(u32); /// Stores all [`Draw`] functions for the [`PhaseItem`] type. @@ -209,6 +209,7 @@ pub trait RenderCommand { } /// The result of a [`RenderCommand`]. +#[derive(Debug)] pub enum RenderCommandResult { Success, Failure, @@ -301,7 +302,7 @@ where /// Registers a [`RenderCommand`] as a [`Draw`] function. /// They are stored inside the [`DrawFunctions`] resource of the app. pub trait AddRenderCommand { - /// Adds the [`RenderCommand`] for the specified [`RenderPhase`](super::RenderPhase) to the app. + /// Adds the [`RenderCommand`] for the specified render phase to the app. fn add_render_command + Send + Sync + 'static>( &mut self, ) -> &mut Self 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_phase/mod.rs b/crates/bevy_render/src/render_phase/mod.rs index 1a3a544b464f4..40c4153f3fde2 100644 --- a/crates/bevy_render/src/render_phase/mod.rs +++ b/crates/bevy_render/src/render_phase/mod.rs @@ -1,7 +1,7 @@ //! The modular rendering abstraction responsible for queuing, preparing, sorting and drawing //! entities as part of separate render phases. //! -//! In Bevy each view (camera, or shadow-casting light, etc.) has one or multiple [`RenderPhase`]s +//! In Bevy each view (camera, or shadow-casting light, etc.) has one or multiple render phases //! (e.g. opaque, transparent, shadow, etc). //! They are used to queue entities for rendering. //! Multiple phases might be required due to different sorting/batching behaviors @@ -29,17 +29,20 @@ mod draw; mod draw_state; mod rangefinder; +use bevy_utils::{default, hashbrown::hash_map::Entry, HashMap}; pub use draw::*; pub use draw_state::*; +use encase::{internal::WriteInto, ShaderSize}; use nonmax::NonMaxU32; pub use rangefinder::*; -use crate::render_resource::{CachedRenderPipelineId, PipelineCache}; +use crate::render_resource::{CachedRenderPipelineId, GpuArrayBufferIndex, PipelineCache}; use bevy_ecs::{ prelude::*, system::{lifetimeless::SRes, SystemParamItem}, }; -use std::{ops::Range, slice::SliceIndex}; +use smallvec::SmallVec; +use std::{hash::Hash, ops::Range, slice::SliceIndex}; /// A collection of all rendering instructions, that will be executed by the GPU, for a /// single render phase for a single view. @@ -51,18 +54,351 @@ use std::{ops::Range, slice::SliceIndex}; /// the rendered texture of the previous phase (e.g. for screen-space reflections). /// All [`PhaseItem`]s are then rendered using a single [`TrackedRenderPass`]. /// The render pass might be reused for multiple phases to reduce GPU overhead. +/// +/// This flavor of render phase is used for phases in which the ordering is less +/// critical: for example, `Opaque3d`. It's generally faster than the +/// alternative [`SortedRenderPhase`]. #[derive(Component)] -pub struct RenderPhase { +pub struct BinnedRenderPhase +where + BPI: BinnedPhaseItem, +{ + /// A list of `BinKey`s for batchable items. + /// + /// These are accumulated in `queue_material_meshes` and then sorted in + /// `batch_and_prepare_binned_render_phase`. + pub batchable_keys: Vec, + + /// The batchable bins themselves. + /// + /// Each bin corresponds to a single batch set. For unbatchable entities, + /// prefer `unbatchable_values` instead. + pub(crate) batchable_values: HashMap>, + + /// A list of `BinKey`s for unbatchable items. + /// + /// These are accumulated in `queue_material_meshes` and then sorted in + /// `batch_and_prepare_binned_render_phase`. + pub unbatchable_keys: Vec, + + /// The unbatchable bins. + /// + /// Each entity here is rendered in a separate drawcall. + pub(crate) unbatchable_values: HashMap, + + /// Information on each batch set. + /// + /// A *batch set* is a set of entities that will be batched together unless + /// we're on a platform that doesn't support storage buffers (e.g. WebGL 2) + /// and differing dynamic uniform indices force us to break batches. On + /// platforms that support storage buffers, a batch set always consists of + /// at most one batch. + /// + /// The unbatchable entities immediately follow the batches in the storage + /// buffers. + pub(crate) batch_sets: Vec>, +} + +/// Information about a single batch of entities rendered using binned phase +/// items. +#[derive(Debug)] +pub struct BinnedRenderPhaseBatch { + /// An entity that's *representative* of this batch. + /// + /// Bevy uses this to fetch the mesh. It can be any entity in the batch. + pub representative_entity: Entity, + + /// The range of instance indices in this batch. + pub instance_range: Range, + + /// The dynamic offset of the batch. + /// + /// Note that dynamic offsets are only used on platforms that don't support + /// storage buffers. + pub dynamic_offset: Option, +} + +/// Information about the unbatchable entities in a bin. +pub(crate) struct UnbatchableBinnedEntities { + /// The entities. + pub(crate) entities: Vec, + + /// The GPU array buffer indices of each unbatchable binned entity. + pub(crate) buffer_indices: UnbatchableBinnedEntityBufferIndex, +} + +/// Stores instance indices and dynamic offsets for unbatchable entities in a +/// binned render phase. +/// +/// This is conceptually `Vec`, but it +/// avoids the overhead of storing dynamic offsets on platforms that support +/// them. In other words, this allows a fast path that avoids allocation on +/// platforms that aren't WebGL 2. +#[derive(Default)] + +pub(crate) enum UnbatchableBinnedEntityBufferIndex { + /// There are no unbatchable entities in this bin (yet). + #[default] + NoEntities, + + /// The instances for all unbatchable entities in this bin are contiguous, + /// and there are no dynamic uniforms. + /// + /// This is the typical case on platforms other than WebGL 2. We special + /// case this to avoid allocation on those platforms. + NoDynamicOffsets { + /// The range of indices. + instance_range: Range, + }, + + /// Dynamic uniforms are present for unbatchable entities in this bin. + /// + /// We fall back to this on WebGL 2. + DynamicOffsets(Vec), +} + +/// The instance index and dynamic offset (if present) for an unbatchable entity. +/// +/// This is only useful on platforms that don't support storage buffers. +#[derive(Clone, Copy)] +pub(crate) struct UnbatchableBinnedEntityDynamicOffset { + /// The instance index. + instance_index: u32, + /// The dynamic offset, if present. + dynamic_offset: Option, +} + +impl BinnedRenderPhase +where + BPI: BinnedPhaseItem, +{ + /// Bins a new entity. + /// + /// `batchable` specifies whether the entity can be batched with other + /// entities of the same type. + pub fn add(&mut self, key: BPI::BinKey, entity: Entity, batchable: bool) { + if batchable { + match self.batchable_values.entry(key.clone()) { + Entry::Occupied(mut entry) => entry.get_mut().push(entity), + Entry::Vacant(entry) => { + self.batchable_keys.push(key); + entry.insert(vec![entity]); + } + } + } else { + match self.unbatchable_values.entry(key.clone()) { + Entry::Occupied(mut entry) => entry.get_mut().entities.push(entity), + Entry::Vacant(entry) => { + self.unbatchable_keys.push(key); + entry.insert(UnbatchableBinnedEntities { + entities: vec![entity], + buffer_indices: default(), + }); + } + } + } + } + + /// Encodes the GPU commands needed to render all entities in this phase. + pub fn render<'w>( + &self, + render_pass: &mut TrackedRenderPass<'w>, + world: &'w World, + view: Entity, + ) { + let draw_functions = world.resource::>(); + let mut draw_functions = draw_functions.write(); + draw_functions.prepare(world); + + // Encode draws for batchables. + debug_assert_eq!(self.batchable_keys.len(), self.batch_sets.len()); + for (key, batch_set) in self.batchable_keys.iter().zip(self.batch_sets.iter()) { + for batch in batch_set { + let binned_phase_item = BPI::new( + key.clone(), + batch.representative_entity, + batch.instance_range.clone(), + batch.dynamic_offset, + ); + + // Fetch the draw function. + let Some(draw_function) = draw_functions.get_mut(binned_phase_item.draw_function()) + else { + continue; + }; + + draw_function.draw(world, render_pass, view, &binned_phase_item); + } + } + + // Encode draws for unbatchables. + + for key in &self.unbatchable_keys { + let unbatchable_entities = &self.unbatchable_values[key]; + for (entity_index, &entity) in unbatchable_entities.entities.iter().enumerate() { + let unbatchable_dynamic_offset = match &unbatchable_entities.buffer_indices { + UnbatchableBinnedEntityBufferIndex::NoEntities => { + // Shouldn't happen… + continue; + } + UnbatchableBinnedEntityBufferIndex::NoDynamicOffsets { instance_range } => { + UnbatchableBinnedEntityDynamicOffset { + instance_index: instance_range.start + entity_index as u32, + dynamic_offset: None, + } + } + UnbatchableBinnedEntityBufferIndex::DynamicOffsets(ref dynamic_offsets) => { + dynamic_offsets[entity_index] + } + }; + + let binned_phase_item = BPI::new( + key.clone(), + entity, + unbatchable_dynamic_offset.instance_index + ..(unbatchable_dynamic_offset.instance_index + 1), + unbatchable_dynamic_offset.dynamic_offset, + ); + + // Fetch the draw function. + let Some(draw_function) = draw_functions.get_mut(binned_phase_item.draw_function()) + else { + continue; + }; + + draw_function.draw(world, render_pass, view, &binned_phase_item); + } + } + } + + pub fn is_empty(&self) -> bool { + self.batchable_keys.is_empty() && self.unbatchable_keys.is_empty() + } +} + +impl Default for BinnedRenderPhase +where + BPI: BinnedPhaseItem, +{ + fn default() -> Self { + Self { + batchable_keys: vec![], + batchable_values: HashMap::default(), + unbatchable_keys: vec![], + unbatchable_values: HashMap::default(), + batch_sets: vec![], + } + } +} + +impl UnbatchableBinnedEntityBufferIndex { + /// Adds a new entity to the list of unbatchable binned entities. + pub fn add(&mut self, gpu_array_buffer_index: GpuArrayBufferIndex) + where + T: ShaderSize + WriteInto + Clone, + { + match (&mut *self, gpu_array_buffer_index.dynamic_offset) { + (UnbatchableBinnedEntityBufferIndex::NoEntities, None) => { + // This is the first entity we've seen, and we're not on WebGL + // 2. Initialize the fast path. + *self = UnbatchableBinnedEntityBufferIndex::NoDynamicOffsets { + instance_range: gpu_array_buffer_index.index + ..(gpu_array_buffer_index.index + 1), + } + } + + (UnbatchableBinnedEntityBufferIndex::NoEntities, Some(dynamic_offset)) => { + // This is the first entity we've seen, and we're on WebGL 2. + // Initialize an array. + *self = UnbatchableBinnedEntityBufferIndex::DynamicOffsets(vec![ + UnbatchableBinnedEntityDynamicOffset { + instance_index: gpu_array_buffer_index.index, + dynamic_offset: Some(dynamic_offset), + }, + ]); + } + + ( + UnbatchableBinnedEntityBufferIndex::NoDynamicOffsets { + ref mut instance_range, + }, + None, + ) if instance_range.end == gpu_array_buffer_index.index => { + // This is the normal case on non-WebGL 2. + instance_range.end += 1; + } + + ( + UnbatchableBinnedEntityBufferIndex::DynamicOffsets(ref mut offsets), + dynamic_offset, + ) => { + // This is the normal case on WebGL 2. + offsets.push(UnbatchableBinnedEntityDynamicOffset { + instance_index: gpu_array_buffer_index.index, + dynamic_offset, + }); + } + + ( + UnbatchableBinnedEntityBufferIndex::NoDynamicOffsets { instance_range }, + dynamic_offset, + ) => { + // We thought we were in non-WebGL 2 mode, but we got a dynamic + // offset or non-contiguous index anyway. This shouldn't happen, + // but let's go ahead and do the sensible thing anyhow: demote + // the compressed `NoDynamicOffsets` field to the full + // `DynamicOffsets` array. + let mut new_dynamic_offsets: Vec<_> = instance_range + .map(|instance_index| UnbatchableBinnedEntityDynamicOffset { + instance_index, + dynamic_offset: None, + }) + .collect(); + new_dynamic_offsets.push(UnbatchableBinnedEntityDynamicOffset { + instance_index: gpu_array_buffer_index.index, + dynamic_offset, + }); + *self = UnbatchableBinnedEntityBufferIndex::DynamicOffsets(new_dynamic_offsets); + } + } + } +} + +/// A collection of all items to be rendered that will be encoded to GPU +/// commands for a single render phase for a single view. +/// +/// Each view (camera, or shadow-casting light, etc.) can have one or multiple render phases. +/// They are used to queue entities for rendering. +/// Multiple phases might be required due to different sorting/batching behaviors +/// (e.g. opaque: front to back, transparent: back to front) or because one phase depends on +/// the rendered texture of the previous phase (e.g. for screen-space reflections). +/// All [`PhaseItem`]s are then rendered using a single [`TrackedRenderPass`]. +/// The render pass might be reused for multiple phases to reduce GPU overhead. +/// +/// This flavor of render phase is used only for meshes that need to be sorted +/// back-to-front, such as transparent meshes. For items that don't need strict +/// sorting, [`BinnedRenderPhase`] is preferred, for performance. +#[derive(Component)] +pub struct SortedRenderPhase +where + I: SortedPhaseItem, +{ pub items: Vec, } -impl Default for RenderPhase { +impl Default for SortedRenderPhase +where + I: SortedPhaseItem, +{ fn default() -> Self { Self { items: Vec::new() } } } -impl RenderPhase { +impl SortedRenderPhase +where + I: SortedPhaseItem, +{ /// Adds a [`PhaseItem`] to this render phase. #[inline] pub fn add(&mut self, item: I) { @@ -123,22 +459,31 @@ impl RenderPhase { } /// An item (entity of the render world) which will be drawn to a texture or the screen, -/// as part of a [`RenderPhase`]. +/// as part of a render phase. /// /// The data required for rendering an entity is extracted from the main world in the /// [`ExtractSchedule`](crate::ExtractSchedule). /// Then it has to be queued up for rendering during the /// [`RenderSet::Queue`](crate::RenderSet::Queue), by adding a corresponding phase item to /// a render phase. -/// Afterwards it will be sorted and rendered automatically in the +/// Afterwards it will be possibly sorted and rendered automatically in the /// [`RenderSet::PhaseSort`](crate::RenderSet::PhaseSort) and /// [`RenderSet::Render`](crate::RenderSet::Render), respectively. +/// +/// `PhaseItem`s come in two flavors: [`BinnedPhaseItem`]s and +/// [`SortedPhaseItem`]s. +/// +/// * Binned phase items have a `BinKey` which specifies what bin they're to be +/// placed in. All items in the same bin are eligible to be batched together. +/// The `BinKey`s are sorted, but the individual bin items aren't. Binned phase +/// items are good for opaque meshes, in which the order of rendering isn't +/// important. Generally, binned phase items are faster than sorted phase items. +/// +/// * Sorted phase items, on the other hand, are placed into one large buffer +/// and then sorted all at once. This is needed for transparent meshes, which +/// have to be sorted back-to-front to render with the painter's algorithm. +/// These types of phase items are generally slower than binned phase items. pub trait PhaseItem: Sized + Send + Sync + 'static { - /// The type used for ordering the items. The smallest values are drawn first. - /// This order can be calculated using the [`ViewRangefinder3d`], - /// based on the view-space `Z` value of the corresponding view matrix. - type SortKey: Ord; - /// Whether or not this `PhaseItem` should be subjected to automatic batching. (Default: `true`) const AUTOMATIC_BATCHING: bool = true; @@ -148,12 +493,63 @@ pub trait PhaseItem: Sized + Send + Sync + 'static { /// from the render world . fn entity(&self) -> Entity; - /// Determines the order in which the items are drawn. - fn sort_key(&self) -> Self::SortKey; - /// Specifies the [`Draw`] function used to render the item. fn draw_function(&self) -> DrawFunctionId; + /// The range of instances that the batch covers. After doing a batched draw, batch range + /// length phase items will be skipped. This design is to avoid having to restructure the + /// render phase unnecessarily. + fn batch_range(&self) -> &Range; + fn batch_range_mut(&mut self) -> &mut Range; + + fn dynamic_offset(&self) -> Option; + fn dynamic_offset_mut(&mut self) -> &mut Option; +} + +/// Represents phase items that are placed into bins. The `BinKey` specifies +/// which bin they're to be placed in. Bin keys are sorted, and items within the +/// same bin are eligible to be batched together. The elements within the bins +/// aren't themselves sorted. +/// +/// An example of a binned phase item is `Opaque3d`, for which the rendering +/// order isn't critical. +pub trait BinnedPhaseItem: PhaseItem { + /// The key used for binning [`PhaseItem`]s into bins. Order the members of + /// [`BinnedPhaseItem::BinKey`] by the order of binding for best + /// performance. For example, pipeline id, draw function id, mesh asset id, + /// lowest variable bind group id such as the material bind group id, and + /// its dynamic offsets if any, next bind group and offsets, etc. This + /// reduces the need for rebinding between bins and improves performance. + type BinKey: Clone + Send + Sync + Eq + Ord + Hash; + + /// Creates a new binned phase item from the key and per-entity data. + /// + /// Unlike [`SortedPhaseItem`]s, this is generally called "just in time" + /// before rendering. The resulting phase item isn't stored in any data + /// structures, resulting in significant memory savings. + fn new( + key: Self::BinKey, + representative_entity: Entity, + batch_range: Range, + dynamic_offset: Option, + ) -> Self; +} + +/// Represents phase items that must be sorted. The `SortKey` specifies the +/// order that these items are drawn in. These are placed into a single array, +/// and the array as a whole is then sorted. +/// +/// An example of a sorted phase item is `Transparent3d`, which must be sorted +/// back to front in order to correctly render with the painter's algorithm. +pub trait SortedPhaseItem: PhaseItem { + /// The type used for ordering the items. The smallest values are drawn first. + /// This order can be calculated using the [`ViewRangefinder3d`], + /// based on the view-space `Z` value of the corresponding view matrix. + type SortKey: Ord; + + /// Determines the order in which the items are drawn. + fn sort_key(&self) -> Self::SortKey; + /// Sorts a slice of phase items into render order. Generally if the same type /// is batched this should use a stable sort like [`slice::sort_by_key`]. /// In almost all other cases, this should not be altered from the default, @@ -170,15 +566,6 @@ pub trait PhaseItem: Sized + Send + Sync + 'static { fn sort(items: &mut [Self]) { items.sort_unstable_by_key(|item| item.sort_key()); } - - /// The range of instances that the batch covers. After doing a batched draw, batch range - /// length phase items will be skipped. This design is to avoid having to restructure the - /// render phase unnecessarily. - fn batch_range(&self) -> &Range; - fn batch_range_mut(&mut self) -> &mut Range; - - fn dynamic_offset(&self) -> Option; - fn dynamic_offset_mut(&mut self) -> &mut Option; } /// A [`PhaseItem`] item, that automatically sets the appropriate render pipeline, @@ -218,8 +605,12 @@ impl RenderCommand

for SetItemPipeline { } } -/// This system sorts the [`PhaseItem`]s of all [`RenderPhase`]s of this type. -pub fn sort_phase_system(mut render_phases: Query<&mut RenderPhase>) { +/// This system sorts the [`PhaseItem`]s of all [`SortedRenderPhase`]s of this +/// type. +pub fn sort_phase_system(mut render_phases: Query<&mut SortedRenderPhase>) +where + I: SortedPhaseItem, +{ for mut phase in &mut render_phases { phase.sort(); } diff --git a/crates/bevy_render/src/render_resource/mod.rs b/crates/bevy_render/src/render_resource/mod.rs index f3b1af2f028c7..388bf0fb08ea3 100644 --- a/crates/bevy_render/src/render_resource/mod.rs +++ b/crates/bevy_render/src/render_resource/mod.rs @@ -51,7 +51,7 @@ pub use wgpu::{ TextureDescriptor, TextureDimension, TextureFormat, TextureSampleType, TextureUsages, TextureViewDescriptor, TextureViewDimension, VertexAttribute, VertexBufferLayout as RawVertexBufferLayout, VertexFormat, VertexState as RawVertexState, - VertexStepMode, + VertexStepMode, COPY_BUFFER_ALIGNMENT, }; pub mod encase { diff --git a/crates/bevy_render/src/render_resource/pipeline_cache.rs b/crates/bevy_render/src/render_resource/pipeline_cache.rs index 2e2f2eeafa78e..f24ab7d8b578d 100644 --- a/crates/bevy_render/src/render_resource/pipeline_cache.rs +++ b/crates/bevy_render/src/render_resource/pipeline_cache.rs @@ -50,7 +50,7 @@ pub enum Pipeline { type CachedPipelineId = usize; /// Index of a cached render pipeline in a [`PipelineCache`]. -#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)] pub struct CachedRenderPipelineId(CachedPipelineId); impl CachedRenderPipelineId { diff --git a/crates/bevy_render/src/render_resource/resource_macros.rs b/crates/bevy_render/src/render_resource/resource_macros.rs index de2ea0ec00e58..68896092ce016 100644 --- a/crates/bevy_render/src/render_resource/resource_macros.rs +++ b/crates/bevy_render/src/render_resource/resource_macros.rs @@ -9,16 +9,25 @@ #[macro_export] macro_rules! render_resource_wrapper { ($wrapper_type:ident, $wgpu_type:ty) => { + #[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] #[derive(Debug)] // SAFETY: while self is live, self.0 comes from `into_raw` of an Arc<$wgpu_type> with a strong ref. pub struct $wrapper_type(*const ()); + #[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] + #[derive(Debug)] + pub struct $wrapper_type(send_wrapper::SendWrapper<*const ()>); + impl $wrapper_type { pub fn new(value: $wgpu_type) -> Self { let arc = std::sync::Arc::new(value); let value_ptr = std::sync::Arc::into_raw(arc); let unit_ptr = value_ptr.cast::<()>(); - Self(unit_ptr) + + #[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] + return Self(unit_ptr); + #[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] + return Self(send_wrapper::SendWrapper::new(unit_ptr)); } pub fn try_unwrap(self) -> Option<$wgpu_type> { @@ -53,13 +62,16 @@ macro_rules! render_resource_wrapper { } } + #[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] // SAFETY: We manually implement Send and Sync, which is valid for Arc when T: Send + Sync. // We ensure correctness by checking that $wgpu_type does implement Send and Sync. // If in future there is a case where a wrapper is required for a non-send/sync type // we can implement a macro variant that omits these manual Send + Sync impls unsafe impl Send for $wrapper_type {} + #[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] // SAFETY: As explained above, we ensure correctness by checking that $wgpu_type implements Send and Sync. unsafe impl Sync for $wrapper_type {} + #[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] const _: () = { trait AssertSendSyncBound: Send + Sync {} impl AssertSendSyncBound for $wgpu_type {} @@ -75,7 +87,14 @@ macro_rules! render_resource_wrapper { std::mem::forget(arc); let cloned_value_ptr = std::sync::Arc::into_raw(cloned); let cloned_unit_ptr = cloned_value_ptr.cast::<()>(); - Self(cloned_unit_ptr) + + #[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] + return Self(cloned_unit_ptr); + + // Note: this implementation means that this Clone will panic + // when called off the wgpu thread. + #[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] + return Self(send_wrapper::SendWrapper::new(cloned_unit_ptr)); } } }; @@ -85,16 +104,28 @@ macro_rules! render_resource_wrapper { #[macro_export] macro_rules! render_resource_wrapper { ($wrapper_type:ident, $wgpu_type:ty) => { + #[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] #[derive(Clone, Debug)] pub struct $wrapper_type(std::sync::Arc<$wgpu_type>); + #[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] + #[derive(Clone, Debug)] + pub struct $wrapper_type(std::sync::Arc>); impl $wrapper_type { pub fn new(value: $wgpu_type) -> Self { - Self(std::sync::Arc::new(value)) + #[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] + return Self(std::sync::Arc::new(value)); + + #[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] + return Self(std::sync::Arc::new(send_wrapper::SendWrapper::new(value))); } pub fn try_unwrap(self) -> Option<$wgpu_type> { - std::sync::Arc::try_unwrap(self.0).ok() + #[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] + return std::sync::Arc::try_unwrap(self.0).ok(); + + #[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] + return std::sync::Arc::try_unwrap(self.0).ok().map(|p| p.take()); } } @@ -106,6 +137,7 @@ macro_rules! render_resource_wrapper { } } + #[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] const _: () = { trait AssertSendSyncBound: Send + Sync {} impl AssertSendSyncBound for $wgpu_type {} @@ -116,7 +148,7 @@ macro_rules! render_resource_wrapper { #[macro_export] macro_rules! define_atomic_id { ($atomic_id_type:ident) => { - #[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)] + #[derive(Copy, Clone, Hash, Eq, PartialEq, PartialOrd, Ord, Debug)] pub struct $atomic_id_type(core::num::NonZeroU32); // We use new instead of default to indicate that each ID created will be unique. 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..3f1620ae876fe 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}"); + } } { @@ -104,23 +117,54 @@ pub fn render_system(world: &mut World, state: &mut SystemState(T); +#[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] +#[derive(Debug, Clone, Deref, DerefMut)] +pub struct WgpuWrapper(send_wrapper::SendWrapper); + +// SAFETY: SendWrapper is always Send + Sync. +#[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] +unsafe impl Send for WgpuWrapper {} +#[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] +unsafe impl Sync for WgpuWrapper {} + +#[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] +impl WgpuWrapper { + pub fn new(t: T) -> Self { + Self(t) + } +} + +#[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] +impl WgpuWrapper { + pub fn new(t: T) -> Self { + Self(send_wrapper::SendWrapper::new(t)) + } +} + /// This queue is used to enqueue tasks for the GPU to execute asynchronously. #[derive(Resource, Clone, Deref, DerefMut)] -pub struct RenderQueue(pub Arc); +pub struct RenderQueue(pub Arc>); /// The handle to the physical device being used for rendering. /// See [`Adapter`] for more info. #[derive(Resource, Clone, Debug, Deref, DerefMut)] -pub struct RenderAdapter(pub Arc); +pub struct RenderAdapter(pub Arc>); /// The GPU instance is used to initialize the [`RenderQueue`] and [`RenderDevice`], /// as well as to create [`WindowSurfaces`](crate::view::window::WindowSurfaces). #[derive(Resource, Clone, Deref, DerefMut)] -pub struct RenderInstance(pub Arc); +pub struct RenderInstance(pub Arc>); /// The [`AdapterInfo`] of the adapter in use by the renderer. #[derive(Resource, Clone, Deref, DerefMut)] -pub struct RenderAdapterInfo(pub AdapterInfo); +pub struct RenderAdapterInfo(pub WgpuWrapper); const GPU_NOT_FOUND_ERROR_MESSAGE: &str = if cfg!(target_os = "linux") { "Unable to find a GPU! Make sure you have installed required drivers! For extra information, see: https://github.com/bevyengine/bevy/blob/latest/docs/linux_dependencies.md" @@ -287,12 +331,12 @@ pub async fn initialize_renderer( ) .await .unwrap(); - let queue = Arc::new(queue); - let adapter = Arc::new(adapter); + let queue = Arc::new(WgpuWrapper::new(queue)); + let adapter = Arc::new(WgpuWrapper::new(adapter)); ( RenderDevice::from(device), RenderQueue(queue), - RenderAdapterInfo(adapter_info), + RenderAdapterInfo(WgpuWrapper::new(adapter_info)), RenderAdapter(adapter), ) } @@ -306,11 +350,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 +375,7 @@ impl<'w> RenderContext<'w> { command_encoder: None, command_buffer_queue: Vec::new(), force_serial, + diagnostics_recorder: diagnostics_recorder.map(Arc::new), } } @@ -334,6 +384,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 +409,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) } @@ -377,7 +434,10 @@ impl<'w> RenderContext<'w> { /// buffer. pub fn add_command_buffer_generation_task( &mut self, + #[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] task: impl FnOnce(RenderDevice) -> CommandBuffer + 'w + Send, + #[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] + task: impl FnOnce(RenderDevice) -> CommandBuffer + 'w, ) { self.flush_encoder(); @@ -389,33 +449,78 @@ 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()); - let mut task_based_command_buffers = ComputeTaskPool::get().scope(|task_pool| { - for (i, queued_command_buffer) in self.command_buffer_queue.into_iter().enumerate() { - match queued_command_buffer { - QueuedCommandBuffer::Ready(command_buffer) => { - command_buffers.push((i, command_buffer)); - } - QueuedCommandBuffer::Task(command_buffer_generation_task) => { - let render_device = self.render_device.clone(); - if self.force_serial { - command_buffers - .push((i, command_buffer_generation_task(render_device))); - } else { - task_pool.spawn(async move { - (i, command_buffer_generation_task(render_device)) - }); + + #[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] + { + let mut task_based_command_buffers = ComputeTaskPool::get().scope(|task_pool| { + for (i, queued_command_buffer) in self.command_buffer_queue.into_iter().enumerate() + { + match queued_command_buffer { + QueuedCommandBuffer::Ready(command_buffer) => { + command_buffers.push((i, command_buffer)); + } + QueuedCommandBuffer::Task(command_buffer_generation_task) => { + let render_device = self.render_device.clone(); + if self.force_serial { + command_buffers + .push((i, command_buffer_generation_task(render_device))); + } else { + task_pool.spawn(async move { + (i, command_buffer_generation_task(render_device)) + }); + } } } } + }); + command_buffers.append(&mut task_based_command_buffers); + } + + #[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] + for (i, queued_command_buffer) in self.command_buffer_queue.into_iter().enumerate() { + match queued_command_buffer { + QueuedCommandBuffer::Ready(command_buffer) => { + command_buffers.push((i, command_buffer)); + } + QueuedCommandBuffer::Task(command_buffer_generation_task) => { + let render_device = self.render_device.clone(); + command_buffers.push((i, command_buffer_generation_task(render_device))); + } } - }); - 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) { @@ -428,5 +533,8 @@ impl<'w> RenderContext<'w> { enum QueuedCommandBuffer<'w> { Ready(CommandBuffer), + #[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] Task(Box CommandBuffer + 'w + Send>), + #[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] + Task(Box CommandBuffer + 'w>), } diff --git a/crates/bevy_render/src/renderer/render_device.rs b/crates/bevy_render/src/renderer/render_device.rs index 45bccf0bbe667..1c0b26b912a42 100644 --- a/crates/bevy_render/src/renderer/render_device.rs +++ b/crates/bevy_render/src/renderer/render_device.rs @@ -11,19 +11,20 @@ use wgpu::{ use super::RenderQueue; use crate::render_resource::resource_macros::*; +use crate::WgpuWrapper; render_resource_wrapper!(ErasedRenderDevice, wgpu::Device); /// This GPU device is responsible for the creation of most rendering and compute resources. #[derive(Resource, Clone)] pub struct RenderDevice { - device: ErasedRenderDevice, + device: WgpuWrapper, } impl From for RenderDevice { fn from(device: wgpu::Device) -> Self { Self { - device: ErasedRenderDevice::new(device), + device: WgpuWrapper::new(ErasedRenderDevice::new(device)), } } } 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..b1ab398f422b1 100644 --- a/crates/bevy_render/src/texture/hdr_texture_loader.rs +++ b/crates/bevy_render/src/texture/hdr_texture_loader.rs @@ -3,6 +3,7 @@ use crate::{ texture::{Image, TextureFormatPixelInfo}, }; use bevy_asset::{io::Reader, AssetLoader, AsyncReadExt, LoadContext}; +use image::DynamicImage; use serde::{Deserialize, Serialize}; use thiserror::Error; use wgpu::{Extent3d, TextureDimension, TextureFormat}; @@ -29,48 +30,49 @@ 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 dynamic_image = DynamicImage::from_decoder(decoder)?; + let image_buffer = dynamic_image + .as_rgb32f() + .expect("HDR Image format should be Rgb32F"); + let mut rgba_data = Vec::with_capacity(image_buffer.pixels().len() * format.pixel_size()); - for rgb in rgb_data { - let alpha = 1.0f32; + for rgb in image_buffer.pixels() { + 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/texture/mod.rs b/crates/bevy_render/src/texture/mod.rs index 986ae7d104ea4..0d186f9aa6139 100644 --- a/crates/bevy_render/src/texture/mod.rs +++ b/crates/bevy_render/src/texture/mod.rs @@ -90,7 +90,7 @@ impl Plugin for ImagePlugin { .register_asset_reflect::(); app.world .resource_mut::>() - .insert(Handle::default(), Image::default()); + .insert(&Handle::default(), Image::default()); #[cfg(feature = "basis-universal")] if let Some(processor) = app .world 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..65ffeda9637b1 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, @@ -357,12 +357,12 @@ fn propagate_recursive( /// Entities that are visible will be marked as such later this frame /// by a [`VisibilitySystems::CheckVisibility`] system. fn reset_view_visibility(mut query: Query<&mut ViewVisibility>) { - for mut view_visibility in &mut query { + query.iter_mut().for_each(|mut view_visibility| { // NOTE: We do not use `set_if_neq` here, as we don't care about // change detection for view visibility, and adding a branch to every // loop iteration would pessimize performance. - *view_visibility = ViewVisibility::HIDDEN; - } + *view_visibility.bypass_change_detection() = ViewVisibility::HIDDEN; + }); } /// System updating the visibility of entities each frame. diff --git a/crates/bevy_render/src/view/window/mod.rs b/crates/bevy_render/src/view/window/mod.rs index 64d089cee0f7c..ddb0f77f98aef 100644 --- a/crates/bevy_render/src/view/window/mod.rs +++ b/crates/bevy_render/src/view/window/mod.rs @@ -4,7 +4,7 @@ use crate::{ }, renderer::{RenderAdapter, RenderDevice, RenderInstance}, texture::TextureFormatPixelInfo, - Extract, ExtractSchedule, Render, RenderApp, RenderSet, + Extract, ExtractSchedule, Render, RenderApp, RenderSet, WgpuWrapper, }; use bevy_app::{App, Plugin}; use bevy_ecs::{entity::EntityHashMap, prelude::*}; @@ -198,7 +198,7 @@ fn extract_windows( struct SurfaceData { // TODO: what lifetime should this be? - surface: wgpu::Surface<'static>, + surface: WgpuWrapper>, configuration: SurfaceConfiguration, } @@ -488,7 +488,7 @@ pub fn create_surfaces( render_device.configure_surface(&surface, &configuration); SurfaceData { - surface, + surface: WgpuWrapper::new(surface), configuration, } }); diff --git a/crates/bevy_scene/Cargo.toml b/crates/bevy_scene/Cargo.toml index 061ec937513d4..7a3236be8ae35 100644 --- a/crates/bevy_scene/Cargo.toml +++ b/crates/bevy_scene/Cargo.toml @@ -40,4 +40,5 @@ rmp-serde = "1.1" workspace = true [package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] all-features = true diff --git a/crates/bevy_scene/src/bundle.rs b/crates/bevy_scene/src/bundle.rs index e47e198728390..b50abbc0e1e3d 100644 --- a/crates/bevy_scene/src/bundle.rs +++ b/crates/bevy_scene/src/bundle.rs @@ -20,7 +20,7 @@ pub struct SceneInstance(InstanceId); /// A component bundle for a [`Scene`] root. /// -/// The scene from `scene` will be spawn as a child of the entity with this component. +/// The scene from `scene` will be spawned as a child of the entity with this component. /// Once it's spawned, the entity will have a [`SceneInstance`] component. #[derive(Default, Bundle)] pub struct SceneBundle { @@ -98,3 +98,78 @@ pub fn scene_spawner( } } } + +#[cfg(test)] +mod tests { + use crate::{DynamicScene, DynamicSceneBundle, ScenePlugin, SceneSpawner}; + use bevy_app::{App, ScheduleRunnerPlugin}; + use bevy_asset::{AssetPlugin, Assets}; + use bevy_ecs::component::Component; + use bevy_ecs::entity::Entity; + use bevy_ecs::prelude::{AppTypeRegistry, ReflectComponent, World}; + use bevy_hierarchy::{Children, HierarchyPlugin}; + use bevy_reflect::Reflect; + use bevy_utils::default; + + #[derive(Component, Reflect, Default)] + #[reflect(Component)] + struct ComponentA { + pub x: f32, + pub y: f32, + } + + #[test] + fn spawn_and_delete() { + let mut app = App::new(); + + app.add_plugins(ScheduleRunnerPlugin::default()) + .add_plugins(HierarchyPlugin) + .add_plugins(AssetPlugin::default()) + .add_plugins(ScenePlugin) + .register_type::(); + app.update(); + + let mut scene_world = World::new(); + + // create a new DynamicScene manually + let type_registry = app.world.resource::().clone(); + scene_world.insert_resource(type_registry); + scene_world.spawn(ComponentA { x: 3.0, y: 4.0 }); + let scene = DynamicScene::from_world(&scene_world); + let scene_handle = app.world.resource_mut::>().add(scene); + + // spawn the scene as a child of `entity` using the `DynamicSceneBundle` + let entity = app + .world + .spawn(DynamicSceneBundle { + scene: scene_handle.clone(), + ..default() + }) + .id(); + + // run the app's schedule once, so that the scene gets spawned + app.update(); + + // make sure that the scene was added as a child of the root entity + let (scene_entity, scene_component_a) = app + .world + .query::<(Entity, &ComponentA)>() + .single(&app.world); + assert_eq!(scene_component_a.x, 3.0); + assert_eq!(scene_component_a.y, 4.0); + assert_eq!(app.world.entity(entity).get::().unwrap().len(), 1); + + // let's try to delete the scene + let mut scene_spawner = app.world.resource_mut::(); + scene_spawner.despawn(&scene_handle); + + // run the scene spawner system to despawn the scene + app.update(); + + // the scene entity does not exist anymore + assert!(app.world.get_entity(scene_entity).is_none()); + + // the root entity does not have any children anymore + assert!(app.world.entity(entity).get::().is_none()); + } +} diff --git a/crates/bevy_scene/src/dynamic_scene.rs b/crates/bevy_scene/src/dynamic_scene.rs index ac2f98ffae8bb..99aa528cda471 100644 --- a/crates/bevy_scene/src/dynamic_scene.rs +++ b/crates/bevy_scene/src/dynamic_scene.rs @@ -5,7 +5,7 @@ use bevy_ecs::{ reflect::{AppTypeRegistry, ReflectComponent, ReflectMapEntities}, world::World, }; -use bevy_reflect::{Reflect, TypePath, TypeRegistryArc}; +use bevy_reflect::{Reflect, TypePath, TypeRegistry}; use bevy_utils::TypeIdMap; #[cfg(feature = "serialize")] @@ -171,9 +171,15 @@ impl DynamicScene { } // TODO: move to AssetSaver when it is implemented - /// Serialize this dynamic scene into rust object notation (ron). + /// Serialize this dynamic scene into the official Bevy scene format (`.scn` / `.scn.ron`). + /// + /// The Bevy scene format is based on [Rusty Object Notation (RON)]. It describes the scene + /// in a human-friendly format. To deserialize the scene, use the [`SceneLoader`]. + /// + /// [`SceneLoader`]: crate::SceneLoader + /// [Rusty Object Notation (RON)]: https://crates.io/crates/ron #[cfg(feature = "serialize")] - pub fn serialize_ron(&self, registry: &TypeRegistryArc) -> Result { + pub fn serialize(&self, registry: &TypeRegistry) -> Result { serialize_ron(SceneSerializer::new(self, registry)) } } diff --git a/crates/bevy_scene/src/lib.rs b/crates/bevy_scene/src/lib.rs index 507e7cbcf619c..0c1a89785412f 100644 --- a/crates/bevy_scene/src/lib.rs +++ b/crates/bevy_scene/src/lib.rs @@ -1,9 +1,15 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![forbid(unsafe_code)] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] + //! Provides scene definition, instantiation and serialization/deserialization. //! //! Scenes are collections of entities and their associated components that can be //! instantiated or removed from a world to allow composition. Scenes can be serialized/deserialized, //! for example to save part of the world state to a file. -#![cfg_attr(docsrs, feature(doc_auto_cfg))] mod bundle; mod dynamic_scene; diff --git a/crates/bevy_scene/src/scene_loader.rs b/crates/bevy_scene/src/scene_loader.rs index f4dce7c66a3d8..46d0484f0e66a 100644 --- a/crates/bevy_scene/src/scene_loader.rs +++ b/crates/bevy_scene/src/scene_loader.rs @@ -6,12 +6,13 @@ 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; -/// [`AssetLoader`] for loading serialized Bevy scene files as [`DynamicScene`]. +/// Asset loader for a Bevy dynamic scene (`.scn` / `.scn.ron`). +/// +/// The loader handles assets serialized with [`DynamicScene::serialize`]. #[derive(Debug)] pub struct SceneLoader { type_registry: TypeRegistryArc, @@ -44,23 +45,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_scene/src/scene_spawner.rs b/crates/bevy_scene/src/scene_spawner.rs index 86c0c30d9a3d9..e89f7f307a139 100644 --- a/crates/bevy_scene/src/scene_spawner.rs +++ b/crates/bevy_scene/src/scene_spawner.rs @@ -8,7 +8,7 @@ use bevy_ecs::{ system::Resource, world::{Command, Mut, World}, }; -use bevy_hierarchy::{Parent, PushChild}; +use bevy_hierarchy::{BuildWorldChildren, DespawnRecursiveExt, Parent, PushChild}; use bevy_utils::{tracing::error, HashMap, HashSet}; use thiserror::Error; use uuid::Uuid; @@ -189,7 +189,10 @@ impl SceneSpawner { pub fn despawn_instance_sync(&mut self, world: &mut World, instance_id: &InstanceId) { if let Some(instance) = self.spawned_instances.remove(instance_id) { for &entity in instance.entity_map.values() { - let _ = world.despawn(entity); + if let Some(mut entity_mut) = world.get_entity_mut(entity) { + entity_mut.remove_parent(); + entity_mut.despawn_recursive(); + }; } } } @@ -482,7 +485,7 @@ mod tests { let scene_id = world.resource_mut::>().add(scene); let instance_id = scene_spawner - .spawn_dynamic_sync(&mut world, scene_id) + .spawn_dynamic_sync(&mut world, &scene_id) .unwrap(); // verify we spawned exactly one new entity with our expected component diff --git a/crates/bevy_scene/src/serde.rs b/crates/bevy_scene/src/serde.rs index 0904502f2d4e4..b5ac7478c99c4 100644 --- a/crates/bevy_scene/src/serde.rs +++ b/crates/bevy_scene/src/serde.rs @@ -4,8 +4,8 @@ use crate::{DynamicEntity, DynamicScene}; use bevy_ecs::entity::Entity; use bevy_reflect::serde::{TypedReflectDeserializer, TypedReflectSerializer}; use bevy_reflect::{ - serde::{TypeRegistrationDeserializer, UntypedReflectDeserializer}, - Reflect, TypeRegistry, TypeRegistryArc, + serde::{ReflectDeserializer, TypeRegistrationDeserializer}, + Reflect, TypeRegistry, }; use bevy_utils::HashSet; use serde::ser::SerializeMap; @@ -28,59 +28,46 @@ pub const ENTITY_STRUCT: &str = "Entity"; /// Name of the serialized component field in an entity struct. pub const ENTITY_FIELD_COMPONENTS: &str = "components"; -/// Handles serialization of a scene as a struct containing its entities and resources. +/// Serializer for a [`DynamicScene`]. /// -/// # Examples +/// Helper object defining Bevy's serialize format for a [`DynamicScene`] and implementing +/// the [`Serialize`] trait for use with Serde. /// -/// ``` -/// # use bevy_scene::{serde::SceneSerializer, DynamicScene}; -/// # use bevy_ecs::{ -/// # prelude::{Component, World}, -/// # reflect::{AppTypeRegistry, ReflectComponent}, -/// # }; -/// # use bevy_reflect::Reflect; -/// // Define an example component type. -/// #[derive(Component, Reflect, Default)] -/// #[reflect(Component)] -/// struct MyComponent { -/// foo: [usize; 3], -/// bar: (f32, f32), -/// baz: String, -/// } -/// -/// // Create our world, provide it with a type registry. -/// // Normally, [`App`] handles providing the type registry. -/// let mut world = World::new(); -/// let registry = AppTypeRegistry::default(); -/// { -/// let mut registry = registry.write(); -/// // Register our component. Primitives and String are registered by default. -/// // Sequence types are automatically handled. -/// registry.register::(); -/// } -/// world.insert_resource(registry); -/// world.spawn(MyComponent { -/// foo: [1, 2, 3], -/// bar: (1.3, 3.7), -/// baz: String::from("test"), -/// }); +/// # Example /// -/// // Print out our serialized scene in the RON format. +/// ``` +/// # use bevy_ecs::prelude::*; +/// # use bevy_scene::{DynamicScene, serde::SceneSerializer}; +/// # let mut world = World::default(); +/// # world.insert_resource(AppTypeRegistry::default()); +/// // Get the type registry /// let registry = world.resource::(); +/// let registry = registry.read(); +/// +/// // Get a DynamicScene to serialize, for example from the World itself /// let scene = DynamicScene::from_world(&world); -/// let scene_serializer = SceneSerializer::new(&scene, ®istry.0); -/// println!("{}", bevy_scene::serialize_ron(scene_serializer).unwrap()); +/// +/// // Create a serializer for that DynamicScene, using the associated TypeRegistry +/// let scene_serializer = SceneSerializer::new(&scene, ®istry); +/// +/// // Serialize through any serde-compatible Serializer +/// let ron_string = bevy_scene::ron::ser::to_string(&scene_serializer); /// ``` pub struct SceneSerializer<'a> { /// The scene to serialize. pub scene: &'a DynamicScene, - /// Type registry in which the components and resources types used in the scene are registered. - pub registry: &'a TypeRegistryArc, + /// The type registry containing the types present in the scene. + pub registry: &'a TypeRegistry, } impl<'a> SceneSerializer<'a> { - /// Creates a scene serializer. - pub fn new(scene: &'a DynamicScene, registry: &'a TypeRegistryArc) -> Self { + /// Create a new serializer from a [`DynamicScene`] and an associated [`TypeRegistry`]. + /// + /// The type registry must contain all types present in the scene. This is generally the case + /// if you obtain both the scene and the registry from the same [`World`]. + /// + /// [`World`]: bevy_ecs::world::World + pub fn new(scene: &'a DynamicScene, registry: &'a TypeRegistry) -> Self { SceneSerializer { scene, registry } } } @@ -114,7 +101,7 @@ pub struct EntitiesSerializer<'a> { /// The entities to serialize. pub entities: &'a [DynamicEntity], /// Type registry in which the component types used by the entities are registered. - pub registry: &'a TypeRegistryArc, + pub registry: &'a TypeRegistry, } impl<'a> Serialize for EntitiesSerializer<'a> { @@ -141,7 +128,7 @@ pub struct EntitySerializer<'a> { /// The entity to serialize. pub entity: &'a DynamicEntity, /// Type registry in which the component types used by the entity are registered. - pub registry: &'a TypeRegistryArc, + pub registry: &'a TypeRegistry, } impl<'a> Serialize for EntitySerializer<'a> { @@ -170,7 +157,7 @@ pub struct SceneMapSerializer<'a> { /// List of boxed values of unique type to serialize. pub entries: &'a [Box], /// Type registry in which the types used in `entries` are registered. - pub registry: &'a TypeRegistryArc, + pub registry: &'a TypeRegistry, } impl<'a> Serialize for SceneMapSerializer<'a> { @@ -182,7 +169,7 @@ impl<'a> Serialize for SceneMapSerializer<'a> { for reflect in self.entries { state.serialize_entry( reflect.get_represented_type_info().unwrap().type_path(), - &TypedReflectSerializer::new(&**reflect, &self.registry.read()), + &TypedReflectSerializer::new(&**reflect, self.registry), )?; } state.end() @@ -460,9 +447,7 @@ impl<'a, 'de> Visitor<'de> for SceneMapVisitor<'a> { A: SeqAccess<'de>, { let mut dynamic_properties = Vec::new(); - while let Some(entity) = - seq.next_element_seed(UntypedReflectDeserializer::new(self.registry))? - { + while let Some(entity) = seq.next_element_seed(ReflectDeserializer::new(self.registry))? { dynamic_properties.push(entity); } @@ -626,7 +611,7 @@ mod tests { }, )"#; let output = scene - .serialize_ron(&world.resource::().0) + .serialize(&world.resource::().read()) .unwrap(); assert_eq!(expected, output); } @@ -709,7 +694,7 @@ mod tests { let scene = DynamicScene::from_world(&world); let serialized = scene - .serialize_ron(&world.resource::().0) + .serialize(&world.resource::().read()) .unwrap(); let mut deserializer = ron::de::Deserializer::from_str(&serialized).unwrap(); let scene_deserializer = SceneDeserializer { @@ -755,10 +740,11 @@ mod tests { }); let registry = world.resource::(); + let registry = ®istry.read(); let scene = DynamicScene::from_world(&world); - let scene_serializer = SceneSerializer::new(&scene, ®istry.0); + let scene_serializer = SceneSerializer::new(&scene, registry); let serialized_scene = postcard::to_allocvec(&scene_serializer).unwrap(); assert_eq!( @@ -772,7 +758,7 @@ mod tests { ); let scene_deserializer = SceneDeserializer { - type_registry: ®istry.0.read(), + type_registry: registry, }; let deserialized_scene = scene_deserializer .deserialize(&mut postcard::Deserializer::from_bytes(&serialized_scene)) @@ -793,10 +779,11 @@ mod tests { }); let registry = world.resource::(); + let registry = ®istry.read(); let scene = DynamicScene::from_world(&world); - let scene_serializer = SceneSerializer::new(&scene, ®istry.0); + let scene_serializer = SceneSerializer::new(&scene, registry); let mut buf = Vec::new(); let mut ser = rmp_serde::Serializer::new(&mut buf); scene_serializer.serialize(&mut ser).unwrap(); @@ -813,7 +800,7 @@ mod tests { ); let scene_deserializer = SceneDeserializer { - type_registry: ®istry.0.read(), + type_registry: registry, }; let mut reader = BufReader::new(buf.as_slice()); @@ -836,10 +823,11 @@ mod tests { }); let registry = world.resource::(); + let registry = ®istry.read(); let scene = DynamicScene::from_world(&world); - let scene_serializer = SceneSerializer::new(&scene, ®istry.0); + let scene_serializer = SceneSerializer::new(&scene, registry); let serialized_scene = bincode::serialize(&scene_serializer).unwrap(); assert_eq!( @@ -855,7 +843,7 @@ mod tests { ); let scene_deserializer = SceneDeserializer { - type_registry: ®istry.0.read(), + type_registry: registry, }; let deserialized_scene = bincode::DefaultOptions::new() diff --git a/crates/bevy_sprite/Cargo.toml b/crates/bevy_sprite/Cargo.toml index 01fea40918265..a2ec75bfde094 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" @@ -39,3 +39,7 @@ radsort = "0.1" [lints] workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +all-features = true diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index 685ecb0d4b54a..de6e35e4489b6 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -1,5 +1,11 @@ // FIXME(3492): remove once docs are ready #![allow(missing_docs)] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![forbid(unsafe_code)] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] //! Provides 2D sprite rendering functionality. mod bundle; diff --git a/crates/bevy_sprite/src/mesh2d/color_material.rs b/crates/bevy_sprite/src/mesh2d/color_material.rs index 59d489361bbe7..6a5f26463d864 100644 --- a/crates/bevy_sprite/src/mesh2d/color_material.rs +++ b/crates/bevy_sprite/src/mesh2d/color_material.rs @@ -25,7 +25,7 @@ impl Plugin for ColorMaterialPlugin { .register_asset_reflect::(); app.world.resource_mut::>().insert( - Handle::::default(), + &Handle::::default(), ColorMaterial { color: Color::srgb(1.0, 0.0, 1.0), ..Default::default() diff --git a/crates/bevy_sprite/src/mesh2d/material.rs b/crates/bevy_sprite/src/mesh2d/material.rs index 0b4a7be28c669..a4533665e4c37 100644 --- a/crates/bevy_sprite/src/mesh2d/material.rs +++ b/crates/bevy_sprite/src/mesh2d/material.rs @@ -10,13 +10,14 @@ use bevy_ecs::{ prelude::*, system::{lifetimeless::SRes, SystemParamItem}, }; +use bevy_math::FloatOrd; use bevy_render::{ mesh::{Mesh, MeshVertexBufferLayoutRef}, prelude::Image, render_asset::{prepare_assets, RenderAssets}, render_phase::{ AddRenderCommand, DrawFunctions, PhaseItem, RenderCommand, RenderCommandResult, - RenderPhase, SetItemPipeline, TrackedRenderPass, + SetItemPipeline, SortedRenderPhase, TrackedRenderPass, }, render_resource::{ AsBindGroup, AsBindGroupError, BindGroup, BindGroupId, BindGroupLayout, @@ -30,7 +31,7 @@ use bevy_render::{ }; use bevy_transform::components::{GlobalTransform, Transform}; use bevy_utils::tracing::error; -use bevy_utils::{FloatOrd, HashMap, HashSet}; +use bevy_utils::{HashMap, HashSet}; use std::hash::Hash; use std::marker::PhantomData; @@ -387,7 +388,7 @@ pub fn queue_material2d_meshes( &VisibleEntities, Option<&Tonemapping>, Option<&DebandDither>, - &mut RenderPhase, + &mut SortedRenderPhase, )>, ) where M::Data: PartialEq + Eq + Hash + Clone, diff --git a/crates/bevy_sprite/src/mesh2d/mesh.rs b/crates/bevy_sprite/src/mesh2d/mesh.rs index 5cd5fcb894374..b014f10071ca7 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh.rs +++ b/crates/bevy_sprite/src/mesh2d/mesh.rs @@ -14,7 +14,7 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::mesh::MeshVertexBufferLayoutRef; use bevy_render::{ batching::{ - batch_and_prepare_render_phase, write_batched_instance_buffer, GetBatchData, + batch_and_prepare_sorted_render_phase, write_batched_instance_buffer, GetBatchData, NoAutomaticBatching, }, globals::{GlobalsBuffer, GlobalsUniform}, @@ -101,7 +101,7 @@ impl Plugin for Mesh2dRenderPlugin { .add_systems( Render, ( - batch_and_prepare_render_phase:: + batch_and_prepare_sorted_render_phase:: .in_set(RenderSet::PrepareResources), write_batched_instance_buffer:: .in_set(RenderSet::PrepareResourcesFlush), diff --git a/crates/bevy_sprite/src/mesh2d/mod.rs b/crates/bevy_sprite/src/mesh2d/mod.rs index 13383ea2d30ab..07e2eb0d6860e 100644 --- a/crates/bevy_sprite/src/mesh2d/mod.rs +++ b/crates/bevy_sprite/src/mesh2d/mod.rs @@ -1,7 +1,9 @@ mod color_material; mod material; mod mesh; +mod wireframe2d; pub use color_material::*; pub use material::*; pub use mesh::*; +pub use wireframe2d::*; diff --git a/crates/bevy_sprite/src/mesh2d/wireframe2d.rs b/crates/bevy_sprite/src/mesh2d/wireframe2d.rs new file mode 100644 index 0000000000000..1009c0c268d32 --- /dev/null +++ b/crates/bevy_sprite/src/mesh2d/wireframe2d.rs @@ -0,0 +1,231 @@ +use crate::{Material2d, Material2dKey, Material2dPlugin, Mesh2dHandle}; +use bevy_app::{Plugin, Startup, Update}; +use bevy_asset::{load_internal_asset, Asset, Assets, Handle}; +use bevy_color::{LinearRgba, Srgba}; +use bevy_ecs::prelude::*; +use bevy_reflect::{std_traits::ReflectDefault, Reflect, TypePath}; +use bevy_render::{ + extract_resource::ExtractResource, mesh::MeshVertexBufferLayoutRef, prelude::*, + render_resource::*, +}; + +pub const WIREFRAME_2D_SHADER_HANDLE: Handle = Handle::weak_from_u128(6920362697190520314); + +/// A [`Plugin`] that draws wireframes for 2D meshes. +/// +/// Wireframes currently do not work when using webgl or webgpu. +/// Supported rendering backends: +/// - DX12 +/// - Vulkan +/// - Metal +/// +/// This is a native only feature. +#[derive(Debug, Default)] +pub struct Wireframe2dPlugin; +impl Plugin for Wireframe2dPlugin { + fn build(&self, app: &mut bevy_app::App) { + load_internal_asset!( + app, + WIREFRAME_2D_SHADER_HANDLE, + "wireframe2d.wgsl", + Shader::from_wgsl + ); + + app.register_type::() + .register_type::() + .register_type::() + .register_type::() + .init_resource::() + .add_plugins(Material2dPlugin::::default()) + .add_systems(Startup, setup_global_wireframe_material) + .add_systems( + Update, + ( + global_color_changed.run_if(resource_changed::), + wireframe_color_changed, + // Run `apply_global_wireframe_material` after `apply_wireframe_material` so that the global + // wireframe setting is applied to a mesh on the same frame its wireframe marker component is removed. + (apply_wireframe_material, apply_global_wireframe_material).chain(), + ), + ); + } +} + +/// Enables wireframe rendering for any entity it is attached to. +/// It will ignore the [`Wireframe2dConfig`] global setting. +/// +/// This requires the [`Wireframe2dPlugin`] to be enabled. +#[derive(Component, Debug, Clone, Default, Reflect, Eq, PartialEq)] +#[reflect(Component, Default)] +pub struct Wireframe2d; + +/// Sets the color of the [`Wireframe2d`] of the entity it is attached to. +/// If this component is present but there's no [`Wireframe2d`] component, +/// it will still affect the color of the wireframe when [`Wireframe2dConfig::global`] is set to true. +/// +/// This overrides the [`Wireframe2dConfig::default_color`]. +#[derive(Component, Debug, Clone, Default, Reflect)] +#[reflect(Component, Default)] +pub struct Wireframe2dColor { + pub color: Srgba, +} + +/// Disables wireframe rendering for any entity it is attached to. +/// It will ignore the [`Wireframe2dConfig`] global setting. +/// +/// This requires the [`Wireframe2dPlugin`] to be enabled. +#[derive(Component, Debug, Clone, Default, Reflect, Eq, PartialEq)] +#[reflect(Component, Default)] +pub struct NoWireframe2d; + +#[derive(Resource, Debug, Clone, Default, ExtractResource, Reflect)] +#[reflect(Resource)] +pub struct Wireframe2dConfig { + /// Whether to show wireframes for all meshes. + /// Can be overridden for individual meshes by adding a [`Wireframe2d`] or [`NoWireframe2d`] component. + pub global: bool, + /// If [`Self::global`] is set, any [`Entity`] that does not have a [`Wireframe2d`] component attached to it will have + /// wireframes using this color. Otherwise, this will be the fallback color for any entity that has a [`Wireframe2d`], + /// but no [`Wireframe2dColor`]. + pub default_color: Srgba, +} + +#[derive(Resource)] +struct GlobalWireframe2dMaterial { + // This handle will be reused when the global config is enabled + handle: Handle, +} + +fn setup_global_wireframe_material( + mut commands: Commands, + mut materials: ResMut>, + config: Res, +) { + // Create the handle used for the global material + commands.insert_resource(GlobalWireframe2dMaterial { + handle: materials.add(Wireframe2dMaterial { + color: config.default_color.into(), + }), + }); +} + +/// Updates the wireframe material of all entities without a [`Wireframe2dColor`] or without a [`Wireframe2d`] component +fn global_color_changed( + config: Res, + mut materials: ResMut>, + global_material: Res, +) { + if let Some(global_material) = materials.get_mut(&global_material.handle) { + global_material.color = config.default_color.into(); + } +} + +/// Updates the wireframe material when the color in [`Wireframe2dColor`] changes +#[allow(clippy::type_complexity)] +fn wireframe_color_changed( + mut materials: ResMut>, + mut colors_changed: Query< + (&mut Handle, &Wireframe2dColor), + (With, Changed), + >, +) { + for (mut handle, wireframe_color) in &mut colors_changed { + *handle = materials.add(Wireframe2dMaterial { + color: wireframe_color.color.into(), + }); + } +} + +/// Applies or remove the wireframe material to any mesh with a [`Wireframe2d`] component, and removes it +/// for any mesh with a [`NoWireframe2d`] component. +fn apply_wireframe_material( + mut commands: Commands, + mut materials: ResMut>, + wireframes: Query< + (Entity, Option<&Wireframe2dColor>), + (With, Without>), + >, + no_wireframes: Query, With>)>, + mut removed_wireframes: RemovedComponents, + global_material: Res, +) { + for e in removed_wireframes.read().chain(no_wireframes.iter()) { + if let Some(mut commands) = commands.get_entity(e) { + commands.remove::>(); + } + } + + let mut wireframes_to_spawn = vec![]; + for (e, wireframe_color) in &wireframes { + let material = if let Some(wireframe_color) = wireframe_color { + materials.add(Wireframe2dMaterial { + color: wireframe_color.color.into(), + }) + } else { + // If there's no color specified we can use the global material since it's already set to use the default_color + global_material.handle.clone() + }; + wireframes_to_spawn.push((e, material)); + } + commands.insert_or_spawn_batch(wireframes_to_spawn); +} + +type Wireframe2dFilter = ( + With, + Without, + Without, +); + +/// Applies or removes a wireframe material on any mesh without a [`Wireframe2d`] or [`NoWireframe2d`] component. +fn apply_global_wireframe_material( + mut commands: Commands, + config: Res, + meshes_without_material: Query< + Entity, + (Wireframe2dFilter, Without>), + >, + meshes_with_global_material: Query< + Entity, + (Wireframe2dFilter, With>), + >, + global_material: Res, +) { + if config.global { + let mut material_to_spawn = vec![]; + for e in &meshes_without_material { + // We only add the material handle but not the Wireframe component + // This makes it easy to detect which mesh is using the global material and which ones are user specified + material_to_spawn.push((e, global_material.handle.clone())); + } + commands.insert_or_spawn_batch(material_to_spawn); + } else { + for e in &meshes_with_global_material { + commands.entity(e).remove::>(); + } + } +} + +#[derive(Default, AsBindGroup, TypePath, Debug, Clone, Asset)] +pub struct Wireframe2dMaterial { + #[uniform(0)] + pub color: LinearRgba, +} + +impl Material2d for Wireframe2dMaterial { + fn fragment_shader() -> ShaderRef { + WIREFRAME_2D_SHADER_HANDLE.into() + } + + fn depth_bias(&self) -> f32 { + 1.0 + } + + fn specialize( + descriptor: &mut RenderPipelineDescriptor, + _layout: &MeshVertexBufferLayoutRef, + _key: Material2dKey, + ) -> Result<(), SpecializedMeshPipelineError> { + descriptor.primitive.polygon_mode = PolygonMode::Line; + Ok(()) + } +} diff --git a/crates/bevy_sprite/src/mesh2d/wireframe2d.wgsl b/crates/bevy_sprite/src/mesh2d/wireframe2d.wgsl new file mode 100644 index 0000000000000..fac02d6456a86 --- /dev/null +++ b/crates/bevy_sprite/src/mesh2d/wireframe2d.wgsl @@ -0,0 +1,11 @@ +#import bevy_sprite::mesh2d_vertex_output::VertexOutput + +struct WireframeMaterial { + color: vec4, +}; + +@group(2) @binding(0) var material: WireframeMaterial; +@fragment +fn fragment(in: VertexOutput) -> @location(0) vec4 { + return material.color; +} diff --git a/crates/bevy_sprite/src/render/mod.rs b/crates/bevy_sprite/src/render/mod.rs index 00cb03febad18..a3160342bcf72 100644 --- a/crates/bevy_sprite/src/render/mod.rs +++ b/crates/bevy_sprite/src/render/mod.rs @@ -15,12 +15,12 @@ use bevy_ecs::{ prelude::*, system::{lifetimeless::*, SystemParamItem, SystemState}, }; -use bevy_math::{Affine3A, Quat, Rect, Vec2, Vec4}; +use bevy_math::{Affine3A, FloatOrd, Quat, Rect, Vec2, Vec4}; use bevy_render::{ render_asset::RenderAssets, render_phase::{ - DrawFunctions, PhaseItem, RenderCommand, RenderCommandResult, RenderPhase, SetItemPipeline, - TrackedRenderPass, + DrawFunctions, PhaseItem, RenderCommand, RenderCommandResult, SetItemPipeline, + SortedRenderPhase, TrackedRenderPass, }, render_resource::{ binding_types::{sampler, texture_2d, uniform_buffer}, @@ -37,7 +37,7 @@ use bevy_render::{ Extract, }; use bevy_transform::components::GlobalTransform; -use bevy_utils::{FloatOrd, HashMap}; +use bevy_utils::HashMap; use bytemuck::{Pod, Zeroable}; use fixedbitset::FixedBitSet; @@ -122,10 +122,9 @@ bitflags::bitflags! { // MSAA uses the highest 3 bits for the MSAA log2(sample count) to support up to 128x MSAA. pub struct SpritePipelineKey: u32 { const NONE = 0; - const COLORED = 1 << 0; - const HDR = 1 << 1; - const TONEMAP_IN_SHADER = 1 << 2; - const DEBAND_DITHER = 1 << 3; + const HDR = 1 << 0; + const TONEMAP_IN_SHADER = 1 << 1; + const DEBAND_DITHER = 1 << 2; const MSAA_RESERVED_BITS = Self::MSAA_MASK_BITS << Self::MSAA_SHIFT_BITS; const TONEMAP_METHOD_RESERVED_BITS = Self::TONEMAP_METHOD_MASK_BITS << Self::TONEMAP_METHOD_SHIFT_BITS; const TONEMAP_METHOD_NONE = 0 << Self::TONEMAP_METHOD_SHIFT_BITS; @@ -158,15 +157,6 @@ impl SpritePipelineKey { 1 << ((self.bits() >> Self::MSAA_SHIFT_BITS) & Self::MSAA_MASK_BITS) } - #[inline] - pub const fn from_colored(colored: bool) -> Self { - if colored { - SpritePipelineKey::COLORED - } else { - SpritePipelineKey::NONE - } - } - #[inline] pub const fn from_hdr(hdr: bool) -> Self { if hdr { @@ -458,7 +448,7 @@ pub fn queue_sprites( msaa: Res, extracted_sprites: Res, mut views: Query<( - &mut RenderPhase, + &mut SortedRenderPhase, &VisibleEntities, &ExtractedView, Option<&Tonemapping>, @@ -495,16 +485,7 @@ pub fn queue_sprites( } } - let pipeline = pipelines.specialize( - &pipeline_cache, - &sprite_pipeline, - view_key | SpritePipelineKey::from_colored(false), - ); - let colored_pipeline = pipelines.specialize( - &pipeline_cache, - &sprite_pipeline, - view_key | SpritePipelineKey::from_colored(true), - ); + let pipeline = pipelines.specialize(&pipeline_cache, &sprite_pipeline, view_key); view_entities.clear(); view_entities.extend(visible_entities.entities.iter().map(|e| e.index() as usize)); @@ -524,27 +505,15 @@ pub fn queue_sprites( let sort_key = FloatOrd(extracted_sprite.transform.translation().z); // Add the item to the render phase - if extracted_sprite.color != LinearRgba::WHITE { - transparent_phase.add(Transparent2d { - draw_function: draw_sprite_function, - pipeline: colored_pipeline, - entity: *entity, - sort_key, - // batch_range and dynamic_offset will be calculated in prepare_sprites - batch_range: 0..0, - dynamic_offset: None, - }); - } else { - transparent_phase.add(Transparent2d { - draw_function: draw_sprite_function, - pipeline, - entity: *entity, - sort_key, - // batch_range and dynamic_offset will be calculated in prepare_sprites - batch_range: 0..0, - dynamic_offset: None, - }); - } + transparent_phase.add(Transparent2d { + draw_function: draw_sprite_function, + pipeline, + entity: *entity, + sort_key, + // batch_range and dynamic_offset will be calculated in prepare_sprites + batch_range: 0..0, + dynamic_offset: None, + }); } } } @@ -561,7 +530,7 @@ pub fn prepare_sprites( mut image_bind_groups: ResMut, gpu_images: Res>, extracted_sprites: Res, - mut phases: Query<&mut RenderPhase>, + mut phases: Query<&mut SortedRenderPhase>, events: Res, ) { // If an image has changed, the GpuImage has (probably) changed diff --git a/crates/bevy_tasks/Cargo.toml b/crates/bevy_tasks/Cargo.toml index ee73b120ac94b..b3a70bafbd4c3 100644 --- a/crates/bevy_tasks/Cargo.toml +++ b/crates/bevy_tasks/Cargo.toml @@ -29,4 +29,5 @@ web-time = { version = "0.2" } workspace = true [package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] all-features = true diff --git a/crates/bevy_tasks/src/lib.rs b/crates/bevy_tasks/src/lib.rs old mode 100755 new mode 100644 index 5be6574a9fecc..34011532d6b96 --- a/crates/bevy_tasks/src/lib.rs +++ b/crates/bevy_tasks/src/lib.rs @@ -1,5 +1,9 @@ #![doc = include_str!("../README.md")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] mod slice; pub use slice::{ParallelSlice, ParallelSliceMut}; diff --git a/crates/bevy_tasks/src/single_threaded_task_pool.rs b/crates/bevy_tasks/src/single_threaded_task_pool.rs index f3837c4766fae..3a32c9e286211 100644 --- a/crates/bevy_tasks/src/single_threaded_task_pool.rs +++ b/crates/bevy_tasks/src/single_threaded_task_pool.rs @@ -94,6 +94,7 @@ impl TaskPool { /// to spawn tasks. This function will await the completion of all tasks before returning. /// /// This is similar to `rayon::scope` and `crossbeam::scope` + #[allow(unsafe_code)] pub fn scope_with_executor<'env, F, T>( &self, _tick_task_pool_executor: bool, diff --git a/crates/bevy_tasks/src/task_pool.rs b/crates/bevy_tasks/src/task_pool.rs index 551bb06311fd2..300373031ad42 100644 --- a/crates/bevy_tasks/src/task_pool.rs +++ b/crates/bevy_tasks/src/task_pool.rs @@ -334,6 +334,7 @@ impl TaskPool { }) } + #[allow(unsafe_code)] fn scope_with_executor_inner<'env, F, T>( &self, tick_task_pool_executor: bool, diff --git a/crates/bevy_text/Cargo.toml b/crates/bevy_text/Cargo.toml index 6fc3deb8a9778..cdd26c618e174 100644 --- a/crates/bevy_text/Cargo.toml +++ b/crates/bevy_text/Cargo.toml @@ -41,4 +41,5 @@ approx = "0.5.1" workspace = true [package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] all-features = true diff --git a/crates/bevy_text/src/font_atlas_set.rs b/crates/bevy_text/src/font_atlas_set.rs index 156192e690e33..25980bdf340e3 100644 --- a/crates/bevy_text/src/font_atlas_set.rs +++ b/crates/bevy_text/src/font_atlas_set.rs @@ -3,11 +3,10 @@ use ab_glyph::{GlyphId, OutlinedGlyph, Point}; use bevy_asset::{AssetEvent, AssetId}; use bevy_asset::{Assets, Handle}; use bevy_ecs::prelude::*; -use bevy_math::UVec2; +use bevy_math::{FloatOrd, UVec2}; use bevy_reflect::Reflect; use bevy_render::texture::Image; use bevy_sprite::TextureAtlasLayout; -use bevy_utils::FloatOrd; use bevy_utils::HashMap; type FontSizeKey = FloatOrd; 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/lib.rs b/crates/bevy_text/src/lib.rs index 6df96c14a2d46..839418174330c 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -1,6 +1,11 @@ // FIXME(3492): remove once docs are ready #![allow(missing_docs)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![forbid(unsafe_code)] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] mod error; mod font; diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index edd76a2de9ec0..26c547466e05f 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -129,32 +129,25 @@ impl TextMeasureInfo { scale_factor: f32, ) -> Result { let sections = &text.sections; - for section in sections { - if !fonts.contains(§ion.style.font) { - return Err(TextError::NoSuchFont); - } - } - let (auto_fonts, sections) = sections - .iter() - .enumerate() - .map(|(i, section)| { - // SAFETY: we exited early earlier in this function if - // one of the fonts was missing. - let font = unsafe { fonts.get(§ion.style.font).unwrap_unchecked() }; - ( - font.font.clone(), - TextMeasureSection { + let mut auto_fonts = Vec::with_capacity(sections.len()); + let mut out_sections = Vec::with_capacity(sections.len()); + for (i, section) in sections.iter().enumerate() { + match fonts.get(§ion.style.font) { + Some(font) => { + auto_fonts.push(font.font.clone()); + out_sections.push(TextMeasureSection { font_id: FontId(i), scale: scale_value(section.style.font_size, scale_factor), text: section.value.clone().into_boxed_str(), - }, - ) - }) - .unzip(); + }); + } + None => return Err(TextError::NoSuchFont), + } + } Ok(Self::new( auto_fonts, - sections, + out_sections, text.justify, text.linebreak_behavior.into(), )) 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_time/Cargo.toml b/crates/bevy_time/Cargo.toml index 5a7ccb9a577f1..a2ea432d86e1f 100644 --- a/crates/bevy_time/Cargo.toml +++ b/crates/bevy_time/Cargo.toml @@ -32,4 +32,5 @@ thiserror = "1.0" workspace = true [package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] all-features = true diff --git a/crates/bevy_time/src/lib.rs b/crates/bevy_time/src/lib.rs old mode 100755 new mode 100644 index 23ec1669632e9..3119f023523e9 --- a/crates/bevy_time/src/lib.rs +++ b/crates/bevy_time/src/lib.rs @@ -1,5 +1,10 @@ #![doc = include_str!("../README.md")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![forbid(unsafe_code)] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] /// Common run conditions pub mod common_conditions; diff --git a/crates/bevy_transform/Cargo.toml b/crates/bevy_transform/Cargo.toml index 5662117c684c7..13173244318ea 100644 --- a/crates/bevy_transform/Cargo.toml +++ b/crates/bevy_transform/Cargo.toml @@ -36,4 +36,5 @@ serialize = ["dep:serde", "bevy_math/serialize"] workspace = true [package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] all-features = true diff --git a/crates/bevy_transform/src/components/transform.rs b/crates/bevy_transform/src/components/transform.rs index ae58c50d850fb..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,39 +133,39 @@ 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 } - /// Returns this [`Transform`] with a rotation so that the `handle` vector, reinterpreted in local coordinates, - /// points in the given `direction`, while `weak_handle` points towards `weak_direction`. - /// + /// 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 `Transform::aligned_by(Vec3::X, v, Vec3::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`. + /// 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 `handle` or `direction` is zero, `Vec3::X` takes its place - /// * if `weak_handle` or `weak_direction` is zero, `Vec3::Y` takes its place - /// * if `handle` is parallel with `weak_handle` or `direction` is parallel with `weak_direction`, a rotation is - /// constructed which takes `handle` to `direction` but ignores the weak counterparts (i.e. is otherwise unspecified) + /// * 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: Vec3, - main_direction: Vec3, - secondary_axis: Vec3, - secondary_direction: Vec3, + main_axis: impl TryInto, + main_direction: impl TryInto, + secondary_axis: impl TryInto, + secondary_direction: impl TryInto, ) -> Self { self.align( main_axis, @@ -373,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); } @@ -384,26 +382,26 @@ 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(Vec3::X, v, Vec3::Y, w)` will make the spaceship's + /// 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: @@ -411,62 +409,62 @@ impl Transform { /// * 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 `Vec3::NEG_Z` (the [`Transform::forward`] - /// direction in the default orientation) and `secondary_axis` is `Vec3::Y` (the [`Transform::up`] direction in the default + /// [`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` is zero, `Vec3::X` takes its place - /// * if `secondary_axis` or `secondary_direction` is zero, `Vec3::Y` takes its place + /// * 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::{Vec3, Quat}; + /// # use bevy_math::{Dir3, Vec3, Quat}; /// # use bevy_transform::components::Transform; /// # let mut t1 = Transform::IDENTITY; /// # let mut t2 = Transform::IDENTITY; - /// t1.align(Vec3::X, Vec3::Y, Vec3::new(1., 1., 0.), Vec3::Z); - /// let main_axis_image = t1.rotation * Vec3::X; + /// 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, Vec3::Z, Vec3::ZERO, Vec3::X); - /// t2.align(Vec3::X, Vec3::Z, Vec3::Y, Vec3::X); + /// 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(Vec3::X, Vec3::Z, Vec3::X, Vec3::Y); + /// 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: Vec3, - main_direction: Vec3, - secondary_axis: Vec3, - secondary_direction: Vec3, + main_axis: impl TryInto, + main_direction: impl TryInto, + secondary_axis: impl TryInto, + secondary_direction: impl TryInto, ) { - let main_axis = main_axis.try_normalize().unwrap_or(Vec3::X); - let main_direction = main_direction.try_normalize().unwrap_or(Vec3::X); - let secondary_axis = secondary_axis.try_normalize().unwrap_or(Vec3::Y); - let secondary_direction = secondary_direction.try_normalize().unwrap_or(Vec3::Y); + 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, 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) + .reject_from_normalized(main_direction.into()) .try_normalize(); let secondary_direction_ortho = secondary_direction - .reject_from_normalized(main_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 diff --git a/crates/bevy_transform/src/lib.rs b/crates/bevy_transform/src/lib.rs old mode 100755 new mode 100644 index f38ca1726b1b6..2d7338cc7d4c8 --- a/crates/bevy_transform/src/lib.rs +++ b/crates/bevy_transform/src/lib.rs @@ -1,5 +1,9 @@ #![doc = include_str!("../README.md")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] pub mod commands; /// The basic components of the transform crate diff --git a/crates/bevy_transform/src/systems.rs b/crates/bevy_transform/src/systems.rs index bdbce91e941bb..4e0a2efe8fa2e 100644 --- a/crates/bevy_transform/src/systems.rs +++ b/crates/bevy_transform/src/systems.rs @@ -79,6 +79,7 @@ pub fn propagate_transforms( // - Since each root entity is unique and the hierarchy is consistent and forest-like, // other root entities' `propagate_recursive` calls will not conflict with this one. // - Since this is the only place where `transform_query` gets used, there will be no conflicting fetches elsewhere. + #[allow(unsafe_code)] unsafe { propagate_recursive( &global_transform, @@ -106,6 +107,7 @@ pub fn propagate_transforms( /// nor any of its descendants. /// - The caller must ensure that the hierarchy leading to `entity` /// is well-formed and must remain as a tree or a forest. Each entity must have at most one parent. +#[allow(unsafe_code)] unsafe fn propagate_recursive( parent: &GlobalTransform, transform_query: &Query< diff --git a/crates/bevy_ui/Cargo.toml b/crates/bevy_ui/Cargo.toml index 92547a2fe3828..d7882c7da4d38 100644 --- a/crates/bevy_ui/Cargo.toml +++ b/crates/bevy_ui/Cargo.toml @@ -41,8 +41,10 @@ smallvec = "1.11" [features] serialize = ["serde", "smallvec/serde"] -[package.metadata.docs.rs] -all-features = true [lints] workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +all-features = true diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index 6b7940e1484ec..7ff8a59229fca 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -52,7 +52,7 @@ struct RootNodePair { #[derive(Resource)] pub struct UiSurface { entity_to_taffy: EntityHashMap, - camera_entity_to_taffy: EntityHashMap, + camera_entity_to_taffy: EntityHashMap>, camera_roots: EntityHashMap>, taffy: Taffy, } @@ -168,10 +168,7 @@ without UI components as a child of an entity with UI components, results may be ..default() }; - let camera_node = *self - .camera_entity_to_taffy - .entry(camera_id) - .or_insert_with(|| self.taffy.new_leaf(viewport_style.clone()).unwrap()); + let camera_root_node_map = self.camera_entity_to_taffy.entry(camera_id).or_default(); let existing_roots = self.camera_roots.entry(camera_id).or_default(); let mut new_roots = Vec::new(); for entity in children { @@ -186,10 +183,13 @@ without UI components as a child of an entity with UI components, results may be self.taffy.remove_child(previous_parent, node).unwrap(); } - self.taffy.add_child(camera_node, node).unwrap(); + let viewport_node = *camera_root_node_map + .entry(entity) + .or_insert_with(|| self.taffy.new_leaf(viewport_style.clone()).unwrap()); + self.taffy.add_child(viewport_node, node).unwrap(); RootNodePair { - implicit_viewport_node: camera_node, + implicit_viewport_node: viewport_node, user_root_node: node, } }); @@ -219,8 +219,10 @@ without UI components as a child of an entity with UI components, results may be /// Removes each camera entity from the internal map and then removes their associated node from taffy pub fn remove_camera_entities(&mut self, entities: impl IntoIterator) { for entity in entities { - if let Some(node) = self.camera_entity_to_taffy.remove(&entity) { - self.taffy.remove(node).unwrap(); + if let Some(camera_root_node_map) = self.camera_entity_to_taffy.remove(&entity) { + for (_, node) in camera_root_node_map.iter() { + self.taffy.remove(*node).unwrap(); + } } } } @@ -543,20 +545,19 @@ mod tests { use bevy_ecs::entity::Entity; use bevy_ecs::event::Events; use bevy_ecs::prelude::{Commands, Component, In, Query, With}; + use bevy_ecs::query::Without; use bevy_ecs::schedule::apply_deferred; use bevy_ecs::schedule::IntoSystemConfigs; use bevy_ecs::schedule::Schedule; use bevy_ecs::system::RunSystemOnce; use bevy_ecs::world::World; - use bevy_hierarchy::despawn_with_children_recursive; - use bevy_hierarchy::BuildWorldChildren; - use bevy_hierarchy::Children; - use bevy_math::Vec2; - use bevy_math::{vec2, UVec2}; + use bevy_hierarchy::{despawn_with_children_recursive, BuildWorldChildren, Children, Parent}; + use bevy_math::{vec2, Rect, UVec2, Vec2}; use bevy_render::camera::ManualTextureViews; use bevy_render::camera::OrthographicProjection; use bevy_render::prelude::Camera; use bevy_render::texture::Image; + use bevy_transform::prelude::{GlobalTransform, Transform}; use bevy_utils::prelude::default; use bevy_utils::HashMap; use bevy_window::PrimaryWindow; @@ -857,6 +858,89 @@ mod tests { assert!(ui_surface.entity_to_taffy.is_empty()); } + /// regression test for >=0.13.1 root node layouts + /// ensure root nodes act like they are absolutely positioned + /// without explicitly declaring it. + #[test] + fn ui_root_node_should_act_like_position_absolute() { + let (mut world, mut ui_schedule) = setup_ui_test_world(); + + let mut size = 150.; + + world.spawn(NodeBundle { + style: Style { + // test should pass without explicitly requiring position_type to be set to Absolute + // position_type: PositionType::Absolute, + width: Val::Px(size), + height: Val::Px(size), + ..default() + }, + ..default() + }); + + size -= 50.; + + world.spawn(NodeBundle { + style: Style { + // position_type: PositionType::Absolute, + width: Val::Px(size), + height: Val::Px(size), + ..default() + }, + ..default() + }); + + size -= 50.; + + world.spawn(NodeBundle { + style: Style { + // position_type: PositionType::Absolute, + width: Val::Px(size), + height: Val::Px(size), + ..default() + }, + ..default() + }); + + ui_schedule.run(&mut world); + + let overlap_check = world + .query_filtered::<(Entity, &Node, &mut GlobalTransform, &Transform), Without>() + .iter_mut(&mut world) + .fold( + Option::<(Rect, bool)>::None, + |option_rect, (entity, node, mut global_transform, transform)| { + // fix global transform - for some reason the global transform isn't populated yet. + // might be related to how these specific tests are working directly with World instead of App + *global_transform = GlobalTransform::from(transform.compute_affine()); + let global_transform = &*global_transform; + let current_rect = node.logical_rect(global_transform); + assert!( + current_rect.height().abs() + current_rect.width().abs() > 0., + "root ui node {entity:?} doesn't have a logical size" + ); + assert_ne!( + global_transform.affine(), + GlobalTransform::default().affine(), + "root ui node {entity:?} global transform is not populated" + ); + let Some((rect, is_overlapping)) = option_rect else { + return Some((current_rect, false)); + }; + if rect.contains(current_rect.center()) { + Some((current_rect, true)) + } else { + Some((current_rect, is_overlapping)) + } + }, + ); + + let Some((_rect, is_overlapping)) = overlap_check else { + unreachable!("test not setup properly"); + }; + assert!(is_overlapping, "root ui nodes are expected to behave like they have absolute position and be independent from each other"); + } + #[test] fn ui_node_should_properly_update_when_changing_target_camera() { #[derive(Component)] diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 486919a8b2d5d..a8f127980d3fc 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -1,11 +1,15 @@ // FIXME(3492): remove once docs are ready #![allow(missing_docs)] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] //! This crate contains Bevy's UI system, which can be used to create UI for both 2D and 3D games //! # Basic usage //! Spawn UI elements with [`node_bundles::ButtonBundle`], [`node_bundles::ImageBundle`], [`node_bundles::TextBundle`] and [`node_bundles::NodeBundle`] //! This UI is laid out with the Flexbox and CSS Grid layout models (see ) -#![cfg_attr(docsrs, feature(doc_auto_cfg))] pub mod measurement; pub mod node_bundles; @@ -112,6 +116,7 @@ impl Plugin for UiPlugin { .register_type::() .register_type::() .register_type::() + .register_type::() .register_type::() .register_type::() .register_type::() @@ -149,17 +154,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 fcec109409d59..5ba9ba77ede78 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -15,20 +15,21 @@ 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::{FloatOrd, Mat4, Rect, URect, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; use bevy_render::{ camera::Camera, render_asset::RenderAssets, render_graph::{RenderGraph, RunGraphOnViewNode}, - render_phase::{sort_phase_system, AddRenderCommand, DrawFunctions, RenderPhase}, + render_phase::{sort_phase_system, AddRenderCommand, DrawFunctions, SortedRenderPhase}, render_resource::*, renderer::{RenderDevice, RenderQueue}, texture::Image, @@ -39,7 +40,7 @@ use bevy_sprite::TextureAtlasLayout; #[cfg(feature = "bevy_text")] use bevy_text::{PositionedGlyph, Text, TextLayoutInfo}; use bevy_transform::components::GlobalTransform; -use bevy_utils::{FloatOrd, HashMap}; +use bevy_utils::HashMap; use bytemuck::{Pod, Zeroable}; use std::ops::Range; @@ -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, }, ); } @@ -585,7 +704,7 @@ pub fn extract_default_ui_camera_view( .id(); commands.get_or_spawn(entity).insert(( DefaultCameraView(default_camera_view), - RenderPhase::::default(), + SortedRenderPhase::::default(), )); } } @@ -680,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, }, ); } @@ -692,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, } @@ -705,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, } } @@ -726,15 +860,21 @@ 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( extracted_uinodes: Res, ui_pipeline: Res, mut pipelines: ResMut>, - mut views: Query<(&ExtractedView, &mut RenderPhase)>, + mut views: Query<(&ExtractedView, &mut SortedRenderPhase)>, pipeline_cache: Res, draw_functions: Res>, ) { @@ -781,7 +921,7 @@ pub fn prepare_uinodes( ui_pipeline: Res, mut image_bind_groups: ResMut, gpu_images: Res>, - mut phases: Query<&mut RenderPhase>, + mut phases: Query<&mut SortedRenderPhase>, events: Res, mut previous_len: Local, ) { @@ -802,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(); @@ -832,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, }; @@ -882,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; @@ -946,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); @@ -986,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(); @@ -1003,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..e398a46d93d24 100644 --- a/crates/bevy_ui/src/render/render_pass.rs +++ b/crates/bevy_ui/src/render/render_pass.rs @@ -6,6 +6,7 @@ use bevy_ecs::{ prelude::*, system::{lifetimeless::*, SystemParamItem}, }; +use bevy_math::FloatOrd; use bevy_render::{ camera::ExtractedCamera, render_graph::*, @@ -14,13 +15,12 @@ use bevy_render::{ renderer::*, view::*, }; -use bevy_utils::FloatOrd; use nonmax::NonMaxU32; pub struct UiPassNode { ui_view_query: QueryState< ( - &'static RenderPhase, + &'static SortedRenderPhase, &'static ViewTarget, &'static ExtractedCamera, ), @@ -96,28 +96,16 @@ pub struct TransparentUi { } impl PhaseItem for TransparentUi { - type SortKey = (FloatOrd, u32); - #[inline] fn entity(&self) -> Entity { self.entity } - #[inline] - fn sort_key(&self) -> Self::SortKey { - self.sort_key - } - #[inline] fn draw_function(&self) -> DrawFunctionId { self.draw_function } - #[inline] - fn sort(items: &mut [Self]) { - items.sort_by_key(|item| item.sort_key()); - } - #[inline] fn batch_range(&self) -> &Range { &self.batch_range @@ -139,6 +127,20 @@ impl PhaseItem for TransparentUi { } } +impl SortedPhaseItem for TransparentUi { + type SortKey = (FloatOrd, u32); + + #[inline] + fn sort_key(&self) -> Self::SortKey { + self.sort_key + } + + #[inline] + fn sort(items: &mut [Self]) { + items.sort_by_key(|item| item.sort_key()); + } +} + impl CachedRenderPipelinePhaseItem for TransparentUi { #[inline] fn cached_pipeline(&self) -> CachedRenderPipelineId { @@ -215,8 +217,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..f5737f35ba514 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_size = min(half_size.x, half_size.y); + + r = min(max(r, vec4(0.0)), vec4(min_size)); + + 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 1245cf3012f7a..2ab70e72235d6 100644 --- a/crates/bevy_ui/src/render/ui_material_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_material_pipeline.rs @@ -9,7 +9,7 @@ use bevy_ecs::{ system::lifetimeless::{Read, SRes}, system::*, }; -use bevy_math::{Mat4, Rect, Vec2, Vec4Swizzles}; +use bevy_math::{FloatOrd, Mat4, Rect, Vec2, Vec4Swizzles}; use bevy_render::{ extract_component::ExtractComponentPlugin, globals::{GlobalsBuffer, GlobalsUniform}, @@ -22,7 +22,7 @@ use bevy_render::{ Extract, ExtractSchedule, Render, RenderSet, }; use bevy_transform::prelude::GlobalTransform; -use bevy_utils::{FloatOrd, HashMap, HashSet}; +use bevy_utils::{HashMap, HashSet}; use bevy_window::{PrimaryWindow, Window}; use bytemuck::{Pod, Zeroable}; @@ -460,7 +460,7 @@ pub fn prepare_uimaterial_nodes( view_uniforms: Res, globals_buffer: Res, ui_material_pipeline: Res>, - mut phases: Query<&mut RenderPhase>, + mut phases: Query<&mut SortedRenderPhase>, mut previous_len: Local, ) { if let (Some(view_binding), Some(globals_binding)) = ( @@ -757,7 +757,7 @@ pub fn queue_ui_material_nodes( mut pipelines: ResMut>>, pipeline_cache: Res, render_materials: Res>, - mut views: Query<(&ExtractedView, &mut RenderPhase)>, + mut views: Query<(&ExtractedView, &mut SortedRenderPhase)>, ) where M::Data: PartialEq + Eq + Hash + Clone, { 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..41be40d3be37f 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. @@ -1424,6 +1424,7 @@ pub struct GridPlacement { } impl GridPlacement { + #[allow(unsafe_code)] pub const DEFAULT: Self = Self { start: None, // SAFETY: This is trivially safe as 1 is non-zero. @@ -1590,7 +1591,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 +1617,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 +1797,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 +1813,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/Cargo.toml b/crates/bevy_utils/Cargo.toml index 88fe6dc0a574c..8b6ac50a76031 100644 --- a/crates/bevy_utils/Cargo.toml +++ b/crates/bevy_utils/Cargo.toml @@ -27,3 +27,7 @@ getrandom = { version = "0.2.0", features = ["js"] } [lints] workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +all-features = true diff --git a/crates/bevy_utils/macros/Cargo.toml b/crates/bevy_utils/macros/Cargo.toml index b30d885a1d018..998bf6e2ffec6 100644 --- a/crates/bevy_utils/macros/Cargo.toml +++ b/crates/bevy_utils/macros/Cargo.toml @@ -15,3 +15,7 @@ proc-macro2 = "1.0" [lints] workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +all-features = true diff --git a/crates/bevy_utils/macros/src/lib.rs b/crates/bevy_utils/macros/src/lib.rs index 9b1fb1cfe5f25..23b11fd7cd2b1 100644 --- a/crates/bevy_utils/macros/src/lib.rs +++ b/crates/bevy_utils/macros/src/lib.rs @@ -1,5 +1,6 @@ // FIXME(3492): remove once docs are ready #![allow(missing_docs)] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] use proc_macro::TokenStream; use quote::{format_ident, quote}; diff --git a/crates/bevy_utils/src/float_ord.rs b/crates/bevy_utils/src/float_ord.rs deleted file mode 100644 index 9e7931d2204a3..0000000000000 --- a/crates/bevy_utils/src/float_ord.rs +++ /dev/null @@ -1,66 +0,0 @@ -use std::{ - cmp::Ordering, - hash::{Hash, Hasher}, - ops::Neg, -}; - -/// A wrapper for floats that implements [`Ord`], [`Eq`], and [`Hash`] traits. -/// -/// This is a work around for the fact that the IEEE 754-2008 standard, -/// implemented by Rust's [`f32`] type, -/// doesn't define an ordering for [`NaN`](f32::NAN), -/// and `NaN` is not considered equal to any other `NaN`. -/// -/// Wrapping a float with `FloatOrd` breaks conformance with the standard -/// by sorting `NaN` as less than all other numbers and equal to any other `NaN`. -#[derive(Debug, Copy, Clone, PartialOrd)] -pub struct FloatOrd(pub f32); - -#[allow(clippy::derive_ord_xor_partial_ord)] -impl Ord for FloatOrd { - fn cmp(&self, other: &Self) -> Ordering { - self.0.partial_cmp(&other.0).unwrap_or_else(|| { - if self.0.is_nan() && !other.0.is_nan() { - Ordering::Less - } else if !self.0.is_nan() && other.0.is_nan() { - Ordering::Greater - } else { - Ordering::Equal - } - }) - } -} - -impl PartialEq for FloatOrd { - fn eq(&self, other: &Self) -> bool { - if self.0.is_nan() && other.0.is_nan() { - true - } else { - self.0 == other.0 - } - } -} - -impl Eq for FloatOrd {} - -impl Hash for FloatOrd { - fn hash(&self, state: &mut H) { - if self.0.is_nan() { - // Ensure all NaN representations hash to the same value - state.write(&f32::to_ne_bytes(f32::NAN)); - } else if self.0 == 0.0 { - // Ensure both zeroes hash to the same value - state.write(&f32::to_ne_bytes(0.0f32)); - } else { - state.write(&f32::to_ne_bytes(self.0)); - } - } -} - -impl Neg for FloatOrd { - type Output = FloatOrd; - - fn neg(self) -> Self::Output { - FloatOrd(-self.0) - } -} diff --git a/crates/bevy_utils/src/lib.rs b/crates/bevy_utils/src/lib.rs index 33f5312171041..04325b64fe805 100644 --- a/crates/bevy_utils/src/lib.rs +++ b/crates/bevy_utils/src/lib.rs @@ -1,3 +1,10 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![allow(unsafe_code)] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] + //! General utilities for first-party [Bevy] engine crates. //! //! [Bevy]: https://bevyengine.org/ @@ -17,7 +24,6 @@ pub mod syncunsafecell; mod cow_arc; mod default; -mod float_ord; pub mod intern; mod once; mod parallel_queue; @@ -26,7 +32,6 @@ pub use ahash::{AHasher, RandomState}; pub use bevy_utils_proc_macros::*; pub use cow_arc::*; pub use default::default; -pub use float_ord::*; pub use hashbrown; pub use parallel_queue::*; pub use tracing; @@ -36,21 +41,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/Cargo.toml b/crates/bevy_window/Cargo.toml index d455a2ebe834b..6555e8acb606f 100644 --- a/crates/bevy_window/Cargo.toml +++ b/crates/bevy_window/Cargo.toml @@ -34,4 +34,5 @@ smol_str = "0.2" workspace = true [package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] all-features = true diff --git a/crates/bevy_window/src/lib.rs b/crates/bevy_window/src/lib.rs index ff722ef9df243..f32e8b75c86b7 100644 --- a/crates/bevy_window/src/lib.rs +++ b/crates/bevy_window/src/lib.rs @@ -1,10 +1,15 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] + //! `bevy_window` provides a platform-agnostic interface for windowing in Bevy. //! //! This crate contains types for window management and events, //! used by windowing implementors such as `bevy_winit`. //! The [`WindowPlugin`] sets up some global window-related parameters and //! is part of the [`DefaultPlugins`](https://docs.rs/bevy/latest/bevy/struct.DefaultPlugins.html). -#![cfg_attr(docsrs, feature(doc_auto_cfg))] use bevy_a11y::Focus; diff --git a/crates/bevy_window/src/raw_handle.rs b/crates/bevy_window/src/raw_handle.rs index f9b6336149c61..eb9382590c883 100644 --- a/crates/bevy_window/src/raw_handle.rs +++ b/crates/bevy_window/src/raw_handle.rs @@ -1,3 +1,5 @@ +#![allow(unsafe_code)] + use bevy_ecs::prelude::Component; use raw_window_handle::{ DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, RawDisplayHandle, 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/Cargo.toml b/crates/bevy_winit/Cargo.toml index 063085b6a01b8..d617ba32970e8 100644 --- a/crates/bevy_winit/Cargo.toml +++ b/crates/bevy_winit/Cargo.toml @@ -50,8 +50,10 @@ wasm-bindgen = { version = "0.2" } web-sys = "0.3" crossbeam-channel = "0.5" -[package.metadata.docs.rs] -all-features = true [lints] workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +all-features = true diff --git a/crates/bevy_winit/src/accessibility.rs b/crates/bevy_winit/src/accessibility.rs index 7836408677f61..1764f9cbaa828 100644 --- a/crates/bevy_winit/src/accessibility.rs +++ b/crates/bevy_winit/src/accessibility.rs @@ -8,7 +8,7 @@ use std::{ use accesskit_winit::Adapter; use bevy_a11y::{ accesskit::{ - ActionHandler, ActionRequest, NodeBuilder, NodeClassSet, NodeId, Role, TreeUpdate, + ActionHandler, ActionRequest, NodeBuilder, NodeClassSet, NodeId, Role, Tree, TreeUpdate, }, AccessibilityNode, AccessibilityRequested, AccessibilitySystem, Focus, }; @@ -44,6 +44,37 @@ impl ActionHandler for WinitActionHandler { } } +/// Prepares accessibility for a winit window. +pub(crate) fn prepare_accessibility_for_window( + winit_window: &winit::window::Window, + entity: Entity, + name: String, + accessibility_requested: AccessibilityRequested, + adapters: &mut AccessKitAdapters, + handlers: &mut WinitActionHandlers, +) { + let mut root_builder = NodeBuilder::new(Role::Window); + root_builder.set_name(name.into_boxed_str()); + let root = root_builder.build(&mut NodeClassSet::lock_global()); + + let accesskit_window_id = NodeId(entity.to_bits()); + let handler = WinitActionHandler::default(); + let adapter = Adapter::with_action_handler( + winit_window, + move || { + accessibility_requested.set(true); + TreeUpdate { + nodes: vec![(accesskit_window_id, root)], + tree: Some(Tree::new(accesskit_window_id)), + focus: accesskit_window_id, + } + }, + Box::new(handler.clone()), + ); + adapters.insert(entity, adapter); + handlers.insert(entity, handler); +} + fn window_closed( mut adapters: NonSendMut, mut receivers: ResMut, diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 9992178a78114..893937d5f2696 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -1,10 +1,16 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![forbid(unsafe_code)] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] + //! `bevy_winit` provides utilities to handle window creation and the eventloop through [`winit`] //! //! Most commonly, the [`WinitPlugin`] is used as part of //! [`DefaultPlugins`](https://docs.rs/bevy/latest/bevy/struct.DefaultPlugins.html). //! The app's [runner](bevy_app::App::runner) is set by `WinitPlugin` and handles the `winit` [`EventLoop`]. //! See `winit_runner` for details. -#![cfg_attr(docsrs, feature(doc_auto_cfg))] pub mod accessibility; mod converters; @@ -770,8 +776,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_config.rs b/crates/bevy_winit/src/winit_config.rs index f2cb424ec5f44..3400e86ba27da 100644 --- a/crates/bevy_winit/src/winit_config.rs +++ b/crates/bevy_winit/src/winit_config.rs @@ -2,7 +2,7 @@ use bevy_ecs::system::Resource; use bevy_utils::Duration; /// Settings for the [`WinitPlugin`](super::WinitPlugin). -#[derive(Debug, Resource)] +#[derive(Debug, Resource, Clone)] pub struct WinitSettings { /// Determines how frequently the application can update when it has focus. pub focused_mode: UpdateMode, diff --git a/crates/bevy_winit/src/winit_windows.rs b/crates/bevy_winit/src/winit_windows.rs index 4375e7c277744..af5d5d5017139 100644 --- a/crates/bevy_winit/src/winit_windows.rs +++ b/crates/bevy_winit/src/winit_windows.rs @@ -1,8 +1,4 @@ -use accesskit_winit::Adapter; -use bevy_a11y::{ - accesskit::{NodeBuilder, NodeClassSet, NodeId, Role, Tree, TreeUpdate}, - AccessibilityRequested, -}; +use bevy_a11y::AccessibilityRequested; use bevy_ecs::entity::Entity; use bevy_ecs::entity::EntityHashMap; @@ -15,7 +11,7 @@ use winit::{ }; use crate::{ - accessibility::{AccessKitAdapters, WinitActionHandler, WinitActionHandlers}, + accessibility::{prepare_accessibility_for_window, AccessKitAdapters, WinitActionHandlers}, converters::{convert_enabled_buttons, convert_window_level, convert_window_theme}, }; @@ -104,6 +100,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", @@ -206,28 +208,14 @@ impl WinitWindows { let winit_window = winit_window_builder.build(event_loop).unwrap(); let name = window.title.clone(); - - let mut root_builder = NodeBuilder::new(Role::Window); - root_builder.set_name(name.into_boxed_str()); - let root = root_builder.build(&mut NodeClassSet::lock_global()); - - let accesskit_window_id = NodeId(entity.to_bits()); - let handler = WinitActionHandler::default(); - let accessibility_requested = accessibility_requested.clone(); - let adapter = Adapter::with_action_handler( + prepare_accessibility_for_window( &winit_window, - move || { - accessibility_requested.set(true); - TreeUpdate { - nodes: vec![(accesskit_window_id, root)], - tree: Some(Tree::new(accesskit_window_id)), - focus: accesskit_window_id, - } - }, - Box::new(handler.clone()), + entity, + name, + accessibility_requested.clone(), + adapters, + handlers, ); - adapters.insert(entity, adapter); - handlers.insert(entity, handler); // Do not set the grab mode on window creation if it's none. It can fail on mobile. if window.cursor.grab_mode != CursorGrabMode::None { @@ -275,7 +263,7 @@ impl WinitWindows { /// This should mostly just be called when the window is closing. pub fn remove_window(&mut self, entity: Entity) -> Option { let winit_id = self.entity_to_winit.remove(&entity)?; - // Don't remove from `winit_to_window_id` so we know the window used to exist. + self.winit_to_entity.remove(&winit_id); self.windows.remove(&winit_id) } } diff --git a/docs/cargo_features.md b/docs/cargo_features.md index c5bf29bd20c84..ff4d5313efc3c 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -64,6 +64,8 @@ The default feature set enables most of the expected features of a game engine, |glam_assert|Enable assertions to check the validity of parameters passed to glam| |ios_simulator|Enable support for the ios_simulator by downgrading some rendering capabilities| |jpeg|JPEG image format support| +|meshlet|Enables the meshlet renderer for dense high-poly scenes (experimental)| +|meshlet_processor|Enables processing meshes into meshlet meshes for bevy_pbr| |minimp3|MP3 audio format support (through minimp3)| |mp3|MP3 audio format support| |pbr_transmission_textures|Enable support for transmission-related textures in the `StandardMaterial`, at the risk of blowing past the global, per-shader texture limit on older/lower-end GPUs| diff --git a/errors/Cargo.toml b/errors/Cargo.toml index 01fe856cc7ce5..d59a0b216a915 100644 --- a/errors/Cargo.toml +++ b/errors/Cargo.toml @@ -6,8 +6,13 @@ description = "Bevy's error codes" publish = false license = "MIT OR Apache-2.0" -[lints] -workspace = true [dependencies] bevy = { path = ".." } + +[lints] +workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +all-features = true 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/2d/mesh2d_manual.rs b/examples/2d/mesh2d_manual.rs index acb0d3b6dce85..25af819323cb7 100644 --- a/examples/2d/mesh2d_manual.rs +++ b/examples/2d/mesh2d_manual.rs @@ -8,16 +8,18 @@ use bevy::{ color::palettes::basic::YELLOW, core_pipeline::core_2d::Transparent2d, + math::FloatOrd, prelude::*, render::{ mesh::{Indices, MeshVertexAttribute}, render_asset::{RenderAssetUsages, RenderAssets}, - render_phase::{AddRenderCommand, DrawFunctions, RenderPhase, SetItemPipeline}, + render_phase::{AddRenderCommand, DrawFunctions, SetItemPipeline, SortedRenderPhase}, render_resource::{ BlendState, ColorTargetState, ColorWrites, Face, FragmentState, FrontFace, MultisampleState, PipelineCache, PolygonMode, PrimitiveState, PrimitiveTopology, - RenderPipelineDescriptor, SpecializedRenderPipeline, SpecializedRenderPipelines, - TextureFormat, VertexBufferLayout, VertexFormat, VertexState, VertexStepMode, + PushConstantRange, RenderPipelineDescriptor, ShaderStages, SpecializedRenderPipeline, + SpecializedRenderPipelines, TextureFormat, VertexBufferLayout, VertexFormat, + VertexState, VertexStepMode, }, texture::BevyDefault, view::{ExtractedView, ViewTarget, VisibleEntities}, @@ -28,7 +30,6 @@ use bevy::{ Mesh2dPipelineKey, Mesh2dTransforms, MeshFlags, RenderMesh2dInstance, RenderMesh2dInstances, SetMesh2dBindGroup, SetMesh2dViewBindGroup, }, - utils::FloatOrd, }; use std::f32::consts::PI; @@ -157,6 +158,18 @@ impl SpecializedRenderPipeline for ColoredMesh2dPipeline { false => TextureFormat::bevy_default(), }; + let mut push_constant_ranges = Vec::with_capacity(1); + if cfg!(all( + feature = "webgl2", + target_arch = "wasm32", + not(feature = "webgpu") + )) { + push_constant_ranges.push(PushConstantRange { + stages: ShaderStages::VERTEX, + range: 0..4, + }); + } + RenderPipelineDescriptor { vertex: VertexState { // Use our custom shader @@ -184,7 +197,7 @@ impl SpecializedRenderPipeline for ColoredMesh2dPipeline { // Bind group 1 is the mesh uniform self.mesh2d_pipeline.mesh_layout.clone(), ], - push_constant_ranges: Vec::new(), + push_constant_ranges, primitive: PrimitiveState { front_face: FrontFace::Ccw, cull_mode: Some(Face::Back), @@ -274,7 +287,7 @@ impl Plugin for ColoredMesh2dPlugin { // Load our custom shader let mut shaders = app.world.resource_mut::>(); shaders.insert( - COLORED_MESH2D_SHADER_HANDLE, + &COLORED_MESH2D_SHADER_HANDLE, Shader::from_wgsl(COLORED_MESH2D_SHADER, file!()), ); @@ -347,7 +360,7 @@ pub fn queue_colored_mesh2d( render_mesh_instances: Res, mut views: Query<( &VisibleEntities, - &mut RenderPhase, + &mut SortedRenderPhase, &ExtractedView, )>, ) { diff --git a/examples/2d/wireframe_2d.rs b/examples/2d/wireframe_2d.rs new file mode 100644 index 0000000000000..677b862073d91 --- /dev/null +++ b/examples/2d/wireframe_2d.rs @@ -0,0 +1,154 @@ +//! Showcases wireframe rendering for 2d meshes. +//! +//! Wireframes currently do not work when using webgl or webgpu. +//! Supported platforms: +//! - DX12 +//! - Vulkan +//! - Metal +//! +//! This is a native only feature. + +use bevy::{ + color::palettes::basic::{GREEN, RED, WHITE}, + prelude::*, + render::{ + render_resource::WgpuFeatures, + settings::{RenderCreation, WgpuSettings}, + RenderPlugin, + }, + sprite::{ + MaterialMesh2dBundle, NoWireframe2d, Wireframe2d, Wireframe2dColor, Wireframe2dConfig, + Wireframe2dPlugin, + }, +}; + +fn main() { + App::new() + .add_plugins(( + DefaultPlugins.set(RenderPlugin { + render_creation: RenderCreation::Automatic(WgpuSettings { + // WARN this is a native only feature. It will not work with webgl or webgpu + features: WgpuFeatures::POLYGON_MODE_LINE, + ..default() + }), + ..default() + }), + // You need to add this plugin to enable wireframe rendering + Wireframe2dPlugin, + )) + // Wireframes can be configured with this resource. This can be changed at runtime. + .insert_resource(Wireframe2dConfig { + // The global wireframe config enables drawing of wireframes on every mesh, + // except those with `NoWireframe2d`. Meshes with `Wireframe2d` will always have a wireframe, + // regardless of the global configuration. + global: true, + // Controls the default color of all wireframes. Used as the default color for global wireframes. + // Can be changed per mesh using the `Wireframe2dColor` component. + default_color: WHITE, + }) + .add_systems(Startup, setup) + .add_systems(Update, update_colors) + .run(); +} + +/// Set up a simple 3D scene +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + // Triangle: Never renders a wireframe + commands.spawn(( + MaterialMesh2dBundle { + mesh: meshes + .add(Triangle2d::new( + Vec2::new(0.0, 50.0), + Vec2::new(-50.0, -50.0), + Vec2::new(50.0, -50.0), + )) + .into(), + material: materials.add(Color::BLACK), + transform: Transform::from_xyz(-150.0, 0.0, 0.0), + ..default() + }, + NoWireframe2d, + )); + // Rectangle: Follows global wireframe setting + commands.spawn(MaterialMesh2dBundle { + mesh: meshes.add(Rectangle::new(100.0, 100.0)).into(), + material: materials.add(Color::BLACK), + transform: Transform::from_xyz(0.0, 0.0, 0.0), + ..default() + }); + // Circle: Always renders a wireframe + commands.spawn(( + MaterialMesh2dBundle { + mesh: meshes.add(Circle::new(50.0)).into(), + material: materials.add(Color::BLACK), + transform: Transform::from_xyz(150.0, 0.0, 0.0), + ..default() + }, + Wireframe2d, + // This lets you configure the wireframe color of this entity. + // If not set, this will use the color in `WireframeConfig` + Wireframe2dColor { color: GREEN }, + )); + + // Camera + commands.spawn(Camera2dBundle::default()); + + // Text used to show controls + commands.spawn( + TextBundle::from_section("", TextStyle::default()).with_style(Style { + position_type: PositionType::Absolute, + top: Val::Px(10.0), + left: Val::Px(10.0), + ..default() + }), + ); +} + +/// This system lets you toggle various wireframe settings +fn update_colors( + keyboard_input: Res>, + mut config: ResMut, + mut wireframe_colors: Query<&mut Wireframe2dColor>, + mut text: Query<&mut Text>, +) { + text.single_mut().sections[0].value = format!( + " +Controls +--------------- +Z - Toggle global +X - Change global color +C - Change color of the circle wireframe + +Wireframe2dConfig +------------- +Global: {} +Color: {:?} +", + config.global, config.default_color, + ); + + // Toggle showing a wireframe on all meshes + if keyboard_input.just_pressed(KeyCode::KeyZ) { + config.global = !config.global; + } + + // Toggle the global wireframe color + if keyboard_input.just_pressed(KeyCode::KeyX) { + config.default_color = if config.default_color == WHITE { + RED + } else { + WHITE + }; + } + + // Toggle the color of a wireframe using `Wireframe2dColor` and not the global color + if keyboard_input.just_pressed(KeyCode::KeyC) { + for mut color in &mut wireframe_colors { + color.color = if color.color == GREEN { RED } else { GREEN }; + } + } +} 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

) -> Self { Self { segments: value.segments.into_iter().map(Into::into).collect(), @@ -1275,6 +1271,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/float_ord.rs b/crates/bevy_math/src/float_ord.rs new file mode 100644 index 0000000000000..2c6bbd4325b68 --- /dev/null +++ b/crates/bevy_math/src/float_ord.rs @@ -0,0 +1,170 @@ +use std::{ + cmp::Ordering, + hash::{Hash, Hasher}, + ops::Neg, +}; + +/// A wrapper for floats that implements [`Ord`], [`Eq`], and [`Hash`] traits. +/// +/// This is a work around for the fact that the IEEE 754-2008 standard, +/// implemented by Rust's [`f32`] type, +/// doesn't define an ordering for [`NaN`](f32::NAN), +/// and `NaN` is not considered equal to any other `NaN`. +/// +/// Wrapping a float with `FloatOrd` breaks conformance with the standard +/// by sorting `NaN` as less than all other numbers and equal to any other `NaN`. +#[derive(Debug, Copy, Clone)] +pub struct FloatOrd(pub f32); + +impl PartialOrd for FloatOrd { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } + + fn lt(&self, other: &Self) -> bool { + !other.le(self) + } + // If `self` is NaN, it is equal to another NaN and less than all other floats, so return true. + // If `self` isn't NaN and `other` is, the float comparison returns false, which match the `FloatOrd` ordering. + // Otherwise, a standard float comparison happens. + fn le(&self, other: &Self) -> bool { + self.0.is_nan() || self.0 <= other.0 + } + fn gt(&self, other: &Self) -> bool { + !self.le(other) + } + fn ge(&self, other: &Self) -> bool { + other.le(self) + } +} + +impl Ord for FloatOrd { + #[allow(clippy::comparison_chain)] + fn cmp(&self, other: &Self) -> Ordering { + if self > other { + Ordering::Greater + } else if self < other { + Ordering::Less + } else { + Ordering::Equal + } + } +} + +impl PartialEq for FloatOrd { + fn eq(&self, other: &Self) -> bool { + if self.0.is_nan() { + other.0.is_nan() + } else { + self.0 == other.0 + } + } +} + +impl Eq for FloatOrd {} + +impl Hash for FloatOrd { + fn hash(&self, state: &mut H) { + if self.0.is_nan() { + // Ensure all NaN representations hash to the same value + state.write(&f32::to_ne_bytes(f32::NAN)); + } else if self.0 == 0.0 { + // Ensure both zeroes hash to the same value + state.write(&f32::to_ne_bytes(0.0f32)); + } else { + state.write(&f32::to_ne_bytes(self.0)); + } + } +} + +impl Neg for FloatOrd { + type Output = FloatOrd; + + fn neg(self) -> Self::Output { + FloatOrd(-self.0) + } +} + +#[cfg(test)] +mod tests { + use std::hash::DefaultHasher; + + use super::*; + + const NAN: FloatOrd = FloatOrd(f32::NAN); + const ZERO: FloatOrd = FloatOrd(0.0); + const ONE: FloatOrd = FloatOrd(1.0); + + #[test] + fn float_ord_eq() { + assert_eq!(NAN, NAN); + + assert_ne!(NAN, ZERO); + assert_ne!(ZERO, NAN); + + assert_eq!(ZERO, ZERO); + } + + #[test] + fn float_ord_cmp() { + assert_eq!(NAN.cmp(&NAN), Ordering::Equal); + + assert_eq!(NAN.cmp(&ZERO), Ordering::Less); + assert_eq!(ZERO.cmp(&NAN), Ordering::Greater); + + assert_eq!(ZERO.cmp(&ZERO), Ordering::Equal); + assert_eq!(ONE.cmp(&ZERO), Ordering::Greater); + assert_eq!(ZERO.cmp(&ONE), Ordering::Less); + } + + #[test] + #[allow(clippy::nonminimal_bool)] + fn float_ord_cmp_operators() { + assert!(!(NAN < NAN)); + assert!(NAN < ZERO); + assert!(!(ZERO < NAN)); + assert!(!(ZERO < ZERO)); + assert!(ZERO < ONE); + assert!(!(ONE < ZERO)); + + assert!(!(NAN > NAN)); + assert!(!(NAN > ZERO)); + assert!(ZERO > NAN); + assert!(!(ZERO > ZERO)); + assert!(!(ZERO > ONE)); + assert!(ONE > ZERO); + + assert!(NAN <= NAN); + assert!(NAN <= ZERO); + assert!(!(ZERO <= NAN)); + assert!(ZERO <= ZERO); + assert!(ZERO <= ONE); + assert!(!(ONE <= ZERO)); + + assert!(NAN >= NAN); + assert!(!(NAN >= ZERO)); + assert!(ZERO >= NAN); + assert!(ZERO >= ZERO); + assert!(!(ZERO >= ONE)); + assert!(ONE >= ZERO); + } + + #[test] + fn float_ord_hash() { + let hash = |num| { + let mut h = DefaultHasher::new(); + FloatOrd(num).hash(&mut h); + h.finish() + }; + + assert_ne!((-0.0f32).to_bits(), 0.0f32.to_bits()); + assert_eq!(hash(-0.0), hash(0.0)); + + let nan_1 = f32::from_bits(0b0111_1111_1000_0000_0000_0000_0000_0001); + assert!(nan_1.is_nan()); + let nan_2 = f32::from_bits(0b0111_1111_1000_0000_0000_0000_0000_0010); + assert!(nan_2.is_nan()); + assert_ne!(nan_1.to_bits(), nan_2.to_bits()); + assert_eq!(hash(nan_1), hash(nan_2)); + } +} diff --git a/crates/bevy_math/src/lib.rs b/crates/bevy_math/src/lib.rs index 604a299ab282c..3075d5be3da6e 100644 --- a/crates/bevy_math/src/lib.rs +++ b/crates/bevy_math/src/lib.rs @@ -1,29 +1,46 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![forbid(unsafe_code)] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] + //! Provides math types and functionality for the Bevy game engine. //! //! The commonly used types are vectors like [`Vec2`] and [`Vec3`], //! matrices like [`Mat2`], [`Mat3`] and [`Mat4`] and orientation representations //! like [`Quat`]. -#![cfg_attr(docsrs, feature(doc_auto_cfg))] mod affine3; mod aspect_ratio; pub mod bounding; +mod common_traits; pub mod cubic_splines; mod direction; +mod float_ord; pub mod primitives; mod ray; mod rects; mod rotation2d; +#[cfg(feature = "rand")] +mod shape_sampling; pub use affine3::*; pub use aspect_ratio::AspectRatio; +pub use common_traits::*; pub use direction::*; +pub use float_ord::*; 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/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index 5106e169f240f..4ae6932eb1c5e 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -125,6 +125,92 @@ impl Ellipse { } } +/// A primitive shape formed by the region between two circles, also known as a ring. +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[doc(alias = "Ring")] +pub struct Annulus { + /// The inner circle of the annulus + pub inner_circle: Circle, + /// The outer circle of the annulus + pub outer_circle: Circle, +} +impl Primitive2d for Annulus {} + +impl Default for Annulus { + /// Returns the default [`Annulus`] with radii of `0.5` and `1.0`. + fn default() -> Self { + Self { + inner_circle: Circle::new(0.5), + outer_circle: Circle::new(1.0), + } + } +} + +impl Annulus { + /// Create a new [`Annulus`] from the radii of the inner and outer circle + #[inline(always)] + pub const fn new(inner_radius: f32, outer_radius: f32) -> Self { + Self { + inner_circle: Circle::new(inner_radius), + outer_circle: Circle::new(outer_radius), + } + } + + /// Get the diameter of the annulus + #[inline(always)] + pub fn diameter(&self) -> f32 { + self.outer_circle.diameter() + } + + /// Get the thickness of the annulus + #[inline(always)] + pub fn thickness(&self) -> f32 { + self.outer_circle.radius - self.inner_circle.radius + } + + /// Get the area of the annulus + #[inline(always)] + pub fn area(&self) -> f32 { + PI * (self.outer_circle.radius.powi(2) - self.inner_circle.radius.powi(2)) + } + + /// Get the perimeter or circumference of the annulus, + /// which is the sum of the perimeters of the inner and outer circles. + #[inline(always)] + #[doc(alias = "circumference")] + pub fn perimeter(&self) -> f32 { + 2.0 * PI * (self.outer_circle.radius + self.inner_circle.radius) + } + + /// Finds the point on the annulus that is closest to the given `point`: + /// + /// - If the point is outside of the annulus completely, the returned point will be on the outer perimeter. + /// - If the point is inside of the inner circle (hole) of the annulus, the returned point will be on the inner perimeter. + /// - Otherwise, the returned point is overlapping the annulus and returned as is. + #[inline(always)] + pub fn closest_point(&self, point: Vec2) -> Vec2 { + let distance_squared = point.length_squared(); + + if self.inner_circle.radius.powi(2) <= distance_squared { + if distance_squared <= self.outer_circle.radius.powi(2) { + // The point is inside the annulus. + point + } else { + // The point is outside the annulus and closer to the outer perimeter. + // Find the closest point on the perimeter of the annulus. + let dir_to_point = point / distance_squared.sqrt(); + self.outer_circle.radius * dir_to_point + } + } else { + // The point is outside the annulus and closer to the inner perimeter. + // Find the closest point on the perimeter of the annulus. + let dir_to_point = point / distance_squared.sqrt(); + self.inner_circle.radius * dir_to_point + } + } +} + /// An unbounded plane in 2D space. It forms a separating surface through the origin, /// stretching infinitely far #[derive(Clone, Copy, Debug, PartialEq)] @@ -376,10 +462,10 @@ impl Triangle2d { } /// Reverse the [`WindingOrder`] of the triangle - /// by swapping the second and third vertices + /// by swapping the first and last vertices #[inline(always)] pub fn reverse(&mut self) { - self.vertices.swap(1, 2); + self.vertices.swap(0, 2); } } @@ -718,6 +804,20 @@ mod tests { ); } + #[test] + fn annulus_closest_point() { + let annulus = Annulus::new(1.5, 2.0); + assert_eq!(annulus.closest_point(Vec2::X * 10.0), Vec2::X * 2.0); + assert_eq!( + annulus.closest_point(Vec2::NEG_ONE), + Vec2::NEG_ONE.normalize() * 1.5 + ); + assert_eq!( + annulus.closest_point(Vec2::new(1.55, 0.85)), + Vec2::new(1.55, 0.85) + ); + } + #[test] fn circle_math() { let circle = Circle { radius: 3.0 }; @@ -726,6 +826,15 @@ mod tests { assert_eq!(circle.perimeter(), 18.849556, "incorrect perimeter"); } + #[test] + fn annulus_math() { + let annulus = Annulus::new(2.5, 3.5); + assert_eq!(annulus.diameter(), 7.0, "incorrect diameter"); + assert_eq!(annulus.thickness(), 1.0, "incorrect thickness"); + assert_eq!(annulus.area(), 18.849556, "incorrect area"); + assert_eq!(annulus.perimeter(), 37.699112, "incorrect perimeter"); + } + #[test] fn ellipse_math() { let ellipse = Ellipse::new(3.0, 1.0); @@ -753,9 +862,9 @@ mod tests { assert_eq!(cw_triangle.winding_order(), WindingOrder::Clockwise); let ccw_triangle = Triangle2d::new( - Vec2::new(0.0, 2.0), Vec2::new(-1.0, -1.0), Vec2::new(-0.5, -1.2), + Vec2::new(0.0, 2.0), ); assert_eq!(ccw_triangle.winding_order(), WindingOrder::CounterClockwise); diff --git a/crates/bevy_math/src/primitives/dim3.rs b/crates/bevy_math/src/primitives/dim3.rs index 88174c5052d53..a469fa7efb91c 100644 --- a/crates/bevy_math/src/primitives/dim3.rs +++ b/crates/bevy_math/src/primitives/dim3.rs @@ -1,7 +1,10 @@ use std::f32::consts::{FRAC_PI_3, PI}; use super::{Circle, Primitive3d}; -use crate::{Dir3, Vec3}; +use crate::{ + bounding::{Aabb3d, Bounded3d, BoundingSphere}, + Dir3, InvalidDirectionError, Quat, Vec3, +}; /// A sphere primitive #[derive(Clone, Copy, Debug, PartialEq)] @@ -629,6 +632,197 @@ impl Torus { } } +/// A 3D triangle primitive. +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +pub struct Triangle3d { + /// The vertices of the triangle. + pub vertices: [Vec3; 3], +} + +impl Primitive3d for Triangle3d {} + +impl Default for Triangle3d { + /// Returns the default [`Triangle3d`] with the vertices `[0.0, 0.5, 0.0]`, `[-0.5, -0.5, 0.0]`, and `[0.5, -0.5, 0.0]`. + fn default() -> Self { + Self { + vertices: [ + Vec3::new(0.0, 0.5, 0.0), + Vec3::new(-0.5, -0.5, 0.0), + Vec3::new(0.5, -0.5, 0.0), + ], + } + } +} + +impl Triangle3d { + /// Create a new [`Triangle3d`] from points `a`, `b`, and `c`. + #[inline(always)] + pub fn new(a: Vec3, b: Vec3, c: Vec3) -> Self { + Self { + vertices: [a, b, c], + } + } + + /// Get the area of the triangle. + #[inline(always)] + pub fn area(&self) -> f32 { + let [a, b, c] = self.vertices; + let ab = b - a; + let ac = c - a; + ab.cross(ac).length() / 2.0 + } + + /// Get the perimeter of the triangle. + #[inline(always)] + pub fn perimeter(&self) -> f32 { + let [a, b, c] = self.vertices; + a.distance(b) + b.distance(c) + c.distance(a) + } + + /// Get the normal of the triangle in the direction of the right-hand rule, assuming + /// the vertices are ordered in a counter-clockwise direction. + /// + /// The normal is computed as the cross product of the vectors `ab` and `ac`. + /// + /// # Errors + /// + /// Returns [`Err(InvalidDirectionError)`](InvalidDirectionError) if the length + /// of the given vector is zero (or very close to zero), infinite, or `NaN`. + #[inline(always)] + pub fn normal(&self) -> Result { + let [a, b, c] = self.vertices; + let ab = b - a; + let ac = c - a; + Dir3::new(ab.cross(ac)) + } + + /// Checks if the triangle is degenerate, meaning it has zero area. + /// + /// A triangle is degenerate if the cross product of the vectors `ab` and `ac` has a length less than `f32::EPSILON`. + /// This indicates that the three vertices are collinear or nearly collinear. + #[inline(always)] + pub fn is_degenerate(&self) -> bool { + let [a, b, c] = self.vertices; + let ab = b - a; + let ac = c - a; + ab.cross(ac).length() < 10e-7 + } + + /// Reverse the triangle by swapping the first and last vertices. + #[inline(always)] + pub fn reverse(&mut self) { + self.vertices.swap(0, 2); + } + + /// Get the centroid of the triangle. + /// + /// This function finds the geometric center of the triangle by averaging the vertices: + /// `centroid = (a + b + c) / 3`. + #[doc(alias("center", "barycenter", "baricenter"))] + #[inline(always)] + pub fn centroid(&self) -> Vec3 { + (self.vertices[0] + self.vertices[1] + self.vertices[2]) / 3.0 + } + + /// Get the largest side of the triangle. + /// + /// Returns the two points that form the largest side of the triangle. + #[inline(always)] + pub fn largest_side(&self) -> (Vec3, Vec3) { + let [a, b, c] = self.vertices; + let ab = b - a; + let bc = c - b; + let ca = a - c; + + let mut largest_side_points = (a, b); + let mut largest_side_length = ab.length(); + + if bc.length() > largest_side_length { + largest_side_points = (b, c); + largest_side_length = bc.length(); + } + + if ca.length() > largest_side_length { + largest_side_points = (a, c); + } + + largest_side_points + } + + /// Get the circumcenter of the triangle. + #[inline(always)] + pub fn circumcenter(&self) -> Vec3 { + if self.is_degenerate() { + // If the triangle is degenerate, the circumcenter is the midpoint of the largest side. + let (p1, p2) = self.largest_side(); + return (p1 + p2) / 2.0; + } + + let [a, b, c] = self.vertices; + let ab = b - a; + let ac = c - a; + let n = ab.cross(ac); + + // Reference: https://gamedev.stackexchange.com/questions/60630/how-do-i-find-the-circumcenter-of-a-triangle-in-3d + a + ((ac.length_squared() * n.cross(ab) + ab.length_squared() * ac.cross(ab).cross(ac)) + / (2.0 * n.length_squared())) + } +} + +impl Bounded3d for Triangle3d { + /// Get the bounding box of the triangle. + fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d { + let [a, b, c] = self.vertices; + + let a = rotation * a; + let b = rotation * b; + let c = rotation * c; + + let min = a.min(b).min(c); + let max = a.max(b).max(c); + + let bounding_center = (max + min) / 2.0 + translation; + let half_extents = (max - min) / 2.0; + + Aabb3d::new(bounding_center, half_extents) + } + + /// Get the bounding sphere of the triangle. + /// + /// The [`Triangle3d`] implements the minimal bounding sphere calculation. For acute triangles, the circumcenter is used as + /// the center of the sphere. For the others, the bounding sphere is the minimal sphere + /// that contains the largest side of the triangle. + fn bounding_sphere(&self, translation: Vec3, rotation: Quat) -> BoundingSphere { + if self.is_degenerate() { + let (p1, p2) = self.largest_side(); + let (segment, _) = Segment3d::from_points(p1, p2); + return segment.bounding_sphere(translation, rotation); + } + + let [a, b, c] = self.vertices; + + let side_opposite_to_non_acute = if (b - a).dot(c - a) <= 0.0 { + Some((b, c)) + } else if (c - b).dot(a - b) <= 0.0 { + Some((c, a)) + } else if (a - c).dot(b - c) <= 0.0 { + Some((a, b)) + } else { + None + }; + + if let Some((p1, p2)) = side_opposite_to_non_acute { + let (segment, _) = Segment3d::from_points(p1, p2); + segment.bounding_sphere(translation, rotation) + } else { + let circumcenter = self.circumcenter(); + let radius = circumcenter.distance(a); + BoundingSphere::new(circumcenter + translation, radius) + } + } +} + #[cfg(test)] mod tests { // Reference values were computed by hand and/or with external tools diff --git a/crates/bevy_math/src/shape_sampling.rs b/crates/bevy_math/src/shape_sampling.rs new file mode 100644 index 0000000000000..3728034413bef --- /dev/null +++ b/crates/bevy_math/src/shape_sampling.rs @@ -0,0 +1,418 @@ +use std::f32::consts::{PI, TAU}; + +use crate::{primitives::*, NormedVectorSpace, 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 + } + } +} + +/// Interior sampling for triangles which doesn't depend on the ambient dimension. +fn sample_triangle_interior( + vertices: [P; 3], + rng: &mut R, +) -> P { + let [a, b, c] = vertices; + let ab = b - a; + let ac = c - a; + + // Generate random points on a parallelipiped and reflect so that + // we can use the points that lie outside the triangle + let u = rng.gen_range(0.0..=1.0); + let v = rng.gen_range(0.0..=1.0); + + if u + v > 1. { + let u1 = 1. - v; + let v1 = 1. - u; + a + (ab * u1 + ac * v1) + } else { + a + (ab * u + ac * v) + } +} + +/// Boundary sampling for triangles which doesn't depend on the ambient dimension. +fn sample_triangle_boundary( + vertices: [P; 3], + rng: &mut R, +) -> P { + let [a, b, c] = vertices; + let ab = b - a; + let ac = c - a; + let bc = c - b; + + let t = rng.gen_range(0.0..=1.0); + + if let Ok(dist) = WeightedIndex::new([ab.norm(), ac.norm(), bc.norm()]) { + match dist.sample(rng) { + 0 => a.lerp(b, t), + 1 => a.lerp(c, t), + 2 => b.lerp(c, t), + _ => unreachable!(), + } + } else { + // This should only occur when the triangle is 0-dimensional degenerate + // so this is actually the correct result. + a + } +} + +impl ShapeSample for Triangle2d { + type Output = Vec2; + + fn sample_interior(&self, rng: &mut R) -> Self::Output { + sample_triangle_interior(self.vertices, rng) + } + + fn sample_boundary(&self, rng: &mut R) -> Self::Output { + sample_triangle_boundary(self.vertices, rng) + } +} + +impl ShapeSample for Triangle3d { + type Output = Vec3; + + fn sample_interior(&self, rng: &mut R) -> Self::Output { + sample_triangle_interior(self.vertices, rng) + } + + fn sample_boundary(&self, rng: &mut R) -> Self::Output { + sample_triangle_boundary(self.vertices, rng) + } +} + +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_mikktspace/Cargo.toml b/crates/bevy_mikktspace/Cargo.toml index 78f66e821a82e..9119e80a83ab6 100644 --- a/crates/bevy_mikktspace/Cargo.toml +++ b/crates/bevy_mikktspace/Cargo.toml @@ -22,3 +22,7 @@ name = "generate" [lints] workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +all-features = true diff --git a/crates/bevy_mikktspace/src/generated.rs b/crates/bevy_mikktspace/src/generated.rs index a69b1e8a27281..91c697b3299d9 100644 --- a/crates/bevy_mikktspace/src/generated.rs +++ b/crates/bevy_mikktspace/src/generated.rs @@ -41,7 +41,8 @@ non_upper_case_globals, unused_mut, unused_assignments, - unused_variables + unused_variables, + unsafe_code )] use std::ptr::null_mut; diff --git a/crates/bevy_mikktspace/src/lib.rs b/crates/bevy_mikktspace/src/lib.rs index cbf2c0c7ad833..d383f8ad5c447 100644 --- a/crates/bevy_mikktspace/src/lib.rs +++ b/crates/bevy_mikktspace/src/lib.rs @@ -6,6 +6,11 @@ )] // FIXME(3492): remove once docs are ready #![allow(missing_docs)] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] use glam::{Vec2, Vec3}; @@ -62,6 +67,7 @@ pub trait Geometry { /// /// Returns `false` if the geometry is unsuitable for tangent generation including, /// but not limited to, lack of vertices. +#[allow(unsafe_code)] pub fn generate_tangents(geometry: &mut I) -> bool { unsafe { generated::genTangSpace(geometry, 180.0) } } diff --git a/crates/bevy_pbr/Cargo.toml b/crates/bevy_pbr/Cargo.toml index b1f45d16ca673..6944aeb5d7c32 100644 --- a/crates/bevy_pbr/Cargo.toml +++ b/crates/bevy_pbr/Cargo.toml @@ -15,6 +15,10 @@ pbr_transmission_textures = [] shader_format_glsl = ["bevy_render/shader_format_glsl"] trace = ["bevy_render/trace"] ios_simulator = ["bevy_render/ios_simulator"] +# Enables the meshlet renderer for dense high-poly scenes (experimental) +meshlet = [] +# Enables processing meshes into meshlet meshes +meshlet_processor = ["dep:meshopt", "dep:thiserror"] [dependencies] # bevy @@ -34,16 +38,22 @@ bevy_window = { path = "../bevy_window", version = "0.14.0-dev" } bevy_derive = { path = "../bevy_derive", version = "0.14.0-dev" } # other +meshopt = { version = "0.2", optional = true } +thiserror = { version = "1", optional = true } bitflags = "2.3" -fixedbitset = "0.4" +fixedbitset = "0.5" # direct dependency required for derive macro bytemuck = { version = "1", features = ["derive"] } radsort = "0.1" smallvec = "1.6" +serde = { version = "1", features = ["derive", "rc"] } +bincode = "1" +range-alloc = "0.1" nonmax = "0.5" [lints] workspace = true [package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] all-features = true diff --git a/crates/bevy_pbr/src/deferred/pbr_deferred_functions.wgsl b/crates/bevy_pbr/src/deferred/pbr_deferred_functions.wgsl index d8d923ede88c2..c02e55a24f8ff 100644 --- a/crates/bevy_pbr/src/deferred/pbr_deferred_functions.wgsl +++ b/crates/bevy_pbr/src/deferred/pbr_deferred_functions.wgsl @@ -7,10 +7,16 @@ rgb9e5, mesh_view_bindings::view, utils::{octahedral_encode, octahedral_decode}, - prepass_io::{VertexOutput, FragmentOutput}, + prepass_io::FragmentOutput, view_transformations::{position_ndc_to_world, frag_coord_to_ndc}, } +#ifdef MESHLET_MESH_MATERIAL_PASS +#import bevy_pbr::meshlet_visibility_buffer_resolve::VertexOutput +#else +#import bevy_pbr::prepass_io::VertexOutput +#endif + #ifdef MOTION_VECTOR_PREPASS #import bevy_pbr::pbr_prepass_functions::calculate_motion_vector #endif @@ -116,7 +122,11 @@ fn deferred_output(in: VertexOutput, pbr_input: PbrInput) -> FragmentOutput { #endif // motion vectors if required #ifdef MOTION_VECTOR_PREPASS +#ifdef MESHLET_MESH_MATERIAL_PASS + out.motion_vector = in.motion_vector; +#else out.motion_vector = calculate_motion_vector(in.world_position, in.previous_world_position); +#endif #endif return out; diff --git a/crates/bevy_pbr/src/extended_material.rs b/crates/bevy_pbr/src/extended_material.rs index 5ad2e1a8eb42b..a5c46ea6ffa8d 100644 --- a/crates/bevy_pbr/src/extended_material.rs +++ b/crates/bevy_pbr/src/extended_material.rs @@ -67,6 +67,30 @@ pub trait MaterialExtension: Asset + AsBindGroup + Clone + Sized { ShaderRef::Default } + /// Returns this material's [`crate::meshlet::MeshletMesh`] fragment shader. If [`ShaderRef::Default`] is returned, + /// the default meshlet mesh fragment shader will be used. + #[allow(unused_variables)] + #[cfg(feature = "meshlet")] + fn meshlet_mesh_fragment_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this material's [`crate::meshlet::MeshletMesh`] prepass fragment shader. If [`ShaderRef::Default`] is returned, + /// the default meshlet mesh prepass fragment shader will be used. + #[allow(unused_variables)] + #[cfg(feature = "meshlet")] + fn meshlet_mesh_prepass_fragment_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this material's [`crate::meshlet::MeshletMesh`] deferred fragment shader. If [`ShaderRef::Default`] is returned, + /// the default meshlet mesh deferred fragment shader will be used. + #[allow(unused_variables)] + #[cfg(feature = "meshlet")] + fn meshlet_mesh_deferred_fragment_shader() -> ShaderRef { + ShaderRef::Default + } + /// Customizes the default [`RenderPipelineDescriptor`] for a specific entity using the entity's /// [`MaterialPipelineKey`] and [`MeshVertexBufferLayoutRef`] as input. /// Specialization for the base material is applied before this function is called. @@ -211,6 +235,30 @@ impl Material for ExtendedMaterial { } } + #[cfg(feature = "meshlet")] + fn meshlet_mesh_fragment_shader() -> ShaderRef { + match E::meshlet_mesh_fragment_shader() { + ShaderRef::Default => B::meshlet_mesh_fragment_shader(), + specified => specified, + } + } + + #[cfg(feature = "meshlet")] + fn meshlet_mesh_prepass_fragment_shader() -> ShaderRef { + match E::meshlet_mesh_prepass_fragment_shader() { + ShaderRef::Default => B::meshlet_mesh_prepass_fragment_shader(), + specified => specified, + } + } + + #[cfg(feature = "meshlet")] + fn meshlet_mesh_deferred_fragment_shader() -> ShaderRef { + match E::meshlet_mesh_deferred_fragment_shader() { + ShaderRef::Default => B::meshlet_mesh_deferred_fragment_shader(), + specified => specified, + } + } + fn specialize( pipeline: &MaterialPipeline, descriptor: &mut RenderPipelineDescriptor, diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index c969db7944c08..0a7f095b2782d 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -1,9 +1,26 @@ // FIXME(3492): remove once docs are ready #![allow(missing_docs)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![forbid(unsafe_code)] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] +#[cfg(feature = "meshlet")] +mod meshlet; pub mod wireframe; +/// Experimental features that are not yet finished. Please report any issues you encounter! +/// +/// Expect bugs, missing features, compatibility issues, low performance, and/or future breaking changes. +#[cfg(feature = "meshlet")] +pub mod experimental { + pub mod meshlet { + pub use crate::meshlet::*; + } +} + mod bundle; pub mod deferred; mod extended_material; @@ -77,7 +94,6 @@ use bevy_render::{ extract_resource::ExtractResourcePlugin, render_asset::prepare_assets, render_graph::RenderGraph, - render_phase::sort_phase_system, render_resource::Shader, texture::Image, view::VisibilitySystems, @@ -107,6 +123,8 @@ pub const PBR_PREPASS_FUNCTIONS_SHADER_HANDLE: Handle = pub const PBR_DEFERRED_TYPES_HANDLE: Handle = Handle::weak_from_u128(3221241127431430599); pub const PBR_DEFERRED_FUNCTIONS_HANDLE: Handle = Handle::weak_from_u128(72019026415438599); pub const RGB9E5_FUNCTIONS_HANDLE: Handle = Handle::weak_from_u128(2659010996143919192); +const MESHLET_VISIBILITY_BUFFER_RESOLVE_SHADER_HANDLE: Handle = + Handle::weak_from_u128(2325134235233421); /// Sets up the entire PBR infrastructure of bevy. pub struct PbrPlugin { @@ -232,6 +250,13 @@ impl Plugin for PbrPlugin { "render/view_transformations.wgsl", Shader::from_wgsl ); + // Setup dummy shaders for when MeshletPlugin is not used to prevent shader import errors. + load_internal_asset!( + app, + MESHLET_VISIBILITY_BUFFER_RESOLVE_SHADER_HANDLE, + "meshlet/dummy_visibility_buffer_resolve.wgsl", + Shader::from_wgsl + ); app.register_asset_reflect::() .register_type::() @@ -328,7 +353,7 @@ impl Plugin for PbrPlugin { } app.world.resource_mut::>().insert( - Handle::::default(), + &Handle::::default(), StandardMaterial { base_color: Color::srgb(1.0, 0.0, 0.5), unlit: true, @@ -349,7 +374,6 @@ impl Plugin for PbrPlugin { prepare_lights .in_set(RenderSet::ManageViews) .after(prepare_assets::), - sort_phase_system::.in_set(RenderSet::PhaseSort), prepare_clusters.in_set(RenderSet::PrepareResources), ), ) @@ -364,7 +388,7 @@ impl Plugin for PbrPlugin { render_app.ignore_ambiguity( bevy_render::Render, bevy_core_pipeline::core_3d::prepare_core_3d_transmission_textures, - bevy_render::batching::batch_and_prepare_render_phase::< + bevy_render::batching::batch_and_prepare_sorted_render_phase::< bevy_core_pipeline::core_3d::Transmissive3d, MeshPipeline, >, diff --git a/crates/bevy_pbr/src/light/directional_light.rs b/crates/bevy_pbr/src/light/directional_light.rs new file mode 100644 index 0000000000000..9c74971ca15a5 --- /dev/null +++ b/crates/bevy_pbr/src/light/directional_light.rs @@ -0,0 +1,83 @@ +use super::*; + +/// A Directional light. +/// +/// Directional lights don't exist in reality but they are a good +/// approximation for light sources VERY far away, like the sun or +/// the moon. +/// +/// The light shines along the forward direction of the entity's transform. With a default transform +/// this would be along the negative-Z axis. +/// +/// Valid values for `illuminance` are: +/// +/// | Illuminance (lux) | Surfaces illuminated by | +/// |-------------------|------------------------------------------------| +/// | 0.0001 | Moonless, overcast night sky (starlight) | +/// | 0.002 | Moonless clear night sky with airglow | +/// | 0.05–0.3 | Full moon on a clear night | +/// | 3.4 | Dark limit of civil twilight under a clear sky | +/// | 20–50 | Public areas with dark surroundings | +/// | 50 | Family living room lights | +/// | 80 | Office building hallway/toilet lighting | +/// | 100 | Very dark overcast day | +/// | 150 | Train station platforms | +/// | 320–500 | Office lighting | +/// | 400 | Sunrise or sunset on a clear day. | +/// | 1000 | Overcast day; typical TV studio lighting | +/// | 10,000–25,000 | Full daylight (not direct sun) | +/// | 32,000–100,000 | Direct sunlight | +/// +/// Source: [Wikipedia](https://en.wikipedia.org/wiki/Lux) +/// +/// ## Shadows +/// +/// To enable shadows, set the `shadows_enabled` property to `true`. +/// +/// Shadows are produced via [cascaded shadow maps](https://developer.download.nvidia.com/SDK/10.5/opengl/src/cascaded_shadow_maps/doc/cascaded_shadow_maps.pdf). +/// +/// To modify the cascade set up, such as the number of cascades or the maximum shadow distance, +/// change the [`CascadeShadowConfig`] component of the [`DirectionalLightBundle`]. +/// +/// To control the resolution of the shadow maps, use the [`DirectionalLightShadowMap`] resource: +/// +/// ``` +/// # use bevy_app::prelude::*; +/// # use bevy_pbr::DirectionalLightShadowMap; +/// App::new() +/// .insert_resource(DirectionalLightShadowMap { size: 2048 }); +/// ``` +#[derive(Component, Debug, Clone, Reflect)] +#[reflect(Component, Default)] +pub struct DirectionalLight { + pub color: Color, + /// Illuminance in lux (lumens per square meter), representing the amount of + /// light projected onto surfaces by this light source. Lux is used here + /// instead of lumens because a directional light illuminates all surfaces + /// more-or-less the same way (depending on the angle of incidence). Lumens + /// can only be specified for light sources which emit light from a specific + /// area. + pub illuminance: f32, + pub shadows_enabled: bool, + pub shadow_depth_bias: f32, + /// A bias applied along the direction of the fragment's surface normal. It is scaled to the + /// shadow map's texel size so that it is automatically adjusted to the orthographic projection. + pub shadow_normal_bias: f32, +} + +impl Default for DirectionalLight { + fn default() -> Self { + DirectionalLight { + color: Color::WHITE, + illuminance: light_consts::lux::AMBIENT_DAYLIGHT, + shadows_enabled: false, + shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS, + shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS, + } + } +} + +impl DirectionalLight { + pub const DEFAULT_SHADOW_DEPTH_BIAS: f32 = 0.02; + pub const DEFAULT_SHADOW_NORMAL_BIAS: f32 = 1.8; +} diff --git a/crates/bevy_pbr/src/light/mod.rs b/crates/bevy_pbr/src/light/mod.rs index 5d6be4fcf48d3..c73dcaf525b2d 100644 --- a/crates/bevy_pbr/src/light/mod.rs +++ b/crates/bevy_pbr/src/light/mod.rs @@ -22,6 +22,12 @@ use crate::*; mod ambient_light; pub use ambient_light::AmbientLight; +mod point_light; +pub use point_light::PointLight; +mod spot_light; +pub use spot_light::SpotLight; +mod directional_light; +pub use directional_light::DirectionalLight; /// Constants for operating with the light units: lumens, and lux. pub mod light_consts { @@ -80,61 +86,6 @@ pub mod light_consts { } } -/// A light that emits light in all directions from a central point. -/// -/// Real-world values for `intensity` (luminous power in lumens) based on the electrical power -/// consumption of the type of real-world light are: -/// -/// | Luminous Power (lumen) (i.e. the intensity member) | Incandescent non-halogen (Watts) | Incandescent halogen (Watts) | Compact fluorescent (Watts) | LED (Watts | -/// |------|-----|----|--------|-------| -/// | 200 | 25 | | 3-5 | 3 | -/// | 450 | 40 | 29 | 9-11 | 5-8 | -/// | 800 | 60 | | 13-15 | 8-12 | -/// | 1100 | 75 | 53 | 18-20 | 10-16 | -/// | 1600 | 100 | 72 | 24-28 | 14-17 | -/// | 2400 | 150 | | 30-52 | 24-30 | -/// | 3100 | 200 | | 49-75 | 32 | -/// | 4000 | 300 | | 75-100 | 40.5 | -/// -/// Source: [Wikipedia](https://en.wikipedia.org/wiki/Lumen_(unit)#Lighting) -#[derive(Component, Debug, Clone, Copy, Reflect)] -#[reflect(Component, Default)] -pub struct PointLight { - pub color: Color, - /// Luminous power in lumens, representing the amount of light emitted by this source in all directions. - pub intensity: f32, - pub range: f32, - pub radius: f32, - pub shadows_enabled: bool, - pub shadow_depth_bias: f32, - /// A bias applied along the direction of the fragment's surface normal. It is scaled to the - /// shadow map's texel size so that it can be small close to the camera and gets larger further - /// away. - pub shadow_normal_bias: f32, -} - -impl Default for PointLight { - fn default() -> Self { - PointLight { - color: Color::WHITE, - // 1,000,000 lumens is a very large "cinema light" capable of registering brightly at Bevy's - // default "very overcast day" exposure level. For "indoor lighting" with a lower exposure, - // this would be way too bright. - intensity: 1_000_000.0, - range: 20.0, - radius: 0.0, - shadows_enabled: false, - shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS, - shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS, - } - } -} - -impl PointLight { - pub const DEFAULT_SHADOW_DEPTH_BIAS: f32 = 0.02; - pub const DEFAULT_SHADOW_NORMAL_BIAS: f32 = 0.6; -} - #[derive(Resource, Clone, Debug, Reflect)] #[reflect(Resource)] pub struct PointLightShadowMap { @@ -147,144 +98,6 @@ impl Default for PointLightShadowMap { } } -/// A light that emits light in a given direction from a central point. -/// Behaves like a point light in a perfectly absorbent housing that -/// shines light only in a given direction. The direction is taken from -/// the transform, and can be specified with [`Transform::looking_at`](Transform::looking_at). -#[derive(Component, Debug, Clone, Copy, Reflect)] -#[reflect(Component, Default)] -pub struct SpotLight { - pub color: Color, - /// Luminous power in lumens, representing the amount of light emitted by this source in all directions. - pub intensity: f32, - pub range: f32, - pub radius: f32, - pub shadows_enabled: bool, - pub shadow_depth_bias: f32, - /// A bias applied along the direction of the fragment's surface normal. It is scaled to the - /// shadow map's texel size so that it can be small close to the camera and gets larger further - /// away. - pub shadow_normal_bias: f32, - /// Angle defining the distance from the spot light direction to the outer limit - /// of the light's cone of effect. - /// `outer_angle` should be < `PI / 2.0`. - /// `PI / 2.0` defines a hemispherical spot light, but shadows become very blocky as the angle - /// approaches this limit. - pub outer_angle: f32, - /// Angle defining the distance from the spot light direction to the inner limit - /// of the light's cone of effect. - /// Light is attenuated from `inner_angle` to `outer_angle` to give a smooth falloff. - /// `inner_angle` should be <= `outer_angle` - pub inner_angle: f32, -} - -impl SpotLight { - pub const DEFAULT_SHADOW_DEPTH_BIAS: f32 = 0.02; - pub const DEFAULT_SHADOW_NORMAL_BIAS: f32 = 1.8; -} - -impl Default for SpotLight { - fn default() -> Self { - // a quarter arc attenuating from the center - Self { - color: Color::WHITE, - // 1,000,000 lumens is a very large "cinema light" capable of registering brightly at Bevy's - // default "very overcast day" exposure level. For "indoor lighting" with a lower exposure, - // this would be way too bright. - intensity: 1_000_000.0, - range: 20.0, - radius: 0.0, - shadows_enabled: false, - shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS, - shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS, - inner_angle: 0.0, - outer_angle: std::f32::consts::FRAC_PI_4, - } - } -} - -/// A Directional light. -/// -/// Directional lights don't exist in reality but they are a good -/// approximation for light sources VERY far away, like the sun or -/// the moon. -/// -/// The light shines along the forward direction of the entity's transform. With a default transform -/// this would be along the negative-Z axis. -/// -/// Valid values for `illuminance` are: -/// -/// | Illuminance (lux) | Surfaces illuminated by | -/// |-------------------|------------------------------------------------| -/// | 0.0001 | Moonless, overcast night sky (starlight) | -/// | 0.002 | Moonless clear night sky with airglow | -/// | 0.05–0.3 | Full moon on a clear night | -/// | 3.4 | Dark limit of civil twilight under a clear sky | -/// | 20–50 | Public areas with dark surroundings | -/// | 50 | Family living room lights | -/// | 80 | Office building hallway/toilet lighting | -/// | 100 | Very dark overcast day | -/// | 150 | Train station platforms | -/// | 320–500 | Office lighting | -/// | 400 | Sunrise or sunset on a clear day. | -/// | 1000 | Overcast day; typical TV studio lighting | -/// | 10,000–25,000 | Full daylight (not direct sun) | -/// | 32,000–100,000 | Direct sunlight | -/// -/// Source: [Wikipedia](https://en.wikipedia.org/wiki/Lux) -/// -/// ## Shadows -/// -/// To enable shadows, set the `shadows_enabled` property to `true`. -/// -/// Shadows are produced via [cascaded shadow maps](https://developer.download.nvidia.com/SDK/10.5/opengl/src/cascaded_shadow_maps/doc/cascaded_shadow_maps.pdf). -/// -/// To modify the cascade set up, such as the number of cascades or the maximum shadow distance, -/// change the [`CascadeShadowConfig`] component of the [`DirectionalLightBundle`]. -/// -/// To control the resolution of the shadow maps, use the [`DirectionalLightShadowMap`] resource: -/// -/// ``` -/// # use bevy_app::prelude::*; -/// # use bevy_pbr::DirectionalLightShadowMap; -/// App::new() -/// .insert_resource(DirectionalLightShadowMap { size: 2048 }); -/// ``` -#[derive(Component, Debug, Clone, Reflect)] -#[reflect(Component, Default)] -pub struct DirectionalLight { - pub color: Color, - /// Illuminance in lux (lumens per square meter), representing the amount of - /// light projected onto surfaces by this light source. Lux is used here - /// instead of lumens because a directional light illuminates all surfaces - /// more-or-less the same way (depending on the angle of incidence). Lumens - /// can only be specified for light sources which emit light from a specific - /// area. - pub illuminance: f32, - pub shadows_enabled: bool, - pub shadow_depth_bias: f32, - /// A bias applied along the direction of the fragment's surface normal. It is scaled to the - /// shadow map's texel size so that it is automatically adjusted to the orthographic projection. - pub shadow_normal_bias: f32, -} - -impl Default for DirectionalLight { - fn default() -> Self { - DirectionalLight { - color: Color::WHITE, - illuminance: light_consts::lux::AMBIENT_DAYLIGHT, - shadows_enabled: false, - shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS, - shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS, - } - } -} - -impl DirectionalLight { - pub const DEFAULT_SHADOW_DEPTH_BIAS: f32 = 0.02; - pub const DEFAULT_SHADOW_NORMAL_BIAS: f32 = 1.8; -} - /// Controls the resolution of [`DirectionalLight`] shadow maps. #[derive(Resource, Clone, Debug, Reflect)] #[reflect(Resource)] diff --git a/crates/bevy_pbr/src/light/point_light.rs b/crates/bevy_pbr/src/light/point_light.rs new file mode 100644 index 0000000000000..6ff5d39e4e8f1 --- /dev/null +++ b/crates/bevy_pbr/src/light/point_light.rs @@ -0,0 +1,56 @@ +use super::*; + +/// A light that emits light in all directions from a central point. +/// +/// Real-world values for `intensity` (luminous power in lumens) based on the electrical power +/// consumption of the type of real-world light are: +/// +/// | Luminous Power (lumen) (i.e. the intensity member) | Incandescent non-halogen (Watts) | Incandescent halogen (Watts) | Compact fluorescent (Watts) | LED (Watts | +/// |------|-----|----|--------|-------| +/// | 200 | 25 | | 3-5 | 3 | +/// | 450 | 40 | 29 | 9-11 | 5-8 | +/// | 800 | 60 | | 13-15 | 8-12 | +/// | 1100 | 75 | 53 | 18-20 | 10-16 | +/// | 1600 | 100 | 72 | 24-28 | 14-17 | +/// | 2400 | 150 | | 30-52 | 24-30 | +/// | 3100 | 200 | | 49-75 | 32 | +/// | 4000 | 300 | | 75-100 | 40.5 | +/// +/// Source: [Wikipedia](https://en.wikipedia.org/wiki/Lumen_(unit)#Lighting) +#[derive(Component, Debug, Clone, Copy, Reflect)] +#[reflect(Component, Default)] +pub struct PointLight { + pub color: Color, + /// Luminous power in lumens, representing the amount of light emitted by this source in all directions. + pub intensity: f32, + pub range: f32, + pub radius: f32, + pub shadows_enabled: bool, + pub shadow_depth_bias: f32, + /// A bias applied along the direction of the fragment's surface normal. It is scaled to the + /// shadow map's texel size so that it can be small close to the camera and gets larger further + /// away. + pub shadow_normal_bias: f32, +} + +impl Default for PointLight { + fn default() -> Self { + PointLight { + color: Color::WHITE, + // 1,000,000 lumens is a very large "cinema light" capable of registering brightly at Bevy's + // default "very overcast day" exposure level. For "indoor lighting" with a lower exposure, + // this would be way too bright. + intensity: 1_000_000.0, + range: 20.0, + radius: 0.0, + shadows_enabled: false, + shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS, + shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS, + } + } +} + +impl PointLight { + pub const DEFAULT_SHADOW_DEPTH_BIAS: f32 = 0.02; + pub const DEFAULT_SHADOW_NORMAL_BIAS: f32 = 0.6; +} diff --git a/crates/bevy_pbr/src/light/spot_light.rs b/crates/bevy_pbr/src/light/spot_light.rs new file mode 100644 index 0000000000000..ab34196ff03fb --- /dev/null +++ b/crates/bevy_pbr/src/light/spot_light.rs @@ -0,0 +1,57 @@ +use super::*; + +/// A light that emits light in a given direction from a central point. +/// Behaves like a point light in a perfectly absorbent housing that +/// shines light only in a given direction. The direction is taken from +/// the transform, and can be specified with [`Transform::looking_at`](Transform::looking_at). +#[derive(Component, Debug, Clone, Copy, Reflect)] +#[reflect(Component, Default)] +pub struct SpotLight { + pub color: Color, + /// Luminous power in lumens, representing the amount of light emitted by this source in all directions. + pub intensity: f32, + pub range: f32, + pub radius: f32, + pub shadows_enabled: bool, + pub shadow_depth_bias: f32, + /// A bias applied along the direction of the fragment's surface normal. It is scaled to the + /// shadow map's texel size so that it can be small close to the camera and gets larger further + /// away. + pub shadow_normal_bias: f32, + /// Angle defining the distance from the spot light direction to the outer limit + /// of the light's cone of effect. + /// `outer_angle` should be < `PI / 2.0`. + /// `PI / 2.0` defines a hemispherical spot light, but shadows become very blocky as the angle + /// approaches this limit. + pub outer_angle: f32, + /// Angle defining the distance from the spot light direction to the inner limit + /// of the light's cone of effect. + /// Light is attenuated from `inner_angle` to `outer_angle` to give a smooth falloff. + /// `inner_angle` should be <= `outer_angle` + pub inner_angle: f32, +} + +impl SpotLight { + pub const DEFAULT_SHADOW_DEPTH_BIAS: f32 = 0.02; + pub const DEFAULT_SHADOW_NORMAL_BIAS: f32 = 1.8; +} + +impl Default for SpotLight { + fn default() -> Self { + // a quarter arc attenuating from the center + Self { + color: Color::WHITE, + // 1,000,000 lumens is a very large "cinema light" capable of registering brightly at Bevy's + // default "very overcast day" exposure level. For "indoor lighting" with a lower exposure, + // this would be way too bright. + intensity: 1_000_000.0, + range: 20.0, + radius: 0.0, + shadows_enabled: false, + shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS, + shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS, + inner_angle: 0.0, + outer_angle: std::f32::consts::FRAC_PI_4, + } + } +} diff --git a/crates/bevy_pbr/src/light_probe/mod.rs b/crates/bevy_pbr/src/light_probe/mod.rs index 4cb9325a410dd..4d681f19ca9d2 100644 --- a/crates/bevy_pbr/src/light_probe/mod.rs +++ b/crates/bevy_pbr/src/light_probe/mod.rs @@ -12,7 +12,7 @@ use bevy_ecs::{ schedule::IntoSystemConfigs, system::{Commands, Local, Query, Res, ResMut, Resource}, }; -use bevy_math::{Affine3A, Mat4, Vec3A, Vec4}; +use bevy_math::{Affine3A, FloatOrd, Mat4, Vec3A, Vec4}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ extract_instances::ExtractInstancesPlugin, @@ -26,7 +26,7 @@ use bevy_render::{ Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; use bevy_transform::prelude::GlobalTransform; -use bevy_utils::{tracing::error, FloatOrd, HashMap}; +use bevy_utils::{tracing::error, HashMap}; use std::hash::Hash; use std::ops::Deref; diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index 59d0e7b67bea7..774aff0745dfa 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -1,11 +1,18 @@ +#[cfg(feature = "meshlet")] +use crate::meshlet::{ + prepare_material_meshlet_meshes_main_opaque_pass, queue_material_meshlet_meshes, + MeshletGpuScene, +}; use crate::*; use bevy_asset::{Asset, AssetEvent, AssetId, AssetServer}; use bevy_core_pipeline::{ core_3d::{ - AlphaMask3d, Camera3d, Opaque3d, ScreenSpaceTransmissionQuality, Transmissive3d, - Transparent3d, + AlphaMask3d, Camera3d, Opaque3d, Opaque3dBinKey, ScreenSpaceTransmissionQuality, + Transmissive3d, Transparent3d, + }, + prepass::{ + DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass, OpaqueNoLightmap3dBinKey, }, - prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass}, tonemapping::{DebandDither, Tonemapping}, }; use bevy_derive::{Deref, DerefMut}; @@ -170,6 +177,36 @@ pub trait Material: Asset + AsBindGroup + Clone + Sized { ShaderRef::Default } + /// Returns this material's [`crate::meshlet::MeshletMesh`] fragment shader. If [`ShaderRef::Default`] is returned, + /// the default meshlet mesh fragment shader will be used. + /// + /// This is part of an experimental feature, and is unnecessary to implement unless you are using `MeshletMesh`'s. + #[allow(unused_variables)] + #[cfg(feature = "meshlet")] + fn meshlet_mesh_fragment_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this material's [`crate::meshlet::MeshletMesh`] prepass fragment shader. If [`ShaderRef::Default`] is returned, + /// the default meshlet mesh prepass fragment shader will be used. + /// + /// This is part of an experimental feature, and is unnecessary to implement unless you are using `MeshletMesh`'s. + #[allow(unused_variables)] + #[cfg(feature = "meshlet")] + fn meshlet_mesh_prepass_fragment_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this material's [`crate::meshlet::MeshletMesh`] deferred fragment shader. If [`ShaderRef::Default`] is returned, + /// the default meshlet mesh deferred fragment shader will be used. + /// + /// This is part of an experimental feature, and is unnecessary to implement unless you are using `MeshletMesh`'s. + #[allow(unused_variables)] + #[cfg(feature = "meshlet")] + fn meshlet_mesh_deferred_fragment_shader() -> ShaderRef { + ShaderRef::Default + } + /// Customizes the default [`RenderPipelineDescriptor`] for a specific entity using the entity's /// [`MaterialPipelineKey`] and [`MeshVertexBufferLayoutRef`] as input. #[allow(unused_variables)] @@ -193,6 +230,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 +239,7 @@ impl Default for MaterialPlugin { fn default() -> Self { Self { prepass_enabled: true, + shadows_enabled: true, _marker: Default::default(), } } @@ -231,18 +271,38 @@ 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::),), + ); + } + + #[cfg(feature = "meshlet")] + render_app.add_systems( + Render, + ( + prepare_material_meshlet_meshes_main_opaque_pass::, + queue_material_meshlet_meshes::, + ) + .chain() + .in_set(RenderSet::Queue) + .run_if(resource_exists::), + ); } - // 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()); @@ -483,10 +543,10 @@ pub fn queue_material_meshes( Option<&Camera3d>, Has, Option<&Projection>, - &mut RenderPhase, - &mut RenderPhase, - &mut RenderPhase, - &mut RenderPhase, + &mut BinnedRenderPhase, + &mut BinnedRenderPhase, + &mut SortedRenderPhase, + &mut SortedRenderPhase, ( Has>, Has>, @@ -621,10 +681,11 @@ pub fn queue_material_meshes( mesh_key |= alpha_mode_pipeline_key(material.properties.alpha_mode); - if render_lightmaps + let lightmap_image = render_lightmaps .render_lightmaps - .contains_key(visible_entity) - { + .get(visible_entity) + .map(|lightmap| lightmap.image); + if lightmap_image.is_some() { mesh_key |= MeshPipelineKey::LIGHTMAPPED; } @@ -664,14 +725,14 @@ pub fn queue_material_meshes( dynamic_offset: None, }); } else if forward { - opaque_phase.add(Opaque3d { - entity: *visible_entity, + let bin_key = Opaque3dBinKey { draw_function: draw_opaque_pbr, pipeline: pipeline_id, asset_id: mesh_instance.mesh_asset_id, - batch_range: 0..1, - dynamic_offset: None, - }); + material_bind_group_id: material.get_bind_group_id().0, + lightmap_image, + }; + opaque_phase.add(bin_key, *visible_entity, mesh_instance.should_batch()); } } AlphaMode::Mask(_) => { @@ -688,14 +749,17 @@ pub fn queue_material_meshes( dynamic_offset: None, }); } else if forward { - alpha_mask_phase.add(AlphaMask3d { - entity: *visible_entity, + let bin_key = OpaqueNoLightmap3dBinKey { draw_function: draw_alpha_mask_pbr, pipeline: pipeline_id, asset_id: mesh_instance.mesh_asset_id, - batch_range: 0..1, - dynamic_offset: None, - }); + material_bind_group_id: material.get_bind_group_id().0, + }; + alpha_mask_phase.add( + bin_key, + *visible_entity, + mesh_instance.should_batch(), + ); } } AlphaMode::Blend diff --git a/crates/bevy_pbr/src/meshlet/asset.rs b/crates/bevy_pbr/src/meshlet/asset.rs new file mode 100644 index 0000000000000..b0c0cd89f19d6 --- /dev/null +++ b/crates/bevy_pbr/src/meshlet/asset.rs @@ -0,0 +1,102 @@ +use bevy_asset::{ + io::{Reader, Writer}, + saver::{AssetSaver, SavedAsset}, + Asset, AssetLoader, AsyncReadExt, AsyncWriteExt, LoadContext, +}; +use bevy_math::Vec3; +use bevy_reflect::TypePath; +use bytemuck::{Pod, Zeroable}; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; + +/// A mesh that has been pre-processed into multiple small clusters of triangles called meshlets. +/// +/// A [`bevy_render::mesh::Mesh`] can be converted to a [`MeshletMesh`] using `MeshletMesh::from_mesh` when the `meshlet_processor` cargo feature is enabled. +/// The conversion step is very slow, and is meant to be ran once ahead of time, and not during runtime. This type of mesh is not suitable for +/// dynamically generated geometry. +/// +/// There are restrictions on the [`crate::Material`] functionality that can be used with this type of mesh. +/// * Materials have no control over the vertex shader or vertex attributes. +/// * Materials must be opaque. Transparent, alpha masked, and transmissive materials are not supported. +/// * Materials must use the [`crate::Material::meshlet_mesh_fragment_shader`] method (and similar variants for prepass/deferred shaders) +/// which requires certain shader patterns that differ from the regular material shaders. +/// * Limited control over [`bevy_render::render_resource::RenderPipelineDescriptor`] attributes. +/// +/// See also [`super::MaterialMeshletMeshBundle`] and [`super::MeshletPlugin`]. +#[derive(Asset, TypePath, Serialize, Deserialize, Clone)] +pub struct MeshletMesh { + /// The total amount of triangles summed across all meshlets in the mesh. + pub total_meshlet_triangles: u64, + /// Raw vertex data bytes for the overall mesh. + pub vertex_data: Arc<[u8]>, + /// Indices into `vertex_data`. + pub vertex_ids: Arc<[u32]>, + /// Indices into `vertex_ids`. + pub indices: Arc<[u8]>, + /// The list of meshlets making up this mesh. + pub meshlets: Arc<[Meshlet]>, + /// A list of spherical bounding volumes, 1 per meshlet. + pub meshlet_bounding_spheres: Arc<[MeshletBoundingSphere]>, +} + +/// A single meshlet within a [`MeshletMesh`]. +#[derive(Serialize, Deserialize, Copy, Clone, Pod, Zeroable)] +#[repr(C)] +pub struct Meshlet { + /// The offset within the parent mesh's [`MeshletMesh::vertex_ids`] buffer where the indices for this meshlet begin. + pub start_vertex_id: u32, + /// The offset within the parent mesh's [`MeshletMesh::indices`] buffer where the indices for this meshlet begin. + pub start_index_id: u32, + /// The amount of triangles in this meshlet. + pub triangle_count: u32, +} + +/// A spherical bounding volume used for culling a [`Meshlet`]. +#[derive(Serialize, Deserialize, Copy, Clone, Pod, Zeroable)] +#[repr(C)] +pub struct MeshletBoundingSphere { + pub center: Vec3, + pub radius: f32, +} + +/// An [`AssetLoader`] and [`AssetSaver`] for `.meshlet_mesh` [`MeshletMesh`] assets. +pub struct MeshletMeshSaverLoad; + +impl AssetLoader for MeshletMeshSaverLoad { + type Asset = MeshletMesh; + type Settings = (); + type Error = bincode::Error; + + async fn load<'a>( + &'a self, + reader: &'a mut Reader<'_>, + _settings: &'a Self::Settings, + _load_context: &'a mut LoadContext<'_>, + ) -> Result { + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).await?; + bincode::deserialize(&bytes) + } + + fn extensions(&self) -> &[&str] { + &["meshlet_mesh"] + } +} + +impl AssetSaver for MeshletMeshSaverLoad { + type Asset = MeshletMesh; + type Settings = (); + type OutputLoader = Self; + type Error = bincode::Error; + + async fn save<'a>( + &'a self, + writer: &'a mut Writer, + asset: SavedAsset<'a, Self::Asset>, + _settings: &'a Self::Settings, + ) -> Result<(), Self::Error> { + let bytes = bincode::serialize(asset.get())?; + writer.write_all(&bytes).await?; + Ok(()) + } +} diff --git a/crates/bevy_pbr/src/meshlet/copy_material_depth.wgsl b/crates/bevy_pbr/src/meshlet/copy_material_depth.wgsl new file mode 100644 index 0000000000000..177cbc35a3424 --- /dev/null +++ b/crates/bevy_pbr/src/meshlet/copy_material_depth.wgsl @@ -0,0 +1,10 @@ +#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput + +@group(0) @binding(0) var material_depth: texture_2d; + +/// This pass copies the R16Uint material depth texture to an actual Depth16Unorm depth texture. + +@fragment +fn copy_material_depth(in: FullscreenVertexOutput) -> @builtin(frag_depth) f32 { + return f32(textureLoad(material_depth, vec2(in.position.xy), 0).r) / 65535.0; +} diff --git a/crates/bevy_pbr/src/meshlet/cull_meshlets.wgsl b/crates/bevy_pbr/src/meshlet/cull_meshlets.wgsl new file mode 100644 index 0000000000000..015ed6ee11ff3 --- /dev/null +++ b/crates/bevy_pbr/src/meshlet/cull_meshlets.wgsl @@ -0,0 +1,118 @@ +#import bevy_pbr::meshlet_bindings::{ + meshlet_thread_meshlet_ids, + meshlet_bounding_spheres, + meshlet_thread_instance_ids, + meshlet_instance_uniforms, + meshlet_occlusion, + view, + should_cull_instance, + get_meshlet_previous_occlusion, +} +#ifdef MESHLET_SECOND_CULLING_PASS +#import bevy_pbr::meshlet_bindings::depth_pyramid +#endif +#import bevy_render::maths::affine3_to_square + +/// Culls individual clusters (1 per thread) in two passes (two pass occlusion culling), and outputs a bitmask of which clusters survived. +/// 1. The first pass is only frustum culling, on only the clusters that were visible last frame. +/// 2. The second pass performs both frustum and occlusion culling (using the depth buffer generated from the first pass), on all clusters. + +@compute +@workgroup_size(128, 1, 1) // 128 threads per workgroup, 1 instanced meshlet per thread +fn cull_meshlets(@builtin(global_invocation_id) cluster_id: vec3) { + // Fetch the instanced meshlet data + if cluster_id.x >= arrayLength(&meshlet_thread_meshlet_ids) { return; } + let instance_id = meshlet_thread_instance_ids[cluster_id.x]; + if should_cull_instance(instance_id) { + return; + } + let meshlet_id = meshlet_thread_meshlet_ids[cluster_id.x]; + let bounding_sphere = meshlet_bounding_spheres[meshlet_id]; + let instance_uniform = meshlet_instance_uniforms[instance_id]; + let model = affine3_to_square(instance_uniform.model); + let model_scale = max(length(model[0]), max(length(model[1]), length(model[2]))); + let bounding_sphere_center = model * vec4(bounding_sphere.center, 1.0); + let bounding_sphere_radius = model_scale * bounding_sphere.radius; + + // In the first pass, operate only on the clusters visible last frame. In the second pass, operate on all clusters. +#ifdef MESHLET_SECOND_CULLING_PASS + var meshlet_visible = true; +#else + var meshlet_visible = get_meshlet_previous_occlusion(cluster_id.x); + if !meshlet_visible { return; } +#endif + + // Frustum culling + // TODO: Faster method from https://vkguide.dev/docs/gpudriven/compute_culling/#frustum-culling-function + for (var i = 0u; i < 6u; i++) { + if !meshlet_visible { break; } + meshlet_visible &= dot(view.frustum[i], bounding_sphere_center) > -bounding_sphere_radius; + } + +#ifdef MESHLET_SECOND_CULLING_PASS + // In the second culling pass, cull against the depth pyramid generated from the first pass + if meshlet_visible { + let bounding_sphere_center_view_space = (view.inverse_view * vec4(bounding_sphere_center.xyz, 1.0)).xyz; + let aabb = project_view_space_sphere_to_screen_space_aabb(bounding_sphere_center_view_space, bounding_sphere_radius); + + // Halve the AABB size because the first depth mip resampling pass cut the full screen resolution into a power of two conservatively + let depth_pyramid_size_mip_0 = vec2(textureDimensions(depth_pyramid, 0)) * 0.5; + let width = (aabb.z - aabb.x) * depth_pyramid_size_mip_0.x; + let height = (aabb.w - aabb.y) * depth_pyramid_size_mip_0.y; + let depth_level = max(0, i32(ceil(log2(max(width, height))))); // TODO: Naga doesn't like this being a u32 + let depth_pyramid_size = vec2(textureDimensions(depth_pyramid, depth_level)); + let aabb_top_left = vec2(aabb.xy * depth_pyramid_size); + + let depth_quad_a = textureLoad(depth_pyramid, aabb_top_left, depth_level).x; + let depth_quad_b = textureLoad(depth_pyramid, aabb_top_left + vec2(1u, 0u), depth_level).x; + let depth_quad_c = textureLoad(depth_pyramid, aabb_top_left + vec2(0u, 1u), depth_level).x; + let depth_quad_d = textureLoad(depth_pyramid, aabb_top_left + vec2(1u, 1u), depth_level).x; + + let occluder_depth = min(min(depth_quad_a, depth_quad_b), min(depth_quad_c, depth_quad_d)); + if view.projection[3][3] == 1.0 { + // Orthographic + let sphere_depth = view.projection[3][2] + (bounding_sphere_center_view_space.z + bounding_sphere_radius) * view.projection[2][2]; + meshlet_visible &= sphere_depth >= occluder_depth; + } else { + // Perspective + let sphere_depth = -view.projection[3][2] / (bounding_sphere_center_view_space.z + bounding_sphere_radius); + meshlet_visible &= sphere_depth >= occluder_depth; + } + } +#endif + + // Write the bitmask of whether or not the cluster was culled + let occlusion_bit = u32(meshlet_visible) << (cluster_id.x % 32u); + atomicOr(&meshlet_occlusion[cluster_id.x / 32u], occlusion_bit); +} + +// https://zeux.io/2023/01/12/approximate-projected-bounds +fn project_view_space_sphere_to_screen_space_aabb(cp: vec3, r: f32) -> vec4 { + let inv_width = view.projection[0][0] * 0.5; + let inv_height = view.projection[1][1] * 0.5; + if view.projection[3][3] == 1.0 { + // Orthographic + let min_x = cp.x - r; + let max_x = cp.x + r; + + let min_y = cp.y - r; + let max_y = cp.y + r; + + return vec4(min_x * inv_width, 1.0 - max_y * inv_height, max_x * inv_width, 1.0 - min_y * inv_height); + } else { + // Perspective + let c = vec3(cp.xy, -cp.z); + let cr = c * r; + let czr2 = c.z * c.z - r * r; + + let vx = sqrt(c.x * c.x + czr2); + let min_x = (vx * c.x - cr.z) / (vx * c.z + cr.x); + let max_x = (vx * c.x + cr.z) / (vx * c.z - cr.x); + + let vy = sqrt(c.y * c.y + czr2); + let min_y = (vy * c.y - cr.z) / (vy * c.z + cr.y); + let max_y = (vy * c.y + cr.z) / (vy * c.z - cr.y); + + return vec4(min_x * inv_width, -max_y * inv_height, max_x * inv_width, -min_y * inv_height) + vec4(0.5); + } +} diff --git a/crates/bevy_pbr/src/meshlet/downsample_depth.wgsl b/crates/bevy_pbr/src/meshlet/downsample_depth.wgsl new file mode 100644 index 0000000000000..fbb70bf31679f --- /dev/null +++ b/crates/bevy_pbr/src/meshlet/downsample_depth.wgsl @@ -0,0 +1,16 @@ +#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput + +@group(0) @binding(0) var input_depth: texture_2d; +@group(0) @binding(1) var samplr: sampler; + +/// Performs a 2x2 downsample on a depth texture to generate the next mip level of a hierarchical depth buffer. + +@fragment +fn downsample_depth(in: FullscreenVertexOutput) -> @location(0) vec4 { + let depth_quad = textureGather(0, input_depth, samplr, in.uv); + let downsampled_depth = min( + min(depth_quad.x, depth_quad.y), + min(depth_quad.z, depth_quad.w), + ); + return vec4(downsampled_depth, 0.0, 0.0, 0.0); +} diff --git a/crates/bevy_pbr/src/meshlet/dummy_visibility_buffer_resolve.wgsl b/crates/bevy_pbr/src/meshlet/dummy_visibility_buffer_resolve.wgsl new file mode 100644 index 0000000000000..243a4009976e4 --- /dev/null +++ b/crates/bevy_pbr/src/meshlet/dummy_visibility_buffer_resolve.wgsl @@ -0,0 +1,4 @@ +#define_import_path bevy_pbr::meshlet_visibility_buffer_resolve + +/// Dummy shader to prevent naga_oil from complaining about missing imports when the MeshletPlugin is not loaded, +/// as naga_oil tries to resolve imports even if they're behind an #ifdef. diff --git a/crates/bevy_pbr/src/meshlet/from_mesh.rs b/crates/bevy_pbr/src/meshlet/from_mesh.rs new file mode 100644 index 0000000000000..c794c11c23885 --- /dev/null +++ b/crates/bevy_pbr/src/meshlet/from_mesh.rs @@ -0,0 +1,98 @@ +use super::asset::{Meshlet, MeshletBoundingSphere, MeshletMesh}; +use bevy_render::{ + mesh::{Indices, Mesh}, + render_resource::PrimitiveTopology, +}; +use meshopt::{build_meshlets, compute_meshlet_bounds_decoder, VertexDataAdapter}; +use std::borrow::Cow; + +impl MeshletMesh { + /// Process a [`Mesh`] to generate a [`MeshletMesh`]. + /// + /// This process is very slow, and should be done ahead of time, and not at runtime. + /// + /// This function requires the `meshlet_processor` cargo feature. + /// + /// The input mesh must: + /// 1. Use [`PrimitiveTopology::TriangleList`] + /// 2. Use indices + /// 3. Have the exact following set of vertex attributes: `{POSITION, NORMAL, UV_0, TANGENT}` + pub fn from_mesh(mesh: &Mesh) -> Result { + // Validate mesh format + if mesh.primitive_topology() != PrimitiveTopology::TriangleList { + return Err(MeshToMeshletMeshConversionError::WrongMeshPrimitiveTopology); + } + if mesh.attributes().map(|(id, _)| id).ne([ + Mesh::ATTRIBUTE_POSITION.id, + Mesh::ATTRIBUTE_NORMAL.id, + Mesh::ATTRIBUTE_UV_0.id, + Mesh::ATTRIBUTE_TANGENT.id, + ]) { + return Err(MeshToMeshletMeshConversionError::WrongMeshVertexAttributes); + } + let indices = match mesh.indices() { + Some(Indices::U32(indices)) => Cow::Borrowed(indices.as_slice()), + Some(Indices::U16(indices)) => indices.iter().map(|i| *i as u32).collect(), + _ => return Err(MeshToMeshletMeshConversionError::MeshMissingIndices), + }; + let vertex_buffer = mesh.get_vertex_buffer_data(); + let vertices = + VertexDataAdapter::new(&vertex_buffer, mesh.get_vertex_size() as usize, 0).unwrap(); + + // Split the mesh into meshlets + let meshopt_meshlets = build_meshlets(&indices, &vertices, 64, 64, 0.0); + + // Calculate meshlet bounding spheres + let meshlet_bounding_spheres = meshopt_meshlets + .iter() + .map(|meshlet| { + compute_meshlet_bounds_decoder( + meshlet, + mesh.attribute(Mesh::ATTRIBUTE_POSITION) + .unwrap() + .as_float3() + .unwrap(), + ) + }) + .map(|bounds| MeshletBoundingSphere { + center: bounds.center.into(), + radius: bounds.radius, + }) + .collect(); + + // Assemble into the final asset + let mut total_meshlet_triangles = 0; + let meshlets = meshopt_meshlets + .meshlets + .into_iter() + .map(|m| { + total_meshlet_triangles += m.triangle_count as u64; + Meshlet { + start_vertex_id: m.vertex_offset, + start_index_id: m.triangle_offset, + triangle_count: m.triangle_count, + } + }) + .collect(); + + Ok(Self { + total_meshlet_triangles, + vertex_data: vertex_buffer.into(), + vertex_ids: meshopt_meshlets.vertices.into(), + indices: meshopt_meshlets.triangles.into(), + meshlets, + meshlet_bounding_spheres, + }) + } +} + +/// An error produced by [`MeshletMesh::from_mesh`]. +#[derive(thiserror::Error, Debug)] +pub enum MeshToMeshletMeshConversionError { + #[error("Mesh primitive topology was not TriangleList")] + WrongMeshPrimitiveTopology, + #[error("Mesh attributes were not {{POSITION, NORMAL, UV_0, TANGENT}}")] + WrongMeshVertexAttributes, + #[error("Mesh had no indices")] + MeshMissingIndices, +} diff --git a/crates/bevy_pbr/src/meshlet/gpu_scene.rs b/crates/bevy_pbr/src/meshlet/gpu_scene.rs new file mode 100644 index 0000000000000..492179dc2bcfe --- /dev/null +++ b/crates/bevy_pbr/src/meshlet/gpu_scene.rs @@ -0,0 +1,977 @@ +use super::{persistent_buffer::PersistentGpuBuffer, Meshlet, MeshletBoundingSphere, MeshletMesh}; +use crate::{ + Material, MeshFlags, MeshTransforms, MeshUniform, NotShadowCaster, NotShadowReceiver, + PreviousGlobalTransform, RenderMaterialInstances, ShadowView, +}; +use bevy_asset::{AssetEvent, AssetId, AssetServer, Assets, Handle, UntypedAssetId}; +use bevy_core_pipeline::core_3d::Camera3d; +use bevy_ecs::{ + component::Component, + entity::{Entity, EntityHashMap}, + event::EventReader, + query::{AnyOf, Has}, + system::{Commands, Query, Res, ResMut, Resource, SystemState}, + world::{FromWorld, World}, +}; +use bevy_render::{ + render_resource::{binding_types::*, *}, + renderer::{RenderDevice, RenderQueue}, + texture::{CachedTexture, TextureCache}, + view::{ExtractedView, RenderLayers, ViewDepthTexture, ViewUniform, ViewUniforms}, + MainWorld, +}; +use bevy_transform::components::GlobalTransform; +use bevy_utils::{default, HashMap, HashSet}; +use encase::internal::WriteInto; +use std::{ + iter, + mem::size_of, + ops::{DerefMut, Range}, + sync::Arc, +}; + +/// Create and queue for uploading to the GPU [`MeshUniform`] components for +/// [`MeshletMesh`] entities, as well as queuing uploads for any new meshlet mesh +/// assets that have not already been uploaded to the GPU. +pub fn extract_meshlet_meshes( + // TODO: Replace main_world when Extract>> is possible + mut main_world: ResMut, + mut gpu_scene: ResMut, +) { + let mut system_state: SystemState<( + Query<( + Entity, + &Handle, + &GlobalTransform, + Option<&PreviousGlobalTransform>, + Option<&RenderLayers>, + Has, + Has, + )>, + Res, + ResMut>, + EventReader>, + )> = SystemState::new(&mut main_world); + let (instances_query, asset_server, mut assets, mut asset_events) = + system_state.get_mut(&mut main_world); + + // Reset all temporary data for MeshletGpuScene + gpu_scene.reset(); + + // Free GPU buffer space for any modified or dropped MeshletMesh assets + for asset_event in asset_events.read() { + if let AssetEvent::Unused { id } | AssetEvent::Modified { id } = asset_event { + if let Some(( + [vertex_data_slice, vertex_ids_slice, indices_slice, meshlets_slice, meshlet_bounding_spheres_slice], + _, + )) = gpu_scene.meshlet_mesh_slices.remove(id) + { + gpu_scene.vertex_data.mark_slice_unused(vertex_data_slice); + gpu_scene.vertex_ids.mark_slice_unused(vertex_ids_slice); + gpu_scene.indices.mark_slice_unused(indices_slice); + gpu_scene.meshlets.mark_slice_unused(meshlets_slice); + gpu_scene + .meshlet_bounding_spheres + .mark_slice_unused(meshlet_bounding_spheres_slice); + } + } + } + + for ( + instance_index, + ( + instance, + handle, + transform, + previous_transform, + render_layers, + not_shadow_receiver, + not_shadow_caster, + ), + ) in instances_query.iter().enumerate() + { + // Skip instances with an unloaded MeshletMesh asset + if asset_server.is_managed(handle.id()) + && !asset_server.is_loaded_with_dependencies(handle.id()) + { + continue; + } + + // Upload the instance's MeshletMesh asset data, if not done already, along with other per-frame per-instance data. + gpu_scene.queue_meshlet_mesh_upload( + instance, + render_layers.cloned().unwrap_or(default()), + not_shadow_caster, + handle, + &mut assets, + instance_index as u32, + ); + + // Build a MeshUniform for each instance + let transform = transform.affine(); + let previous_transform = previous_transform.map(|t| t.0).unwrap_or(transform); + let mut flags = if not_shadow_receiver { + MeshFlags::empty() + } else { + MeshFlags::SHADOW_RECEIVER + }; + if transform.matrix3.determinant().is_sign_positive() { + flags |= MeshFlags::SIGN_DETERMINANT_MODEL_3X3; + } + let transforms = MeshTransforms { + transform: (&transform).into(), + previous_transform: (&previous_transform).into(), + flags: flags.bits(), + }; + gpu_scene + .instance_uniforms + .get_mut() + .push(MeshUniform::new(&transforms, None)); + } +} + +/// Upload all newly queued [`MeshletMesh`] asset data from [`extract_meshlet_meshes`] to the GPU. +pub fn perform_pending_meshlet_mesh_writes( + mut gpu_scene: ResMut, + render_queue: Res, + render_device: Res, +) { + gpu_scene + .vertex_data + .perform_writes(&render_queue, &render_device); + gpu_scene + .vertex_ids + .perform_writes(&render_queue, &render_device); + gpu_scene + .indices + .perform_writes(&render_queue, &render_device); + gpu_scene + .meshlets + .perform_writes(&render_queue, &render_device); + gpu_scene + .meshlet_bounding_spheres + .perform_writes(&render_queue, &render_device); +} + +/// For each entity in the scene, record what material ID (for use with depth testing during the meshlet mesh material draw nodes) +/// its material was assigned in the `prepare_material_meshlet_meshes` systems, and note that the material is used by at least one entity in the scene. +pub fn queue_material_meshlet_meshes( + mut gpu_scene: ResMut, + render_material_instances: Res>, +) { + // TODO: Ideally we could parallelize this system, both between different materials, and the loop over instances + let gpu_scene = gpu_scene.deref_mut(); + + for (i, (instance, _, _)) in gpu_scene.instances.iter().enumerate() { + if let Some(material_asset_id) = render_material_instances.get(instance) { + let material_asset_id = material_asset_id.untyped(); + if let Some(material_id) = gpu_scene.material_id_lookup.get(&material_asset_id) { + gpu_scene.material_ids_present_in_scene.insert(*material_id); + gpu_scene.instance_material_ids.get_mut()[i] = *material_id; + } + } + } +} + +// TODO: Try using Queue::write_buffer_with() in queue_meshlet_mesh_upload() to reduce copies +fn upload_storage_buffer( + buffer: &mut StorageBuffer>, + render_device: &RenderDevice, + render_queue: &RenderQueue, +) where + Vec: WriteInto, +{ + let inner = buffer.buffer(); + let capacity = inner.map_or(0, |b| b.size()); + let size = buffer.get().size().get() as BufferAddress; + + if capacity >= size { + let inner = inner.unwrap(); + let bytes = bytemuck::cast_slice(buffer.get().as_slice()); + render_queue.write_buffer(inner, 0, bytes); + } else { + buffer.write_buffer(render_device, render_queue); + } +} + +pub fn prepare_meshlet_per_frame_resources( + mut gpu_scene: ResMut, + views: Query<( + Entity, + &ExtractedView, + Option<&RenderLayers>, + AnyOf<(&Camera3d, &ShadowView)>, + )>, + mut texture_cache: ResMut, + render_queue: Res, + render_device: Res, + mut commands: Commands, +) { + gpu_scene + .previous_cluster_id_starts + .retain(|_, (_, active)| *active); + + if gpu_scene.scene_meshlet_count == 0 { + return; + } + + let gpu_scene = gpu_scene.as_mut(); + + gpu_scene + .instance_uniforms + .write_buffer(&render_device, &render_queue); + upload_storage_buffer( + &mut gpu_scene.instance_material_ids, + &render_device, + &render_queue, + ); + upload_storage_buffer( + &mut gpu_scene.thread_instance_ids, + &render_device, + &render_queue, + ); + upload_storage_buffer( + &mut gpu_scene.thread_meshlet_ids, + &render_device, + &render_queue, + ); + upload_storage_buffer( + &mut gpu_scene.previous_cluster_ids, + &render_device, + &render_queue, + ); + + let needed_buffer_size = 4 * gpu_scene.scene_triangle_count; + let visibility_buffer_draw_index_buffer = + match &mut gpu_scene.visibility_buffer_draw_index_buffer { + Some(buffer) if buffer.size() >= needed_buffer_size => buffer.clone(), + slot => { + let buffer = render_device.create_buffer(&BufferDescriptor { + label: Some("meshlet_visibility_buffer_draw_index_buffer"), + size: needed_buffer_size, + usage: BufferUsages::STORAGE | BufferUsages::INDEX, + mapped_at_creation: false, + }); + *slot = Some(buffer.clone()); + buffer + } + }; + + let needed_buffer_size = gpu_scene.scene_meshlet_count.div_ceil(32) as u64 * 4; + for (view_entity, view, render_layers, (_, shadow_view)) in &views { + let instance_visibility = gpu_scene + .view_instance_visibility + .entry(view_entity) + .or_insert_with(|| { + let mut buffer = StorageBuffer::default(); + buffer.set_label(Some("meshlet_view_instance_visibility")); + buffer + }); + for (instance_index, (_, layers, not_shadow_caster)) in + gpu_scene.instances.iter().enumerate() + { + // If either the layers don't match the view's layers or this is a shadow view + // and the instance is not a shadow caster, hide the instance for this view + if !render_layers.unwrap_or(&default()).intersects(layers) + || (shadow_view.is_some() && *not_shadow_caster) + { + let vec = instance_visibility.get_mut(); + let index = instance_index / 32; + let bit = instance_index - index * 32; + if vec.len() <= index { + vec.extend(iter::repeat(0).take(index - vec.len() + 1)); + } + vec[index] |= 1 << bit; + } + } + upload_storage_buffer(instance_visibility, &render_device, &render_queue); + let instance_visibility = instance_visibility.buffer().unwrap().clone(); + + // Early submission for GPU data uploads to start while the render graph records commands + render_queue.submit([]); + + let create_occlusion_buffer = || { + render_device.create_buffer(&BufferDescriptor { + label: Some("meshlet_occlusion_buffer"), + size: needed_buffer_size, + usage: BufferUsages::STORAGE | BufferUsages::COPY_DST, + mapped_at_creation: false, + }) + }; + let (previous_occlusion_buffer, occlusion_buffer, occlusion_buffer_needs_clearing) = + match gpu_scene.previous_occlusion_buffers.get(&view_entity) { + Some((buffer_a, buffer_b)) if buffer_b.size() >= needed_buffer_size => { + (buffer_a.clone(), buffer_b.clone(), true) + } + Some((buffer_a, _)) => (buffer_a.clone(), create_occlusion_buffer(), false), + None => (create_occlusion_buffer(), create_occlusion_buffer(), false), + }; + gpu_scene.previous_occlusion_buffers.insert( + view_entity, + (occlusion_buffer.clone(), previous_occlusion_buffer.clone()), + ); + + let visibility_buffer = TextureDescriptor { + label: Some("meshlet_visibility_buffer"), + size: Extent3d { + width: view.viewport.z, + height: view.viewport.w, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::R32Uint, + usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }; + + let visibility_buffer_draw_indirect_args_first = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_visibility_buffer_draw_indirect_args_first"), + contents: DrawIndirectArgs { + vertex_count: 0, + instance_count: 1, + first_vertex: 0, + first_instance: 0, + } + .as_bytes(), + usage: BufferUsages::STORAGE | BufferUsages::INDIRECT, + }); + let visibility_buffer_draw_indirect_args_second = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_visibility_buffer_draw_indirect_args_second"), + contents: DrawIndirectArgs { + vertex_count: 0, + instance_count: 1, + first_vertex: 0, + first_instance: 0, + } + .as_bytes(), + usage: BufferUsages::STORAGE | BufferUsages::INDIRECT, + }); + + let depth_size = Extent3d { + // If not a power of 2, round down to the nearest power of 2 to ensure depth is conservative + width: previous_power_of_2(view.viewport.z), + height: previous_power_of_2(view.viewport.w), + depth_or_array_layers: 1, + }; + let depth_mip_count = depth_size.width.max(depth_size.height).ilog2() + 1; + let depth_pyramid = texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("meshlet_depth_pyramid"), + size: depth_size, + mip_level_count: depth_mip_count, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::R32Float, + usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + ); + let depth_pyramid_mips = (0..depth_mip_count) + .map(|i| { + depth_pyramid.texture.create_view(&TextureViewDescriptor { + label: Some("meshlet_depth_pyramid_texture_view"), + format: Some(TextureFormat::R32Float), + dimension: Some(TextureViewDimension::D2), + aspect: TextureAspect::All, + base_mip_level: i, + mip_level_count: Some(1), + base_array_layer: 0, + array_layer_count: None, + }) + }) + .collect(); + + let material_depth_color = TextureDescriptor { + label: Some("meshlet_material_depth_color"), + size: Extent3d { + width: view.viewport.z, + height: view.viewport.w, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::R16Uint, + usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }; + + let material_depth = TextureDescriptor { + label: Some("meshlet_material_depth"), + size: Extent3d { + width: view.viewport.z, + height: view.viewport.w, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Depth16Unorm, + usage: TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }; + + let not_shadow_view = shadow_view.is_none(); + commands.entity(view_entity).insert(MeshletViewResources { + scene_meshlet_count: gpu_scene.scene_meshlet_count, + previous_occlusion_buffer, + occlusion_buffer, + occlusion_buffer_needs_clearing, + instance_visibility, + visibility_buffer: not_shadow_view + .then(|| texture_cache.get(&render_device, visibility_buffer)), + visibility_buffer_draw_indirect_args_first, + visibility_buffer_draw_indirect_args_second, + visibility_buffer_draw_index_buffer: visibility_buffer_draw_index_buffer.clone(), + depth_pyramid, + depth_pyramid_mips, + material_depth_color: not_shadow_view + .then(|| texture_cache.get(&render_device, material_depth_color)), + material_depth: not_shadow_view + .then(|| texture_cache.get(&render_device, material_depth)), + }); + } +} + +pub fn prepare_meshlet_view_bind_groups( + gpu_scene: Res, + views: Query<( + Entity, + &MeshletViewResources, + AnyOf<(&ViewDepthTexture, &ShadowView)>, + )>, + view_uniforms: Res, + render_device: Res, + mut commands: Commands, +) { + let Some(view_uniforms) = view_uniforms.uniforms.binding() else { + return; + }; + + for (view_entity, view_resources, view_depth) in &views { + let entries = BindGroupEntries::sequential(( + gpu_scene.thread_meshlet_ids.binding().unwrap(), + gpu_scene.meshlet_bounding_spheres.binding(), + gpu_scene.thread_instance_ids.binding().unwrap(), + gpu_scene.instance_uniforms.binding().unwrap(), + gpu_scene.view_instance_visibility[&view_entity] + .binding() + .unwrap(), + view_resources.occlusion_buffer.as_entire_binding(), + gpu_scene.previous_cluster_ids.binding().unwrap(), + view_resources.previous_occlusion_buffer.as_entire_binding(), + view_uniforms.clone(), + &view_resources.depth_pyramid.default_view, + )); + let culling = render_device.create_bind_group( + "meshlet_culling_bind_group", + &gpu_scene.culling_bind_group_layout, + &entries, + ); + + let entries = BindGroupEntries::sequential(( + view_resources.occlusion_buffer.as_entire_binding(), + gpu_scene.thread_meshlet_ids.binding().unwrap(), + gpu_scene.previous_cluster_ids.binding().unwrap(), + view_resources.previous_occlusion_buffer.as_entire_binding(), + gpu_scene.meshlets.binding(), + view_resources + .visibility_buffer_draw_indirect_args_first + .as_entire_binding(), + view_resources + .visibility_buffer_draw_index_buffer + .as_entire_binding(), + )); + let write_index_buffer_first = render_device.create_bind_group( + "meshlet_write_index_buffer_first_bind_group", + &gpu_scene.write_index_buffer_bind_group_layout, + &entries, + ); + + let entries = BindGroupEntries::sequential(( + view_resources.occlusion_buffer.as_entire_binding(), + gpu_scene.thread_meshlet_ids.binding().unwrap(), + gpu_scene.previous_cluster_ids.binding().unwrap(), + view_resources.previous_occlusion_buffer.as_entire_binding(), + gpu_scene.meshlets.binding(), + view_resources + .visibility_buffer_draw_indirect_args_second + .as_entire_binding(), + view_resources + .visibility_buffer_draw_index_buffer + .as_entire_binding(), + )); + let write_index_buffer_second = render_device.create_bind_group( + "meshlet_write_index_buffer_second_bind_group", + &gpu_scene.write_index_buffer_bind_group_layout, + &entries, + ); + + let view_depth_texture = match view_depth { + (Some(view_depth), None) => view_depth.view(), + (None, Some(shadow_view)) => &shadow_view.depth_attachment.view, + _ => unreachable!(), + }; + let downsample_depth = (0..view_resources.depth_pyramid_mips.len()) + .map(|i| { + render_device.create_bind_group( + "meshlet_downsample_depth_bind_group", + &gpu_scene.downsample_depth_bind_group_layout, + &BindGroupEntries::sequential(( + if i == 0 { + view_depth_texture + } else { + &view_resources.depth_pyramid_mips[i - 1] + }, + &gpu_scene.depth_pyramid_sampler, + )), + ) + }) + .collect(); + + let entries = BindGroupEntries::sequential(( + gpu_scene.thread_meshlet_ids.binding().unwrap(), + gpu_scene.meshlets.binding(), + gpu_scene.indices.binding(), + gpu_scene.vertex_ids.binding(), + gpu_scene.vertex_data.binding(), + gpu_scene.thread_instance_ids.binding().unwrap(), + gpu_scene.instance_uniforms.binding().unwrap(), + gpu_scene.instance_material_ids.binding().unwrap(), + view_resources + .visibility_buffer_draw_index_buffer + .as_entire_binding(), + view_uniforms.clone(), + )); + let visibility_buffer_raster = render_device.create_bind_group( + "meshlet_visibility_raster_buffer_bind_group", + &gpu_scene.visibility_buffer_raster_bind_group_layout, + &entries, + ); + + let copy_material_depth = + view_resources + .material_depth_color + .as_ref() + .map(|material_depth_color| { + render_device.create_bind_group( + "meshlet_copy_material_depth_bind_group", + &gpu_scene.copy_material_depth_bind_group_layout, + &[BindGroupEntry { + binding: 0, + resource: BindingResource::TextureView( + &material_depth_color.default_view, + ), + }], + ) + }); + + let material_draw = view_resources + .visibility_buffer + .as_ref() + .map(|visibility_buffer| { + let entries = BindGroupEntries::sequential(( + &visibility_buffer.default_view, + gpu_scene.thread_meshlet_ids.binding().unwrap(), + gpu_scene.meshlets.binding(), + gpu_scene.indices.binding(), + gpu_scene.vertex_ids.binding(), + gpu_scene.vertex_data.binding(), + gpu_scene.thread_instance_ids.binding().unwrap(), + gpu_scene.instance_uniforms.binding().unwrap(), + )); + render_device.create_bind_group( + "meshlet_mesh_material_draw_bind_group", + &gpu_scene.material_draw_bind_group_layout, + &entries, + ) + }); + + commands.entity(view_entity).insert(MeshletViewBindGroups { + culling, + write_index_buffer_first, + write_index_buffer_second, + downsample_depth, + visibility_buffer_raster, + copy_material_depth, + material_draw, + }); + } +} + +/// A resource that manages GPU data for rendering [`MeshletMesh`]'s. +#[derive(Resource)] +pub struct MeshletGpuScene { + vertex_data: PersistentGpuBuffer>, + vertex_ids: PersistentGpuBuffer>, + indices: PersistentGpuBuffer>, + meshlets: PersistentGpuBuffer>, + meshlet_bounding_spheres: PersistentGpuBuffer>, + meshlet_mesh_slices: HashMap, ([Range; 5], u64)>, + + scene_meshlet_count: u32, + scene_triangle_count: u64, + next_material_id: u32, + material_id_lookup: HashMap, + material_ids_present_in_scene: HashSet, + /// Per-instance Entity, RenderLayers, and NotShadowCaster + instances: Vec<(Entity, RenderLayers, bool)>, + /// Per-instance transforms, model matrices, and render flags + instance_uniforms: StorageBuffer>, + /// Per-view per-instance visibility bit. Used for RenderLayer and NotShadowCaster support. + view_instance_visibility: EntityHashMap>>, + instance_material_ids: StorageBuffer>, + thread_instance_ids: StorageBuffer>, + thread_meshlet_ids: StorageBuffer>, + previous_cluster_ids: StorageBuffer>, + previous_cluster_id_starts: HashMap<(Entity, AssetId), (u32, bool)>, + previous_occlusion_buffers: EntityHashMap<(Buffer, Buffer)>, + visibility_buffer_draw_index_buffer: Option, + + culling_bind_group_layout: BindGroupLayout, + write_index_buffer_bind_group_layout: BindGroupLayout, + visibility_buffer_raster_bind_group_layout: BindGroupLayout, + downsample_depth_bind_group_layout: BindGroupLayout, + copy_material_depth_bind_group_layout: BindGroupLayout, + material_draw_bind_group_layout: BindGroupLayout, + depth_pyramid_sampler: Sampler, +} + +impl FromWorld for MeshletGpuScene { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + + Self { + vertex_data: PersistentGpuBuffer::new("meshlet_vertex_data", render_device), + vertex_ids: PersistentGpuBuffer::new("meshlet_vertex_ids", render_device), + indices: PersistentGpuBuffer::new("meshlet_indices", render_device), + meshlets: PersistentGpuBuffer::new("meshlets", render_device), + meshlet_bounding_spheres: PersistentGpuBuffer::new( + "meshlet_bounding_spheres", + render_device, + ), + meshlet_mesh_slices: HashMap::new(), + + scene_meshlet_count: 0, + scene_triangle_count: 0, + next_material_id: 0, + material_id_lookup: HashMap::new(), + material_ids_present_in_scene: HashSet::new(), + instances: Vec::new(), + instance_uniforms: { + let mut buffer = StorageBuffer::default(); + buffer.set_label(Some("meshlet_instance_uniforms")); + buffer + }, + view_instance_visibility: EntityHashMap::default(), + instance_material_ids: { + let mut buffer = StorageBuffer::default(); + buffer.set_label(Some("meshlet_instance_material_ids")); + buffer + }, + thread_instance_ids: { + let mut buffer = StorageBuffer::default(); + buffer.set_label(Some("meshlet_thread_instance_ids")); + buffer + }, + thread_meshlet_ids: { + let mut buffer = StorageBuffer::default(); + buffer.set_label(Some("meshlet_thread_meshlet_ids")); + buffer + }, + previous_cluster_ids: { + let mut buffer = StorageBuffer::default(); + buffer.set_label(Some("meshlet_previous_cluster_ids")); + buffer + }, + previous_cluster_id_starts: HashMap::new(), + previous_occlusion_buffers: EntityHashMap::default(), + visibility_buffer_draw_index_buffer: None, + + // TODO: Buffer min sizes + culling_bind_group_layout: render_device.create_bind_group_layout( + "meshlet_culling_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + uniform_buffer::(true), + texture_2d(TextureSampleType::Float { filterable: false }), + ), + ), + ), + write_index_buffer_bind_group_layout: render_device.create_bind_group_layout( + "meshlet_write_index_buffer_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + ), + ), + ), + downsample_depth_bind_group_layout: render_device.create_bind_group_layout( + "meshlet_downsample_depth_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + texture_2d(TextureSampleType::Float { filterable: false }), + sampler(SamplerBindingType::NonFiltering), + ), + ), + ), + visibility_buffer_raster_bind_group_layout: render_device.create_bind_group_layout( + "meshlet_visibility_buffer_raster_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::VERTEX, + ( + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + uniform_buffer::(true), + ), + ), + ), + copy_material_depth_bind_group_layout: render_device.create_bind_group_layout( + "meshlet_copy_material_depth_bind_group_layout", + &BindGroupLayoutEntries::single( + ShaderStages::FRAGMENT, + texture_2d(TextureSampleType::Uint), + ), + ), + material_draw_bind_group_layout: render_device.create_bind_group_layout( + "meshlet_mesh_material_draw_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + texture_2d(TextureSampleType::Uint), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + ), + ), + ), + depth_pyramid_sampler: render_device.create_sampler(&SamplerDescriptor { + label: Some("meshlet_depth_pyramid_sampler"), + ..default() + }), + } + } +} + +impl MeshletGpuScene { + /// Clear per-frame CPU->GPU upload buffers and reset all per-frame data. + fn reset(&mut self) { + // TODO: Shrink capacity if saturation is low + self.scene_meshlet_count = 0; + self.scene_triangle_count = 0; + self.next_material_id = 0; + self.material_id_lookup.clear(); + self.material_ids_present_in_scene.clear(); + self.instances.clear(); + self.view_instance_visibility + .values_mut() + .for_each(|b| b.get_mut().clear()); + self.instance_uniforms.get_mut().clear(); + self.instance_material_ids.get_mut().clear(); + self.thread_instance_ids.get_mut().clear(); + self.thread_meshlet_ids.get_mut().clear(); + self.previous_cluster_ids.get_mut().clear(); + self.previous_cluster_id_starts + .values_mut() + .for_each(|(_, active)| *active = false); + // TODO: Remove unused entries for previous_occlusion_buffers + } + + fn queue_meshlet_mesh_upload( + &mut self, + instance: Entity, + render_layers: RenderLayers, + not_shadow_caster: bool, + handle: &Handle, + assets: &mut Assets, + instance_index: u32, + ) { + let queue_meshlet_mesh = |asset_id: &AssetId| { + let meshlet_mesh = assets.remove_untracked(*asset_id).expect( + "MeshletMesh asset was already unloaded but is not registered with MeshletGpuScene", + ); + + let vertex_data_slice = self + .vertex_data + .queue_write(Arc::clone(&meshlet_mesh.vertex_data), ()); + let vertex_ids_slice = self.vertex_ids.queue_write( + Arc::clone(&meshlet_mesh.vertex_ids), + vertex_data_slice.start, + ); + let indices_slice = self + .indices + .queue_write(Arc::clone(&meshlet_mesh.indices), ()); + let meshlets_slice = self.meshlets.queue_write( + Arc::clone(&meshlet_mesh.meshlets), + (vertex_ids_slice.start, indices_slice.start), + ); + let meshlet_bounding_spheres_slice = self + .meshlet_bounding_spheres + .queue_write(Arc::clone(&meshlet_mesh.meshlet_bounding_spheres), ()); + + ( + [ + vertex_data_slice, + vertex_ids_slice, + indices_slice, + meshlets_slice, + meshlet_bounding_spheres_slice, + ], + meshlet_mesh.total_meshlet_triangles, + ) + }; + + // Append instance data for this frame + self.instances + .push((instance, render_layers, not_shadow_caster)); + self.instance_material_ids.get_mut().push(0); + + // If the MeshletMesh asset has not been uploaded to the GPU yet, queue it for uploading + let ([_, _, _, meshlets_slice, _], triangle_count) = self + .meshlet_mesh_slices + .entry(handle.id()) + .or_insert_with_key(queue_meshlet_mesh) + .clone(); + + let meshlets_slice = (meshlets_slice.start as u32 / size_of::() as u32) + ..(meshlets_slice.end as u32 / size_of::() as u32); + + let current_cluster_id_start = self.scene_meshlet_count; + + self.scene_meshlet_count += meshlets_slice.end - meshlets_slice.start; + self.scene_triangle_count += triangle_count; + + // Calculate the previous cluster IDs for each meshlet for this instance + let previous_cluster_id_start = self + .previous_cluster_id_starts + .entry((instance, handle.id())) + .or_insert((0, true)); + let previous_cluster_ids = if previous_cluster_id_start.1 { + 0..(meshlets_slice.len() as u32) + } else { + let start = previous_cluster_id_start.0; + start..(meshlets_slice.len() as u32 + start) + }; + + // Append per-cluster data for this frame + self.thread_instance_ids + .get_mut() + .extend(std::iter::repeat(instance_index).take(meshlets_slice.len())); + self.thread_meshlet_ids.get_mut().extend(meshlets_slice); + self.previous_cluster_ids + .get_mut() + .extend(previous_cluster_ids); + + *previous_cluster_id_start = (current_cluster_id_start, true); + } + + /// Get the depth value for use with the material depth texture for a given [`Material`] asset. + pub fn get_material_id(&mut self, material_id: UntypedAssetId) -> u32 { + *self + .material_id_lookup + .entry(material_id) + .or_insert_with(|| { + self.next_material_id += 1; + self.next_material_id + }) + } + + pub fn material_present_in_scene(&self, material_id: &u32) -> bool { + self.material_ids_present_in_scene.contains(material_id) + } + + pub fn culling_bind_group_layout(&self) -> BindGroupLayout { + self.culling_bind_group_layout.clone() + } + + pub fn write_index_buffer_bind_group_layout(&self) -> BindGroupLayout { + self.write_index_buffer_bind_group_layout.clone() + } + + pub fn downsample_depth_bind_group_layout(&self) -> BindGroupLayout { + self.downsample_depth_bind_group_layout.clone() + } + + pub fn visibility_buffer_raster_bind_group_layout(&self) -> BindGroupLayout { + self.visibility_buffer_raster_bind_group_layout.clone() + } + + pub fn copy_material_depth_bind_group_layout(&self) -> BindGroupLayout { + self.copy_material_depth_bind_group_layout.clone() + } + + pub fn material_draw_bind_group_layout(&self) -> BindGroupLayout { + self.material_draw_bind_group_layout.clone() + } +} + +#[derive(Component)] +pub struct MeshletViewResources { + pub scene_meshlet_count: u32, + previous_occlusion_buffer: Buffer, + pub occlusion_buffer: Buffer, + pub occlusion_buffer_needs_clearing: bool, + pub instance_visibility: Buffer, + pub visibility_buffer: Option, + pub visibility_buffer_draw_indirect_args_first: Buffer, + pub visibility_buffer_draw_indirect_args_second: Buffer, + visibility_buffer_draw_index_buffer: Buffer, + pub depth_pyramid: CachedTexture, + pub depth_pyramid_mips: Box<[TextureView]>, + pub material_depth_color: Option, + pub material_depth: Option, +} + +#[derive(Component)] +pub struct MeshletViewBindGroups { + pub culling: BindGroup, + pub write_index_buffer_first: BindGroup, + pub write_index_buffer_second: BindGroup, + pub downsample_depth: Box<[BindGroup]>, + pub visibility_buffer_raster: BindGroup, + pub copy_material_depth: Option, + pub material_draw: Option, +} + +fn previous_power_of_2(x: u32) -> u32 { + // If x is a power of 2, halve it + if x.count_ones() == 1 { + x / 2 + } else { + // Else calculate the largest power of 2 that is less than x + 1 << (31 - x.leading_zeros()) + } +} diff --git a/crates/bevy_pbr/src/meshlet/material_draw_nodes.rs b/crates/bevy_pbr/src/meshlet/material_draw_nodes.rs new file mode 100644 index 0000000000000..f87751ba4b9c1 --- /dev/null +++ b/crates/bevy_pbr/src/meshlet/material_draw_nodes.rs @@ -0,0 +1,379 @@ +use super::{ + gpu_scene::{MeshletViewBindGroups, MeshletViewResources}, + material_draw_prepare::{ + MeshletViewMaterialsDeferredGBufferPrepass, MeshletViewMaterialsMainOpaquePass, + MeshletViewMaterialsPrepass, + }, + MeshletGpuScene, +}; +use crate::{ + MeshViewBindGroup, PrepassViewBindGroup, PreviousViewProjectionUniformOffset, + ViewFogUniformOffset, ViewLightProbesUniformOffset, ViewLightsUniformOffset, +}; +use bevy_core_pipeline::prepass::ViewPrepassTextures; +use bevy_ecs::{query::QueryItem, world::World}; +use bevy_render::{ + camera::ExtractedCamera, + render_graph::{NodeRunError, RenderGraphContext, ViewNode}, + render_resource::{ + LoadOp, Operations, PipelineCache, RenderPassDepthStencilAttachment, RenderPassDescriptor, + StoreOp, + }, + renderer::RenderContext, + view::{ViewTarget, ViewUniformOffset}, +}; + +/// Fullscreen shading pass based on the visibility buffer generated from rasterizing meshlets. +#[derive(Default)] +pub struct MeshletMainOpaquePass3dNode; +impl ViewNode for MeshletMainOpaquePass3dNode { + type ViewQuery = ( + &'static ExtractedCamera, + &'static ViewTarget, + &'static MeshViewBindGroup, + &'static ViewUniformOffset, + &'static ViewLightsUniformOffset, + &'static ViewFogUniformOffset, + &'static ViewLightProbesUniformOffset, + &'static MeshletViewMaterialsMainOpaquePass, + &'static MeshletViewBindGroups, + &'static MeshletViewResources, + ); + + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + ( + camera, + target, + mesh_view_bind_group, + view_uniform_offset, + view_lights_offset, + view_fog_offset, + view_light_probes_offset, + meshlet_view_materials, + meshlet_view_bind_groups, + meshlet_view_resources, + ): QueryItem, + world: &World, + ) -> Result<(), NodeRunError> { + if meshlet_view_materials.is_empty() { + return Ok(()); + } + + let ( + Some(meshlet_gpu_scene), + Some(pipeline_cache), + Some(meshlet_material_depth), + Some(meshlet_material_draw_bind_group), + ) = ( + world.get_resource::(), + world.get_resource::(), + meshlet_view_resources.material_depth.as_ref(), + meshlet_view_bind_groups.material_draw.as_ref(), + ) + else { + return Ok(()); + }; + + let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("meshlet_main_opaque_pass_3d"), + color_attachments: &[Some(target.get_color_attachment())], + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &meshlet_material_depth.default_view, + depth_ops: Some(Operations { + load: LoadOp::Load, + store: StoreOp::Store, + }), + stencil_ops: None, + }), + timestamp_writes: None, + occlusion_query_set: None, + }); + if let Some(viewport) = camera.viewport.as_ref() { + render_pass.set_camera_viewport(viewport); + } + + render_pass.set_bind_group( + 0, + &mesh_view_bind_group.value, + &[ + view_uniform_offset.offset, + view_lights_offset.offset, + view_fog_offset.offset, + **view_light_probes_offset, + ], + ); + render_pass.set_bind_group(1, meshlet_material_draw_bind_group, &[]); + + // 1 fullscreen triangle draw per material + for (material_id, material_pipeline_id, material_bind_group) in + meshlet_view_materials.iter() + { + if meshlet_gpu_scene.material_present_in_scene(material_id) { + if let Some(material_pipeline) = + pipeline_cache.get_render_pipeline(*material_pipeline_id) + { + let x = *material_id * 3; + render_pass.set_bind_group(2, material_bind_group, &[]); + render_pass.set_render_pipeline(material_pipeline); + render_pass.draw(x..(x + 3), 0..1); + } + } + } + + Ok(()) + } +} + +/// Fullscreen pass to generate prepass textures based on the visibility buffer generated from rasterizing meshlets. +#[derive(Default)] +pub struct MeshletPrepassNode; +impl ViewNode for MeshletPrepassNode { + type ViewQuery = ( + &'static ExtractedCamera, + &'static ViewPrepassTextures, + &'static ViewUniformOffset, + Option<&'static PreviousViewProjectionUniformOffset>, + &'static MeshletViewMaterialsPrepass, + &'static MeshletViewBindGroups, + &'static MeshletViewResources, + ); + + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + ( + camera, + view_prepass_textures, + view_uniform_offset, + previous_view_projection_uniform_offset, + meshlet_view_materials, + meshlet_view_bind_groups, + meshlet_view_resources, + ): QueryItem, + world: &World, + ) -> Result<(), NodeRunError> { + if meshlet_view_materials.is_empty() { + return Ok(()); + } + + let ( + Some(prepass_view_bind_group), + Some(meshlet_gpu_scene), + Some(pipeline_cache), + Some(meshlet_material_depth), + Some(meshlet_material_draw_bind_group), + ) = ( + world.get_resource::(), + world.get_resource::(), + world.get_resource::(), + meshlet_view_resources.material_depth.as_ref(), + meshlet_view_bind_groups.material_draw.as_ref(), + ) + else { + return Ok(()); + }; + + let color_attachments = vec![ + view_prepass_textures + .normal + .as_ref() + .map(|normals_texture| normals_texture.get_attachment()), + view_prepass_textures + .motion_vectors + .as_ref() + .map(|motion_vectors_texture| motion_vectors_texture.get_attachment()), + // Use None in place of Deferred attachments + None, + None, + ]; + + let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("meshlet_prepass"), + color_attachments: &color_attachments, + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &meshlet_material_depth.default_view, + depth_ops: Some(Operations { + load: LoadOp::Load, + store: StoreOp::Store, + }), + stencil_ops: None, + }), + timestamp_writes: None, + occlusion_query_set: None, + }); + if let Some(viewport) = camera.viewport.as_ref() { + render_pass.set_camera_viewport(viewport); + } + + if let Some(previous_view_projection_uniform_offset) = + previous_view_projection_uniform_offset + { + render_pass.set_bind_group( + 0, + prepass_view_bind_group.motion_vectors.as_ref().unwrap(), + &[ + view_uniform_offset.offset, + previous_view_projection_uniform_offset.offset, + ], + ); + } else { + render_pass.set_bind_group( + 0, + prepass_view_bind_group.no_motion_vectors.as_ref().unwrap(), + &[view_uniform_offset.offset], + ); + } + + render_pass.set_bind_group(1, meshlet_material_draw_bind_group, &[]); + + // 1 fullscreen triangle draw per material + for (material_id, material_pipeline_id, material_bind_group) in + meshlet_view_materials.iter() + { + if meshlet_gpu_scene.material_present_in_scene(material_id) { + if let Some(material_pipeline) = + pipeline_cache.get_render_pipeline(*material_pipeline_id) + { + let x = *material_id * 3; + render_pass.set_bind_group(2, material_bind_group, &[]); + render_pass.set_render_pipeline(material_pipeline); + render_pass.draw(x..(x + 3), 0..1); + } + } + } + + Ok(()) + } +} + +/// Fullscreen pass to generate a gbuffer based on the visibility buffer generated from rasterizing meshlets. +#[derive(Default)] +pub struct MeshletDeferredGBufferPrepassNode; +impl ViewNode for MeshletDeferredGBufferPrepassNode { + type ViewQuery = ( + &'static ExtractedCamera, + &'static ViewPrepassTextures, + &'static ViewUniformOffset, + Option<&'static PreviousViewProjectionUniformOffset>, + &'static MeshletViewMaterialsDeferredGBufferPrepass, + &'static MeshletViewBindGroups, + &'static MeshletViewResources, + ); + + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + ( + camera, + view_prepass_textures, + view_uniform_offset, + previous_view_projection_uniform_offset, + meshlet_view_materials, + meshlet_view_bind_groups, + meshlet_view_resources, + ): QueryItem, + world: &World, + ) -> Result<(), NodeRunError> { + if meshlet_view_materials.is_empty() { + return Ok(()); + } + + let ( + Some(prepass_view_bind_group), + Some(meshlet_gpu_scene), + Some(pipeline_cache), + Some(meshlet_material_depth), + Some(meshlet_material_draw_bind_group), + ) = ( + world.get_resource::(), + world.get_resource::(), + world.get_resource::(), + meshlet_view_resources.material_depth.as_ref(), + meshlet_view_bind_groups.material_draw.as_ref(), + ) + else { + return Ok(()); + }; + + let color_attachments = vec![ + view_prepass_textures + .normal + .as_ref() + .map(|normals_texture| normals_texture.get_attachment()), + view_prepass_textures + .motion_vectors + .as_ref() + .map(|motion_vectors_texture| motion_vectors_texture.get_attachment()), + view_prepass_textures + .deferred + .as_ref() + .map(|deferred_texture| deferred_texture.get_attachment()), + view_prepass_textures + .deferred_lighting_pass_id + .as_ref() + .map(|deferred_lighting_pass_id| deferred_lighting_pass_id.get_attachment()), + ]; + + let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("meshlet_deferred_prepass"), + color_attachments: &color_attachments, + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &meshlet_material_depth.default_view, + depth_ops: Some(Operations { + load: LoadOp::Load, + store: StoreOp::Store, + }), + stencil_ops: None, + }), + timestamp_writes: None, + occlusion_query_set: None, + }); + if let Some(viewport) = camera.viewport.as_ref() { + render_pass.set_camera_viewport(viewport); + } + + if let Some(previous_view_projection_uniform_offset) = + previous_view_projection_uniform_offset + { + render_pass.set_bind_group( + 0, + prepass_view_bind_group.motion_vectors.as_ref().unwrap(), + &[ + view_uniform_offset.offset, + previous_view_projection_uniform_offset.offset, + ], + ); + } else { + render_pass.set_bind_group( + 0, + prepass_view_bind_group.no_motion_vectors.as_ref().unwrap(), + &[view_uniform_offset.offset], + ); + } + + render_pass.set_bind_group(1, meshlet_material_draw_bind_group, &[]); + + // 1 fullscreen triangle draw per material + for (material_id, material_pipeline_id, material_bind_group) in + meshlet_view_materials.iter() + { + if meshlet_gpu_scene.material_present_in_scene(material_id) { + if let Some(material_pipeline) = + pipeline_cache.get_render_pipeline(*material_pipeline_id) + { + let x = *material_id * 3; + render_pass.set_bind_group(2, material_bind_group, &[]); + render_pass.set_render_pipeline(material_pipeline); + render_pass.draw(x..(x + 3), 0..1); + } + } + } + + Ok(()) + } +} diff --git a/crates/bevy_pbr/src/meshlet/material_draw_prepare.rs b/crates/bevy_pbr/src/meshlet/material_draw_prepare.rs new file mode 100644 index 0000000000000..937651834c10c --- /dev/null +++ b/crates/bevy_pbr/src/meshlet/material_draw_prepare.rs @@ -0,0 +1,405 @@ +use super::{MeshletGpuScene, MESHLET_MESH_MATERIAL_SHADER_HANDLE}; +use crate::{environment_map::EnvironmentMapLight, irradiance_volume::IrradianceVolume, *}; +use bevy_asset::AssetServer; +use bevy_core_pipeline::{ + core_3d::Camera3d, + prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass}, + tonemapping::{DebandDither, Tonemapping}, +}; +use bevy_derive::{Deref, DerefMut}; +use bevy_render::{ + camera::TemporalJitter, + mesh::{Mesh, MeshVertexBufferLayout, MeshVertexBufferLayoutRef, MeshVertexBufferLayouts}, + render_resource::*, + view::ExtractedView, +}; +use bevy_utils::HashMap; +use std::hash::Hash; + +/// A list of `(Material ID, Pipeline, BindGroup)` for a view for use in [`super::MeshletMainOpaquePass3dNode`]. +#[derive(Component, Deref, DerefMut, Default)] +pub struct MeshletViewMaterialsMainOpaquePass(pub Vec<(u32, CachedRenderPipelineId, BindGroup)>); + +/// Prepare [`Material`] pipelines for [`super::MeshletMesh`] entities for use in [`super::MeshletMainOpaquePass3dNode`], +/// and register the material with [`MeshletGpuScene`]. +#[allow(clippy::too_many_arguments)] +pub fn prepare_material_meshlet_meshes_main_opaque_pass( + mut gpu_scene: ResMut, + mut cache: Local>, + pipeline_cache: Res, + material_pipeline: Res>, + mesh_pipeline: Res, + render_materials: Res>, + render_material_instances: Res>, + asset_server: Res, + mut mesh_vertex_buffer_layouts: ResMut, + mut views: Query< + ( + &mut MeshletViewMaterialsMainOpaquePass, + &ExtractedView, + Option<&Tonemapping>, + Option<&DebandDither>, + Option<&ShadowFilteringMethod>, + Has, + ( + Has, + Has, + Has, + Has, + ), + Has, + Option<&Projection>, + Has>, + Has>, + ), + With, + >, +) where + M::Data: PartialEq + Eq + Hash + Clone, +{ + let fake_vertex_buffer_layout = &fake_vertex_buffer_layout(&mut mesh_vertex_buffer_layouts); + + for ( + mut materials, + view, + tonemapping, + dither, + shadow_filter_method, + ssao, + (normal_prepass, depth_prepass, motion_vector_prepass, deferred_prepass), + temporal_jitter, + projection, + has_environment_maps, + has_irradiance_volumes, + ) in &mut views + { + let mut view_key = + MeshPipelineKey::from_msaa_samples(1) | MeshPipelineKey::from_hdr(view.hdr); + + if normal_prepass { + view_key |= MeshPipelineKey::NORMAL_PREPASS; + } + if depth_prepass { + view_key |= MeshPipelineKey::DEPTH_PREPASS; + } + if motion_vector_prepass { + view_key |= MeshPipelineKey::MOTION_VECTOR_PREPASS; + } + if deferred_prepass { + view_key |= MeshPipelineKey::DEFERRED_PREPASS; + } + + if temporal_jitter { + view_key |= MeshPipelineKey::TEMPORAL_JITTER; + } + + if has_environment_maps { + view_key |= MeshPipelineKey::ENVIRONMENT_MAP; + } + + if has_irradiance_volumes { + view_key |= MeshPipelineKey::IRRADIANCE_VOLUME; + } + + if let Some(projection) = projection { + view_key |= match projection { + Projection::Perspective(_) => MeshPipelineKey::VIEW_PROJECTION_PERSPECTIVE, + Projection::Orthographic(_) => MeshPipelineKey::VIEW_PROJECTION_ORTHOGRAPHIC, + }; + } + + match shadow_filter_method.unwrap_or(&ShadowFilteringMethod::default()) { + ShadowFilteringMethod::Hardware2x2 => { + view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_HARDWARE_2X2; + } + ShadowFilteringMethod::Castano13 => { + view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_CASTANO_13; + } + ShadowFilteringMethod::Jimenez14 => { + view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_JIMENEZ_14; + } + } + + if !view.hdr { + if let Some(tonemapping) = tonemapping { + view_key |= MeshPipelineKey::TONEMAP_IN_SHADER; + view_key |= tonemapping_pipeline_key(*tonemapping); + } + if let Some(DebandDither::Enabled) = dither { + view_key |= MeshPipelineKey::DEBAND_DITHER; + } + } + + if ssao { + view_key |= MeshPipelineKey::SCREEN_SPACE_AMBIENT_OCCLUSION; + } + + // TODO: Lightmaps + + view_key |= MeshPipelineKey::from_primitive_topology(PrimitiveTopology::TriangleList); + + for material_id in render_material_instances.values() { + let Some(material) = render_materials.get(material_id) else { + continue; + }; + + if material.properties.alpha_mode != AlphaMode::Opaque + || material.properties.reads_view_transmission_texture + { + continue; + } + + let Ok(material_pipeline_descriptor) = material_pipeline.specialize( + MaterialPipelineKey { + mesh_key: view_key, + bind_group_data: material.key.clone(), + }, + fake_vertex_buffer_layout, + ) else { + continue; + }; + let material_fragment = material_pipeline_descriptor.fragment.unwrap(); + + let mut shader_defs = material_fragment.shader_defs; + shader_defs.push("MESHLET_MESH_MATERIAL_PASS".into()); + + let pipeline_descriptor = RenderPipelineDescriptor { + label: material_pipeline_descriptor.label, + layout: vec![ + mesh_pipeline.get_view_layout(view_key.into()).clone(), + gpu_scene.material_draw_bind_group_layout(), + material_pipeline.material_layout.clone(), + ], + push_constant_ranges: vec![], + vertex: VertexState { + shader: MESHLET_MESH_MATERIAL_SHADER_HANDLE, + shader_defs: shader_defs.clone(), + entry_point: material_pipeline_descriptor.vertex.entry_point, + buffers: Vec::new(), + }, + primitive: PrimitiveState::default(), + depth_stencil: Some(DepthStencilState { + format: TextureFormat::Depth16Unorm, + depth_write_enabled: false, + depth_compare: CompareFunction::Equal, + stencil: StencilState::default(), + bias: DepthBiasState::default(), + }), + multisample: MultisampleState::default(), + fragment: Some(FragmentState { + shader: match M::meshlet_mesh_fragment_shader() { + ShaderRef::Default => MESHLET_MESH_MATERIAL_SHADER_HANDLE, + ShaderRef::Handle(handle) => handle, + ShaderRef::Path(path) => asset_server.load(path), + }, + shader_defs, + entry_point: material_fragment.entry_point, + targets: material_fragment.targets, + }), + }; + + let material_id = gpu_scene.get_material_id(material_id.untyped()); + + let pipeline_id = *cache.entry(view_key).or_insert_with(|| { + pipeline_cache.queue_render_pipeline(pipeline_descriptor.clone()) + }); + materials.push((material_id, pipeline_id, material.bind_group.clone())); + } + } +} + +/// A list of `(Material ID, Pipeline, BindGroup)` for a view for use in [`super::MeshletPrepassNode`]. +#[derive(Component, Deref, DerefMut, Default)] +pub struct MeshletViewMaterialsPrepass(pub Vec<(u32, CachedRenderPipelineId, BindGroup)>); + +/// A list of `(Material ID, Pipeline, BindGroup)` for a view for use in [`super::MeshletDeferredGBufferPrepassNode`]. +#[derive(Component, Deref, DerefMut, Default)] +pub struct MeshletViewMaterialsDeferredGBufferPrepass( + pub Vec<(u32, CachedRenderPipelineId, BindGroup)>, +); + +/// Prepare [`Material`] pipelines for [`super::MeshletMesh`] entities for use in [`super::MeshletPrepassNode`], +/// and [`super::MeshletDeferredGBufferPrepassNode`] and register the material with [`MeshletGpuScene`]. +#[allow(clippy::too_many_arguments)] +pub fn prepare_material_meshlet_meshes_prepass( + mut gpu_scene: ResMut, + mut cache: Local>, + pipeline_cache: Res, + prepass_pipeline: Res>, + render_materials: Res>, + render_material_instances: Res>, + mut mesh_vertex_buffer_layouts: ResMut, + asset_server: Res, + mut views: Query< + ( + &mut MeshletViewMaterialsPrepass, + &mut MeshletViewMaterialsDeferredGBufferPrepass, + &ExtractedView, + AnyOf<(&NormalPrepass, &MotionVectorPrepass, &DeferredPrepass)>, + ), + With, + >, +) where + M::Data: PartialEq + Eq + Hash + Clone, +{ + let fake_vertex_buffer_layout = &fake_vertex_buffer_layout(&mut mesh_vertex_buffer_layouts); + + for ( + mut materials, + mut deferred_materials, + view, + (normal_prepass, motion_vector_prepass, deferred_prepass), + ) in &mut views + { + let mut view_key = + MeshPipelineKey::from_msaa_samples(1) | MeshPipelineKey::from_hdr(view.hdr); + + if normal_prepass.is_some() { + view_key |= MeshPipelineKey::NORMAL_PREPASS; + } + if motion_vector_prepass.is_some() { + view_key |= MeshPipelineKey::MOTION_VECTOR_PREPASS; + } + + view_key |= MeshPipelineKey::from_primitive_topology(PrimitiveTopology::TriangleList); + + for material_id in render_material_instances.values() { + let Some(material) = render_materials.get(material_id) else { + continue; + }; + + if material.properties.alpha_mode != AlphaMode::Opaque + || material.properties.reads_view_transmission_texture + { + continue; + } + + let material_wants_deferred = matches!( + material.properties.render_method, + OpaqueRendererMethod::Deferred + ); + if deferred_prepass.is_some() && material_wants_deferred { + view_key |= MeshPipelineKey::DEFERRED_PREPASS; + } else if normal_prepass.is_none() && motion_vector_prepass.is_none() { + continue; + } + + let Ok(material_pipeline_descriptor) = prepass_pipeline.specialize( + MaterialPipelineKey { + mesh_key: view_key, + bind_group_data: material.key.clone(), + }, + fake_vertex_buffer_layout, + ) else { + continue; + }; + let material_fragment = material_pipeline_descriptor.fragment.unwrap(); + + let mut shader_defs = material_fragment.shader_defs; + shader_defs.push("MESHLET_MESH_MATERIAL_PASS".into()); + + let view_layout = if view_key.contains(MeshPipelineKey::MOTION_VECTOR_PREPASS) { + prepass_pipeline.view_layout_motion_vectors.clone() + } else { + prepass_pipeline.view_layout_no_motion_vectors.clone() + }; + + let fragment_shader = if view_key.contains(MeshPipelineKey::DEFERRED_PREPASS) { + M::meshlet_mesh_deferred_fragment_shader() + } else { + M::meshlet_mesh_prepass_fragment_shader() + }; + + let entry_point = match fragment_shader { + ShaderRef::Default => "prepass_fragment".into(), + _ => material_fragment.entry_point, + }; + + let pipeline_descriptor = RenderPipelineDescriptor { + label: material_pipeline_descriptor.label, + layout: vec![ + view_layout, + gpu_scene.material_draw_bind_group_layout(), + prepass_pipeline.material_layout.clone(), + ], + push_constant_ranges: vec![], + vertex: VertexState { + shader: MESHLET_MESH_MATERIAL_SHADER_HANDLE, + shader_defs: shader_defs.clone(), + entry_point: material_pipeline_descriptor.vertex.entry_point, + buffers: Vec::new(), + }, + primitive: PrimitiveState::default(), + depth_stencil: Some(DepthStencilState { + format: TextureFormat::Depth16Unorm, + depth_write_enabled: false, + depth_compare: CompareFunction::Equal, + stencil: StencilState::default(), + bias: DepthBiasState::default(), + }), + multisample: MultisampleState::default(), + fragment: Some(FragmentState { + shader: match fragment_shader { + ShaderRef::Default => MESHLET_MESH_MATERIAL_SHADER_HANDLE, + ShaderRef::Handle(handle) => handle, + ShaderRef::Path(path) => asset_server.load(path), + }, + shader_defs, + entry_point, + targets: material_fragment.targets, + }), + }; + + let material_id = gpu_scene.get_material_id(material_id.untyped()); + + let pipeline_id = *cache.entry(view_key).or_insert_with(|| { + pipeline_cache.queue_render_pipeline(pipeline_descriptor.clone()) + }); + + let item = (material_id, pipeline_id, material.bind_group.clone()); + if view_key.contains(MeshPipelineKey::DEFERRED_PREPASS) { + deferred_materials.push(item); + } else { + materials.push(item); + } + } + } +} + +// Meshlet materials don't use a traditional vertex buffer, but the material specialization requires one. +fn fake_vertex_buffer_layout(layouts: &mut MeshVertexBufferLayouts) -> MeshVertexBufferLayoutRef { + layouts.insert(MeshVertexBufferLayout::new( + vec![ + Mesh::ATTRIBUTE_POSITION.id, + Mesh::ATTRIBUTE_NORMAL.id, + Mesh::ATTRIBUTE_UV_0.id, + Mesh::ATTRIBUTE_TANGENT.id, + ], + VertexBufferLayout { + array_stride: 48, + step_mode: VertexStepMode::Vertex, + attributes: vec![ + VertexAttribute { + format: Mesh::ATTRIBUTE_POSITION.format, + offset: 0, + shader_location: 0, + }, + VertexAttribute { + format: Mesh::ATTRIBUTE_NORMAL.format, + offset: 12, + shader_location: 1, + }, + VertexAttribute { + format: Mesh::ATTRIBUTE_UV_0.format, + offset: 24, + shader_location: 2, + }, + VertexAttribute { + format: Mesh::ATTRIBUTE_TANGENT.format, + offset: 32, + shader_location: 3, + }, + ], + }, + )) +} diff --git a/crates/bevy_pbr/src/meshlet/meshlet_bindings.wgsl b/crates/bevy_pbr/src/meshlet/meshlet_bindings.wgsl new file mode 100644 index 0000000000000..0d9bc3144345c --- /dev/null +++ b/crates/bevy_pbr/src/meshlet/meshlet_bindings.wgsl @@ -0,0 +1,130 @@ +#define_import_path bevy_pbr::meshlet_bindings + +#import bevy_pbr::mesh_types::Mesh +#import bevy_render::view::View + +struct PackedMeshletVertex { + a: vec4, + b: vec4, + tangent: vec4, +} + +// TODO: Octahedral encode normal, remove tangent and derive from UV derivatives +struct MeshletVertex { + position: vec3, + normal: vec3, + uv: vec2, + tangent: vec4, +} + +fn unpack_meshlet_vertex(packed: PackedMeshletVertex) -> MeshletVertex { + var vertex: MeshletVertex; + vertex.position = packed.a.xyz; + vertex.normal = vec3(packed.a.w, packed.b.xy); + vertex.uv = packed.b.zw; + vertex.tangent = packed.tangent; + return vertex; +} + +struct Meshlet { + start_vertex_id: u32, + start_index_id: u32, + triangle_count: u32, +} + +struct MeshletBoundingSphere { + center: vec3, + radius: f32, +} + +struct DrawIndirectArgs { + vertex_count: atomic, + instance_count: u32, + first_vertex: u32, + first_instance: u32, +} + +#ifdef MESHLET_CULLING_PASS +@group(0) @binding(0) var meshlet_thread_meshlet_ids: array; // Per cluster (instance of a meshlet) +@group(0) @binding(1) var meshlet_bounding_spheres: array; // Per asset meshlet +@group(0) @binding(2) var meshlet_thread_instance_ids: array; // Per cluster (instance of a meshlet) +@group(0) @binding(3) var meshlet_instance_uniforms: array; // Per entity instance +@group(0) @binding(4) var meshlet_view_instance_visibility: array; // 1 bit per entity instance, packed as a bitmask +@group(0) @binding(5) var meshlet_occlusion: array>; // 1 bit per cluster (instance of a meshlet), packed as a bitmask +@group(0) @binding(6) var meshlet_previous_cluster_ids: array; // Per cluster (instance of a meshlet) +@group(0) @binding(7) var meshlet_previous_occlusion: array; // 1 bit per cluster (instance of a meshlet), packed as a bitmask +@group(0) @binding(8) var view: View; +@group(0) @binding(9) var depth_pyramid: texture_2d; // Generated from the first raster pass (unused in the first pass but still bound) + +fn should_cull_instance(instance_id: u32) -> bool { + let bit_offset = instance_id % 32u; + let packed_visibility = meshlet_view_instance_visibility[instance_id / 32u]; + return bool(extractBits(packed_visibility, bit_offset, 1u)); +} + +fn get_meshlet_previous_occlusion(cluster_id: u32) -> bool { + let previous_cluster_id = meshlet_previous_cluster_ids[cluster_id]; + let packed_occlusion = meshlet_previous_occlusion[previous_cluster_id / 32u]; + let bit_offset = previous_cluster_id % 32u; + return bool(extractBits(packed_occlusion, bit_offset, 1u)); +} +#endif + +#ifdef MESHLET_WRITE_INDEX_BUFFER_PASS +@group(0) @binding(0) var meshlet_occlusion: array; // 1 bit per cluster (instance of a meshlet), packed as a bitmask +@group(0) @binding(1) var meshlet_thread_meshlet_ids: array; // Per cluster (instance of a meshlet) +@group(0) @binding(2) var meshlet_previous_cluster_ids: array; // Per cluster (instance of a meshlet) +@group(0) @binding(3) var meshlet_previous_occlusion: array; // 1 bit per cluster (instance of a meshlet), packed as a bitmask +@group(0) @binding(4) var meshlets: array; // Per asset meshlet +@group(0) @binding(5) var draw_indirect_args: DrawIndirectArgs; // Single object shared between all workgroups/meshlets/triangles +@group(0) @binding(6) var draw_index_buffer: array; // Single object shared between all workgroups/meshlets/triangles + +fn get_meshlet_occlusion(cluster_id: u32) -> bool { + let packed_occlusion = meshlet_occlusion[cluster_id / 32u]; + let bit_offset = cluster_id % 32u; + return bool(extractBits(packed_occlusion, bit_offset, 1u)); +} + +fn get_meshlet_previous_occlusion(cluster_id: u32) -> bool { + let previous_cluster_id = meshlet_previous_cluster_ids[cluster_id]; + let packed_occlusion = meshlet_previous_occlusion[previous_cluster_id / 32u]; + let bit_offset = previous_cluster_id % 32u; + return bool(extractBits(packed_occlusion, bit_offset, 1u)); +} +#endif + +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS +@group(0) @binding(0) var meshlet_thread_meshlet_ids: array; // Per cluster (instance of a meshlet) +@group(0) @binding(1) var meshlets: array; // Per asset meshlet +@group(0) @binding(2) var meshlet_indices: array; // Many per asset meshlet +@group(0) @binding(3) var meshlet_vertex_ids: array; // Many per asset meshlet +@group(0) @binding(4) var meshlet_vertex_data: array; // Many per asset meshlet +@group(0) @binding(5) var meshlet_thread_instance_ids: array; // Per cluster (instance of a meshlet) +@group(0) @binding(6) var meshlet_instance_uniforms: array; // Per entity instance +@group(0) @binding(7) var meshlet_instance_material_ids: array; // Per entity instance +@group(0) @binding(8) var draw_index_buffer: array; // Single object shared between all workgroups/meshlets/triangles +@group(0) @binding(9) var view: View; + +fn get_meshlet_index(index_id: u32) -> u32 { + let packed_index = meshlet_indices[index_id / 4u]; + let bit_offset = (index_id % 4u) * 8u; + return extractBits(packed_index, bit_offset, 8u); +} +#endif + +#ifdef MESHLET_MESH_MATERIAL_PASS +@group(1) @binding(0) var meshlet_visibility_buffer: texture_2d; // Generated from the meshlet raster passes +@group(1) @binding(1) var meshlet_thread_meshlet_ids: array; // Per cluster (instance of a meshlet) +@group(1) @binding(2) var meshlets: array; // Per asset meshlet +@group(1) @binding(3) var meshlet_indices: array; // Many per asset meshlet +@group(1) @binding(4) var meshlet_vertex_ids: array; // Many per asset meshlet +@group(1) @binding(5) var meshlet_vertex_data: array; // Many per asset meshlet +@group(1) @binding(6) var meshlet_thread_instance_ids: array; // Per cluster (instance of a meshlet) +@group(1) @binding(7) var meshlet_instance_uniforms: array; // Per entity instance + +fn get_meshlet_index(index_id: u32) -> u32 { + let packed_index = meshlet_indices[index_id / 4u]; + let bit_offset = (index_id % 4u) * 8u; + return extractBits(packed_index, bit_offset, 8u); +} +#endif diff --git a/crates/bevy_pbr/src/meshlet/meshlet_mesh_material.wgsl b/crates/bevy_pbr/src/meshlet/meshlet_mesh_material.wgsl new file mode 100644 index 0000000000000..ec67868aad0df --- /dev/null +++ b/crates/bevy_pbr/src/meshlet/meshlet_mesh_material.wgsl @@ -0,0 +1,52 @@ +#import bevy_pbr::{ + meshlet_visibility_buffer_resolve::resolve_vertex_output, + view_transformations::uv_to_ndc, + prepass_io, + pbr_prepass_functions, + utils::rand_f, +} + +@vertex +fn vertex(@builtin(vertex_index) vertex_input: u32) -> @builtin(position) vec4 { + let vertex_index = vertex_input % 3u; + let material_id = vertex_input / 3u; + let material_depth = f32(material_id) / 65535.0; + let uv = vec2(vec2(vertex_index >> 1u, vertex_index & 1u)) * 2.0; + return vec4(uv_to_ndc(uv), material_depth, 1.0); +} + +@fragment +fn fragment(@builtin(position) frag_coord: vec4) -> @location(0) vec4 { + let vertex_output = resolve_vertex_output(frag_coord); + var rng = vertex_output.meshlet_id; + let color = vec3(rand_f(&rng), rand_f(&rng), rand_f(&rng)); + return vec4(color, 1.0); +} + +#ifdef PREPASS_FRAGMENT +@fragment +fn prepass_fragment(@builtin(position) frag_coord: vec4) -> prepass_io::FragmentOutput { + let vertex_output = resolve_vertex_output(frag_coord); + + var out: prepass_io::FragmentOutput; + +#ifdef NORMAL_PREPASS + out.normal = vec4(vertex_output.world_normal * 0.5 + vec3(0.5), 1.0); +#endif + +#ifdef MOTION_VECTOR_PREPASS + out.motion_vector = vertex_output.motion_vector; +#endif + +#ifdef DEFERRED_PREPASS + // There isn't any material info available for this default prepass shader so we are just writing  + // emissive magenta out to the deferred gbuffer to be rendered by the first deferred lighting pass layer. + // This is here so if the default prepass fragment is used for deferred magenta will be rendered, and also + // as an example to show that a user could write to the deferred gbuffer if they were to start from this shader. + out.deferred = vec4(0u, bevy_pbr::rgb9e5::vec3_to_rgb9e5_(vec3(1.0, 0.0, 1.0)), 0u, 0u); + out.deferred_lighting_pass_id = 1u; +#endif + + return out; +} +#endif diff --git a/crates/bevy_pbr/src/meshlet/meshlet_preview.png b/crates/bevy_pbr/src/meshlet/meshlet_preview.png new file mode 100644 index 0000000000000..2c319a8987720 Binary files /dev/null and b/crates/bevy_pbr/src/meshlet/meshlet_preview.png differ diff --git a/crates/bevy_pbr/src/meshlet/mod.rs b/crates/bevy_pbr/src/meshlet/mod.rs new file mode 100644 index 0000000000000..128a183c98edc --- /dev/null +++ b/crates/bevy_pbr/src/meshlet/mod.rs @@ -0,0 +1,280 @@ +//! Render high-poly 3d meshes using an efficient GPU-driven method. See [`MeshletPlugin`] and [`MeshletMesh`] for details. + +mod asset; +#[cfg(feature = "meshlet_processor")] +mod from_mesh; +mod gpu_scene; +mod material_draw_nodes; +mod material_draw_prepare; +mod persistent_buffer; +mod persistent_buffer_impls; +mod pipelines; +mod visibility_buffer_raster_node; + +pub mod graph { + use bevy_render::render_graph::RenderLabel; + + #[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)] + pub enum NodeMeshlet { + VisibilityBufferRasterPass, + Prepass, + DeferredPrepass, + MainOpaquePass, + } +} + +pub(crate) use self::{ + gpu_scene::{queue_material_meshlet_meshes, MeshletGpuScene}, + material_draw_prepare::{ + prepare_material_meshlet_meshes_main_opaque_pass, prepare_material_meshlet_meshes_prepass, + }, +}; + +pub use self::asset::{Meshlet, MeshletBoundingSphere, MeshletMesh}; +#[cfg(feature = "meshlet_processor")] +pub use self::from_mesh::MeshToMeshletMeshConversionError; + +use self::{ + asset::MeshletMeshSaverLoad, + gpu_scene::{ + extract_meshlet_meshes, perform_pending_meshlet_mesh_writes, + prepare_meshlet_per_frame_resources, prepare_meshlet_view_bind_groups, + }, + graph::NodeMeshlet, + material_draw_nodes::{ + MeshletDeferredGBufferPrepassNode, MeshletMainOpaquePass3dNode, MeshletPrepassNode, + }, + material_draw_prepare::{ + MeshletViewMaterialsDeferredGBufferPrepass, MeshletViewMaterialsMainOpaquePass, + MeshletViewMaterialsPrepass, + }, + pipelines::{ + MeshletPipelines, MESHLET_COPY_MATERIAL_DEPTH_SHADER_HANDLE, MESHLET_CULLING_SHADER_HANDLE, + MESHLET_DOWNSAMPLE_DEPTH_SHADER_HANDLE, MESHLET_VISIBILITY_BUFFER_RASTER_SHADER_HANDLE, + MESHLET_WRITE_INDEX_BUFFER_SHADER_HANDLE, + }, + visibility_buffer_raster_node::MeshletVisibilityBufferRasterPassNode, +}; +use crate::{graph::NodePbr, Material}; +use bevy_app::{App, Plugin}; +use bevy_asset::{load_internal_asset, AssetApp, Handle}; +use bevy_core_pipeline::{ + core_3d::{ + graph::{Core3d, Node3d}, + Camera3d, + }, + prepass::{DeferredPrepass, MotionVectorPrepass, NormalPrepass}, +}; +use bevy_ecs::{ + bundle::Bundle, + entity::Entity, + query::Has, + schedule::IntoSystemConfigs, + system::{Commands, Query}, +}; +use bevy_render::{ + render_graph::{RenderGraphApp, ViewNodeRunner}, + render_resource::{Shader, TextureUsages}, + view::{prepare_view_targets, InheritedVisibility, Msaa, ViewVisibility, Visibility}, + ExtractSchedule, Render, RenderApp, RenderSet, +}; +use bevy_transform::components::{GlobalTransform, Transform}; + +const MESHLET_BINDINGS_SHADER_HANDLE: Handle = Handle::weak_from_u128(1325134235233421); +const MESHLET_MESH_MATERIAL_SHADER_HANDLE: Handle = + Handle::weak_from_u128(3325134235233421); + +/// Provides a plugin for rendering large amounts of high-poly 3d meshes using an efficient GPU-driven method. See also [`MeshletMesh`]. +/// +/// Rendering dense scenes made of high-poly meshes with thousands or millions of triangles is extremely expensive in Bevy's standard renderer. +/// Once meshes are pre-processed into a [`MeshletMesh`], this plugin can render these kinds of scenes very efficiently. +/// +/// In comparison to Bevy's standard renderer: +/// * Minimal rendering work is done on the CPU. All rendering is GPU-driven. +/// * Much more efficient culling. Meshlets can be culled individually, instead of all or nothing culling for entire meshes at a time. +/// Additionally, occlusion culling can eliminate meshlets that would cause overdraw. +/// * Much more efficient batching. All geometry can be rasterized in a single indirect draw. +/// * Scales better with large amounts of dense geometry and overdraw. Bevy's standard renderer will bottleneck sooner. +/// * Much greater base overhead. Rendering will be slower than Bevy's standard renderer with small amounts of geometry and overdraw. +/// * Much greater memory usage. +/// * Requires preprocessing meshes. See [`MeshletMesh`] for details. +/// * More limitations on the kinds of materials you can use. See [`MeshletMesh`] for details. +/// +/// This plugin is not compatible with [`Msaa`], and adding this plugin will disable it. +/// +/// This plugin does not work on the WebGL2 backend. +/// +/// ![A render of the Stanford dragon as a `MeshletMesh`](https://raw.githubusercontent.com/bevyengine/bevy/meshlet/crates/bevy_pbr/src/meshlet/meshlet_preview.png) +pub struct MeshletPlugin; + +impl Plugin for MeshletPlugin { + fn build(&self, app: &mut App) { + load_internal_asset!( + app, + MESHLET_BINDINGS_SHADER_HANDLE, + "meshlet_bindings.wgsl", + Shader::from_wgsl + ); + load_internal_asset!( + app, + super::MESHLET_VISIBILITY_BUFFER_RESOLVE_SHADER_HANDLE, + "visibility_buffer_resolve.wgsl", + Shader::from_wgsl + ); + load_internal_asset!( + app, + MESHLET_CULLING_SHADER_HANDLE, + "cull_meshlets.wgsl", + Shader::from_wgsl + ); + load_internal_asset!( + app, + MESHLET_WRITE_INDEX_BUFFER_SHADER_HANDLE, + "write_index_buffer.wgsl", + Shader::from_wgsl + ); + load_internal_asset!( + app, + MESHLET_DOWNSAMPLE_DEPTH_SHADER_HANDLE, + "downsample_depth.wgsl", + Shader::from_wgsl + ); + load_internal_asset!( + app, + MESHLET_VISIBILITY_BUFFER_RASTER_SHADER_HANDLE, + "visibility_buffer_raster.wgsl", + Shader::from_wgsl + ); + load_internal_asset!( + app, + MESHLET_MESH_MATERIAL_SHADER_HANDLE, + "meshlet_mesh_material.wgsl", + Shader::from_wgsl + ); + load_internal_asset!( + app, + MESHLET_COPY_MATERIAL_DEPTH_SHADER_HANDLE, + "copy_material_depth.wgsl", + Shader::from_wgsl + ); + + app.init_asset::() + .register_asset_loader(MeshletMeshSaverLoad) + .insert_resource(Msaa::Off); + } + + fn finish(&self, app: &mut App) { + let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .add_render_graph_node::( + Core3d, + NodeMeshlet::VisibilityBufferRasterPass, + ) + .add_render_graph_node::>( + Core3d, + NodeMeshlet::Prepass, + ) + .add_render_graph_node::>( + Core3d, + NodeMeshlet::DeferredPrepass, + ) + .add_render_graph_node::>( + Core3d, + NodeMeshlet::MainOpaquePass, + ) + .add_render_graph_edges( + Core3d, + ( + NodeMeshlet::VisibilityBufferRasterPass, + NodePbr::ShadowPass, + NodeMeshlet::Prepass, + NodeMeshlet::DeferredPrepass, + Node3d::Prepass, + Node3d::DeferredPrepass, + Node3d::CopyDeferredLightingId, + Node3d::EndPrepasses, + Node3d::StartMainPass, + NodeMeshlet::MainOpaquePass, + Node3d::MainOpaquePass, + Node3d::EndMainPass, + ), + ) + .init_resource::() + .init_resource::() + .add_systems(ExtractSchedule, extract_meshlet_meshes) + .add_systems( + Render, + ( + perform_pending_meshlet_mesh_writes.in_set(RenderSet::PrepareAssets), + configure_meshlet_views + .after(prepare_view_targets) + .in_set(RenderSet::ManageViews), + prepare_meshlet_per_frame_resources.in_set(RenderSet::PrepareResources), + prepare_meshlet_view_bind_groups.in_set(RenderSet::PrepareBindGroups), + ), + ); + } +} + +/// A component bundle for entities with a [`MeshletMesh`] and a [`Material`]. +#[derive(Bundle, Clone)] +pub struct MaterialMeshletMeshBundle { + pub meshlet_mesh: Handle, + pub material: Handle, + pub transform: Transform, + pub global_transform: GlobalTransform, + /// User indication of whether an entity is visible + pub visibility: Visibility, + /// Inherited visibility of an entity. + pub inherited_visibility: InheritedVisibility, + /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering + pub view_visibility: ViewVisibility, +} + +impl Default for MaterialMeshletMeshBundle { + fn default() -> Self { + Self { + meshlet_mesh: Default::default(), + material: Default::default(), + transform: Default::default(), + global_transform: Default::default(), + visibility: Default::default(), + inherited_visibility: Default::default(), + view_visibility: Default::default(), + } + } +} + +fn configure_meshlet_views( + mut views_3d: Query<( + Entity, + &mut Camera3d, + Has, + Has, + Has, + )>, + mut commands: Commands, +) { + for (entity, mut camera_3d, normal_prepass, motion_vector_prepass, deferred_prepass) in + &mut views_3d + { + let mut usages: TextureUsages = camera_3d.depth_texture_usages.into(); + usages |= TextureUsages::TEXTURE_BINDING; + camera_3d.depth_texture_usages = usages.into(); + + if !(normal_prepass || motion_vector_prepass || deferred_prepass) { + commands + .entity(entity) + .insert(MeshletViewMaterialsMainOpaquePass::default()); + } else { + commands.entity(entity).insert(( + MeshletViewMaterialsMainOpaquePass::default(), + MeshletViewMaterialsPrepass::default(), + MeshletViewMaterialsDeferredGBufferPrepass::default(), + )); + } + } +} diff --git a/crates/bevy_pbr/src/meshlet/persistent_buffer.rs b/crates/bevy_pbr/src/meshlet/persistent_buffer.rs new file mode 100644 index 0000000000000..60e163a87446a --- /dev/null +++ b/crates/bevy_pbr/src/meshlet/persistent_buffer.rs @@ -0,0 +1,126 @@ +use bevy_render::{ + render_resource::{ + BindingResource, Buffer, BufferAddress, BufferDescriptor, BufferUsages, + CommandEncoderDescriptor, COPY_BUFFER_ALIGNMENT, + }, + renderer::{RenderDevice, RenderQueue}, +}; +use range_alloc::RangeAllocator; +use std::{num::NonZeroU64, ops::Range}; + +/// Wrapper for a GPU buffer holding a large amount of data that persists across frames. +pub struct PersistentGpuBuffer { + /// Debug label for the buffer. + label: &'static str, + /// Handle to the GPU buffer. + buffer: Buffer, + /// Tracks free slices of the buffer. + allocation_planner: RangeAllocator, + /// Queue of pending writes, and associated metadata. + write_queue: Vec<(T, T::Metadata, Range)>, +} + +impl PersistentGpuBuffer { + /// Create a new persistent buffer. + pub fn new(label: &'static str, render_device: &RenderDevice) -> Self { + Self { + label, + buffer: render_device.create_buffer(&BufferDescriptor { + label: Some(label), + size: 0, + usage: BufferUsages::STORAGE | BufferUsages::COPY_DST | BufferUsages::COPY_SRC, + mapped_at_creation: false, + }), + allocation_planner: RangeAllocator::new(0..0), + write_queue: Vec::new(), + } + } + + /// Queue an item of type T to be added to the buffer, returning the byte range within the buffer that it will be located at. + pub fn queue_write(&mut self, data: T, metadata: T::Metadata) -> Range { + let data_size = data.size_in_bytes() as u64; + debug_assert!(data_size % COPY_BUFFER_ALIGNMENT == 0); + if let Ok(buffer_slice) = self.allocation_planner.allocate_range(data_size) { + self.write_queue + .push((data, metadata, buffer_slice.clone())); + return buffer_slice; + } + + let buffer_size = self.allocation_planner.initial_range(); + let double_buffer_size = (buffer_size.end - buffer_size.start) * 2; + let new_size = double_buffer_size.max(data_size); + self.allocation_planner.grow_to(buffer_size.end + new_size); + + let buffer_slice = self.allocation_planner.allocate_range(data_size).unwrap(); + self.write_queue + .push((data, metadata, buffer_slice.clone())); + buffer_slice + } + + /// Upload all pending data to the GPU buffer. + pub fn perform_writes(&mut self, render_queue: &RenderQueue, render_device: &RenderDevice) { + if self.allocation_planner.initial_range().end > self.buffer.size() { + self.expand_buffer(render_device, render_queue); + } + + let queue_count = self.write_queue.len(); + + for (data, metadata, buffer_slice) in self.write_queue.drain(..) { + let buffer_slice_size = NonZeroU64::new(buffer_slice.end - buffer_slice.start).unwrap(); + let mut buffer_view = render_queue + .write_buffer_with(&self.buffer, buffer_slice.start, buffer_slice_size) + .unwrap(); + data.write_bytes_le(metadata, &mut buffer_view); + } + + let queue_saturation = queue_count as f32 / self.write_queue.capacity() as f32; + if queue_saturation < 0.3 { + self.write_queue = Vec::new(); + } + } + + /// Mark a section of the GPU buffer as no longer needed. + pub fn mark_slice_unused(&mut self, buffer_slice: Range) { + self.allocation_planner.free_range(buffer_slice); + } + + pub fn binding(&self) -> BindingResource<'_> { + self.buffer.as_entire_binding() + } + + /// Expand the buffer by creating a new buffer and copying old data over. + fn expand_buffer(&mut self, render_device: &RenderDevice, render_queue: &RenderQueue) { + let size = self.allocation_planner.initial_range(); + let new_buffer = render_device.create_buffer(&BufferDescriptor { + label: Some(self.label), + size: size.end - size.start, + usage: BufferUsages::STORAGE | BufferUsages::COPY_DST | BufferUsages::COPY_SRC, + mapped_at_creation: false, + }); + + let mut command_encoder = render_device.create_command_encoder(&CommandEncoderDescriptor { + label: Some("persistent_gpu_buffer_expand"), + }); + command_encoder.copy_buffer_to_buffer(&self.buffer, 0, &new_buffer, 0, self.buffer.size()); + render_queue.submit([command_encoder.finish()]); + + self.buffer = new_buffer; + } +} + +/// A trait representing data that can be written to a [`PersistentGpuBuffer`]. +pub trait PersistentGpuBufferable { + /// Additional metadata associated with each item, made available during `write_bytes_le`. + type Metadata; + + /// The size in bytes of `self`. This will determine the size of the buffer passed into + /// `write_bytes_le`. + /// + /// All data written must be in a multiple of `wgpu::COPY_BUFFER_ALIGNMENT` bytes. Failure to do so will + /// result in a panic when using [`PersistentGpuBuffer`]. + fn size_in_bytes(&self) -> usize; + + /// Convert `self` + `metadata` into bytes (little-endian), and write to the provided buffer slice. + /// Any bytes not written to in the slice will be zeroed out when uploaded to the GPU. + fn write_bytes_le(&self, metadata: Self::Metadata, buffer_slice: &mut [u8]); +} diff --git a/crates/bevy_pbr/src/meshlet/persistent_buffer_impls.rs b/crates/bevy_pbr/src/meshlet/persistent_buffer_impls.rs new file mode 100644 index 0000000000000..e95aaa8d82374 --- /dev/null +++ b/crates/bevy_pbr/src/meshlet/persistent_buffer_impls.rs @@ -0,0 +1,75 @@ +use super::{persistent_buffer::PersistentGpuBufferable, Meshlet, MeshletBoundingSphere}; +use std::{mem::size_of, sync::Arc}; + +const MESHLET_VERTEX_SIZE_IN_BYTES: u32 = 48; + +impl PersistentGpuBufferable for Arc<[u8]> { + type Metadata = (); + + fn size_in_bytes(&self) -> usize { + self.len() + } + + fn write_bytes_le(&self, _: Self::Metadata, buffer_slice: &mut [u8]) { + buffer_slice.clone_from_slice(self); + } +} + +impl PersistentGpuBufferable for Arc<[u32]> { + type Metadata = u64; + + fn size_in_bytes(&self) -> usize { + self.len() * size_of::() + } + + fn write_bytes_le(&self, offset: Self::Metadata, buffer_slice: &mut [u8]) { + let offset = offset as u32 / MESHLET_VERTEX_SIZE_IN_BYTES; + + for (i, index) in self.iter().enumerate() { + let size = size_of::(); + let i = i * size; + let bytes = (*index + offset).to_le_bytes(); + buffer_slice[i..(i + size)].clone_from_slice(&bytes); + } + } +} + +impl PersistentGpuBufferable for Arc<[Meshlet]> { + type Metadata = (u64, u64); + + fn size_in_bytes(&self) -> usize { + self.len() * size_of::() + } + + fn write_bytes_le( + &self, + (vertex_offset, index_offset): Self::Metadata, + buffer_slice: &mut [u8], + ) { + let vertex_offset = (vertex_offset as usize / size_of::()) as u32; + let index_offset = index_offset as u32; + + for (i, meshlet) in self.iter().enumerate() { + let size = size_of::(); + let i = i * size; + let bytes = bytemuck::cast::<_, [u8; size_of::()]>(Meshlet { + start_vertex_id: meshlet.start_vertex_id + vertex_offset, + start_index_id: meshlet.start_index_id + index_offset, + triangle_count: meshlet.triangle_count, + }); + buffer_slice[i..(i + size)].clone_from_slice(&bytes); + } + } +} + +impl PersistentGpuBufferable for Arc<[MeshletBoundingSphere]> { + type Metadata = (); + + fn size_in_bytes(&self) -> usize { + self.len() * size_of::() + } + + fn write_bytes_le(&self, _: Self::Metadata, buffer_slice: &mut [u8]) { + buffer_slice.clone_from_slice(bytemuck::cast_slice(self)); + } +} diff --git a/crates/bevy_pbr/src/meshlet/pipelines.rs b/crates/bevy_pbr/src/meshlet/pipelines.rs new file mode 100644 index 0000000000000..1452905d7bc7d --- /dev/null +++ b/crates/bevy_pbr/src/meshlet/pipelines.rs @@ -0,0 +1,295 @@ +use super::gpu_scene::MeshletGpuScene; +use bevy_asset::Handle; +use bevy_core_pipeline::{ + core_3d::CORE_3D_DEPTH_FORMAT, fullscreen_vertex_shader::fullscreen_shader_vertex_state, +}; +use bevy_ecs::{ + system::Resource, + world::{FromWorld, World}, +}; +use bevy_render::render_resource::*; + +pub const MESHLET_CULLING_SHADER_HANDLE: Handle = Handle::weak_from_u128(4325134235233421); +pub const MESHLET_WRITE_INDEX_BUFFER_SHADER_HANDLE: Handle = + Handle::weak_from_u128(5325134235233421); +pub const MESHLET_DOWNSAMPLE_DEPTH_SHADER_HANDLE: Handle = + Handle::weak_from_u128(6325134235233421); +pub const MESHLET_VISIBILITY_BUFFER_RASTER_SHADER_HANDLE: Handle = + Handle::weak_from_u128(7325134235233421); +pub const MESHLET_COPY_MATERIAL_DEPTH_SHADER_HANDLE: Handle = + Handle::weak_from_u128(8325134235233421); + +#[derive(Resource)] +pub struct MeshletPipelines { + cull_first: CachedComputePipelineId, + cull_second: CachedComputePipelineId, + write_index_buffer_first: CachedComputePipelineId, + write_index_buffer_second: CachedComputePipelineId, + downsample_depth: CachedRenderPipelineId, + visibility_buffer_raster: CachedRenderPipelineId, + visibility_buffer_raster_depth_only: CachedRenderPipelineId, + visibility_buffer_raster_depth_only_clamp_ortho: CachedRenderPipelineId, + copy_material_depth: CachedRenderPipelineId, +} + +impl FromWorld for MeshletPipelines { + fn from_world(world: &mut World) -> Self { + let gpu_scene = world.resource::(); + let cull_layout = gpu_scene.culling_bind_group_layout(); + let write_index_buffer_layout = gpu_scene.write_index_buffer_bind_group_layout(); + let downsample_depth_layout = gpu_scene.downsample_depth_bind_group_layout(); + let visibility_buffer_layout = gpu_scene.visibility_buffer_raster_bind_group_layout(); + let copy_material_depth_layout = gpu_scene.copy_material_depth_bind_group_layout(); + let pipeline_cache = world.resource_mut::(); + + Self { + cull_first: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("meshlet_culling_first_pipeline".into()), + layout: vec![cull_layout.clone()], + push_constant_ranges: vec![], + shader: MESHLET_CULLING_SHADER_HANDLE, + shader_defs: vec!["MESHLET_CULLING_PASS".into()], + entry_point: "cull_meshlets".into(), + }), + + cull_second: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("meshlet_culling_second_pipeline".into()), + layout: vec![cull_layout], + push_constant_ranges: vec![], + shader: MESHLET_CULLING_SHADER_HANDLE, + shader_defs: vec![ + "MESHLET_CULLING_PASS".into(), + "MESHLET_SECOND_CULLING_PASS".into(), + ], + entry_point: "cull_meshlets".into(), + }), + + write_index_buffer_first: pipeline_cache.queue_compute_pipeline( + ComputePipelineDescriptor { + label: Some("meshlet_write_index_buffer_first_pipeline".into()), + layout: vec![write_index_buffer_layout.clone()], + push_constant_ranges: vec![], + shader: MESHLET_WRITE_INDEX_BUFFER_SHADER_HANDLE, + shader_defs: vec!["MESHLET_WRITE_INDEX_BUFFER_PASS".into()], + entry_point: "write_index_buffer".into(), + }, + ), + + write_index_buffer_second: pipeline_cache.queue_compute_pipeline( + ComputePipelineDescriptor { + label: Some("meshlet_write_index_buffer_second_pipeline".into()), + layout: vec![write_index_buffer_layout], + push_constant_ranges: vec![], + shader: MESHLET_WRITE_INDEX_BUFFER_SHADER_HANDLE, + shader_defs: vec![ + "MESHLET_WRITE_INDEX_BUFFER_PASS".into(), + "MESHLET_SECOND_WRITE_INDEX_BUFFER_PASS".into(), + ], + entry_point: "write_index_buffer".into(), + }, + ), + + downsample_depth: pipeline_cache.queue_render_pipeline(RenderPipelineDescriptor { + label: Some("meshlet_downsample_depth".into()), + layout: vec![downsample_depth_layout], + push_constant_ranges: vec![], + vertex: fullscreen_shader_vertex_state(), + primitive: PrimitiveState::default(), + depth_stencil: None, + multisample: MultisampleState::default(), + fragment: Some(FragmentState { + shader: MESHLET_DOWNSAMPLE_DEPTH_SHADER_HANDLE, + shader_defs: vec![], + entry_point: "downsample_depth".into(), + targets: vec![Some(ColorTargetState { + format: TextureFormat::R32Float, + blend: None, + write_mask: ColorWrites::ALL, + })], + }), + }), + + visibility_buffer_raster: pipeline_cache.queue_render_pipeline( + RenderPipelineDescriptor { + label: Some("meshlet_visibility_buffer_raster_pipeline".into()), + layout: vec![visibility_buffer_layout.clone()], + push_constant_ranges: vec![], + vertex: VertexState { + shader: MESHLET_VISIBILITY_BUFFER_RASTER_SHADER_HANDLE, + shader_defs: vec![ + "MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into(), + "MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into(), + ], + entry_point: "vertex".into(), + buffers: vec![], + }, + primitive: PrimitiveState { + topology: PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: FrontFace::Ccw, + cull_mode: None, + unclipped_depth: false, + polygon_mode: PolygonMode::Fill, + conservative: false, + }, + depth_stencil: Some(DepthStencilState { + format: CORE_3D_DEPTH_FORMAT, + depth_write_enabled: true, + depth_compare: CompareFunction::GreaterEqual, + stencil: StencilState::default(), + bias: DepthBiasState::default(), + }), + multisample: MultisampleState::default(), + fragment: Some(FragmentState { + shader: MESHLET_VISIBILITY_BUFFER_RASTER_SHADER_HANDLE, + shader_defs: vec![ + "MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into(), + "MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into(), + ], + entry_point: "fragment".into(), + targets: vec![ + Some(ColorTargetState { + format: TextureFormat::R32Uint, + blend: None, + write_mask: ColorWrites::ALL, + }), + Some(ColorTargetState { + format: TextureFormat::R16Uint, + blend: None, + write_mask: ColorWrites::ALL, + }), + ], + }), + }, + ), + + visibility_buffer_raster_depth_only: pipeline_cache.queue_render_pipeline( + RenderPipelineDescriptor { + label: Some("meshlet_visibility_buffer_raster_depth_only_pipeline".into()), + layout: vec![visibility_buffer_layout.clone()], + push_constant_ranges: vec![], + vertex: VertexState { + shader: MESHLET_VISIBILITY_BUFFER_RASTER_SHADER_HANDLE, + shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into()], + entry_point: "vertex".into(), + buffers: vec![], + }, + primitive: PrimitiveState { + topology: PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: FrontFace::Ccw, + cull_mode: None, + unclipped_depth: false, + polygon_mode: PolygonMode::Fill, + conservative: false, + }, + depth_stencil: Some(DepthStencilState { + format: CORE_3D_DEPTH_FORMAT, + depth_write_enabled: true, + depth_compare: CompareFunction::GreaterEqual, + stencil: StencilState::default(), + bias: DepthBiasState::default(), + }), + multisample: MultisampleState::default(), + fragment: None, + }, + ), + + visibility_buffer_raster_depth_only_clamp_ortho: pipeline_cache.queue_render_pipeline( + RenderPipelineDescriptor { + label: Some("visibility_buffer_raster_depth_only_clamp_ortho_pipeline".into()), + layout: vec![visibility_buffer_layout], + push_constant_ranges: vec![], + vertex: VertexState { + shader: MESHLET_VISIBILITY_BUFFER_RASTER_SHADER_HANDLE, + shader_defs: vec![ + "MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into(), + "DEPTH_CLAMP_ORTHO".into(), + ], + entry_point: "vertex".into(), + buffers: vec![], + }, + primitive: PrimitiveState { + topology: PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: FrontFace::Ccw, + cull_mode: None, + unclipped_depth: false, + polygon_mode: PolygonMode::Fill, + conservative: false, + }, + depth_stencil: Some(DepthStencilState { + format: CORE_3D_DEPTH_FORMAT, + depth_write_enabled: true, + depth_compare: CompareFunction::GreaterEqual, + stencil: StencilState::default(), + bias: DepthBiasState::default(), + }), + multisample: MultisampleState::default(), + fragment: Some(FragmentState { + shader: MESHLET_VISIBILITY_BUFFER_RASTER_SHADER_HANDLE, + shader_defs: vec![ + "MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into(), + "DEPTH_CLAMP_ORTHO".into(), + ], + entry_point: "fragment".into(), + targets: vec![], + }), + }, + ), + + copy_material_depth: pipeline_cache.queue_render_pipeline(RenderPipelineDescriptor { + label: Some("meshlet_copy_material_depth".into()), + layout: vec![copy_material_depth_layout], + push_constant_ranges: vec![], + vertex: fullscreen_shader_vertex_state(), + primitive: PrimitiveState::default(), + depth_stencil: Some(DepthStencilState { + format: TextureFormat::Depth16Unorm, + depth_write_enabled: true, + depth_compare: CompareFunction::Always, + stencil: StencilState::default(), + bias: DepthBiasState::default(), + }), + multisample: MultisampleState::default(), + fragment: Some(FragmentState { + shader: MESHLET_COPY_MATERIAL_DEPTH_SHADER_HANDLE, + shader_defs: vec![], + entry_point: "copy_material_depth".into(), + targets: vec![], + }), + }), + } + } +} + +impl MeshletPipelines { + pub fn get( + world: &World, + ) -> Option<( + &ComputePipeline, + &ComputePipeline, + &ComputePipeline, + &ComputePipeline, + &RenderPipeline, + &RenderPipeline, + &RenderPipeline, + &RenderPipeline, + &RenderPipeline, + )> { + let pipeline_cache = world.get_resource::()?; + let pipeline = world.get_resource::()?; + Some(( + pipeline_cache.get_compute_pipeline(pipeline.cull_first)?, + pipeline_cache.get_compute_pipeline(pipeline.cull_second)?, + pipeline_cache.get_compute_pipeline(pipeline.write_index_buffer_first)?, + pipeline_cache.get_compute_pipeline(pipeline.write_index_buffer_second)?, + pipeline_cache.get_render_pipeline(pipeline.downsample_depth)?, + pipeline_cache.get_render_pipeline(pipeline.visibility_buffer_raster)?, + pipeline_cache.get_render_pipeline(pipeline.visibility_buffer_raster_depth_only)?, + pipeline_cache + .get_render_pipeline(pipeline.visibility_buffer_raster_depth_only_clamp_ortho)?, + pipeline_cache.get_render_pipeline(pipeline.copy_material_depth)?, + )) + } +} diff --git a/crates/bevy_pbr/src/meshlet/visibility_buffer_raster.wgsl b/crates/bevy_pbr/src/meshlet/visibility_buffer_raster.wgsl new file mode 100644 index 0000000000000..dde6d2655dd7c --- /dev/null +++ b/crates/bevy_pbr/src/meshlet/visibility_buffer_raster.wgsl @@ -0,0 +1,88 @@ +#import bevy_pbr::{ + meshlet_bindings::{ + meshlet_thread_meshlet_ids, + meshlets, + meshlet_vertex_ids, + meshlet_vertex_data, + meshlet_thread_instance_ids, + meshlet_instance_uniforms, + meshlet_instance_material_ids, + draw_index_buffer, + view, + get_meshlet_index, + unpack_meshlet_vertex, + }, + mesh_functions::mesh_position_local_to_world, +} +#import bevy_render::maths::affine3_to_square + +/// Vertex/fragment shader for rasterizing meshlets into a visibility buffer. + +struct VertexOutput { + @builtin(position) clip_position: vec4, +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT + @location(0) @interpolate(flat) visibility: u32, + @location(1) @interpolate(flat) material_depth: u32, +#endif +#ifdef DEPTH_CLAMP_ORTHO + @location(0) unclamped_clip_depth: f32, +#endif +} + +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT +struct FragmentOutput { + @location(0) visibility: vec4, + @location(1) material_depth: vec4, +} +#endif + +@vertex +fn vertex(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { + let packed_ids = draw_index_buffer[vertex_index / 3u]; + let cluster_id = packed_ids >> 8u; + let triangle_id = extractBits(packed_ids, 0u, 8u); + let index_id = (triangle_id * 3u) + (vertex_index % 3u); + let meshlet_id = meshlet_thread_meshlet_ids[cluster_id]; + let meshlet = meshlets[meshlet_id]; + let index = get_meshlet_index(meshlet.start_index_id + index_id); + let vertex_id = meshlet_vertex_ids[meshlet.start_vertex_id + index]; + let vertex = unpack_meshlet_vertex(meshlet_vertex_data[vertex_id]); + let instance_id = meshlet_thread_instance_ids[cluster_id]; + let instance_uniform = meshlet_instance_uniforms[instance_id]; + + let model = affine3_to_square(instance_uniform.model); + let world_position = mesh_position_local_to_world(model, vec4(vertex.position, 1.0)); + var clip_position = view.view_proj * vec4(world_position.xyz, 1.0); +#ifdef DEPTH_CLAMP_ORTHO + let unclamped_clip_depth = clip_position.z; + clip_position.z = min(clip_position.z, 1.0); +#endif + + return VertexOutput( + clip_position, +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT + packed_ids, + meshlet_instance_material_ids[instance_id], +#endif +#ifdef DEPTH_CLAMP_ORTHO + unclamped_clip_depth, +#endif + ); +} + +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT +@fragment +fn fragment(vertex_output: VertexOutput) -> FragmentOutput { + return FragmentOutput( + vec4(vertex_output.visibility, 0u, 0u, 0u), + vec4(vertex_output.material_depth, 0u, 0u, 0u), + ); +} +#endif + +#ifdef DEPTH_CLAMP_ORTHO +@fragment +fn fragment(vertex_output: VertexOutput) -> @builtin(frag_depth) f32 { + return vertex_output.unclamped_clip_depth; +} +#endif diff --git a/crates/bevy_pbr/src/meshlet/visibility_buffer_raster_node.rs b/crates/bevy_pbr/src/meshlet/visibility_buffer_raster_node.rs new file mode 100644 index 0000000000000..965a4135b21d6 --- /dev/null +++ b/crates/bevy_pbr/src/meshlet/visibility_buffer_raster_node.rs @@ -0,0 +1,448 @@ +use super::{ + gpu_scene::{MeshletViewBindGroups, MeshletViewResources}, + pipelines::MeshletPipelines, +}; +use crate::{LightEntity, ShadowView, ViewLightEntities}; +use bevy_color::LinearRgba; +use bevy_ecs::{ + query::QueryState, + world::{FromWorld, World}, +}; +use bevy_render::{ + camera::ExtractedCamera, + render_graph::{Node, NodeRunError, RenderGraphContext}, + render_resource::*, + renderer::RenderContext, + view::{ViewDepthTexture, ViewUniformOffset}, +}; + +/// Rasterize meshlets into a depth buffer, and optional visibility buffer + material depth buffer for shading passes. +pub struct MeshletVisibilityBufferRasterPassNode { + main_view_query: QueryState<( + &'static ExtractedCamera, + &'static ViewDepthTexture, + &'static ViewUniformOffset, + &'static MeshletViewBindGroups, + &'static MeshletViewResources, + &'static ViewLightEntities, + )>, + view_light_query: QueryState<( + &'static ShadowView, + &'static LightEntity, + &'static ViewUniformOffset, + &'static MeshletViewBindGroups, + &'static MeshletViewResources, + )>, +} + +impl FromWorld for MeshletVisibilityBufferRasterPassNode { + fn from_world(world: &mut World) -> Self { + Self { + main_view_query: QueryState::new(world), + view_light_query: QueryState::new(world), + } + } +} + +impl Node for MeshletVisibilityBufferRasterPassNode { + fn update(&mut self, world: &mut World) { + self.main_view_query.update_archetypes(world); + self.view_light_query.update_archetypes(world); + } + + fn run( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + world: &World, + ) -> Result<(), NodeRunError> { + let Ok(( + camera, + view_depth, + view_offset, + meshlet_view_bind_groups, + meshlet_view_resources, + lights, + )) = self.main_view_query.get_manual(world, graph.view_entity()) + else { + return Ok(()); + }; + + let Some(( + culling_first_pipeline, + culling_second_pipeline, + write_index_buffer_first_pipeline, + write_index_buffer_second_pipeline, + downsample_depth_pipeline, + visibility_buffer_raster_pipeline, + visibility_buffer_raster_depth_only_pipeline, + visibility_buffer_raster_depth_only_clamp_ortho, + copy_material_depth_pipeline, + )) = MeshletPipelines::get(world) + else { + return Ok(()); + }; + + let culling_workgroups = meshlet_view_resources.scene_meshlet_count.div_ceil(128); + let write_index_buffer_workgroups = (meshlet_view_resources.scene_meshlet_count as f32) + .cbrt() + .ceil() as u32; + + render_context + .command_encoder() + .push_debug_group("meshlet_visibility_buffer_raster_pass"); + if meshlet_view_resources.occlusion_buffer_needs_clearing { + render_context.command_encoder().clear_buffer( + &meshlet_view_resources.occlusion_buffer, + 0, + None, + ); + } + cull_pass( + "meshlet_culling_first_pass", + render_context, + meshlet_view_bind_groups, + view_offset, + culling_first_pipeline, + culling_workgroups, + ); + write_index_buffer_pass( + "meshlet_write_index_buffer_first_pass", + render_context, + &meshlet_view_bind_groups.write_index_buffer_first, + write_index_buffer_first_pipeline, + write_index_buffer_workgroups, + ); + render_context.command_encoder().clear_buffer( + &meshlet_view_resources.occlusion_buffer, + 0, + None, + ); + raster_pass( + true, + render_context, + meshlet_view_resources, + &meshlet_view_resources.visibility_buffer_draw_indirect_args_first, + view_depth.get_attachment(StoreOp::Store), + meshlet_view_bind_groups, + view_offset, + visibility_buffer_raster_pipeline, + Some(camera), + ); + downsample_depth( + render_context, + meshlet_view_resources, + meshlet_view_bind_groups, + downsample_depth_pipeline, + ); + cull_pass( + "meshlet_culling_second_pass", + render_context, + meshlet_view_bind_groups, + view_offset, + culling_second_pipeline, + culling_workgroups, + ); + write_index_buffer_pass( + "meshlet_write_index_buffer_second_pass", + render_context, + &meshlet_view_bind_groups.write_index_buffer_second, + write_index_buffer_second_pipeline, + write_index_buffer_workgroups, + ); + raster_pass( + false, + render_context, + meshlet_view_resources, + &meshlet_view_resources.visibility_buffer_draw_indirect_args_second, + view_depth.get_attachment(StoreOp::Store), + meshlet_view_bind_groups, + view_offset, + visibility_buffer_raster_pipeline, + Some(camera), + ); + copy_material_depth_pass( + render_context, + meshlet_view_resources, + meshlet_view_bind_groups, + copy_material_depth_pipeline, + camera, + ); + render_context.command_encoder().pop_debug_group(); + + for light_entity in &lights.lights { + let Ok(( + shadow_view, + light_type, + view_offset, + meshlet_view_bind_groups, + meshlet_view_resources, + )) = self.view_light_query.get_manual(world, *light_entity) + else { + continue; + }; + + let shadow_visibility_buffer_pipeline = match light_type { + LightEntity::Directional { .. } => visibility_buffer_raster_depth_only_clamp_ortho, + _ => visibility_buffer_raster_depth_only_pipeline, + }; + + render_context.command_encoder().push_debug_group(&format!( + "meshlet_visibility_buffer_raster_pass: {}", + shadow_view.pass_name + )); + if meshlet_view_resources.occlusion_buffer_needs_clearing { + render_context.command_encoder().clear_buffer( + &meshlet_view_resources.occlusion_buffer, + 0, + None, + ); + } + cull_pass( + "meshlet_culling_first_pass", + render_context, + meshlet_view_bind_groups, + view_offset, + culling_first_pipeline, + culling_workgroups, + ); + write_index_buffer_pass( + "meshlet_write_index_buffer_first_pass", + render_context, + &meshlet_view_bind_groups.write_index_buffer_first, + write_index_buffer_first_pipeline, + write_index_buffer_workgroups, + ); + render_context.command_encoder().clear_buffer( + &meshlet_view_resources.occlusion_buffer, + 0, + None, + ); + raster_pass( + true, + render_context, + meshlet_view_resources, + &meshlet_view_resources.visibility_buffer_draw_indirect_args_first, + shadow_view.depth_attachment.get_attachment(StoreOp::Store), + meshlet_view_bind_groups, + view_offset, + shadow_visibility_buffer_pipeline, + None, + ); + downsample_depth( + render_context, + meshlet_view_resources, + meshlet_view_bind_groups, + downsample_depth_pipeline, + ); + cull_pass( + "meshlet_culling_second_pass", + render_context, + meshlet_view_bind_groups, + view_offset, + culling_second_pipeline, + culling_workgroups, + ); + write_index_buffer_pass( + "meshlet_write_index_buffer_second_pass", + render_context, + &meshlet_view_bind_groups.write_index_buffer_second, + write_index_buffer_second_pipeline, + write_index_buffer_workgroups, + ); + raster_pass( + false, + render_context, + meshlet_view_resources, + &meshlet_view_resources.visibility_buffer_draw_indirect_args_second, + shadow_view.depth_attachment.get_attachment(StoreOp::Store), + meshlet_view_bind_groups, + view_offset, + shadow_visibility_buffer_pipeline, + None, + ); + render_context.command_encoder().pop_debug_group(); + } + + Ok(()) + } +} + +fn cull_pass( + label: &'static str, + render_context: &mut RenderContext, + meshlet_view_bind_groups: &MeshletViewBindGroups, + view_offset: &ViewUniformOffset, + culling_pipeline: &ComputePipeline, + culling_workgroups: u32, +) { + let command_encoder = render_context.command_encoder(); + let mut cull_pass = command_encoder.begin_compute_pass(&ComputePassDescriptor { + label: Some(label), + timestamp_writes: None, + }); + cull_pass.set_bind_group(0, &meshlet_view_bind_groups.culling, &[view_offset.offset]); + cull_pass.set_pipeline(culling_pipeline); + cull_pass.dispatch_workgroups(culling_workgroups, 1, 1); +} + +fn write_index_buffer_pass( + label: &'static str, + render_context: &mut RenderContext, + write_index_buffer_bind_group: &BindGroup, + write_index_buffer_pipeline: &ComputePipeline, + write_index_buffer_workgroups: u32, +) { + let command_encoder = render_context.command_encoder(); + let mut cull_pass = command_encoder.begin_compute_pass(&ComputePassDescriptor { + label: Some(label), + timestamp_writes: None, + }); + cull_pass.set_bind_group(0, write_index_buffer_bind_group, &[]); + cull_pass.set_pipeline(write_index_buffer_pipeline); + cull_pass.dispatch_workgroups( + write_index_buffer_workgroups, + write_index_buffer_workgroups, + write_index_buffer_workgroups, + ); +} + +#[allow(clippy::too_many_arguments)] +fn raster_pass( + first_pass: bool, + render_context: &mut RenderContext, + meshlet_view_resources: &MeshletViewResources, + visibility_buffer_draw_indirect_args: &Buffer, + depth_stencil_attachment: RenderPassDepthStencilAttachment, + meshlet_view_bind_groups: &MeshletViewBindGroups, + view_offset: &ViewUniformOffset, + visibility_buffer_raster_pipeline: &RenderPipeline, + camera: Option<&ExtractedCamera>, +) { + let mut color_attachments_filled = [None, None]; + if let (Some(visibility_buffer), Some(material_depth_color)) = ( + meshlet_view_resources.visibility_buffer.as_ref(), + meshlet_view_resources.material_depth_color.as_ref(), + ) { + let load = if first_pass { + LoadOp::Clear(LinearRgba::BLACK.into()) + } else { + LoadOp::Load + }; + color_attachments_filled = [ + Some(RenderPassColorAttachment { + view: &visibility_buffer.default_view, + resolve_target: None, + ops: Operations { + load, + store: StoreOp::Store, + }, + }), + Some(RenderPassColorAttachment { + view: &material_depth_color.default_view, + resolve_target: None, + ops: Operations { + load, + store: StoreOp::Store, + }, + }), + ]; + } + + let mut draw_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some(if first_pass { + "meshlet_visibility_buffer_raster_first_pass" + } else { + "meshlet_visibility_buffer_raster_second_pass" + }), + color_attachments: if color_attachments_filled[0].is_none() { + &[] + } else { + &color_attachments_filled + }, + depth_stencil_attachment: Some(depth_stencil_attachment), + timestamp_writes: None, + occlusion_query_set: None, + }); + if let Some(viewport) = camera.and_then(|camera| camera.viewport.as_ref()) { + draw_pass.set_camera_viewport(viewport); + } + + draw_pass.set_bind_group( + 0, + &meshlet_view_bind_groups.visibility_buffer_raster, + &[view_offset.offset], + ); + draw_pass.set_render_pipeline(visibility_buffer_raster_pipeline); + draw_pass.draw_indirect(visibility_buffer_draw_indirect_args, 0); +} + +fn downsample_depth( + render_context: &mut RenderContext, + meshlet_view_resources: &MeshletViewResources, + meshlet_view_bind_groups: &MeshletViewBindGroups, + downsample_depth_pipeline: &RenderPipeline, +) { + render_context + .command_encoder() + .push_debug_group("meshlet_downsample_depth"); + + for i in 0..meshlet_view_resources.depth_pyramid_mips.len() { + let downsample_pass = RenderPassDescriptor { + label: Some("meshlet_downsample_depth_pass"), + color_attachments: &[Some(RenderPassColorAttachment { + view: &meshlet_view_resources.depth_pyramid_mips[i], + resolve_target: None, + ops: Operations { + load: LoadOp::Clear(LinearRgba::BLACK.into()), + store: StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }; + + let mut downsample_pass = render_context.begin_tracked_render_pass(downsample_pass); + downsample_pass.set_bind_group(0, &meshlet_view_bind_groups.downsample_depth[i], &[]); + downsample_pass.set_render_pipeline(downsample_depth_pipeline); + downsample_pass.draw(0..3, 0..1); + } + + render_context.command_encoder().pop_debug_group(); +} + +fn copy_material_depth_pass( + render_context: &mut RenderContext, + meshlet_view_resources: &MeshletViewResources, + meshlet_view_bind_groups: &MeshletViewBindGroups, + copy_material_depth_pipeline: &RenderPipeline, + camera: &ExtractedCamera, +) { + if let (Some(material_depth), Some(copy_material_depth_bind_group)) = ( + meshlet_view_resources.material_depth.as_ref(), + meshlet_view_bind_groups.copy_material_depth.as_ref(), + ) { + let mut copy_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("meshlet_copy_material_depth_pass"), + color_attachments: &[], + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &material_depth.default_view, + depth_ops: Some(Operations { + load: LoadOp::Clear(0.0), + store: StoreOp::Store, + }), + stencil_ops: None, + }), + timestamp_writes: None, + occlusion_query_set: None, + }); + if let Some(viewport) = &camera.viewport { + copy_pass.set_camera_viewport(viewport); + } + + copy_pass.set_bind_group(0, copy_material_depth_bind_group, &[]); + copy_pass.set_render_pipeline(copy_material_depth_pipeline); + copy_pass.draw(0..3, 0..1); + } +} diff --git a/crates/bevy_pbr/src/meshlet/visibility_buffer_resolve.wgsl b/crates/bevy_pbr/src/meshlet/visibility_buffer_resolve.wgsl new file mode 100644 index 0000000000000..0325ba96b9713 --- /dev/null +++ b/crates/bevy_pbr/src/meshlet/visibility_buffer_resolve.wgsl @@ -0,0 +1,186 @@ +#define_import_path bevy_pbr::meshlet_visibility_buffer_resolve + +#import bevy_pbr::{ + meshlet_bindings::{ + meshlet_visibility_buffer, + meshlet_thread_meshlet_ids, + meshlets, + meshlet_vertex_ids, + meshlet_vertex_data, + meshlet_thread_instance_ids, + meshlet_instance_uniforms, + get_meshlet_index, + unpack_meshlet_vertex, + }, + mesh_view_bindings::view, + mesh_functions::mesh_position_local_to_world, + mesh_types::MESH_FLAGS_SIGN_DETERMINANT_MODEL_3X3_BIT, + view_transformations::{position_world_to_clip, frag_coord_to_ndc}, +} +#import bevy_render::maths::{affine3_to_square, mat2x4_f32_to_mat3x3_unpack} + +#ifdef PREPASS_FRAGMENT +#ifdef MOTION_VECTOR_PREPASS +#import bevy_pbr::{ + prepass_bindings::previous_view_proj, + pbr_prepass_functions::calculate_motion_vector, +} +#endif +#endif + +/// Functions to be used by materials for reading from a meshlet visibility buffer texture. + +#ifdef MESHLET_MESH_MATERIAL_PASS +struct PartialDerivatives { + barycentrics: vec3, + ddx: vec3, + ddy: vec3, +} + +// https://github.com/ConfettiFX/The-Forge/blob/2d453f376ef278f66f97cbaf36c0d12e4361e275/Examples_3/Visibility_Buffer/src/Shaders/FSL/visibilityBuffer_shade.frag.fsl#L83-L139 +fn compute_partial_derivatives(vertex_clip_positions: array, 3>, ndc_uv: vec2, screen_size: vec2) -> PartialDerivatives { + var result: PartialDerivatives; + + let inv_w = 1.0 / vec3(vertex_clip_positions[0].w, vertex_clip_positions[1].w, vertex_clip_positions[2].w); + let ndc_0 = vertex_clip_positions[0].xy * inv_w[0]; + let ndc_1 = vertex_clip_positions[1].xy * inv_w[1]; + let ndc_2 = vertex_clip_positions[2].xy * inv_w[2]; + + let inv_det = 1.0 / determinant(mat2x2(ndc_2 - ndc_1, ndc_0 - ndc_1)); + result.ddx = vec3(ndc_1.y - ndc_2.y, ndc_2.y - ndc_0.y, ndc_0.y - ndc_1.y) * inv_det * inv_w; + result.ddy = vec3(ndc_2.x - ndc_1.x, ndc_0.x - ndc_2.x, ndc_1.x - ndc_0.x) * inv_det * inv_w; + + var ddx_sum = dot(result.ddx, vec3(1.0)); + var ddy_sum = dot(result.ddy, vec3(1.0)); + + let delta_v = ndc_uv - ndc_0; + let interp_inv_w = inv_w.x + delta_v.x * ddx_sum + delta_v.y * ddy_sum; + let interp_w = 1.0 / interp_inv_w; + + result.barycentrics = vec3( + interp_w * (delta_v.x * result.ddx.x + delta_v.y * result.ddy.x + inv_w.x), + interp_w * (delta_v.x * result.ddx.y + delta_v.y * result.ddy.y), + interp_w * (delta_v.x * result.ddx.z + delta_v.y * result.ddy.z), + ); + + result.ddx *= 2.0 / screen_size.x; + result.ddy *= 2.0 / screen_size.y; + ddx_sum *= 2.0 / screen_size.x; + ddy_sum *= 2.0 / screen_size.y; + + let interp_ddx_w = 1.0 / (interp_inv_w + ddx_sum); + let interp_ddy_w = 1.0 / (interp_inv_w + ddy_sum); + + result.ddx = interp_ddx_w * (result.barycentrics * interp_inv_w + result.ddx) - result.barycentrics; + result.ddy = interp_ddy_w * (result.barycentrics * interp_inv_w + result.ddy) - result.barycentrics; + return result; +} + +struct VertexOutput { + position: vec4, + world_position: vec4, + world_normal: vec3, + uv: vec2, + ddx_uv: vec2, + ddy_uv: vec2, + world_tangent: vec4, + mesh_flags: u32, + meshlet_id: u32, +#ifdef PREPASS_FRAGMENT +#ifdef MOTION_VECTOR_PREPASS + motion_vector: vec2, +#endif +#endif +} + +/// Load the visibility buffer texture and resolve it into a VertexOutput. +fn resolve_vertex_output(frag_coord: vec4) -> VertexOutput { + let vbuffer = textureLoad(meshlet_visibility_buffer, vec2(frag_coord.xy), 0).r; + let cluster_id = vbuffer >> 8u; + let meshlet_id = meshlet_thread_meshlet_ids[cluster_id]; + let meshlet = meshlets[meshlet_id]; + let triangle_id = extractBits(vbuffer, 0u, 8u); + let index_ids = meshlet.start_index_id + vec3(triangle_id * 3u) + vec3(0u, 1u, 2u); + let indices = meshlet.start_vertex_id + vec3(get_meshlet_index(index_ids.x), get_meshlet_index(index_ids.y), get_meshlet_index(index_ids.z)); + let vertex_ids = vec3(meshlet_vertex_ids[indices.x], meshlet_vertex_ids[indices.y], meshlet_vertex_ids[indices.z]); + let vertex_1 = unpack_meshlet_vertex(meshlet_vertex_data[vertex_ids.x]); + let vertex_2 = unpack_meshlet_vertex(meshlet_vertex_data[vertex_ids.y]); + let vertex_3 = unpack_meshlet_vertex(meshlet_vertex_data[vertex_ids.z]); + + let instance_id = meshlet_thread_instance_ids[cluster_id]; + let instance_uniform = meshlet_instance_uniforms[instance_id]; + let model = affine3_to_square(instance_uniform.model); + + let world_position_1 = mesh_position_local_to_world(model, vec4(vertex_1.position, 1.0)); + let world_position_2 = mesh_position_local_to_world(model, vec4(vertex_2.position, 1.0)); + let world_position_3 = mesh_position_local_to_world(model, vec4(vertex_3.position, 1.0)); + let clip_position_1 = position_world_to_clip(world_position_1.xyz); + let clip_position_2 = position_world_to_clip(world_position_2.xyz); + let clip_position_3 = position_world_to_clip(world_position_3.xyz); + let frag_coord_ndc = frag_coord_to_ndc(frag_coord).xy; + let partial_derivatives = compute_partial_derivatives( + array(clip_position_1, clip_position_2, clip_position_3), + frag_coord_ndc, + view.viewport.zw, + ); + + let world_position = mat3x4(world_position_1, world_position_2, world_position_3) * partial_derivatives.barycentrics; + let vertex_normal = mat3x3(vertex_1.normal, vertex_2.normal, vertex_3.normal) * partial_derivatives.barycentrics; + let world_normal = normalize( + mat2x4_f32_to_mat3x3_unpack( + instance_uniform.inverse_transpose_model_a, + instance_uniform.inverse_transpose_model_b, + ) * vertex_normal + ); + let uv = mat3x2(vertex_1.uv, vertex_2.uv, vertex_3.uv) * partial_derivatives.barycentrics; + let ddx_uv = mat3x2(vertex_1.uv, vertex_2.uv, vertex_3.uv) * partial_derivatives.ddx; + let ddy_uv = mat3x2(vertex_1.uv, vertex_2.uv, vertex_3.uv) * partial_derivatives.ddy; + let vertex_tangent = mat3x4(vertex_1.tangent, vertex_2.tangent, vertex_3.tangent) * partial_derivatives.barycentrics; + let world_tangent = vec4( + normalize( + mat3x3( + model[0].xyz, + model[1].xyz, + model[2].xyz + ) * vertex_tangent.xyz + ), + vertex_tangent.w * (f32(bool(instance_uniform.flags & MESH_FLAGS_SIGN_DETERMINANT_MODEL_3X3_BIT)) * 2.0 - 1.0) + ); + +#ifdef PREPASS_FRAGMENT +#ifdef MOTION_VECTOR_PREPASS + let previous_model = affine3_to_square(instance_uniform.previous_model); + let previous_world_position_1 = mesh_position_local_to_world(previous_model, vec4(vertex_1.position, 1.0)); + let previous_world_position_2 = mesh_position_local_to_world(previous_model, vec4(vertex_2.position, 1.0)); + let previous_world_position_3 = mesh_position_local_to_world(previous_model, vec4(vertex_3.position, 1.0)); + let previous_clip_position_1 = previous_view_proj * vec4(previous_world_position_1.xyz, 1.0); + let previous_clip_position_2 = previous_view_proj * vec4(previous_world_position_2.xyz, 1.0); + let previous_clip_position_3 = previous_view_proj * vec4(previous_world_position_3.xyz, 1.0); + let previous_partial_derivatives = compute_partial_derivatives( + array(previous_clip_position_1, previous_clip_position_2, previous_clip_position_3), + frag_coord_ndc, + view.viewport.zw, + ); + let previous_world_position = mat3x4(previous_world_position_1, previous_world_position_2, previous_world_position_3) * previous_partial_derivatives.barycentrics; + let motion_vector = calculate_motion_vector(world_position, previous_world_position); +#endif +#endif + + return VertexOutput( + frag_coord, + world_position, + world_normal, + uv, + ddx_uv, + ddy_uv, + world_tangent, + instance_uniform.flags, + meshlet_id, +#ifdef PREPASS_FRAGMENT +#ifdef MOTION_VECTOR_PREPASS + motion_vector, +#endif +#endif + ); +} +#endif diff --git a/crates/bevy_pbr/src/meshlet/write_index_buffer.wgsl b/crates/bevy_pbr/src/meshlet/write_index_buffer.wgsl new file mode 100644 index 0000000000000..f7ea7dae56aac --- /dev/null +++ b/crates/bevy_pbr/src/meshlet/write_index_buffer.wgsl @@ -0,0 +1,43 @@ +#import bevy_pbr::meshlet_bindings::{ + meshlet_thread_meshlet_ids, + meshlets, + draw_indirect_args, + draw_index_buffer, + get_meshlet_occlusion, + get_meshlet_previous_occlusion, +} + +var draw_index_buffer_start_workgroup: u32; + +/// This pass writes out a buffer of cluster + triangle IDs for the draw_indirect() call to rasterize each visible meshlet. + +@compute +@workgroup_size(64, 1, 1) // 64 threads per workgroup, 1 workgroup per cluster, 1 thread per triangle +fn write_index_buffer(@builtin(workgroup_id) workgroup_id: vec3, @builtin(num_workgroups) num_workgroups: vec3, @builtin(local_invocation_index) triangle_id: u32) { + // Calculate the cluster ID for this workgroup + let cluster_id = dot(workgroup_id, vec3(num_workgroups.x * num_workgroups.x, num_workgroups.x, 1u)); + if cluster_id >= arrayLength(&meshlet_thread_meshlet_ids) { return; } + + // If the meshlet was culled, then we don't need to draw it + if !get_meshlet_occlusion(cluster_id) { return; } + + // If the meshlet was drawn in the first pass, and this is the second pass, then we don't need to draw it +#ifdef MESHLET_SECOND_WRITE_INDEX_BUFFER_PASS + if get_meshlet_previous_occlusion(cluster_id) { return; } +#endif + + let meshlet_id = meshlet_thread_meshlet_ids[cluster_id]; + let meshlet = meshlets[meshlet_id]; + + // Reserve space in the buffer for this meshlet's triangles, and broadcast the start of that slice to all threads + if triangle_id == 0u { + draw_index_buffer_start_workgroup = atomicAdd(&draw_indirect_args.vertex_count, meshlet.triangle_count * 3u); + draw_index_buffer_start_workgroup /= 3u; + } + workgroupBarrier(); + + // Each thread writes one triangle of the meshlet to the buffer slice reserved for the meshlet + if triangle_id < meshlet.triangle_count { + draw_index_buffer[draw_index_buffer_start_workgroup + triangle_id] = (cluster_id << 8u) | triangle_id; + } +} diff --git a/crates/bevy_pbr/src/pbr_material.rs b/crates/bevy_pbr/src/pbr_material.rs index 3231cb8df5d6d..27c44a43dc7e9 100644 --- a/crates/bevy_pbr/src/pbr_material.rs +++ b/crates/bevy_pbr/src/pbr_material.rs @@ -5,6 +5,7 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ mesh::MeshVertexBufferLayoutRef, render_asset::RenderAssets, render_resource::*, }; +use bitflags::bitflags; use crate::deferred::DEFAULT_PBR_DEFERRED_LIGHTING_PASS_ID; use crate::*; @@ -751,30 +752,56 @@ impl AsBindGroupShaderType for StandardMaterial { } } -/// The pipeline key for [`StandardMaterial`]. -#[derive(Clone, PartialEq, Eq, Hash)] -pub struct StandardMaterialKey { - normal_map: bool, - cull_mode: Option, - depth_bias: i32, - relief_mapping: bool, - diffuse_transmission: bool, - specular_transmission: bool, +bitflags! { + /// The pipeline key for `StandardMaterial`, packed into 64 bits. + #[derive(Clone, Copy, PartialEq, Eq, Hash)] + pub struct StandardMaterialKey: u64 { + const CULL_FRONT = 0x01; + const CULL_BACK = 0x02; + const NORMAL_MAP = 0x04; + const RELIEF_MAPPING = 0x08; + const DIFFUSE_TRANSMISSION = 0x10; + const SPECULAR_TRANSMISSION = 0x20; + const DEPTH_BIAS = 0xffffffff_00000000; + } } +const STANDARD_MATERIAL_KEY_DEPTH_BIAS_SHIFT: u64 = 32; + impl From<&StandardMaterial> for StandardMaterialKey { fn from(material: &StandardMaterial) -> Self { - StandardMaterialKey { - normal_map: material.normal_map_texture.is_some(), - cull_mode: material.cull_mode, - depth_bias: material.depth_bias as i32, - relief_mapping: matches!( + let mut key = StandardMaterialKey::empty(); + key.set( + StandardMaterialKey::CULL_FRONT, + material.cull_mode == Some(Face::Front), + ); + key.set( + StandardMaterialKey::CULL_BACK, + material.cull_mode == Some(Face::Back), + ); + key.set( + StandardMaterialKey::NORMAL_MAP, + material.normal_map_texture.is_some(), + ); + key.set( + StandardMaterialKey::RELIEF_MAPPING, + matches!( material.parallax_mapping_method, ParallaxMappingMethod::Relief { .. } ), - diffuse_transmission: material.diffuse_transmission > 0.0, - specular_transmission: material.specular_transmission > 0.0, - } + ); + key.set( + StandardMaterialKey::DIFFUSE_TRANSMISSION, + material.diffuse_transmission > 0.0, + ); + key.set( + StandardMaterialKey::SPECULAR_TRANSMISSION, + material.specular_transmission > 0.0, + ); + key.insert(StandardMaterialKey::from_bits_retain( + (material.depth_bias as u64) << STANDARD_MATERIAL_KEY_DEPTH_BIAS_SHIFT, + )); + key } } @@ -823,6 +850,21 @@ impl Material for StandardMaterial { PBR_SHADER_HANDLE.into() } + #[cfg(feature = "meshlet")] + fn meshlet_mesh_fragment_shader() -> ShaderRef { + Self::fragment_shader() + } + + #[cfg(feature = "meshlet")] + fn meshlet_mesh_prepass_fragment_shader() -> ShaderRef { + Self::prepass_fragment_shader() + } + + #[cfg(feature = "meshlet")] + fn meshlet_mesh_deferred_fragment_shader() -> ShaderRef { + Self::deferred_fragment_shader() + } + fn specialize( _pipeline: &MaterialPipeline, descriptor: &mut RenderPipelineDescriptor, @@ -832,32 +874,58 @@ impl Material for StandardMaterial { if let Some(fragment) = descriptor.fragment.as_mut() { let shader_defs = &mut fragment.shader_defs; - if key.bind_group_data.normal_map { + if key + .bind_group_data + .contains(StandardMaterialKey::NORMAL_MAP) + { shader_defs.push("STANDARD_MATERIAL_NORMAL_MAP".into()); } - if key.bind_group_data.relief_mapping { + if key + .bind_group_data + .contains(StandardMaterialKey::RELIEF_MAPPING) + { shader_defs.push("RELIEF_MAPPING".into()); } - if key.bind_group_data.diffuse_transmission { + if key + .bind_group_data + .contains(StandardMaterialKey::DIFFUSE_TRANSMISSION) + { shader_defs.push("STANDARD_MATERIAL_DIFFUSE_TRANSMISSION".into()); } - if key.bind_group_data.specular_transmission { + if key + .bind_group_data + .contains(StandardMaterialKey::SPECULAR_TRANSMISSION) + { shader_defs.push("STANDARD_MATERIAL_SPECULAR_TRANSMISSION".into()); } - if key.bind_group_data.diffuse_transmission || key.bind_group_data.specular_transmission - { + if key.bind_group_data.intersects( + StandardMaterialKey::DIFFUSE_TRANSMISSION + | StandardMaterialKey::SPECULAR_TRANSMISSION, + ) { shader_defs.push("STANDARD_MATERIAL_SPECULAR_OR_DIFFUSE_TRANSMISSION".into()); } } - descriptor.primitive.cull_mode = key.bind_group_data.cull_mode; + + descriptor.primitive.cull_mode = if key + .bind_group_data + .contains(StandardMaterialKey::CULL_FRONT) + { + Some(Face::Front) + } else if key.bind_group_data.contains(StandardMaterialKey::CULL_BACK) { + Some(Face::Back) + } else { + None + }; + if let Some(label) = &mut descriptor.label { *label = format!("pbr_{}", *label).into(); } if let Some(depth_stencil) = descriptor.depth_stencil.as_mut() { - depth_stencil.bias.constant = key.bind_group_data.depth_bias; + depth_stencil.bias.constant = + (key.bind_group_data.bits() >> STANDARD_MATERIAL_KEY_DEPTH_BIAS_SHIFT) as i32; } Ok(()) } diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index f8d9ecc18808c..ab8ea45d080d1 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -1,5 +1,6 @@ mod prepass_bindings; +use bevy_render::batching::{batch_and_prepare_binned_render_phase, sort_binned_render_phase}; use bevy_render::mesh::MeshVertexBufferLayoutRef; use bevy_render::render_resource::binding_types::uniform_buffer; pub use prepass_bindings::*; @@ -16,7 +17,6 @@ use bevy_ecs::{ }; use bevy_math::{Affine3A, Mat4}; use bevy_render::{ - batching::batch_and_prepare_render_phase, globals::{GlobalsBuffer, GlobalsUniform}, prelude::{Camera, Mesh}, render_asset::RenderAssets, @@ -29,6 +29,10 @@ use bevy_render::{ use bevy_transform::prelude::GlobalTransform; use bevy_utils::tracing::error; +#[cfg(feature = "meshlet")] +use crate::meshlet::{ + prepare_material_meshlet_meshes_prepass, queue_material_meshlet_meshes, MeshletGpuScene, +}; use crate::*; use std::{hash::Hash, marker::PhantomData}; @@ -151,11 +155,17 @@ where .add_systems( Render, ( - prepare_previous_view_projection_uniforms, - batch_and_prepare_render_phase::, - batch_and_prepare_render_phase::, + ( + sort_binned_render_phase::, + sort_binned_render_phase:: + ).in_set(RenderSet::PhaseSort), + ( + prepare_previous_view_projection_uniforms, + batch_and_prepare_binned_render_phase::, + batch_and_prepare_binned_render_phase::, + ).in_set(RenderSet::PrepareResources), ) - .in_set(RenderSet::PrepareResources), ); } @@ -172,6 +182,15 @@ where // queue_material_meshes only writes to `material_bind_group_id`, which `queue_prepass_material_meshes` doesn't read .ambiguous_with(queue_material_meshes::), ); + + #[cfg(feature = "meshlet")] + render_app.add_systems( + Render, + prepare_material_meshlet_meshes_prepass:: + .in_set(RenderSet::Queue) + .before(queue_material_meshlet_meshes::) + .run_if(resource_exists::), + ); } } @@ -697,20 +716,20 @@ pub fn queue_prepass_material_meshes( ( &ExtractedView, &VisibleEntities, - Option<&mut RenderPhase>, - Option<&mut RenderPhase>, - Option<&mut RenderPhase>, - Option<&mut RenderPhase>, + Option<&mut BinnedRenderPhase>, + Option<&mut BinnedRenderPhase>, + Option<&mut BinnedRenderPhase>, + Option<&mut BinnedRenderPhase>, Option<&DepthPrepass>, Option<&NormalPrepass>, Option<&MotionVectorPrepass>, Option<&DeferredPrepass>, ), Or<( - With>, - With>, - With>, - With>, + With>, + With>, + With>, + With>, )>, >, ) where @@ -835,50 +854,54 @@ pub fn queue_prepass_material_meshes( match alpha_mode { AlphaMode::Opaque => { if deferred { - opaque_deferred_phase - .as_mut() - .unwrap() - .add(Opaque3dDeferred { - entity: *visible_entity, + opaque_deferred_phase.as_mut().unwrap().add( + OpaqueNoLightmap3dBinKey { draw_function: opaque_draw_deferred, - pipeline_id, + pipeline: pipeline_id, asset_id: mesh_instance.mesh_asset_id, - batch_range: 0..1, - dynamic_offset: None, - }); + material_bind_group_id: material.get_bind_group_id().0, + }, + *visible_entity, + mesh_instance.should_batch(), + ); } else if let Some(opaque_phase) = opaque_phase.as_mut() { - opaque_phase.add(Opaque3dPrepass { - entity: *visible_entity, - draw_function: opaque_draw_prepass, - pipeline_id, - asset_id: mesh_instance.mesh_asset_id, - batch_range: 0..1, - dynamic_offset: None, - }); + opaque_phase.add( + OpaqueNoLightmap3dBinKey { + draw_function: opaque_draw_prepass, + pipeline: pipeline_id, + asset_id: mesh_instance.mesh_asset_id, + material_bind_group_id: material.get_bind_group_id().0, + }, + *visible_entity, + mesh_instance.should_batch(), + ); } } AlphaMode::Mask(_) => { if deferred { - alpha_mask_deferred_phase - .as_mut() - .unwrap() - .add(AlphaMask3dDeferred { - entity: *visible_entity, - draw_function: alpha_mask_draw_deferred, - pipeline_id, - asset_id: mesh_instance.mesh_asset_id, - batch_range: 0..1, - dynamic_offset: None, - }); + let bin_key = OpaqueNoLightmap3dBinKey { + pipeline: pipeline_id, + draw_function: alpha_mask_draw_deferred, + asset_id: mesh_instance.mesh_asset_id, + material_bind_group_id: material.get_bind_group_id().0, + }; + alpha_mask_deferred_phase.as_mut().unwrap().add( + bin_key, + *visible_entity, + mesh_instance.should_batch(), + ); } else if let Some(alpha_mask_phase) = alpha_mask_phase.as_mut() { - alpha_mask_phase.add(AlphaMask3dPrepass { - entity: *visible_entity, + let bin_key = OpaqueNoLightmap3dBinKey { + pipeline: pipeline_id, draw_function: alpha_mask_draw_prepass, - pipeline_id, asset_id: mesh_instance.mesh_asset_id, - batch_range: 0..1, - dynamic_offset: None, - }); + material_bind_group_id: material.get_bind_group_id().0, + }; + alpha_mask_phase.add( + bin_key, + *visible_entity, + mesh_instance.should_batch(), + ); } } AlphaMode::Blend diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 061c34a8b2f3b..2dacc9943c57d 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -1,9 +1,11 @@ +use bevy_asset::AssetId; use bevy_core_pipeline::core_3d::{Transparent3d, CORE_3D_DEPTH_FORMAT}; -use bevy_ecs::entity::EntityHashMap; use bevy_ecs::prelude::*; +use bevy_ecs::{entity::EntityHashMap, system::lifetimeless::Read}; 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, @@ -683,7 +685,7 @@ pub fn prepare_lights( mut light_meta: ResMut, views: Query< (Entity, &ExtractedView, &ExtractedClusterConfig), - With>, + With>, >, ambient_light: Res, point_light_shadow_map: Res, @@ -1056,7 +1058,7 @@ pub fn prepare_lights( color_grading: Default::default(), }, *frustum, - RenderPhase::::default(), + BinnedRenderPhase::::default(), LightEntity::Point { light_entity, face_index, @@ -1115,7 +1117,7 @@ pub fn prepare_lights( color_grading: Default::default(), }, *spot_light_frustum.unwrap(), - RenderPhase::::default(), + BinnedRenderPhase::::default(), LightEntity::Spot { light_entity }, )) .id(); @@ -1195,7 +1197,7 @@ pub fn prepare_lights( color_grading: Default::default(), }, frustum, - RenderPhase::::default(), + BinnedRenderPhase::::default(), LightEntity::Directional { light_entity, cascade_index, @@ -1547,7 +1549,7 @@ pub fn prepare_clusters( render_queue: Res, mesh_pipeline: Res, global_light_meta: Res, - views: Query<(Entity, &ExtractedClustersPointLights), With>>, + views: Query<(Entity, &ExtractedClustersPointLights), With>>, ) { let render_device = render_device.into_inner(); let supports_storage_buffers = matches!( @@ -1604,7 +1606,7 @@ pub fn queue_shadows( pipeline_cache: Res, render_lightmaps: Res, view_lights: Query<(Entity, &ViewLightEntities)>, - mut view_light_shadow_phases: Query<(&LightEntity, &mut RenderPhase)>, + mut view_light_shadow_phases: Query<(&LightEntity, &mut BinnedRenderPhase)>, point_light_entities: Query<&CubemapVisibleEntities, With>, directional_light_entities: Query<&CascadesVisibleEntities, With>, spot_light_entities: Query<&VisibleEntities, With>, @@ -1707,52 +1709,48 @@ pub fn queue_shadows( .material_bind_group_id .set(material.get_bind_group_id()); - shadow_phase.add(Shadow { - draw_function: draw_shadow_mesh, - pipeline: pipeline_id, + shadow_phase.add( + ShadowBinKey { + draw_function: draw_shadow_mesh, + pipeline: pipeline_id, + asset_id: mesh_instance.mesh_asset_id, + }, entity, - distance: 0.0, // TODO: sort front-to-back - batch_range: 0..1, - dynamic_offset: None, - }); + mesh_instance.should_batch(), + ); } } } } pub struct Shadow { - pub distance: f32, - pub entity: Entity, - pub pipeline: CachedRenderPipelineId, - pub draw_function: DrawFunctionId, + pub key: ShadowBinKey, + pub representative_entity: Entity, pub batch_range: Range, pub dynamic_offset: Option, } -impl PhaseItem for Shadow { - type SortKey = usize; +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ShadowBinKey { + /// The identifier of the render pipeline. + pub pipeline: CachedRenderPipelineId, - #[inline] - fn entity(&self) -> Entity { - self.entity - } + /// The function used to draw. + pub draw_function: DrawFunctionId, - #[inline] - fn sort_key(&self) -> Self::SortKey { - self.pipeline.id() - } + /// The mesh. + pub asset_id: AssetId, +} +impl PhaseItem for Shadow { #[inline] - fn draw_function(&self) -> DrawFunctionId { - self.draw_function + fn entity(&self) -> Entity { + self.representative_entity } #[inline] - fn sort(items: &mut [Self]) { - // The shadow phase is sorted by pipeline id for performance reasons. - // Grouping all draw commands using the same pipeline together performs - // better than rebinding everything at a high rate. - radsort::sort_by_key(items, |item| item.sort_key()); + fn draw_function(&self) -> DrawFunctionId { + self.key.draw_function } #[inline] @@ -1776,16 +1774,35 @@ impl PhaseItem for Shadow { } } +impl BinnedPhaseItem for Shadow { + type BinKey = ShadowBinKey; + + #[inline] + fn new( + key: Self::BinKey, + representative_entity: Entity, + batch_range: Range, + dynamic_offset: Option, + ) -> Self { + Shadow { + key, + representative_entity, + batch_range, + dynamic_offset, + } + } +} + impl CachedRenderPipelinePhaseItem for Shadow { #[inline] fn cached_pipeline(&self) -> CachedRenderPipelineId { - self.pipeline + self.key.pipeline } } pub struct ShadowPassNode { - main_view_query: QueryState<&'static ViewLightEntities>, - view_light_query: QueryState<(&'static ShadowView, &'static RenderPhase)>, + main_view_query: QueryState>, + view_light_query: QueryState<(Read, Read>)>, } impl ShadowPassNode { @@ -1809,6 +1826,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 +1840,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 +1857,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_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 31f3a29352a28..bd06ceca9c5c3 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -13,7 +13,8 @@ use bevy_ecs::{ use bevy_math::{Affine3, Rect, UVec2, Vec4}; use bevy_render::{ batching::{ - batch_and_prepare_render_phase, write_batched_instance_buffer, GetBatchData, + batch_and_prepare_binned_render_phase, batch_and_prepare_sorted_render_phase, + sort_binned_render_phase, write_batched_instance_buffer, GetBatchData, GetBinnedBatchData, NoAutomaticBatching, }, mesh::*, @@ -125,13 +126,24 @@ impl Plugin for MeshRenderPlugin { Render, ( ( - batch_and_prepare_render_phase::, - batch_and_prepare_render_phase::, - batch_and_prepare_render_phase::, - batch_and_prepare_render_phase::, - batch_and_prepare_render_phase::, - batch_and_prepare_render_phase::, - batch_and_prepare_render_phase::, + sort_binned_render_phase::, + sort_binned_render_phase::, + sort_binned_render_phase::, + sort_binned_render_phase::, + sort_binned_render_phase::, + ) + .in_set(RenderSet::PhaseSort), + ( + batch_and_prepare_binned_render_phase::, + batch_and_prepare_sorted_render_phase::, + batch_and_prepare_sorted_render_phase::, + batch_and_prepare_binned_render_phase::, + batch_and_prepare_binned_render_phase::, + batch_and_prepare_binned_render_phase::, + batch_and_prepare_binned_render_phase::< + AlphaMask3dDeferred, + MeshPipeline, + >, ) .in_set(RenderSet::PrepareResources), write_batched_instance_buffer:: @@ -471,6 +483,25 @@ impl GetBatchData for MeshPipeline { } } +impl GetBinnedBatchData for MeshPipeline { + type Param = (SRes, SRes); + + type BufferData = MeshUniform; + + fn get_batch_data( + (mesh_instances, lightmaps): &SystemParamItem, + entity: Entity, + ) -> Option { + let mesh_instance = mesh_instances.get(&entity)?; + let maybe_lightmap = lightmaps.render_lightmaps.get(&entity); + + Some(MeshUniform::new( + &mesh_instance.transforms, + maybe_lightmap.map(|lightmap| lightmap.uv_rect), + )) + } +} + bitflags::bitflags! { #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] #[repr(transparent)] diff --git a/crates/bevy_pbr/src/render/pbr.wgsl b/crates/bevy_pbr/src/render/pbr.wgsl index def70dd89b3ed..7421f1e381353 100644 --- a/crates/bevy_pbr/src/render/pbr.wgsl +++ b/crates/bevy_pbr/src/render/pbr.wgsl @@ -16,11 +16,24 @@ } #endif +#ifdef MESHLET_MESH_MATERIAL_PASS +#import bevy_pbr::meshlet_visibility_buffer_resolve::resolve_vertex_output +#endif + @fragment fn fragment( +#ifdef MESHLET_MESH_MATERIAL_PASS + @builtin(position) frag_coord: vec4, +#else in: VertexOutput, @builtin(front_facing) is_front: bool, +#endif ) -> FragmentOutput { +#ifdef MESHLET_MESH_MATERIAL_PASS + let in = resolve_vertex_output(frag_coord); + let is_front = true; +#endif + // generate a PbrInput struct from the StandardMaterial bindings var pbr_input = pbr_input_from_standard_material(in, is_front); diff --git a/crates/bevy_pbr/src/render/pbr_fragment.wgsl b/crates/bevy_pbr/src/render/pbr_fragment.wgsl index 07b37c3b46962..c3b3e949888dd 100644 --- a/crates/bevy_pbr/src/render/pbr_fragment.wgsl +++ b/crates/bevy_pbr/src/render/pbr_fragment.wgsl @@ -17,7 +17,9 @@ #import bevy_pbr::gtao_utils::gtao_multibounce #endif -#ifdef PREPASS_PIPELINE +#ifdef MESHLET_MESH_MATERIAL_PASS +#import bevy_pbr::meshlet_visibility_buffer_resolve::VertexOutput +#else ifdef PREPASS_PIPELINE #import bevy_pbr::prepass_io::VertexOutput #else #import bevy_pbr::forward_io::VertexOutput @@ -31,7 +33,12 @@ fn pbr_input_from_vertex_output( ) -> pbr_types::PbrInput { var pbr_input: pbr_types::PbrInput = pbr_types::pbr_input_new(); +#ifdef MESHLET_MESH_MATERIAL_PASS + pbr_input.flags = in.mesh_flags; +#else pbr_input.flags = mesh[in.instance_index].flags; +#endif + pbr_input.is_orthographic = view.projection[3].w == 1.0; pbr_input.V = pbr_functions::calculate_view(in.world_position, pbr_input.is_orthographic); pbr_input.frag_coord = in.position; @@ -98,7 +105,11 @@ fn pbr_input_from_standard_material( #endif // VERTEX_TANGENTS if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_BASE_COLOR_TEXTURE_BIT) != 0u) { +#ifdef MESHLET_MESH_MATERIAL_PASS + pbr_input.material.base_color *= textureSampleGrad(pbr_bindings::base_color_texture, pbr_bindings::base_color_sampler, uv, in.ddx_uv, in.ddy_uv); +#else pbr_input.material.base_color *= textureSampleBias(pbr_bindings::base_color_texture, pbr_bindings::base_color_sampler, uv, view.mip_bias); +#endif } #endif // VERTEX_UVS @@ -117,7 +128,11 @@ fn pbr_input_from_standard_material( var emissive: vec4 = pbr_bindings::material.emissive; #ifdef VERTEX_UVS if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_EMISSIVE_TEXTURE_BIT) != 0u) { +#ifdef MESHLET_MESH_MATERIAL_PASS + emissive = vec4(emissive.rgb * textureSampleGrad(pbr_bindings::emissive_texture, pbr_bindings::emissive_sampler, uv, in.ddx_uv, in.ddy_uv).rgb, 1.0); +#else emissive = vec4(emissive.rgb * textureSampleBias(pbr_bindings::emissive_texture, pbr_bindings::emissive_sampler, uv, view.mip_bias).rgb, 1.0); +#endif } #endif pbr_input.material.emissive = emissive; @@ -128,7 +143,11 @@ fn pbr_input_from_standard_material( let roughness = lighting::perceptualRoughnessToRoughness(perceptual_roughness); #ifdef VERTEX_UVS if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_METALLIC_ROUGHNESS_TEXTURE_BIT) != 0u) { +#ifdef MESHLET_MESH_MATERIAL_PASS + let metallic_roughness = textureSampleGrad(pbr_bindings::metallic_roughness_texture, pbr_bindings::metallic_roughness_sampler, uv, in.ddx_uv, in.ddy_uv); +#else let metallic_roughness = textureSampleBias(pbr_bindings::metallic_roughness_texture, pbr_bindings::metallic_roughness_sampler, uv, view.mip_bias); +#endif // Sampling from GLTF standard channels for now metallic *= metallic_roughness.b; perceptual_roughness *= metallic_roughness.g; @@ -140,7 +159,11 @@ fn pbr_input_from_standard_material( var specular_transmission: f32 = pbr_bindings::material.specular_transmission; #ifdef PBR_TRANSMISSION_TEXTURES_SUPPORTED if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_SPECULAR_TRANSMISSION_TEXTURE_BIT) != 0u) { - specular_transmission *= textureSample(pbr_bindings::specular_transmission_texture, pbr_bindings::specular_transmission_sampler, uv).r; +#ifdef MESHLET_MESH_MATERIAL_PASS + specular_transmission *= textureSampleGrad(pbr_bindings::specular_transmission_texture, pbr_bindings::specular_transmission_sampler, uv, in.ddx_uv, in.ddy_uv).r; +#else + specular_transmission *= textureSampleBias(pbr_bindings::specular_transmission_texture, pbr_bindings::specular_transmission_sampler, uv, view.mip_bias).r; +#endif } #endif pbr_input.material.specular_transmission = specular_transmission; @@ -148,19 +171,30 @@ fn pbr_input_from_standard_material( var thickness: f32 = pbr_bindings::material.thickness; #ifdef PBR_TRANSMISSION_TEXTURES_SUPPORTED if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_THICKNESS_TEXTURE_BIT) != 0u) { - thickness *= textureSample(pbr_bindings::thickness_texture, pbr_bindings::thickness_sampler, uv).g; +#ifdef MESHLET_MESH_MATERIAL_PASS + thickness *= textureSampleGrad(pbr_bindings::thickness_texture, pbr_bindings::thickness_sampler, uv, in.ddx_uv, in.ddy_uv).g; +#else + thickness *= textureSampleBias(pbr_bindings::thickness_texture, pbr_bindings::thickness_sampler, uv, view.mip_bias).g; +#endif } #endif // scale thickness, accounting for non-uniform scaling (e.g. a “squished” mesh) + // TODO: Meshlet support +#ifndef MESHLET_MESH_MATERIAL_PASS thickness *= length( (transpose(mesh[in.instance_index].model) * vec4(pbr_input.N, 0.0)).xyz ); +#endif pbr_input.material.thickness = thickness; var diffuse_transmission = pbr_bindings::material.diffuse_transmission; #ifdef PBR_TRANSMISSION_TEXTURES_SUPPORTED if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_DIFFUSE_TRANSMISSION_TEXTURE_BIT) != 0u) { - diffuse_transmission *= textureSample(pbr_bindings::diffuse_transmission_texture, pbr_bindings::diffuse_transmission_sampler, uv).a; +#ifdef MESHLET_MESH_MATERIAL_PASS + diffuse_transmission *= textureSampleGrad(pbr_bindings::diffuse_transmission_texture, pbr_bindings::diffuse_transmission_sampler, uv, in.ddx_uv, in.ddy_uv).a; +#else + diffuse_transmission *= textureSampleBias(pbr_bindings::diffuse_transmission_texture, pbr_bindings::diffuse_transmission_sampler, uv, view.mip_bias).a; +#endif } #endif pbr_input.material.diffuse_transmission = diffuse_transmission; @@ -169,7 +203,11 @@ fn pbr_input_from_standard_material( var specular_occlusion: f32 = 1.0; #ifdef VERTEX_UVS if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_OCCLUSION_TEXTURE_BIT) != 0u) { +#ifdef MESHLET_MESH_MATERIAL_PASS + diffuse_occlusion = vec3(textureSampleGrad(pbr_bindings::occlusion_texture, pbr_bindings::occlusion_sampler, uv, in.ddx_uv, in.ddy_uv).r); +#else diffuse_occlusion = vec3(textureSampleBias(pbr_bindings::occlusion_texture, pbr_bindings::occlusion_sampler, uv, view.mip_bias).r); +#endif } #endif #ifdef SCREEN_SPACE_AMBIENT_OCCLUSION @@ -199,9 +237,14 @@ fn pbr_input_from_standard_material( uv, #endif view.mip_bias, +#ifdef MESHLET_MESH_MATERIAL_PASS + in.ddx_uv, + in.ddy_uv, +#endif ); #endif +// TODO: Meshlet support #ifdef LIGHTMAP pbr_input.lightmap_light = lightmap( in.uv_b, diff --git a/crates/bevy_pbr/src/render/pbr_functions.wgsl b/crates/bevy_pbr/src/render/pbr_functions.wgsl index 24090aab329a5..1c602944662c5 100644 --- a/crates/bevy_pbr/src/render/pbr_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_functions.wgsl @@ -74,6 +74,10 @@ fn apply_normal_mapping( uv: vec2, #endif mip_bias: f32, +#ifdef MESHLET_MESH_MATERIAL_PASS + ddx_uv: vec2, + ddy_uv: vec2, +#endif ) -> vec3 { // NOTE: The mikktspace method of normal mapping explicitly requires that the world normal NOT // be re-normalized in the fragment shader. This is primarily to match the way mikktspace @@ -98,7 +102,11 @@ fn apply_normal_mapping( #ifdef VERTEX_UVS #ifdef STANDARD_MATERIAL_NORMAL_MAP // Nt is the tangent-space normal. +#ifdef MESHLET_MESH_MATERIAL_PASS + var Nt = textureSampleGrad(pbr_bindings::normal_map_texture, pbr_bindings::normal_map_sampler, uv, ddx_uv, ddy_uv).rgb; +#else var Nt = textureSampleBias(pbr_bindings::normal_map_texture, pbr_bindings::normal_map_sampler, uv, mip_bias).rgb; +#endif if (standard_material_flags & pbr_types::STANDARD_MATERIAL_FLAGS_TWO_COMPONENT_NORMAL_MAP) != 0u { // Only use the xy components and derive z for 2-component normal maps. Nt = vec3(Nt.rg * 2.0 - 1.0, 0.0); diff --git a/crates/bevy_pbr/src/render/pbr_prepass.wgsl b/crates/bevy_pbr/src/render/pbr_prepass.wgsl index 8be86b5af2175..c77d71ebca16d 100644 --- a/crates/bevy_pbr/src/render/pbr_prepass.wgsl +++ b/crates/bevy_pbr/src/render/pbr_prepass.wgsl @@ -6,14 +6,27 @@ prepass_io, mesh_view_bindings::view, } - + +#ifdef MESHLET_MESH_MATERIAL_PASS +#import bevy_pbr::meshlet_visibility_buffer_resolve::resolve_vertex_output +#endif + #ifdef PREPASS_FRAGMENT @fragment fn fragment( +#ifdef MESHLET_MESH_MATERIAL_PASS + @builtin(position) frag_coord: vec4, +#else in: prepass_io::VertexOutput, @builtin(front_facing) is_front: bool, +#endif ) -> prepass_io::FragmentOutput { +#ifdef MESHLET_MESH_MATERIAL_PASS + let in = resolve_vertex_output(frag_coord); + let is_front = true; +#else pbr_prepass_functions::prepass_alpha_discard(in); +#endif var out: prepass_io::FragmentOutput; @@ -46,6 +59,10 @@ fn fragment( in.uv, #endif // VERTEX_UVS view.mip_bias, +#ifdef MESHLET_MESH_MATERIAL_PASS + in.ddx_uv, + in.ddy_uv, +#endif // MESHLET_MESH_MATERIAL_PASS ); out.normal = vec4(normal * 0.5 + vec3(0.5), 1.0); @@ -55,7 +72,11 @@ fn fragment( #endif // NORMAL_PREPASS #ifdef MOTION_VECTOR_PREPASS +#ifdef MESHLET_MESH_MATERIAL_PASS + out.motion_vector = in.motion_vector; +#else out.motion_vector = pbr_prepass_functions::calculate_motion_vector(in.world_position, in.previous_world_position); +#endif #endif return out; diff --git a/crates/bevy_ptr/Cargo.toml b/crates/bevy_ptr/Cargo.toml index ae443652d11da..d76b911e78945 100644 --- a/crates/bevy_ptr/Cargo.toml +++ b/crates/bevy_ptr/Cargo.toml @@ -12,3 +12,7 @@ keywords = ["bevy", "no_std"] [lints] workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +all-features = true diff --git a/crates/bevy_ptr/src/lib.rs b/crates/bevy_ptr/src/lib.rs index d6c0322461528..aae2a4dfd8414 100644 --- a/crates/bevy_ptr/src/lib.rs +++ b/crates/bevy_ptr/src/lib.rs @@ -1,5 +1,11 @@ #![doc = include_str!("../README.md")] #![no_std] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![allow(unsafe_code)] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] use core::fmt::{self, Formatter, Pointer}; use core::{ diff --git a/crates/bevy_reflect/Cargo.toml b/crates/bevy_reflect/Cargo.toml index 5cb69dc88edbb..b9fe3e4bb6896 100644 --- a/crates/bevy_reflect/Cargo.toml +++ b/crates/bevy_reflect/Cargo.toml @@ -59,4 +59,5 @@ required-features = ["documentation"] workspace = true [package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] all-features = true diff --git a/crates/bevy_reflect/bevy_reflect_derive/Cargo.toml b/crates/bevy_reflect/bevy_reflect_derive/Cargo.toml index e076283ac8453..deee32d6664c4 100644 --- a/crates/bevy_reflect/bevy_reflect_derive/Cargo.toml +++ b/crates/bevy_reflect/bevy_reflect_derive/Cargo.toml @@ -26,3 +26,7 @@ uuid = { version = "1.1", features = ["v4"] } [lints] workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +all-features = true diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/lib.rs b/crates/bevy_reflect/bevy_reflect_derive/src/lib.rs index 5be3723b7b0d9..743c558dded22 100644 --- a/crates/bevy_reflect/bevy_reflect_derive/src/lib.rs +++ b/crates/bevy_reflect/bevy_reflect_derive/src/lib.rs @@ -1,3 +1,5 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] + //! This crate contains macros used by Bevy's `Reflect` API. //! //! The main export of this crate is the derive macro for [`Reflect`]. This allows diff --git a/crates/bevy_reflect/src/impls/std.rs b/crates/bevy_reflect/src/impls/std.rs index c35ec3d40ca34..f81db9d6c7506 100644 --- a/crates/bevy_reflect/src/impls/std.rs +++ b/crates/bevy_reflect/src/impls/std.rs @@ -1,17 +1,16 @@ use crate::std_traits::ReflectDefault; use crate::{self as bevy_reflect, ReflectFromPtr, ReflectFromReflect, ReflectOwned, TypeRegistry}; use crate::{ - impl_type_path, map_apply, map_partial_eq, Array, ArrayInfo, ArrayIter, DynamicEnum, - DynamicMap, Enum, EnumInfo, FromReflect, FromType, GetTypeRegistration, List, ListInfo, - ListIter, Map, MapInfo, MapIter, Reflect, ReflectDeserialize, ReflectKind, ReflectMut, - ReflectRef, ReflectSerialize, TupleVariantInfo, TypeInfo, TypePath, TypeRegistration, Typed, - UnitVariantInfo, UnnamedField, ValueInfo, VariantFieldIter, VariantInfo, VariantType, + impl_type_path, map_apply, map_partial_eq, Array, ArrayInfo, ArrayIter, DynamicMap, + FromReflect, FromType, GetTypeRegistration, List, ListInfo, ListIter, Map, MapInfo, MapIter, + Reflect, ReflectDeserialize, ReflectKind, ReflectMut, ReflectRef, ReflectSerialize, TypeInfo, + TypePath, TypeRegistration, Typed, ValueInfo, }; use crate::utility::{ reflect_hasher, GenericTypeInfoCell, GenericTypePathCell, NonGenericTypeInfoCell, }; -use bevy_reflect_derive::impl_reflect_value; +use bevy_reflect_derive::{impl_reflect, impl_reflect_value}; use std::fmt; use std::{ any::Any, @@ -219,6 +218,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) => { @@ -995,252 +995,14 @@ impl GetTypeRegistr } } -impl GetTypeRegistration for Option { - fn get_type_registration() -> TypeRegistration { - TypeRegistration::of::>() - } - - fn register_type_dependencies(registry: &mut TypeRegistry) { - registry.register::(); +impl_reflect! { + #[type_path = "core::option"] + enum Option { + None, + Some(T), } } -impl Enum for Option { - fn field(&self, _name: &str) -> Option<&dyn Reflect> { - None - } - - fn field_at(&self, index: usize) -> Option<&dyn Reflect> { - match self { - Some(value) if index == 0 => Some(value), - _ => None, - } - } - - fn field_mut(&mut self, _name: &str) -> Option<&mut dyn Reflect> { - None - } - - fn field_at_mut(&mut self, index: usize) -> Option<&mut dyn Reflect> { - match self { - Some(value) if index == 0 => Some(value), - _ => None, - } - } - - fn index_of(&self, _name: &str) -> Option { - None - } - - fn name_at(&self, _index: usize) -> Option<&str> { - None - } - - fn iter_fields(&self) -> VariantFieldIter { - VariantFieldIter::new(self) - } - - #[inline] - fn field_len(&self) -> usize { - match self { - Some(..) => 1, - None => 0, - } - } - - #[inline] - fn variant_name(&self) -> &str { - match self { - Some(..) => "Some", - None => "None", - } - } - - fn variant_index(&self) -> usize { - match self { - None => 0, - Some(..) => 1, - } - } - - #[inline] - fn variant_type(&self) -> VariantType { - match self { - Some(..) => VariantType::Tuple, - None => VariantType::Unit, - } - } - - fn clone_dynamic(&self) -> DynamicEnum { - DynamicEnum::from_ref::(self) - } -} - -impl Reflect for Option { - #[inline] - fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { - Some(::type_info()) - } - - #[inline] - fn into_any(self: Box) -> Box { - self - } - - #[inline] - fn as_any(&self) -> &dyn Any { - self - } - - #[inline] - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - #[inline] - fn into_reflect(self: Box) -> Box { - self - } - - fn as_reflect(&self) -> &dyn Reflect { - self - } - - fn as_reflect_mut(&mut self) -> &mut dyn Reflect { - self - } - - #[inline] - fn apply(&mut self, value: &dyn Reflect) { - if let ReflectRef::Enum(value) = value.reflect_ref() { - if self.variant_name() == value.variant_name() { - // Same variant -> just update fields - for (index, field) in value.iter_fields().enumerate() { - if let Some(v) = self.field_at_mut(index) { - v.apply(field.value()); - } - } - } else { - // New variant -> perform a switch - match value.variant_name() { - "Some" => { - let field = T::take_from_reflect( - value - .field_at(0) - .unwrap_or_else(|| { - panic!( - "Field in `Some` variant of {} should exist", - Self::type_path() - ) - }) - .clone_value(), - ) - .unwrap_or_else(|_| { - panic!( - "Field in `Some` variant of {} should be of type {}", - Self::type_path(), - T::type_path() - ) - }); - *self = Some(field); - } - "None" => { - *self = None; - } - _ => panic!("Enum is not a {}.", Self::type_path()), - } - } - } - } - - #[inline] - fn set(&mut self, value: Box) -> Result<(), Box> { - *self = value.take()?; - Ok(()) - } - - fn reflect_kind(&self) -> ReflectKind { - ReflectKind::Enum - } - - fn reflect_ref(&self) -> ReflectRef { - ReflectRef::Enum(self) - } - - fn reflect_mut(&mut self) -> ReflectMut { - ReflectMut::Enum(self) - } - - fn reflect_owned(self: Box) -> ReflectOwned { - ReflectOwned::Enum(self) - } - - #[inline] - fn clone_value(&self) -> Box { - Box::new(Enum::clone_dynamic(self)) - } - - fn reflect_hash(&self) -> Option { - crate::enum_hash(self) - } - - fn reflect_partial_eq(&self, value: &dyn Reflect) -> Option { - crate::enum_partial_eq(self, value) - } -} - -impl FromReflect for Option { - fn from_reflect(reflect: &dyn Reflect) -> Option { - if let ReflectRef::Enum(dyn_enum) = reflect.reflect_ref() { - match dyn_enum.variant_name() { - "Some" => { - let field = T::take_from_reflect( - dyn_enum - .field_at(0) - .unwrap_or_else(|| { - panic!( - "Field in `Some` variant of {} should exist", - Option::::type_path() - ) - }) - .clone_value(), - ) - .unwrap_or_else(|_| { - panic!( - "Field in `Some` variant of {} should be of type {}", - Option::::type_path(), - T::type_path() - ) - }); - Some(Some(field)) - } - "None" => Some(None), - name => panic!( - "variant with name `{}` does not exist on enum `{}`", - name, - Self::type_path() - ), - } - } else { - None - } - } -} - -impl Typed for Option { - fn type_info() -> &'static TypeInfo { - static CELL: GenericTypeInfoCell = GenericTypeInfoCell::new(); - CELL.get_or_insert::(|| { - let none_variant = VariantInfo::Unit(UnitVariantInfo::new("None")); - let some_variant = - VariantInfo::Tuple(TupleVariantInfo::new("Some", &[UnnamedField::new::(0)])); - TypeInfo::Enum(EnumInfo::new::(&[none_variant, some_variant])) - }) - } -} - -impl_type_path!(::core::option::Option); - impl TypePath for &'static T { fn type_path() -> &'static str { static CELL: GenericTypePathCell = GenericTypePathCell::new(); diff --git a/crates/bevy_reflect/src/lib.rs b/crates/bevy_reflect/src/lib.rs index cf4a181cbfc79..073fb5000bfee 100644 --- a/crates/bevy_reflect/src/lib.rs +++ b/crates/bevy_reflect/src/lib.rs @@ -1,5 +1,10 @@ // FIXME(3492): remove once docs are ready #![allow(missing_docs)] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc( + html_logo_url = "https://bevyengine.org/assets/icon.png", + html_favicon_url = "https://bevyengine.org/assets/icon.png" +)] //! Reflection in Rust. //! @@ -329,7 +334,7 @@ //! The way it works is by moving the serialization logic into common serializers and deserializers: //! * [`ReflectSerializer`] //! * [`TypedReflectSerializer`] -//! * [`UntypedReflectDeserializer`] +//! * [`ReflectDeserializer`] //! * [`TypedReflectDeserializer`] //! //! All of these structs require a reference to the [registry] so that [type information] can be retrieved, @@ -342,7 +347,7 @@ //! and the value is the serialized data. //! The `TypedReflectSerializer` will simply output the serialized data. //! -//! The `UntypedReflectDeserializer` can be used to deserialize this map and return a `Box`, +//! The `ReflectDeserializer` can be used to deserialize this map and return a `Box`, //! where the underlying type will be a dynamic type representing some concrete type (except for value types). //! //! Again, it's important to remember that dynamic types may need to be converted to their concrete counterparts @@ -352,7 +357,7 @@ //! ``` //! # use serde::de::DeserializeSeed; //! # use bevy_reflect::{ -//! # serde::{ReflectSerializer, UntypedReflectDeserializer}, +//! # serde::{ReflectSerializer, ReflectDeserializer}, //! # Reflect, FromReflect, TypeRegistry //! # }; //! #[derive(Reflect, PartialEq, Debug)] @@ -373,7 +378,7 @@ //! let serialized_value: String = ron::to_string(&reflect_serializer).unwrap(); //! //! // Deserialize -//! let reflect_deserializer = UntypedReflectDeserializer::new(®istry); +//! let reflect_deserializer = ReflectDeserializer::new(®istry); //! let deserialized_value: Box = reflect_deserializer.deserialize( //! &mut ron::Deserializer::from_str(&serialized_value).unwrap() //! ).unwrap(); @@ -455,7 +460,7 @@ //! [`serde`]: ::serde //! [`ReflectSerializer`]: serde::ReflectSerializer //! [`TypedReflectSerializer`]: serde::TypedReflectSerializer -//! [`UntypedReflectDeserializer`]: serde::UntypedReflectDeserializer +//! [`ReflectDeserializer`]: serde::ReflectDeserializer //! [`TypedReflectDeserializer`]: serde::TypedReflectDeserializer //! [registry]: TypeRegistry //! [type information]: TypeInfo @@ -467,7 +472,6 @@ //! [orphan rule]: https://doc.rust-lang.org/book/ch10-02-traits.html#implementing-a-trait-on-a-type:~:text=But%20we%20can%E2%80%99t,implementation%20to%20use. //! [`bevy_reflect_derive/documentation`]: bevy_reflect_derive //! [derive `Reflect`]: derive@crate::Reflect -#![cfg_attr(docsrs, feature(doc_auto_cfg))] mod array; mod fields; @@ -606,7 +610,7 @@ mod tests { use super::prelude::*; use super::*; use crate as bevy_reflect; - use crate::serde::{ReflectSerializer, UntypedReflectDeserializer}; + use crate::serde::{ReflectDeserializer, ReflectSerializer}; use crate::utility::GenericTypePathCell; #[test] @@ -1219,7 +1223,7 @@ mod tests { let serialized = to_string_pretty(&serializer, PrettyConfig::default()).unwrap(); let mut deserializer = Deserializer::from_str(&serialized).unwrap(); - let reflect_deserializer = UntypedReflectDeserializer::new(®istry); + let reflect_deserializer = ReflectDeserializer::new(®istry); let value = reflect_deserializer.deserialize(&mut deserializer).unwrap(); let dynamic_struct = value.take::().unwrap(); @@ -2379,7 +2383,7 @@ bevy_reflect::tests::Test { registry.register::(); registry.register::(); - let de = UntypedReflectDeserializer::new(®istry); + let de = ReflectDeserializer::new(®istry); let mut deserializer = Deserializer::from_str(data).expect("Failed to acquire deserializer"); @@ -2436,7 +2440,7 @@ bevy_reflect::tests::Test { registry.add_registration(Vec3::get_type_registration()); registry.add_registration(f32::get_type_registration()); - let de = UntypedReflectDeserializer::new(®istry); + let de = ReflectDeserializer::new(®istry); let mut deserializer = Deserializer::from_str(data).expect("Failed to acquire deserializer"); diff --git a/crates/bevy_reflect/src/path/parse.rs b/crates/bevy_reflect/src/path/parse.rs index 1a90586859ec9..8195a23a31cb4 100644 --- a/crates/bevy_reflect/src/path/parse.rs +++ b/crates/bevy_reflect/src/path/parse.rs @@ -65,6 +65,7 @@ impl<'a> PathParser<'a> { // the last byte before an ASCII utf-8 character (ie: it is a char // boundary). // - The slice always starts after a symbol ie: an ASCII character's boundary. + #[allow(unsafe_code)] let ident = unsafe { from_utf8_unchecked(ident) }; self.remaining = remaining; diff --git a/crates/bevy_reflect/src/serde/de.rs b/crates/bevy_reflect/src/serde/de.rs index a121a7831c188..3087298a8d1e8 100644 --- a/crates/bevy_reflect/src/serde/de.rs +++ b/crates/bevy_reflect/src/serde/de.rs @@ -240,51 +240,6 @@ impl<'de> Deserialize<'de> for Ident { } } -/// A general purpose deserializer for reflected types. -/// -/// This will return a [`Box`] containing the deserialized data. -/// For non-value types, this `Box` will contain the dynamic equivalent. For example, a -/// deserialized struct will return a [`DynamicStruct`] and a `Vec` will return a -/// [`DynamicList`]. For value types, this `Box` will contain the actual value. -/// For example, an `f32` will contain the actual `f32` type. -/// -/// This means that converting to any concrete instance will require the use of -/// [`FromReflect`], or downcasting for value types. -/// -/// Because the type isn't known ahead of time, the serialized data must take the form of -/// a map containing the following entries (in order): -/// 1. `type`: The _full_ [type path] -/// 2. `value`: The serialized value of the reflected type -/// -/// If the type is already known and the [`TypeInfo`] for it can be retrieved, -/// [`TypedReflectDeserializer`] may be used instead to avoid requiring these entries. -/// -/// [`Box`]: crate::Reflect -/// [`FromReflect`]: crate::FromReflect -/// [type path]: crate::TypePath::type_path -pub struct UntypedReflectDeserializer<'a> { - registry: &'a TypeRegistry, -} - -impl<'a> UntypedReflectDeserializer<'a> { - pub fn new(registry: &'a TypeRegistry) -> Self { - Self { registry } - } -} - -impl<'a, 'de> DeserializeSeed<'de> for UntypedReflectDeserializer<'a> { - type Value = Box; - - fn deserialize(self, deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - deserializer.deserialize_map(UntypedReflectDeserializerVisitor { - registry: self.registry, - }) - } -} - /// A deserializer for type registrations. /// /// This will return a [`&TypeRegistration`] corresponding to the given type. @@ -333,53 +288,217 @@ impl<'a, 'de> DeserializeSeed<'de> for TypeRegistrationDeserializer<'a> { } } -struct UntypedReflectDeserializerVisitor<'a> { +/// A general purpose deserializer for reflected types. +/// +/// This is the deserializer counterpart to [`ReflectSerializer`]. +/// +/// See [`TypedReflectDeserializer`] for a deserializer that expects a known type. +/// +/// # Input +/// +/// This deserializer expects a map with a single entry, +/// where the key is the _full_ [type path] of the reflected type +/// and the value is the serialized data. +/// +/// # Output +/// +/// This deserializer will return a [`Box`] containing the deserialized data. +/// +/// For value types (i.e. [`ReflectKind::Value`]) or types that register [`ReflectDeserialize`] type data, +/// this `Box` will contain the expected type. +/// For example, deserializing an `i32` will return a `Box` (as a `Box`). +/// +/// Otherwise, this `Box` will contain the dynamic equivalent. +/// For example, a deserialized struct might return a [`Box`] +/// and a deserialized `Vec` might return a [`Box`]. +/// +/// This means that if the actual type is needed, these dynamic representations will need to +/// be converted to the concrete type using [`FromReflect`] or [`ReflectFromReflect`]. +/// +/// # Example +/// +/// ``` +/// # use serde::de::DeserializeSeed; +/// # use bevy_reflect::prelude::*; +/// # use bevy_reflect::{DynamicStruct, TypeRegistry, serde::ReflectDeserializer}; +/// #[derive(Reflect, PartialEq, Debug)] +/// #[type_path = "my_crate"] +/// struct MyStruct { +/// value: i32 +/// } +/// +/// let mut registry = TypeRegistry::default(); +/// registry.register::(); +/// +/// let input = r#"{ +/// "my_crate::MyStruct": ( +/// value: 123 +/// ) +/// }"#; +/// +/// let mut deserializer = ron::Deserializer::from_str(input).unwrap(); +/// let reflect_deserializer = ReflectDeserializer::new(®istry); +/// +/// let output: Box = reflect_deserializer.deserialize(&mut deserializer).unwrap(); +/// +/// // Since `MyStruct` is not a value type and does not register `ReflectDeserialize`, +/// // we know that its deserialized representation will be a `DynamicStruct`. +/// assert!(output.is::()); +/// assert!(output.represents::()); +/// +/// // We can convert back to `MyStruct` using `FromReflect`. +/// let value: MyStruct = ::from_reflect(&*output).unwrap(); +/// assert_eq!(value, MyStruct { value: 123 }); +/// +/// // We can also do this dynamically with `ReflectFromReflect`. +/// let type_id = output.get_represented_type_info().unwrap().type_id(); +/// let reflect_from_reflect = registry.get_type_data::(type_id).unwrap(); +/// let value: Box = reflect_from_reflect.from_reflect(&*output).unwrap(); +/// assert!(value.is::()); +/// assert_eq!(value.take::().unwrap(), MyStruct { value: 123 }); +/// ``` +/// +/// [`ReflectSerializer`]: crate::serde::ReflectSerializer +/// [type path]: crate::TypePath::type_path +/// [`Box`]: crate::Reflect +/// [`ReflectKind::Value`]: crate::ReflectKind::Value +/// [`ReflectDeserialize`]: crate::ReflectDeserialize +/// [`Box`]: crate::DynamicStruct +/// [`Box`]: crate::DynamicList +/// [`FromReflect`]: crate::FromReflect +/// [`ReflectFromReflect`]: crate::ReflectFromReflect +pub struct ReflectDeserializer<'a> { registry: &'a TypeRegistry, } -impl<'a, 'de> Visitor<'de> for UntypedReflectDeserializerVisitor<'a> { - type Value = Box; - - fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { - formatter.write_str("map containing `type` and `value` entries for the reflected value") +impl<'a> ReflectDeserializer<'a> { + pub fn new(registry: &'a TypeRegistry) -> Self { + Self { registry } } +} + +impl<'a, 'de> DeserializeSeed<'de> for ReflectDeserializer<'a> { + type Value = Box; - fn visit_map(self, mut map: A) -> Result + fn deserialize(self, deserializer: D) -> Result where - A: MapAccess<'de>, + D: serde::Deserializer<'de>, { - let registration = map - .next_key_seed(TypeRegistrationDeserializer::new(self.registry))? - .ok_or_else(|| Error::invalid_length(0, &"a single entry"))?; + struct UntypedReflectDeserializerVisitor<'a> { + registry: &'a TypeRegistry, + } - let value = map.next_value_seed(TypedReflectDeserializer { - registration, - registry: self.registry, - })?; + impl<'a, 'de> Visitor<'de> for UntypedReflectDeserializerVisitor<'a> { + type Value = Box; + + fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { + formatter + .write_str("map containing `type` and `value` entries for the reflected value") + } + + fn visit_map(self, mut map: A) -> Result + where + A: MapAccess<'de>, + { + let registration = map + .next_key_seed(TypeRegistrationDeserializer::new(self.registry))? + .ok_or_else(|| Error::invalid_length(0, &"a single entry"))?; + + let value = map.next_value_seed(TypedReflectDeserializer { + registration, + registry: self.registry, + })?; - if map.next_key::()?.is_some() { - return Err(Error::invalid_length(2, &"a single entry")); + if map.next_key::()?.is_some() { + return Err(Error::invalid_length(2, &"a single entry")); + } + + Ok(value) + } } - Ok(value) + deserializer.deserialize_map(UntypedReflectDeserializerVisitor { + registry: self.registry, + }) } } -/// A deserializer for reflected types whose [`TypeInfo`] is known. +/// A deserializer for reflected types whose [`TypeRegistration`] is known. +/// +/// This is the deserializer counterpart to [`TypedReflectSerializer`]. +/// +/// See [`ReflectDeserializer`] for a deserializer that expects an unknown type. +/// +/// # Input +/// +/// Since the type is already known, the input is just the serialized data. +/// +/// # Output +/// +/// This deserializer will return a [`Box`] containing the deserialized data. /// -/// This will return a [`Box`] containing the deserialized data. -/// For non-value types, this `Box` will contain the dynamic equivalent. For example, a -/// deserialized struct will return a [`DynamicStruct`] and a `Vec` will return a -/// [`DynamicList`]. For value types, this `Box` will contain the actual value. -/// For example, an `f32` will contain the actual `f32` type. +/// For value types (i.e. [`ReflectKind::Value`]) or types that register [`ReflectDeserialize`] type data, +/// this `Box` will contain the expected type. +/// For example, deserializing an `i32` will return a `Box` (as a `Box`). /// -/// This means that converting to any concrete instance will require the use of -/// [`FromReflect`], or downcasting for value types. +/// Otherwise, this `Box` will contain the dynamic equivalent. +/// For example, a deserialized struct might return a [`Box`] +/// and a deserialized `Vec` might return a [`Box`]. /// -/// If the type is not known ahead of time, use [`UntypedReflectDeserializer`] instead. +/// This means that if the actual type is needed, these dynamic representations will need to +/// be converted to the concrete type using [`FromReflect`] or [`ReflectFromReflect`]. +/// +/// # Example +/// +/// ``` +/// # use std::any::TypeId; +/// # use serde::de::DeserializeSeed; +/// # use bevy_reflect::prelude::*; +/// # use bevy_reflect::{DynamicStruct, TypeRegistry, serde::TypedReflectDeserializer}; +/// #[derive(Reflect, PartialEq, Debug)] +/// struct MyStruct { +/// value: i32 +/// } +/// +/// let mut registry = TypeRegistry::default(); +/// registry.register::(); +/// +/// let input = r#"( +/// value: 123 +/// )"#; +/// +/// let registration = registry.get(TypeId::of::()).unwrap(); +/// +/// let mut deserializer = ron::Deserializer::from_str(input).unwrap(); +/// let reflect_deserializer = TypedReflectDeserializer::new(registration, ®istry); +/// +/// let output: Box = reflect_deserializer.deserialize(&mut deserializer).unwrap(); +/// +/// // Since `MyStruct` is not a value type and does not register `ReflectDeserialize`, +/// // we know that its deserialized representation will be a `DynamicStruct`. +/// assert!(output.is::()); +/// assert!(output.represents::()); +/// +/// // We can convert back to `MyStruct` using `FromReflect`. +/// let value: MyStruct = ::from_reflect(&*output).unwrap(); +/// assert_eq!(value, MyStruct { value: 123 }); +/// +/// // We can also do this dynamically with `ReflectFromReflect`. +/// let type_id = output.get_represented_type_info().unwrap().type_id(); +/// let reflect_from_reflect = registry.get_type_data::(type_id).unwrap(); +/// let value: Box = reflect_from_reflect.from_reflect(&*output).unwrap(); +/// assert!(value.is::()); +/// assert_eq!(value.take::().unwrap(), MyStruct { value: 123 }); +/// ``` /// +/// [`TypedReflectSerializer`]: crate::serde::TypedReflectSerializer /// [`Box`]: crate::Reflect +/// [`ReflectKind::Value`]: crate::ReflectKind::Value +/// [`ReflectDeserialize`]: crate::ReflectDeserialize +/// [`Box`]: crate::DynamicStruct +/// [`Box`]: crate::DynamicList /// [`FromReflect`]: crate::FromReflect +/// [`ReflectFromReflect`]: crate::ReflectFromReflect pub struct TypedReflectDeserializer<'a> { registration: &'a TypeRegistration, registry: &'a TypeRegistry, @@ -1062,7 +1181,7 @@ mod tests { use bevy_utils::HashMap; use crate as bevy_reflect; - use crate::serde::{ReflectSerializer, TypedReflectDeserializer, UntypedReflectDeserializer}; + use crate::serde::{ReflectDeserializer, ReflectSerializer, TypedReflectDeserializer}; use crate::{DynamicEnum, FromReflect, Reflect, ReflectDeserialize, TypeRegistry}; #[derive(Reflect, Debug, PartialEq)] @@ -1252,7 +1371,7 @@ mod tests { ), }"#; - let reflect_deserializer = UntypedReflectDeserializer::new(®istry); + let reflect_deserializer = ReflectDeserializer::new(®istry); let mut ron_deserializer = ron::de::Deserializer::from_str(input).unwrap(); let dynamic_output = reflect_deserializer .deserialize(&mut ron_deserializer) @@ -1269,7 +1388,7 @@ mod tests { }"#; let registry = get_registry(); - let reflect_deserializer = UntypedReflectDeserializer::new(®istry); + let reflect_deserializer = ReflectDeserializer::new(®istry); let mut ron_deserializer = ron::de::Deserializer::from_str(input).unwrap(); let dynamic_output = reflect_deserializer .deserialize(&mut ron_deserializer) @@ -1336,7 +1455,7 @@ mod tests { ), }"#; - let reflect_deserializer = UntypedReflectDeserializer::new(®istry); + let reflect_deserializer = ReflectDeserializer::new(®istry); let mut ron_deserializer = ron::de::Deserializer::from_str(input).unwrap(); let dynamic_output = reflect_deserializer .deserialize(&mut ron_deserializer) @@ -1358,7 +1477,7 @@ mod tests { ), }"#; - let reflect_deserializer = UntypedReflectDeserializer::new(®istry); + let reflect_deserializer = ReflectDeserializer::new(®istry); let mut ron_deserializer = ron::de::Deserializer::from_str(input).unwrap(); let dynamic_output = reflect_deserializer .deserialize(&mut ron_deserializer) @@ -1388,7 +1507,7 @@ mod tests { let input = r#"{ "bevy_reflect::serde::de::tests::MyEnum": Unit, }"#; - let reflect_deserializer = UntypedReflectDeserializer::new(®istry); + let reflect_deserializer = ReflectDeserializer::new(®istry); let mut deserializer = ron::de::Deserializer::from_str(input).unwrap(); let output = reflect_deserializer.deserialize(&mut deserializer).unwrap(); @@ -1399,7 +1518,7 @@ mod tests { let input = r#"{ "bevy_reflect::serde::de::tests::MyEnum": NewType(123), }"#; - let reflect_deserializer = UntypedReflectDeserializer::new(®istry); + let reflect_deserializer = ReflectDeserializer::new(®istry); let mut deserializer = ron::de::Deserializer::from_str(input).unwrap(); let output = reflect_deserializer.deserialize(&mut deserializer).unwrap(); @@ -1410,7 +1529,7 @@ mod tests { let input = r#"{ "bevy_reflect::serde::de::tests::MyEnum": Tuple(1.23, 3.21), }"#; - let reflect_deserializer = UntypedReflectDeserializer::new(®istry); + let reflect_deserializer = ReflectDeserializer::new(®istry); let mut deserializer = ron::de::Deserializer::from_str(input).unwrap(); let output = reflect_deserializer.deserialize(&mut deserializer).unwrap(); @@ -1423,7 +1542,7 @@ mod tests { value: "I <3 Enums", ), }"#; - let reflect_deserializer = UntypedReflectDeserializer::new(®istry); + let reflect_deserializer = ReflectDeserializer::new(®istry); let mut deserializer = ron::de::Deserializer::from_str(input).unwrap(); let output = reflect_deserializer.deserialize(&mut deserializer).unwrap(); @@ -1443,7 +1562,7 @@ mod tests { 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 reflect_deserializer = ReflectDeserializer::new(®istry); let input2 = reflect_deserializer.deserialize(&mut deserializer).unwrap(); let serializer2 = ReflectSerializer::new(&*input2, ®istry); @@ -1473,7 +1592,7 @@ mod tests { 0, ]; - let deserializer = UntypedReflectDeserializer::new(®istry); + let deserializer = ReflectDeserializer::new(®istry); let dynamic_output = bincode::DefaultOptions::new() .with_fixint_encoding() @@ -1505,7 +1624,7 @@ mod tests { let mut reader = std::io::BufReader::new(input.as_slice()); - let deserializer = UntypedReflectDeserializer::new(®istry); + let deserializer = ReflectDeserializer::new(®istry); let dynamic_output = deserializer .deserialize(&mut rmp_serde::Deserializer::new(&mut reader)) .unwrap(); diff --git a/crates/bevy_reflect/src/serde/mod.rs b/crates/bevy_reflect/src/serde/mod.rs index c444279fa928a..0f9833c57efe4 100644 --- a/crates/bevy_reflect/src/serde/mod.rs +++ b/crates/bevy_reflect/src/serde/mod.rs @@ -10,7 +10,7 @@ pub use type_data::*; mod tests { use crate::{self as bevy_reflect, DynamicTupleStruct, Struct}; use crate::{ - serde::{ReflectSerializer, UntypedReflectDeserializer}, + serde::{ReflectDeserializer, ReflectSerializer}, type_registry::TypeRegistry, DynamicStruct, FromReflect, Reflect, }; @@ -52,7 +52,7 @@ mod tests { ron::ser::to_string_pretty(&serializer, ron::ser::PrettyConfig::default()).unwrap(); let mut deserializer = ron::de::Deserializer::from_str(&serialized).unwrap(); - let reflect_deserializer = UntypedReflectDeserializer::new(®istry); + let reflect_deserializer = ReflectDeserializer::new(®istry); let value = reflect_deserializer.deserialize(&mut deserializer).unwrap(); let deserialized = value.take::().unwrap(); @@ -111,7 +111,7 @@ mod tests { ron::ser::to_string_pretty(&serializer, ron::ser::PrettyConfig::default()).unwrap(); let mut deserializer = ron::de::Deserializer::from_str(&serialized).unwrap(); - let reflect_deserializer = UntypedReflectDeserializer::new(®istry); + let reflect_deserializer = ReflectDeserializer::new(®istry); let value = reflect_deserializer.deserialize(&mut deserializer).unwrap(); let deserialized = value.take::().unwrap(); @@ -170,7 +170,7 @@ mod tests { assert_eq!(expected, result); let mut deserializer = ron::de::Deserializer::from_str(&result).unwrap(); - let reflect_deserializer = UntypedReflectDeserializer::new(®istry); + let reflect_deserializer = ReflectDeserializer::new(®istry); let expected = value.clone_value(); let result = reflect_deserializer diff --git a/crates/bevy_reflect/src/serde/ser.rs b/crates/bevy_reflect/src/serde/ser.rs index c67b81e8cc2e2..08e124945cbb6 100644 --- a/crates/bevy_reflect/src/serde/ser.rs +++ b/crates/bevy_reflect/src/serde/ser.rs @@ -52,10 +52,39 @@ fn get_serializable<'a, E: Error>( /// A general purpose serializer for reflected types. /// -/// The serialized data will take the form of a map containing the following entries: -/// 1. `type`: The _full_ [type path] -/// 2. `value`: The serialized value of the reflected type +/// This is the serializer counterpart to [`ReflectDeserializer`]. /// +/// See [`TypedReflectSerializer`] for a serializer that serializes a known type. +/// +/// # Output +/// +/// This serializer will output a map with a single entry, +/// where the key is the _full_ [type path] of the reflected type +/// and the value is the serialized data. +/// +/// # Example +/// +/// ``` +/// # use bevy_reflect::prelude::*; +/// # use bevy_reflect::{TypeRegistry, serde::ReflectSerializer}; +/// #[derive(Reflect, PartialEq, Debug)] +/// #[type_path = "my_crate"] +/// struct MyStruct { +/// value: i32 +/// } +/// +/// let mut registry = TypeRegistry::default(); +/// registry.register::(); +/// +/// let input = MyStruct { value: 123 }; +/// +/// let reflect_serializer = ReflectSerializer::new(&input, ®istry); +/// let output = ron::to_string(&reflect_serializer).unwrap(); +/// +/// assert_eq!(output, r#"{"my_crate::MyStruct":(value:123)}"#); +/// ``` +/// +/// [`ReflectDeserializer`]: crate::serde::ReflectDeserializer /// [type path]: crate::TypePath::type_path pub struct ReflectSerializer<'a> { pub value: &'a dyn Reflect, @@ -97,8 +126,44 @@ impl<'a> Serialize for ReflectSerializer<'a> { } } -/// A serializer for reflected types whose type is known and does not require -/// serialization to include other metadata about it. +/// A serializer for reflected types whose type will be known during deserialization. +/// +/// This is the serializer counterpart to [`TypedReflectDeserializer`]. +/// +/// See [`ReflectSerializer`] for a serializer that serializes an unknown type. +/// +/// # Output +/// +/// Since the type is expected to be known during deserialization, +/// this serializer will not output any additional type information, +/// such as the [type path]. +/// +/// Instead, it will output just the serialized data. +/// +/// # Example +/// +/// ``` +/// # use bevy_reflect::prelude::*; +/// # use bevy_reflect::{TypeRegistry, serde::TypedReflectSerializer}; +/// #[derive(Reflect, PartialEq, Debug)] +/// #[type_path = "my_crate"] +/// struct MyStruct { +/// value: i32 +/// } +/// +/// let mut registry = TypeRegistry::default(); +/// registry.register::(); +/// +/// let input = MyStruct { value: 123 }; +/// +/// let reflect_serializer = TypedReflectSerializer::new(&input, ®istry); +/// let output = ron::to_string(&reflect_serializer).unwrap(); +/// +/// assert_eq!(output, r#"(value:123)"#); +/// ``` +/// +/// [`TypedReflectDeserializer`]: crate::serde::TypedReflectDeserializer +/// [type path]: crate::TypePath::type_path pub struct TypedReflectSerializer<'a> { pub value: &'a dyn Reflect, pub registry: &'a TypeRegistry, diff --git a/crates/bevy_reflect/src/type_path.rs b/crates/bevy_reflect/src/type_path.rs index d48e620d68cd5..9d446cc81088e 100644 --- a/crates/bevy_reflect/src/type_path.rs +++ b/crates/bevy_reflect/src/type_path.rs @@ -72,7 +72,7 @@ use std::fmt; /// ``` /// /// [utility]: crate::utility -/// [(de)serialization]: crate::serde::UntypedReflectDeserializer +/// [(de)serialization]: crate::serde::ReflectDeserializer /// [`Reflect`]: crate::Reflect /// [`type_path`]: TypePath::type_path /// [`short_type_path`]: TypePath::short_type_path diff --git a/crates/bevy_reflect/src/type_registry.rs b/crates/bevy_reflect/src/type_registry.rs index 2846781f47792..2090094fea70a 100644 --- a/crates/bevy_reflect/src/type_registry.rs +++ b/crates/bevy_reflect/src/type_registry.rs @@ -663,6 +663,7 @@ pub struct ReflectFromPtr { from_ptr_mut: unsafe fn(PtrMut) -> &mut dyn Reflect, } +#[allow(unsafe_code)] impl ReflectFromPtr { /// Returns the [`TypeId`] that the [`ReflectFromPtr`] was constructed for. pub fn type_id(&self) -> TypeId { @@ -714,6 +715,7 @@ impl ReflectFromPtr { } } +#[allow(unsafe_code)] impl FromType for ReflectFromPtr { fn from_type() -> Self { ReflectFromPtr { @@ -733,6 +735,7 @@ impl FromType for ReflectFromPtr { } #[cfg(test)] +#[allow(unsafe_code)] mod test { use crate::{GetTypeRegistration, ReflectFromPtr}; use bevy_ptr::{Ptr, PtrMut}; 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..cbc25b9146b56 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -39,9 +39,12 @@ ios_simulator = [] # 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_color = { path = "../bevy_color", version = "0.14.0-dev", features = [ + "serialize", +] } 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" } @@ -58,12 +61,14 @@ bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" } bevy_tasks = { path = "../bevy_tasks", version = "0.14.0-dev" } # rendering -image = { version = "0.24", default-features = false } +image = { version = "0.25", default-features = false } # misc codespan-reporting = "0.11.0" # `fragile-send-sync-non-atomic-wasm` feature means we can't use WASM threads for rendering -# It is enabled for now to avoid having to do a significant overhaul of the renderer just for wasm +# It is enabled for now to avoid having to do a significant overhaul of the renderer just for wasm. +# When the 'atomics' feature is enabled `fragile-send-sync-non-atomic` does nothing +# and Bevy instead wraps `wgpu` types to verify they are not used off their origin thread. wgpu = { version = "0.19.3", default-features = false, features = [ "wgsl", "dx12", @@ -117,8 +122,12 @@ web-sys = { version = "0.3.67", features = [ ] } wasm-bindgen = "0.2" +[target.'cfg(all(target_arch = "wasm32", target_feature = "atomics"))'.dependencies] +send_wrapper = "0.6.0" + [lints] workspace = true [package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] all-features = true diff --git a/crates/bevy_render/macros/Cargo.toml b/crates/bevy_render/macros/Cargo.toml index 231662720b351..f00570e012377 100644 --- a/crates/bevy_render/macros/Cargo.toml +++ b/crates/bevy_render/macros/Cargo.toml @@ -20,3 +20,7 @@ quote = "1.0" [lints] workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +all-features = true 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/macros/src/lib.rs b/crates/bevy_render/macros/src/lib.rs index 43af3eff89b64..5398037e5b28e 100644 --- a/crates/bevy_render/macros/src/lib.rs +++ b/crates/bevy_render/macros/src/lib.rs @@ -1,5 +1,6 @@ // FIXME(3492): remove once docs are ready #![allow(missing_docs)] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] mod as_bind_group; mod extract_component; diff --git a/crates/bevy_render/src/batching/mod.rs b/crates/bevy_render/src/batching/mod.rs index 54b0573081f67..576bf641fb71f 100644 --- a/crates/bevy_render/src/batching/mod.rs +++ b/crates/bevy_render/src/batching/mod.rs @@ -5,9 +5,13 @@ use bevy_ecs::{ system::{Query, ResMut, StaticSystemParam, SystemParam, SystemParamItem}, }; use nonmax::NonMaxU32; +use smallvec::{smallvec, SmallVec}; use crate::{ - render_phase::{CachedRenderPipelinePhaseItem, DrawFunctionId, RenderPhase}, + render_phase::{ + BinnedPhaseItem, BinnedRenderPhase, BinnedRenderPhaseBatch, CachedRenderPipelinePhaseItem, + DrawFunctionId, SortedPhaseItem, SortedRenderPhase, + }, render_resource::{CachedRenderPipelineId, GpuArrayBuffer, GpuArrayBufferable}, renderer::{RenderDevice, RenderQueue}, }; @@ -56,6 +60,8 @@ impl BatchMeta { /// A trait to support getting data used for batching draw commands via phase /// items. pub trait GetBatchData { + /// The system parameters [`GetBatchData::get_batch_data`] needs in + /// order to compute the batch data. type Param: SystemParam + 'static; /// Data used for comparison between phase items. If the pipeline id, draw /// function id, per-instance data buffer dynamic offset and this data @@ -74,13 +80,35 @@ pub trait GetBatchData { ) -> Option<(Self::BufferData, Option)>; } -/// Batch the items in a render phase. This means comparing metadata needed to draw each phase item -/// and trying to combine the draws into a batch. -pub fn batch_and_prepare_render_phase( +/// When implemented on a pipeline, this trait allows the batching logic to +/// compute the per-batch data that will be uploaded to the GPU. +/// +/// This includes things like the mesh transforms. +pub trait GetBinnedBatchData { + /// The system parameters [`GetBinnedBatchData::get_batch_data`] needs + /// in order to compute the batch data. + type Param: SystemParam + 'static; + /// The per-instance data to be inserted into the [`GpuArrayBuffer`] + /// containing these data for all instances. + type BufferData: GpuArrayBufferable + Sync + Send + 'static; + + /// Get the per-instance data to be inserted into the [`GpuArrayBuffer`]. + fn get_batch_data( + param: &SystemParamItem, + entity: Entity, + ) -> Option; +} + +/// Batch the items in a sorted render phase. This means comparing metadata +/// needed to draw each phase item and trying to combine the draws into a batch. +pub fn batch_and_prepare_sorted_render_phase( gpu_array_buffer: ResMut>, - mut views: Query<&mut RenderPhase>, + mut views: Query<&mut SortedRenderPhase>, param: StaticSystemParam, -) { +) where + I: CachedRenderPipelinePhaseItem + SortedPhaseItem, + F: GetBatchData, +{ let gpu_array_buffer = gpu_array_buffer.into_inner(); let system_param_item = param.into_inner(); @@ -115,6 +143,80 @@ pub fn batch_and_prepare_render_phase(mut views: Query<&mut BinnedRenderPhase>) +where + BPI: BinnedPhaseItem, +{ + for mut phase in &mut views { + phase.batchable_keys.sort_unstable(); + phase.unbatchable_keys.sort_unstable(); + } +} + +/// Creates batches for a render phase that uses bins. +pub fn batch_and_prepare_binned_render_phase( + gpu_array_buffer: ResMut>, + mut views: Query<&mut BinnedRenderPhase>, + param: StaticSystemParam, +) where + BPI: BinnedPhaseItem, + GBBD: GetBinnedBatchData, +{ + let gpu_array_buffer = gpu_array_buffer.into_inner(); + let system_param_item = param.into_inner(); + + for mut phase in &mut views { + let phase = &mut *phase; // Borrow checker. + + // Prepare batchables. + + for key in &phase.batchable_keys { + let mut batch_set: SmallVec<[BinnedRenderPhaseBatch; 1]> = smallvec![]; + for &entity in &phase.batchable_values[key] { + let Some(buffer_data) = GBBD::get_batch_data(&system_param_item, entity) else { + continue; + }; + + let instance = gpu_array_buffer.push(buffer_data); + + // If the dynamic offset has changed, flush the batch. + // + // This is the only time we ever have more than one batch per + // bin. Note that dynamic offsets are only used on platforms + // with no storage buffers. + if !batch_set.last().is_some_and(|batch| { + batch.instance_range.end == instance.index + && batch.dynamic_offset == instance.dynamic_offset + }) { + batch_set.push(BinnedRenderPhaseBatch { + representative_entity: entity, + instance_range: instance.index..instance.index, + dynamic_offset: instance.dynamic_offset, + }); + } + + if let Some(batch) = batch_set.last_mut() { + batch.instance_range.end = instance.index + 1; + } + } + + phase.batch_sets.push(batch_set); + } + + // Prepare unbatchables. + for key in &phase.unbatchable_keys { + let unbatchables = phase.unbatchable_values.get_mut(key).unwrap(); + for &entity in &unbatchables.entities { + if let Some(buffer_data) = GBBD::get_batch_data(&system_param_item, entity) { + let instance = gpu_array_buffer.push(buffer_data); + unbatchables.buffer_indices.add(instance); + } + } + } + } +} + pub fn write_batched_instance_buffer( render_device: Res, render_queue: Res, 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..c70b262cc0ee3 --- /dev/null +++ b/crates/bevy_render/src/diagnostic/internal.rs @@ -0,0 +1,662 @@ +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, WgpuWrapper}; + +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; + +struct DiagnosticsRecorderInternal { + timestamp_period_ns: f32, + features: Features, + current_frame: Mutex, + submitted_frames: Vec, + finished_frames: Vec, +} + +/// 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(WgpuWrapper); + +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(WgpuWrapper::new(DiagnosticsRecorderInternal { + 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.0.current_frame.get_mut().expect("lock poisoned") + } + + fn current_frame_lock(&self) -> impl DerefMut + '_ { + self.0.current_frame.lock().expect("lock poisoned") + } + + /// Begins recording diagnostics for a new frame. + pub fn begin_frame(&mut self) { + let internal = &mut self.0; + let mut idx = 0; + while idx < internal.submitted_frames.len() { + let timestamp = internal.timestamp_period_ns; + if internal.submitted_frames[idx].run_mapped_callback(timestamp) { + let removed = internal.submitted_frames.swap_remove(idx); + internal.finished_frames.push(removed); + } 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, + ) { + let internal = &mut self.0; + internal + .current_frame + .get_mut() + .expect("lock poisoned") + .finish(callback); + + // reuse one of the finished frames, if we can + let new_frame = match internal.finished_frames.pop() { + Some(frame) => frame, + None => FrameData::new(device, internal.features), + }; + + let old_frame = std::mem::replace( + internal.current_frame.get_mut().expect("lock poisoned"), + new_frame, + ); + internal.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