diff --git a/.github/ISSUE_TEMPLATE/performance_regression.md b/.github/ISSUE_TEMPLATE/performance_regression.md index c1ff69775c721..0f8cd00eeb4dd 100644 --- a/.github/ISSUE_TEMPLATE/performance_regression.md +++ b/.github/ISSUE_TEMPLATE/performance_regression.md @@ -2,7 +2,7 @@ name: Performance Regression about: Bevy running slowly after upgrading? Report a performance regression. title: '' -labels: C-Bug, C-Performance, C-Regression, S-Needs-Triage +labels: C-Bug, C-Performance, P-Regression, S-Needs-Triage assignees: '' --- diff --git a/.github/example-run/alien_cake_addict.ron b/.github/example-run/alien_cake_addict.ron deleted file mode 100644 index a8113dad0434d..0000000000000 --- a/.github/example-run/alien_cake_addict.ron +++ /dev/null @@ -1,5 +0,0 @@ -( - events: [ - (300, AppExit), - ] -) diff --git a/.github/example-run/breakout.ron b/.github/example-run/breakout.ron deleted file mode 100644 index a169090336007..0000000000000 --- a/.github/example-run/breakout.ron +++ /dev/null @@ -1,8 +0,0 @@ -( - setup: ( - fixed_frame_time: Some(0.03), - ), - events: [ - (900, AppExit), - ] -) diff --git a/.github/example-run/testbed_ui.ron b/.github/example-run/testbed_ui.ron new file mode 100644 index 0000000000000..579f791d66400 --- /dev/null +++ b/.github/example-run/testbed_ui.ron @@ -0,0 +1,6 @@ +( + events: [ + (100, Screenshot), + (200, AppExit), + ] +) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4d6ab6f153230..b8cb0c7acf6ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,8 +72,7 @@ jobs: run: cargo run -p ci -- lints miri: - # Explicitly use macOS 14 to take advantage of M1 chip. - runs-on: macos-14 + runs-on: macos-latest timeout-minutes: 60 steps: - uses: actions/checkout@v4 @@ -127,7 +126,7 @@ jobs: - name: Check Compile # See tools/ci/src/main.rs for the commands this runs run: cargo run -p ci -- compile - + check-compiles-no-std: runs-on: ubuntu-latest timeout-minutes: 30 @@ -231,10 +230,10 @@ jobs: - name: Taplo info if: failure() run: | - echo 'To fix toml fmt, please run `taplo fmt`' - echo 'To check for a diff, run `taplo fmt --check --diff' + echo 'To fix toml fmt, please run `taplo fmt`.' + echo 'To check for a diff, run `taplo fmt --check --diff`.' echo 'You can find taplo here: https://taplo.tamasfe.dev/' - echo 'Or if you use VSCode, use the `Even Better Toml` extension with 2 spaces' + echo 'Or if you use VSCode, use the `Even Better Toml` extension.' echo 'You can find the extension here: https://marketplace.visualstudio.com/items?itemName=tamasfe.even-better-toml' typos: @@ -243,7 +242,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Check for typos - uses: crate-ci/typos@v1.27.0 + uses: crate-ci/typos@v1.27.3 - name: Typos info if: failure() run: | @@ -254,8 +253,7 @@ jobs: echo 'You can find the extension here: https://marketplace.visualstudio.com/items?itemName=tekumara.typos-vscode' run-examples-macos-metal: - # Explicitly use macOS 14 to take advantage of M1 chip. - runs-on: macos-14 + runs-on: macos-latest timeout-minutes: 30 steps: - uses: actions/checkout@v4 @@ -263,10 +261,6 @@ jobs: - name: Disable audio # Disable audio through a patch. on github m1 runners, audio timeouts after 15 minutes run: git apply --ignore-whitespace tools/example-showcase/disable-audio.patch - - name: Build bevy - # this uses the same command as when running the example to ensure build is reused - run: | - TRACE_CHROME=trace-alien_cake_addict.json CI_TESTING_CONFIG=.github/example-run/alien_cake_addict.ron cargo build --example alien_cake_addict --features "bevy_ci_testing,trace,trace_chrome" - name: Run examples run: | for example in .github/example-run/*.ron; do @@ -301,7 +295,7 @@ jobs: with: name: example-run-macos path: example-run/ - + check-doc: runs-on: ubuntu-latest timeout-minutes: 30 @@ -316,7 +310,9 @@ jobs: ~/.cargo/git/db/ target/ key: ${{ runner.os }}-check-doc-${{ hashFiles('**/Cargo.toml') }} - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.NIGHTLY_TOOLCHAIN }} - name: Install Linux dependencies uses: ./.github/actions/install-linux-deps with: @@ -327,7 +323,7 @@ jobs: run: cargo run -p ci -- doc env: CARGO_INCREMENTAL: 0 - RUSTFLAGS: "-C debuginfo=0" + RUSTFLAGS: "-C debuginfo=0 --cfg docsrs_dep" # This currently report a lot of false positives # Enable it again once it's fixed - https://github.com/bevyengine/bevy/issues/1983 # - name: Installs cargo-deadlinks diff --git a/.github/workflows/validation-jobs.yml b/.github/workflows/validation-jobs.yml index ba79f4c834f7b..4c576ac1e176a 100644 --- a/.github/workflows/validation-jobs.yml +++ b/.github/workflows/validation-jobs.yml @@ -31,7 +31,7 @@ jobs: with: path: | target - key: ${{ runner.os }}-ios-install-${{ matrix.toolchain }}-${{ hashFiles('**/Cargo.lock') }} + key: ${{ runner.os }}-ios-install-${{ hashFiles('**/Cargo.lock') }} # TODO: remove x86 target once it always run on arm GitHub runners - name: Add iOS targets @@ -101,10 +101,6 @@ jobs: target/ key: ${{ runner.os }}-cargo-run-examples-${{ hashFiles('**/Cargo.toml') }} - uses: dtolnay/rust-toolchain@stable - - name: Build bevy - # this uses the same command as when running the example to ensure build is reused - run: | - TRACE_CHROME=trace-alien_cake_addict.json CI_TESTING_CONFIG=.github/example-run/alien_cake_addict.ron cargo build --example alien_cake_addict --features "bevy_ci_testing,trace,trace_chrome" - name: Run examples run: | for example in .github/example-run/*.ron; do @@ -155,11 +151,6 @@ jobs: steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - - name: Build bevy - shell: bash - # this uses the same command as when running the example to ensure build is reused - run: | - WGPU_BACKEND=dx12 TRACE_CHROME=trace-alien_cake_addict.json CI_TESTING_CONFIG=.github/example-run/alien_cake_addict.ron cargo build --example alien_cake_addict --features "bevy_ci_testing,trace,trace_chrome" - name: Run examples shell: bash run: | @@ -192,17 +183,6 @@ jobs: name: example-run-windows path: example-run/ - compare-windows-screenshots: - name: Compare Windows screenshots - needs: [run-examples-on-windows-dx12] - uses: ./.github/workflows/send-screenshots-to-pixeleagle.yml - with: - commit: ${{ github.sha }} - branch: ${{ github.ref_name }} - artifact: screenshots-windows - os: windows - secrets: inherit - run-examples-on-wasm: if: ${{ github.event_name == 'merge_group' }} runs-on: ubuntu-22.04 @@ -244,7 +224,7 @@ jobs: - name: First Wasm build run: | - cargo build --release --example ui --target wasm32-unknown-unknown + cargo build --release --example testbed_ui --target wasm32-unknown-unknown - name: Run examples shell: bash diff --git a/.github/workflows/weekly.yml b/.github/workflows/weekly.yml index 8c78f165feb49..6ff0f5d2f049d 100644 --- a/.github/workflows/weekly.yml +++ b/.github/workflows/weekly.yml @@ -59,31 +59,13 @@ jobs: # See tools/ci/src/main.rs for the commands this runs run: cargo run -p ci -- compile - check-doc: - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@beta - - name: Install Linux dependencies - uses: ./.github/actions/install-linux-deps - with: - wayland: true - xkb: true - - name: Build and check docs - # See tools/ci/src/main.rs for the commands this runs - run: cargo run -p ci -- doc - env: - CARGO_INCREMENTAL: 0 - RUSTFLAGS: "-C debuginfo=0" - open-issue: name: Warn that weekly CI fails runs-on: ubuntu-latest - needs: [test, lint, check-compiles, check-doc] + needs: [test, lint, check-compiles] permissions: issues: write - # Use always() so the job doesn't get canceled if any other jobs fail + # Use always() so the job doesn't get canceled if any other jobs fail if: ${{ always() && contains(needs.*.result, 'failure') }} steps: - name: Create issue @@ -94,7 +76,7 @@ jobs: --jq '.[0].number') if [[ -n $previous_issue_number ]]; then gh issue comment $previous_issue_number \ - --body "Weekly pipeline still fails: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + --body "Weekly pipeline still fails: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" else gh issue create \ --title "$TITLE" \ @@ -106,6 +88,6 @@ jobs: GH_REPO: ${{ github.repository }} TITLE: Main branch fails to compile on Rust beta. LABELS: C-Bug,S-Needs-Triage - BODY: | + BODY: | ## Weekly CI run has failed. [The offending run.](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) diff --git a/.gitignore b/.gitignore index 42b22faa253ce..d3b84d9590bb8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,9 @@ # Rust build artifacts -/target -crates/*/target +target +crates/**/target +benches/**/target +tools/**/target **/*.rs.bk -/benches/target -/tools/compile_fail_utils/target # Cargo Cargo.lock @@ -11,8 +11,8 @@ Cargo.lock .cargo/config.toml # IDE files -/.idea -/.vscode +.idea +.vscode .zed dxcompiler.dll dxil.dll diff --git a/Cargo.toml b/Cargo.toml index 9e1ea7c57d00b..dc4fb9be494a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ semicolon_if_nothing_returned = "warn" type_complexity = "allow" undocumented_unsafe_blocks = "warn" unwrap_or_default = "warn" +needless_lifetimes = "allow" ptr_as_ptr = "warn" ptr_cast_constness = "warn" @@ -80,6 +81,7 @@ semicolon_if_nothing_returned = "warn" type_complexity = "allow" undocumented_unsafe_blocks = "warn" unwrap_or_default = "warn" +needless_lifetimes = "allow" ptr_as_ptr = "warn" ptr_cast_constness = "warn" @@ -100,7 +102,6 @@ unused_qualifications = "warn" [features] default = [ - "android-game-activity", "android-game-activity", "android_shared_stdcxx", "animation", @@ -114,7 +115,6 @@ default = [ "bevy_mesh_picking_backend", "bevy_pbr", "bevy_picking", - "bevy_remote", "bevy_render", "bevy_scene", "bevy_sprite", @@ -123,6 +123,7 @@ default = [ "bevy_text", "bevy_ui", "bevy_ui_picking_backend", + "bevy_window", "bevy_winit", "custom_cursor", "default_font", @@ -138,13 +139,22 @@ default = [ ] # Provides an implementation for picking meshes -bevy_mesh_picking_backend = ["bevy_picking"] +bevy_mesh_picking_backend = [ + "bevy_picking", + "bevy_internal/bevy_mesh_picking_backend", +] # Provides an implementation for picking sprites -bevy_sprite_picking_backend = ["bevy_picking"] +bevy_sprite_picking_backend = [ + "bevy_picking", + "bevy_internal/bevy_sprite_picking_backend", +] -# Provides an implementation for picking ui -bevy_ui_picking_backend = ["bevy_picking"] +# Provides an implementation for picking UI +bevy_ui_picking_backend = [ + "bevy_picking", + "bevy_internal/bevy_ui_picking_backend", +] # Force dynamic linking, which improves iterative compile times dynamic_linking = ["dep:bevy_dylib", "bevy_internal/dynamic_linking"] @@ -216,6 +226,9 @@ bevy_ui = [ "bevy_ui_picking_backend", ] +# Windowing layer +bevy_window = ["bevy_internal/bevy_window"] + # winit window and input backend bevy_winit = ["bevy_internal/bevy_winit"] @@ -403,7 +416,7 @@ pbr_multi_layer_material_textures = [ pbr_anisotropy_texture = ["bevy_internal/pbr_anisotropy_texture"] # Enable support for PCSS, at the risk of blowing past the global, per-shader sampler limit on older/lower-end GPUs -pbr_pcss = ["bevy_internal/pbr_pcss"] +experimental_pbr_pcss = ["bevy_internal/experimental_pbr_pcss"] # Enable some limitations to be able to use WebGL2. Please refer to the [WebGL2 and WebGPU](https://github.com/bevyengine/bevy/tree/latest/examples#webgl2-and-webgpu) section of the examples README for more information on how to run Wasm builds with WebGPU. webgl2 = ["bevy_internal/webgl"] @@ -474,6 +487,7 @@ hyper = { version = "1", features = ["server", "http1"] } http-body-util = "0.1" anyhow = "1" macro_rules_attribute = "0.2" +accesskit = "0.17" [target.'cfg(not(target_family = "wasm"))'.dev-dependencies] smol = "2" @@ -2703,6 +2717,17 @@ description = "Test rendering of many UI elements" category = "Stress Tests" wasm = true +[[example]] +name = "many_cameras_lights" +path = "examples/stress_tests/many_cameras_lights.rs" +doc-scrape-examples = true + +[package.metadata.example.many_cameras_lights] +name = "Many Cameras & Lights" +description = "Test rendering of many cameras and lights" +category = "Stress Tests" +wasm = true + [[example]] name = "many_cubes" path = "examples/stress_tests/many_cubes.rs" @@ -3136,17 +3161,6 @@ description = "Demonstrates how to control the relative depth (z-position) of UI category = "UI (User Interface)" wasm = true -[[example]] -name = "ui" -path = "examples/ui/ui.rs" -doc-scrape-examples = true - -[package.metadata.example.ui] -name = "UI" -description = "Illustrates various features of Bevy UI" -category = "UI (User Interface)" -wasm = true - [[example]] name = "ui_scaling" path = "examples/ui/ui_scaling.rs" @@ -3640,6 +3654,7 @@ wasm = true name = "client" path = "examples/remote/client.rs" doc-scrape-examples = true +required-features = ["bevy_remote"] [package.metadata.example.client] name = "client" @@ -3651,6 +3666,7 @@ wasm = false name = "server" path = "examples/remote/server.rs" doc-scrape-examples = true +required-features = ["bevy_remote"] [package.metadata.example.server] name = "server" @@ -3789,7 +3805,7 @@ wasm = true name = "pcss" path = "examples/3d/pcss.rs" doc-scrape-examples = true -required-features = ["pbr_pcss"] +required-features = ["experimental_pbr_pcss"] [package.metadata.example.pcss] name = "Percentage-closer soft shadows" @@ -3857,6 +3873,13 @@ doc-scrape-examples = true [package.metadata.example.testbed_3d] hidden = true +[[example]] +name = "testbed_ui" +path = "examples/testbed/ui.rs" +doc-scrape-examples = true + +[package.metadata.example.testbed_ui] +hidden = true [[example]] name = "testbed_ui_layout_rounding" diff --git a/README.md b/README.md index a8018aa0daad2..be1bcf6bfec92 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Before contributing or participating in discussions with the community, you shou ### Contributing -If you'd like to help build Bevy, check out the **[Contributor's Guide](https://github.com/bevyengine/bevy/blob/main/CONTRIBUTING.md)**. +If you'd like to help build Bevy, check out the **[Contributor's Guide](https://bevyengine.org/learn/contribute/introduction)**. For simple problems, feel free to [open an issue](https://github.com/bevyengine/bevy/issues) or [PR](https://github.com/bevyengine/bevy/pulls) and tackle it yourself! diff --git a/benches/Cargo.toml b/benches/Cargo.toml index 25fa79256d914..6375af6d5e198 100644 --- a/benches/Cargo.toml +++ b/benches/Cargo.toml @@ -14,7 +14,9 @@ bevy_app = { path = "../crates/bevy_app" } bevy_ecs = { path = "../crates/bevy_ecs", features = ["multi_threaded"] } bevy_hierarchy = { path = "../crates/bevy_hierarchy" } bevy_math = { path = "../crates/bevy_math" } -bevy_picking = { path = "../crates/bevy_picking", features = ["bevy_mesh"] } +bevy_picking = { path = "../crates/bevy_picking", features = [ + "bevy_mesh_picking_backend", +] } bevy_reflect = { path = "../crates/bevy_reflect", features = ["functions"] } bevy_render = { path = "../crates/bevy_render" } bevy_tasks = { path = "../crates/bevy_tasks" } diff --git a/benches/benches/bevy_ecs/benches.rs b/benches/benches/bevy_ecs/benches.rs index af5a38c963a01..58300bda95b5c 100644 --- a/benches/benches/bevy_ecs/benches.rs +++ b/benches/benches/bevy_ecs/benches.rs @@ -1,3 +1,5 @@ +#![expect(dead_code, reason = "Many fields are unused/unread as they are just for benchmarking purposes.")] + use criterion::criterion_main; mod components; diff --git a/benches/benches/bevy_ecs/components/add_remove.rs b/benches/benches/bevy_ecs/components/add_remove.rs index a41f8ed9538f4..b381ccb434fe5 100644 --- a/benches/benches/bevy_ecs/components/add_remove.rs +++ b/benches/benches/bevy_ecs/components/add_remove.rs @@ -1,6 +1,6 @@ -use bevy::prelude::*; +use bevy_ecs::prelude::*; -#[derive(Component)] +#[derive(Component, Clone)] struct A(f32); #[derive(Component)] struct B(f32); @@ -12,19 +12,18 @@ impl Benchmark { let mut world = World::default(); let entities = world - .spawn_batch((0..10000).map(|_| A(0.0))) - .collect::>(); - + .spawn_batch(core::iter::repeat(A(0.)).take(10000)) + .collect(); Self(world, entities) } pub fn run(&mut self) { for entity in &self.1 { - self.0.insert_one(*entity, B(0.0)).unwrap(); + self.0.entity_mut(*entity).insert(B(0.)); } for entity in &self.1 { - self.0.remove_one::(*entity).unwrap(); + self.0.entity_mut(*entity).remove::(); } } } diff --git a/benches/benches/bevy_ecs/components/mod.rs b/benches/benches/bevy_ecs/components/mod.rs index 592f40fba7d07..f696d33870756 100644 --- a/benches/benches/bevy_ecs/components/mod.rs +++ b/benches/benches/bevy_ecs/components/mod.rs @@ -5,6 +5,7 @@ mod add_remove_big_table; mod add_remove_sparse_set; mod add_remove_table; mod add_remove_very_big_table; +mod add_remove; mod archetype_updates; mod insert_simple; mod insert_simple_unbatched; diff --git a/benches/benches/bevy_math/bezier.rs b/benches/benches/bevy_math/bezier.rs index 69590aa80412d..4728d2b058ef0 100644 --- a/benches/benches/bevy_math/bezier.rs +++ b/benches/benches/bevy_math/bezier.rs @@ -1,6 +1,6 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use bevy_math::{prelude::*, *}; +use bevy_math::prelude::*; fn easing(c: &mut Criterion) { let cubic_bezier = CubicSegment::new_bezier(vec2(0.25, 0.1), vec2(0.25, 1.0)); diff --git a/crates/bevy_a11y/src/lib.rs b/crates/bevy_a11y/src/lib.rs index 49943bd719726..77ddf2073d9fa 100644 --- a/crates/bevy_a11y/src/lib.rs +++ b/crates/bevy_a11y/src/lib.rs @@ -6,13 +6,18 @@ )] //! Accessibility for Bevy +//! +//! As of Bevy version 0.15 `accesskit` is no longer re-exported from this crate. +//! +//! If you need to use `accesskit`, you will need to add it as a separate dependency in your `Cargo.toml`. +//! +//! Make sure to use the same version of `accesskit` as Bevy. extern crate alloc; use alloc::sync::Arc; use core::sync::atomic::{AtomicBool, Ordering}; -pub use accesskit; use accesskit::Node; use bevy_app::Plugin; use bevy_derive::{Deref, DerefMut}; diff --git a/crates/bevy_animation/Cargo.toml b/crates/bevy_animation/Cargo.toml index 6fb9fb9f5992b..4f493ceb34366 100644 --- a/crates/bevy_animation/Cargo.toml +++ b/crates/bevy_animation/Cargo.toml @@ -10,7 +10,6 @@ keywords = ["bevy"] [dependencies] # bevy -bevy_animation_derive = { path = "derive", version = "0.15.0-dev" } bevy_app = { path = "../bevy_app", version = "0.15.0-dev" } bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" } bevy_color = { path = "../bevy_color", version = "0.15.0-dev" } @@ -34,6 +33,7 @@ petgraph = { version = "0.6", features = ["serde-1"] } ron = "0.8" serde = "1" blake3 = { version = "1.0" } +downcast-rs = "1.2.0" derive_more = { version = "1", default-features = false, features = [ "error", "from", diff --git a/crates/bevy_animation/derive/Cargo.toml b/crates/bevy_animation/derive/Cargo.toml deleted file mode 100644 index ad44a82b0029f..0000000000000 --- a/crates/bevy_animation/derive/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "bevy_animation_derive" -version = "0.15.0-dev" -edition = "2021" -description = "Derive implementations for bevy_animation" -homepage = "https://bevyengine.org" -repository = "https://github.com/bevyengine/bevy" -license = "MIT OR Apache-2.0" -keywords = ["bevy"] - -[lib] -proc-macro = true - -[dependencies] -bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.15.0-dev" } -proc-macro2 = "1.0" -quote = "1.0" -syn = { version = "2.0", features = ["full"] } - -[lints] -workspace = true - -[package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] -all-features = true diff --git a/crates/bevy_animation/derive/src/lib.rs b/crates/bevy_animation/derive/src/lib.rs deleted file mode 100644 index 48bce1163eae9..0000000000000 --- a/crates/bevy_animation/derive/src/lib.rs +++ /dev/null @@ -1,29 +0,0 @@ -//! Derive macros for `bevy_animation`. - -extern crate proc_macro; - -use bevy_macro_utils::BevyManifest; -use proc_macro::TokenStream; -use quote::quote; -use syn::{parse_macro_input, DeriveInput}; - -/// Used to derive `AnimationEvent` for a type. -#[proc_macro_derive(AnimationEvent)] -pub fn derive_animation_event(input: TokenStream) -> TokenStream { - let ast = parse_macro_input!(input as DeriveInput); - let name = ast.ident; - let manifest = BevyManifest::default(); - let bevy_animation_path = manifest.get_path("bevy_animation"); - let bevy_ecs_path = manifest.get_path("bevy_ecs"); - let animation_event_path = quote! { #bevy_animation_path::animation_event }; - let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl(); - // TODO: This could derive Event as well. - quote! { - impl #impl_generics #animation_event_path::AnimationEvent for #name #ty_generics #where_clause { - fn trigger(&self, _time: f32, _weight: f32, entity: #bevy_ecs_path::entity::Entity, world: &mut #bevy_ecs_path::world::World) { - world.entity_mut(entity).trigger(Clone::clone(self)); - } - } - } - .into() -} diff --git a/crates/bevy_animation/src/animation_curves.rs b/crates/bevy_animation/src/animation_curves.rs index 14ee7f24fc8f8..d91f376b7945a 100644 --- a/crates/bevy_animation/src/animation_curves.rs +++ b/crates/bevy_animation/src/animation_curves.rs @@ -7,9 +7,9 @@ //! `Curve` that we want to use to animate something. That could be defined in //! a number of different ways, but let's imagine that we've defined it [using a function]: //! -//! # use bevy_math::curve::{Curve, Interval, function_curve}; +//! # use bevy_math::curve::{Curve, Interval, FunctionCurve}; //! # use bevy_math::vec3; -//! let wobble_curve = function_curve( +//! let wobble_curve = FunctionCurve::new( //! Interval::UNIT, //! |t| { vec3(t.cos(), 0.0, 0.0) }, //! ); @@ -22,30 +22,32 @@ //! //! For instance, let's imagine that we want to use the `Vec3` output //! from our curve to animate the [translation component of a `Transform`]. For this, there is -//! the adaptor [`TranslationCurve`], which wraps any `Curve` and turns it into an -//! [`AnimationCurve`] that will use the given curve to animate the entity's translation: +//! the adaptor [`AnimatableCurve`], which wraps any [`Curve`] and [`AnimatableProperty`] and turns it into an +//! [`AnimationCurve`] that will use the given curve to animate the entity's property: //! -//! # use bevy_math::curve::{Curve, Interval, function_curve}; +//! # use bevy_math::curve::{Curve, Interval, FunctionCurve}; //! # use bevy_math::vec3; -//! # use bevy_animation::animation_curves::*; -//! # let wobble_curve = function_curve( +//! # use bevy_transform::components::Transform; +//! # use bevy_animation::{animated_field, animation_curves::*}; +//! # let wobble_curve = FunctionCurve::new( //! # Interval::UNIT, //! # |t| vec3(t.cos(), 0.0, 0.0) //! # ); -//! let wobble_animation = TranslationCurve(wobble_curve); +//! let wobble_animation = AnimatableCurve::new(animated_field!(Transform::translation), wobble_curve); //! -//! And finally, this `AnimationCurve` needs to be added to an [`AnimationClip`] in order to +//! And finally, this [`AnimationCurve`] needs to be added to an [`AnimationClip`] in order to //! actually animate something. This is what that looks like: //! -//! # use bevy_math::curve::{Curve, Interval, function_curve}; -//! # use bevy_animation::{AnimationClip, AnimationTargetId, animation_curves::*}; +//! # use bevy_math::curve::{Curve, Interval, FunctionCurve}; +//! # use bevy_animation::{AnimationClip, AnimationTargetId, animated_field, animation_curves::*}; +//! # use bevy_transform::components::Transform; //! # use bevy_core::Name; //! # use bevy_math::vec3; -//! # let wobble_curve = function_curve( +//! # let wobble_curve = FunctionCurve::new( //! # Interval::UNIT, //! # |t| { vec3(t.cos(), 0.0, 0.0) }, //! # ); -//! # let wobble_animation = TranslationCurve(wobble_curve); +//! # let wobble_animation = AnimatableCurve::new(animated_field!(Transform::translation), wobble_curve); //! # let animation_target_id = AnimationTargetId::from(&Name::new("Test")); //! let mut animation_clip = AnimationClip::default(); //! animation_clip.add_curve_to_target( @@ -59,22 +61,27 @@ //! a [`Curve`], which produces time-related data of some kind, to an [`AnimationCurve`], which //! knows how to apply that data to an entity. //! -//! ## `Transform` +//! ## Animated Fields //! -//! [`Transform`] is special and has its own adaptors: -//! - [`TranslationCurve`], which uses `Vec3` output to animate [`Transform::translation`] -//! - [`RotationCurve`], which uses `Quat` output to animate [`Transform::rotation`] -//! - [`ScaleCurve`], which uses `Vec3` output to animate [`Transform::scale`] +//! The [`animated_field`] macro (which returns an [`AnimatedField`]), in combination with [`AnimatableCurve`] +//! is the easiest way to make an animation curve (see the example above). //! -//! ## Animatable properties +//! This will select a field on a component and pass it to a [`Curve`] with a type that matches the field. //! -//! Animation of arbitrary components can be accomplished using [`AnimatableProperty`] in +//! ## Animatable Properties +//! +//! Animation of arbitrary aspects of entities can be accomplished using [`AnimatableProperty`] in //! conjunction with [`AnimatableCurve`]. See the documentation [there] for details. //! -//! [using a function]: bevy_math::curve::function_curve +//! ## Custom [`AnimationCurve`] and [`AnimationCurveEvaluator`] +//! +//! This is the lowest-level option with the most control, but it is also the most complicated. +//! +//! [using a function]: bevy_math::curve::FunctionCurve //! [translation component of a `Transform`]: bevy_transform::prelude::Transform::translation //! [`AnimationClip`]: crate::AnimationClip //! [there]: AnimatableProperty +//! [`animated_field`]: crate::animated_field use core::{ any::TypeId, @@ -82,24 +89,22 @@ use core::{ marker::PhantomData, }; -use bevy_ecs::{component::Component, world::Mut}; -use bevy_math::{ - curve::{ - cores::{UnevenCore, UnevenCoreError}, - iterable::IterableCurve, - Curve, Interval, - }, - Quat, Vec3, +use bevy_ecs::component::Component; +use bevy_math::curve::{ + cores::{UnevenCore, UnevenCoreError}, + iterable::IterableCurve, + Curve, Interval, }; -use bevy_reflect::{FromReflect, Reflect, Reflectable, TypePath}; +use bevy_reflect::{FromReflect, Reflect, Reflectable, TypeInfo, Typed}; use bevy_render::mesh::morph::MorphWeights; -use bevy_transform::prelude::Transform; use crate::{ graph::AnimationNodeIndex, prelude::{Animatable, BlendInput}, AnimationEntityMut, AnimationEvaluationError, }; +use bevy_utils::Hashed; +use downcast_rs::{impl_downcast, Downcast}; /// A value on a component that Bevy can animate. /// @@ -109,68 +114,154 @@ use crate::{ /// to define the animation itself). /// For example, in order to animate field of view, you might use: /// -/// # use bevy_animation::prelude::AnimatableProperty; +/// # use bevy_animation::{prelude::AnimatableProperty, AnimationEntityMut, AnimationEvaluationError, animation_curves::EvaluatorId}; /// # use bevy_reflect::Reflect; +/// # use std::any::TypeId; /// # use bevy_render::camera::PerspectiveProjection; /// #[derive(Reflect)] /// struct FieldOfViewProperty; /// /// impl AnimatableProperty for FieldOfViewProperty { -/// type Component = PerspectiveProjection; /// type Property = f32; -/// fn get_mut(component: &mut Self::Component) -> Option<&mut Self::Property> { -/// Some(&mut component.fov) +/// fn get_mut<'a>(&self, entity: &'a mut AnimationEntityMut) -> Result<&'a mut Self::Property, AnimationEvaluationError> { +/// let component = entity +/// .get_mut::() +/// .ok_or( +/// AnimationEvaluationError::ComponentNotPresent( +/// TypeId::of::() +/// ) +/// )? +/// .into_inner(); +/// Ok(&mut component.fov) +/// } +/// +/// fn evaluator_id(&self) -> EvaluatorId { +/// EvaluatorId::Type(TypeId::of::()) /// } /// } /// /// You can then create an [`AnimationClip`] to animate this property like so: /// -/// # use bevy_animation::{AnimationClip, AnimationTargetId, VariableCurve}; +/// # use bevy_animation::{AnimationClip, AnimationTargetId, VariableCurve, AnimationEntityMut, AnimationEvaluationError, animation_curves::EvaluatorId}; /// # use bevy_animation::prelude::{AnimatableProperty, AnimatableKeyframeCurve, AnimatableCurve}; /// # use bevy_core::Name; /// # use bevy_reflect::Reflect; /// # use bevy_render::camera::PerspectiveProjection; +/// # use std::any::TypeId; /// # let animation_target_id = AnimationTargetId::from(&Name::new("Test")); -/// # #[derive(Reflect)] +/// # #[derive(Reflect, Clone)] /// # struct FieldOfViewProperty; /// # impl AnimatableProperty for FieldOfViewProperty { -/// # type Component = PerspectiveProjection; -/// # type Property = f32; -/// # fn get_mut(component: &mut Self::Component) -> Option<&mut Self::Property> { -/// # Some(&mut component.fov) -/// # } +/// # type Property = f32; +/// # fn get_mut<'a>(&self, entity: &'a mut AnimationEntityMut) -> Result<&'a mut Self::Property, AnimationEvaluationError> { +/// # let component = entity +/// # .get_mut::() +/// # .ok_or( +/// # AnimationEvaluationError::ComponentNotPresent( +/// # TypeId::of::() +/// # ) +/// # )? +/// # .into_inner(); +/// # Ok(&mut component.fov) +/// # } +/// # fn evaluator_id(&self) -> EvaluatorId { +/// # EvaluatorId::Type(TypeId::of::()) +/// # } /// # } /// let mut animation_clip = AnimationClip::default(); /// animation_clip.add_curve_to_target( /// animation_target_id, -/// AnimatableKeyframeCurve::new( -/// [ +/// AnimatableCurve::new( +/// FieldOfViewProperty, +/// AnimatableKeyframeCurve::new([ /// (0.0, core::f32::consts::PI / 4.0), /// (1.0, core::f32::consts::PI / 3.0), -/// ] +/// ]).expect("Failed to create font size curve") /// ) -/// .map(AnimatableCurve::::from_curve) -/// .expect("Failed to create font size curve") /// ); /// /// Here, the use of [`AnimatableKeyframeCurve`] creates a curve out of the given keyframe time-value /// pairs, using the [`Animatable`] implementation of `f32` to interpolate between them. The -/// invocation of [`AnimatableCurve::from_curve`] with `FieldOfViewProperty` indicates that the `f32` +/// invocation of [`AnimatableCurve::new`] with `FieldOfViewProperty` indicates that the `f32` /// output from that curve is to be used to animate the font size of a `PerspectiveProjection` component (as /// configured above). /// /// [`AnimationClip`]: crate::AnimationClip -pub trait AnimatableProperty: Reflect + TypePath { - /// The type of the component that the property lives on. - type Component: Component; +pub trait AnimatableProperty: Send + Sync + 'static { + /// The animated property type. + type Property: Animatable; + + /// Retrieves the property from the given `entity`. + fn get_mut<'a>( + &self, + entity: &'a mut AnimationEntityMut, + ) -> Result<&'a mut Self::Property, AnimationEvaluationError>; + + /// The [`EvaluatorId`] used to look up the [`AnimationCurveEvaluator`] for this [`AnimatableProperty`]. + /// For a given animated property, this ID should always be the same to allow things like animation blending to occur. + fn evaluator_id(&self) -> EvaluatorId; +} - /// The type of the property to be animated. - type Property: Animatable + FromReflect + Reflectable + Clone + Sync + Debug; +/// A [`Component`] field that can be animated, defined by a function that reads the component and returns +/// the accessed field / property. +/// +/// The best way to create an instance of this type is via the [`animated_field`] macro. +/// +/// `C` is the component being animated, `A` is the type of the [`Animatable`] field on the component, and `F` is an accessor +/// function that accepts a reference to `C` and retrieves the field `A`. +/// +/// [`animated_field`]: crate::animated_field +#[derive(Clone)] +pub struct AnimatedField &mut A> { + func: F, + /// A pre-hashed (component-type-id, reflected-field-index) pair, uniquely identifying a component field + evaluator_id: Hashed<(TypeId, usize)>, + marker: PhantomData<(C, A)>, +} + +impl AnimatableProperty for AnimatedField +where + C: Component, + A: Animatable + Clone + Sync + Debug, + F: Fn(&mut C) -> &mut A + Send + Sync + 'static, +{ + type Property = A; + fn get_mut<'a>( + &self, + entity: &'a mut AnimationEntityMut, + ) -> Result<&'a mut A, AnimationEvaluationError> { + let c = entity + .get_mut::() + .ok_or_else(|| AnimationEvaluationError::ComponentNotPresent(TypeId::of::()))?; + Ok((self.func)(c.into_inner())) + } - /// Given a reference to the component, returns a reference to the property. + fn evaluator_id(&self) -> EvaluatorId { + EvaluatorId::ComponentField(&self.evaluator_id) + } +} + +impl &mut P + 'static> AnimatedField { + /// Creates a new instance of [`AnimatedField`]. This operates under the assumption that + /// `C` is a reflect-able struct with named fields, and that `field_name` is a valid field on that struct. /// - /// If the property couldn't be found, returns `None`. - fn get_mut(component: &mut Self::Component) -> Option<&mut Self::Property>; + /// # Panics + /// If the type of `C` is not a struct with named fields or if the `field_name` does not exist. + pub fn new_unchecked(field_name: &str, func: F) -> Self { + let TypeInfo::Struct(struct_info) = C::type_info() else { + panic!("Only structs are supported in `AnimatedField::new_unchecked`") + }; + + let field_index = struct_info + .index_of(field_name) + .expect("Field name should exist"); + + Self { + func, + evaluator_id: Hashed::new((TypeId::of::(), field_index)), + marker: PhantomData, + } + } } /// This trait collects the additional requirements on top of [`Curve`] needed for a @@ -187,12 +278,14 @@ impl AnimationCompatibleCurve for C where C: Curve + Debug + Clone + #[derive(Reflect, FromReflect)] #[reflect(from_reflect = false)] pub struct AnimatableCurve { + /// The property selector, which defines what component to access and how to access + /// a property on that component. + pub property: P, + /// The inner [curve] whose values are used to animate the property. /// /// [curve]: Curve pub curve: C, - #[reflect(ignore)] - _phantom: PhantomData

, } /// An [`AnimatableCurveEvaluator`] for [`AnimatableProperty`] instances. @@ -200,13 +293,9 @@ pub struct AnimatableCurve { /// You shouldn't ordinarily need to instantiate one of these manually. Bevy /// will automatically do so when you use an [`AnimatableCurve`] instance. #[derive(Reflect)] -pub struct AnimatableCurveEvaluator

-where - P: AnimatableProperty, -{ - evaluator: BasicAnimationCurveEvaluator, - #[reflect(ignore)] - phantom: PhantomData

, +pub struct AnimatableCurveEvaluator { + evaluator: BasicAnimationCurveEvaluator, + property: Box>, } impl AnimatableCurve @@ -218,22 +307,20 @@ where /// valued in an [animatable property]. /// /// [animatable property]: AnimatableProperty::Property - pub fn from_curve(curve: C) -> Self { - Self { - curve, - _phantom: PhantomData, - } + pub fn new(property: P, curve: C) -> Self { + Self { property, curve } } } impl Clone for AnimatableCurve where C: Clone, + P: Clone, { fn clone(&self) -> Self { Self { curve: self.curve.clone(), - _phantom: PhantomData, + property: self.property.clone(), } } } @@ -249,10 +336,10 @@ where } } -impl AnimationCurve for AnimatableCurve +impl AnimationCurve for AnimatableCurve where - P: AnimatableProperty, - C: AnimationCompatibleCurve, + P: AnimatableProperty + Clone, + C: AnimationCompatibleCurve + Clone, { fn clone_value(&self) -> Box { Box::new(self.clone()) @@ -262,14 +349,14 @@ where self.curve.domain() } - fn evaluator_type(&self) -> TypeId { - TypeId::of::>() + fn evaluator_id(&self) -> EvaluatorId { + self.property.evaluator_id() } fn create_evaluator(&self) -> Box { - Box::new(AnimatableCurveEvaluator { + Box::new(AnimatableCurveEvaluator:: { evaluator: BasicAnimationCurveEvaluator::default(), - phantom: PhantomData::

, + property: Box::new(self.property.clone()), }) } @@ -280,8 +367,8 @@ where weight: f32, graph_node: AnimationNodeIndex, ) -> Result<(), AnimationEvaluationError> { - let curve_evaluator = (*Reflect::as_any_mut(curve_evaluator)) - .downcast_mut::>() + let curve_evaluator = curve_evaluator + .downcast_mut::>() .unwrap(); let value = self.curve.sample_clamped(t); curve_evaluator @@ -296,10 +383,7 @@ where } } -impl

AnimationCurveEvaluator for AnimatableCurveEvaluator

-where - P: AnimatableProperty, -{ +impl AnimationCurveEvaluator for AnimatableCurveEvaluator { fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { self.evaluator.combine(graph_node, /*additive=*/ false) } @@ -318,310 +402,14 @@ where fn commit<'a>( &mut self, - _: Option>, mut entity: AnimationEntityMut<'a>, ) -> Result<(), AnimationEvaluationError> { - let mut component = entity.get_mut::().ok_or_else(|| { - AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) - })?; - let property = P::get_mut(&mut component) - .ok_or_else(|| AnimationEvaluationError::PropertyNotPresent(TypeId::of::

()))?; + let property = self.property.get_mut(&mut entity)?; *property = self .evaluator .stack .pop() - .ok_or_else(inconsistent::>)? - .value; - Ok(()) - } -} - -/// This type allows a [curve] valued in `Vec3` to become an [`AnimationCurve`] that animates -/// the translation component of a transform. -/// -/// [curve]: Curve -#[derive(Debug, Clone, Reflect, FromReflect)] -#[reflect(from_reflect = false)] -pub struct TranslationCurve(pub C); - -/// An [`AnimationCurveEvaluator`] for use with [`TranslationCurve`]s. -/// -/// You shouldn't need to instantiate this manually; Bevy will automatically do -/// so. -#[derive(Reflect)] -pub struct TranslationCurveEvaluator { - evaluator: BasicAnimationCurveEvaluator, -} - -impl AnimationCurve for TranslationCurve -where - C: AnimationCompatibleCurve, -{ - fn clone_value(&self) -> Box { - Box::new(self.clone()) - } - - fn domain(&self) -> Interval { - self.0.domain() - } - - fn evaluator_type(&self) -> TypeId { - TypeId::of::() - } - - fn create_evaluator(&self) -> Box { - Box::new(TranslationCurveEvaluator { - evaluator: BasicAnimationCurveEvaluator::default(), - }) - } - - fn apply( - &self, - curve_evaluator: &mut dyn AnimationCurveEvaluator, - t: f32, - weight: f32, - graph_node: AnimationNodeIndex, - ) -> Result<(), AnimationEvaluationError> { - let curve_evaluator = (*Reflect::as_any_mut(curve_evaluator)) - .downcast_mut::() - .unwrap(); - let value = self.0.sample_clamped(t); - curve_evaluator - .evaluator - .stack - .push(BasicAnimationCurveEvaluatorStackElement { - value, - weight, - graph_node, - }); - Ok(()) - } -} - -impl AnimationCurveEvaluator for TranslationCurveEvaluator { - fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { - self.evaluator.combine(graph_node, /*additive=*/ false) - } - - fn add(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { - self.evaluator.combine(graph_node, /*additive=*/ true) - } - - fn push_blend_register( - &mut self, - weight: f32, - graph_node: AnimationNodeIndex, - ) -> Result<(), AnimationEvaluationError> { - self.evaluator.push_blend_register(weight, graph_node) - } - - fn commit<'a>( - &mut self, - transform: Option>, - _: AnimationEntityMut<'a>, - ) -> Result<(), AnimationEvaluationError> { - let mut component = transform.ok_or_else(|| { - AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) - })?; - component.translation = self - .evaluator - .stack - .pop() - .ok_or_else(inconsistent::)? - .value; - Ok(()) - } -} - -/// This type allows a [curve] valued in `Quat` to become an [`AnimationCurve`] that animates -/// the rotation component of a transform. -/// -/// [curve]: Curve -#[derive(Debug, Clone, Reflect, FromReflect)] -#[reflect(from_reflect = false)] -pub struct RotationCurve(pub C); - -/// An [`AnimationCurveEvaluator`] for use with [`RotationCurve`]s. -/// -/// You shouldn't need to instantiate this manually; Bevy will automatically do -/// so. -#[derive(Reflect)] -pub struct RotationCurveEvaluator { - evaluator: BasicAnimationCurveEvaluator, -} - -impl AnimationCurve for RotationCurve -where - C: AnimationCompatibleCurve, -{ - fn clone_value(&self) -> Box { - Box::new(self.clone()) - } - - fn domain(&self) -> Interval { - self.0.domain() - } - - fn evaluator_type(&self) -> TypeId { - TypeId::of::() - } - - fn create_evaluator(&self) -> Box { - Box::new(RotationCurveEvaluator { - evaluator: BasicAnimationCurveEvaluator::default(), - }) - } - - fn apply( - &self, - curve_evaluator: &mut dyn AnimationCurveEvaluator, - t: f32, - weight: f32, - graph_node: AnimationNodeIndex, - ) -> Result<(), AnimationEvaluationError> { - let curve_evaluator = (*Reflect::as_any_mut(curve_evaluator)) - .downcast_mut::() - .unwrap(); - let value = self.0.sample_clamped(t); - curve_evaluator - .evaluator - .stack - .push(BasicAnimationCurveEvaluatorStackElement { - value, - weight, - graph_node, - }); - Ok(()) - } -} - -impl AnimationCurveEvaluator for RotationCurveEvaluator { - fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { - self.evaluator.combine(graph_node, /*additive=*/ false) - } - - fn add(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { - self.evaluator.combine(graph_node, /*additive=*/ true) - } - - fn push_blend_register( - &mut self, - weight: f32, - graph_node: AnimationNodeIndex, - ) -> Result<(), AnimationEvaluationError> { - self.evaluator.push_blend_register(weight, graph_node) - } - - fn commit<'a>( - &mut self, - transform: Option>, - _: AnimationEntityMut<'a>, - ) -> Result<(), AnimationEvaluationError> { - let mut component = transform.ok_or_else(|| { - AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) - })?; - component.rotation = self - .evaluator - .stack - .pop() - .ok_or_else(inconsistent::)? - .value; - Ok(()) - } -} - -/// This type allows a [curve] valued in `Vec3` to become an [`AnimationCurve`] that animates -/// the scale component of a transform. -/// -/// [curve]: Curve -#[derive(Debug, Clone, Reflect, FromReflect)] -#[reflect(from_reflect = false)] -pub struct ScaleCurve(pub C); - -/// An [`AnimationCurveEvaluator`] for use with [`ScaleCurve`]s. -/// -/// You shouldn't need to instantiate this manually; Bevy will automatically do -/// so. -#[derive(Reflect)] -pub struct ScaleCurveEvaluator { - evaluator: BasicAnimationCurveEvaluator, -} - -impl AnimationCurve for ScaleCurve -where - C: AnimationCompatibleCurve, -{ - fn clone_value(&self) -> Box { - Box::new(self.clone()) - } - - fn domain(&self) -> Interval { - self.0.domain() - } - - fn evaluator_type(&self) -> TypeId { - TypeId::of::() - } - - fn create_evaluator(&self) -> Box { - Box::new(ScaleCurveEvaluator { - evaluator: BasicAnimationCurveEvaluator::default(), - }) - } - - fn apply( - &self, - curve_evaluator: &mut dyn AnimationCurveEvaluator, - t: f32, - weight: f32, - graph_node: AnimationNodeIndex, - ) -> Result<(), AnimationEvaluationError> { - let curve_evaluator = (*Reflect::as_any_mut(curve_evaluator)) - .downcast_mut::() - .unwrap(); - let value = self.0.sample_clamped(t); - curve_evaluator - .evaluator - .stack - .push(BasicAnimationCurveEvaluatorStackElement { - value, - weight, - graph_node, - }); - Ok(()) - } -} - -impl AnimationCurveEvaluator for ScaleCurveEvaluator { - fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { - self.evaluator.combine(graph_node, /*additive=*/ false) - } - - fn add(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { - self.evaluator.combine(graph_node, /*additive=*/ true) - } - - fn push_blend_register( - &mut self, - weight: f32, - graph_node: AnimationNodeIndex, - ) -> Result<(), AnimationEvaluationError> { - self.evaluator.push_blend_register(weight, graph_node) - } - - fn commit<'a>( - &mut self, - transform: Option>, - _: AnimationEntityMut<'a>, - ) -> Result<(), AnimationEvaluationError> { - let mut component = transform.ok_or_else(|| { - AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) - })?; - component.scale = self - .evaluator - .stack - .pop() - .ok_or_else(inconsistent::)? + .ok_or_else(inconsistent::>)? .value; Ok(()) } @@ -683,8 +471,8 @@ where self.0.domain() } - fn evaluator_type(&self) -> TypeId { - TypeId::of::() + fn evaluator_id(&self) -> EvaluatorId { + EvaluatorId::Type(TypeId::of::()) } fn create_evaluator(&self) -> Box { @@ -704,7 +492,7 @@ where weight: f32, graph_node: AnimationNodeIndex, ) -> Result<(), AnimationEvaluationError> { - let curve_evaluator = (*Reflect::as_any_mut(curve_evaluator)) + let curve_evaluator = curve_evaluator .downcast_mut::() .unwrap(); @@ -746,7 +534,15 @@ impl WeightsCurveEvaluator { None => { self.blend_register_blend_weight = Some(weight_to_blend); self.blend_register_morph_target_weights.clear(); - self.blend_register_morph_target_weights.extend(stack_iter); + + // In the additive case, the values pushed onto the blend register need + // to be scaled by the weight. + if additive { + self.blend_register_morph_target_weights + .extend(stack_iter.map(|m| m * weight_to_blend)); + } else { + self.blend_register_morph_target_weights.extend(stack_iter); + } } Some(ref mut current_weight) => { @@ -794,7 +590,6 @@ impl AnimationCurveEvaluator for WeightsCurveEvaluator { fn commit<'a>( &mut self, - _: Option>, mut entity: AnimationEntityMut<'a>, ) -> Result<(), AnimationEvaluationError> { if self.stack_morph_target_weights.is_empty() { @@ -877,7 +672,9 @@ where } = self.stack.pop().unwrap(); match self.blend_register.take() { - None => self.blend_register = Some((value_to_blend, weight_to_blend)), + None => { + self.initialize_blend_register(value_to_blend, weight_to_blend, additive); + } Some((mut current_value, mut current_weight)) => { current_weight += weight_to_blend; @@ -912,6 +709,22 @@ where Ok(()) } + fn initialize_blend_register(&mut self, value: A, weight: f32, additive: bool) { + if additive { + let scaled_value = A::blend( + [BlendInput { + weight, + value, + additive: true, + }] + .into_iter(), + ); + self.blend_register = Some((scaled_value, weight)); + } else { + self.blend_register = Some((value, weight)); + } + } + fn push_blend_register( &mut self, weight: f32, @@ -942,7 +755,7 @@ where /// mutated in the implementation of [`apply`]. /// /// [`apply`]: AnimationCurve::apply -pub trait AnimationCurve: Reflect + Debug + Send + Sync { +pub trait AnimationCurve: Debug + Send + Sync + 'static { /// Returns a boxed clone of this value. fn clone_value(&self) -> Box; @@ -953,14 +766,14 @@ pub trait AnimationCurve: Reflect + Debug + Send + Sync { /// /// This must match the type returned by [`Self::create_evaluator`]. It must /// be a single type that doesn't depend on the type of the curve. - fn evaluator_type(&self) -> TypeId; + fn evaluator_id(&self) -> EvaluatorId; /// Returns a newly-instantiated [`AnimationCurveEvaluator`] for use with /// this curve. /// /// All curve types must return the same type of /// [`AnimationCurveEvaluator`]. The returned value must match the type - /// returned by [`Self::evaluator_type`]. + /// returned by [`Self::evaluator_id`]. fn create_evaluator(&self) -> Box; /// Samples the curve at the given time `t`, and pushes the sampled value @@ -970,7 +783,7 @@ pub trait AnimationCurve: Reflect + Debug + Send + Sync { /// [`Self::create_evaluator`], upcast to an `&mut dyn /// AnimationCurveEvaluator`. Typically, implementations of [`Self::apply`] /// will want to downcast the `curve_evaluator` parameter to the concrete - /// type [`Self::evaluator_type`] in order to push values of the appropriate + /// type [`Self::evaluator_id`] in order to push values of the appropriate /// type onto its evaluation stack. /// /// Be sure not to confuse the `t` and `weight` values. The former @@ -986,6 +799,22 @@ pub trait AnimationCurve: Reflect + Debug + Send + Sync { ) -> Result<(), AnimationEvaluationError>; } +/// The [`EvaluatorId`] is used to look up the [`AnimationCurveEvaluator`] for an [`AnimatableProperty`]. +/// For a given animated property, this ID should always be the same to allow things like animation blending to occur. +#[derive(Clone)] +pub enum EvaluatorId<'a> { + /// Corresponds to a specific field on a specific component type. + /// The `TypeId` should correspond to the component type, and the `usize` + /// should correspond to the Reflect-ed field index of the field. + // + // IMPLEMENTATION NOTE: The Hashed<(TypeId, usize) is intentionally cheap to clone, as it will be cloned per frame by the evaluator + // Switching the field index `usize` for something like a field name `String` would probably be too expensive to justify + ComponentField(&'a Hashed<(TypeId, usize)>), + /// Corresponds to a custom property of a given type. This should be the [`TypeId`] + /// of the custom [`AnimatableProperty`]. + Type(TypeId), +} + /// A low-level trait for use in [`crate::VariableCurve`] that provides fine /// control over how animations are evaluated. /// @@ -1005,7 +834,9 @@ pub trait AnimationCurve: Reflect + Debug + Send + Sync { /// translation keyframes. The stack stores intermediate values generated while /// evaluating the [`crate::graph::AnimationGraph`], while the blend register /// stores the result of a blend operation. -pub trait AnimationCurveEvaluator: Reflect { +/// +/// [`Vec3`]: bevy_math::Vec3 +pub trait AnimationCurveEvaluator: Downcast + Send + Sync + 'static { /// Blends the top element of the stack with the blend register. /// /// The semantics of this method are as follows: @@ -1068,11 +899,12 @@ pub trait AnimationCurveEvaluator: Reflect { /// the stack, not blended with it. fn commit<'a>( &mut self, - transform: Option>, entity: AnimationEntityMut<'a>, ) -> Result<(), AnimationEvaluationError>; } +impl_downcast!(AnimationCurveEvaluator); + /// A [curve] defined by keyframes with values in an [animatable] type. /// /// The keyframes are interpolated using the type's [`Animatable::interpolate`] implementation. @@ -1127,3 +959,28 @@ where { AnimationEvaluationError::InconsistentEvaluatorImplementation(TypeId::of::

()) } + +/// Returns an [`AnimatedField`] with a given `$component` and `$field`. +/// +/// This can be used in the following way: +/// +/// ``` +/// # use bevy_animation::{animation_curves::AnimatedField, animated_field}; +/// # use bevy_ecs::component::Component; +/// # use bevy_math::Vec3; +/// # use bevy_reflect::Reflect; +/// #[derive(Component, Reflect)] +/// struct Transform { +/// translation: Vec3, +/// } +/// +/// let field = animated_field!(Transform::translation); +/// ``` +#[macro_export] +macro_rules! animated_field { + ($component:ident::$field:ident) => { + AnimatedField::new_unchecked(stringify!($field), |component: &mut $component| { + &mut component.$field + }) + }; +} diff --git a/crates/bevy_animation/src/animation_event.rs b/crates/bevy_animation/src/animation_event.rs deleted file mode 100644 index a2cf2da785a34..0000000000000 --- a/crates/bevy_animation/src/animation_event.rs +++ /dev/null @@ -1,281 +0,0 @@ -//! Traits and types for triggering events from animations. - -use core::{any::Any, fmt::Debug}; - -use bevy_ecs::prelude::*; -use bevy_reflect::{ - prelude::*, utility::NonGenericTypeInfoCell, ApplyError, DynamicTupleStruct, FromType, - GetTypeRegistration, ReflectFromPtr, ReflectKind, ReflectMut, ReflectOwned, ReflectRef, - TupleStructFieldIter, TupleStructInfo, TypeInfo, TypeRegistration, Typed, UnnamedField, -}; - -pub use bevy_animation_derive::AnimationEvent; - -pub(crate) fn trigger_animation_event( - entity: Entity, - time: f32, - weight: f32, - event: Box, -) -> impl Command { - move |world: &mut World| { - event.trigger(time, weight, entity, world); - } -} - -/// An event that can be used with animations. -/// It can be derived to trigger as an observer event, -/// if you need more complex behavior, consider -/// a manual implementation. -/// -/// # Example -/// -/// ```rust -/// # use bevy_animation::prelude::*; -/// # use bevy_ecs::prelude::*; -/// # use bevy_reflect::prelude::*; -/// # use bevy_asset::prelude::*; -/// # -/// #[derive(Event, AnimationEvent, Reflect, Clone)] -/// struct Say(String); -/// -/// fn on_say(trigger: Trigger) { -/// println!("{}", trigger.event().0); -/// } -/// -/// fn setup_animation( -/// mut commands: Commands, -/// mut animations: ResMut>, -/// mut graphs: ResMut>, -/// ) { -/// // Create a new animation and add an event at 1.0s. -/// let mut animation = AnimationClip::default(); -/// animation.add_event(1.0, Say("Hello".into())); -/// -/// // Create an animation graph. -/// let (graph, animation_index) = AnimationGraph::from_clip(animations.add(animation)); -/// -/// // Start playing the animation. -/// let mut player = AnimationPlayer::default(); -/// player.play(animation_index).repeat(); -/// -/// commands.spawn((AnimationGraphHandle(graphs.add(graph)), player)); -/// } -/// # -/// # bevy_ecs::system::assert_is_system(setup_animation); -/// ``` -#[reflect_trait] -pub trait AnimationEvent: CloneableAnimationEvent + Reflect + Send + Sync { - /// Trigger the event, targeting `entity`. - fn trigger(&self, time: f32, weight: f32, entity: Entity, world: &mut World); -} - -/// This trait exist so that manual implementors of [`AnimationEvent`] -/// do not have to implement `clone_value`. -#[diagnostic::on_unimplemented( - message = "`{Self}` does not implement `Clone`", - note = "consider annotating `{Self}` with `#[derive(Clone)]`" -)] -pub trait CloneableAnimationEvent { - /// Clone this value into a new `Box` - fn clone_value(&self) -> Box; -} - -impl CloneableAnimationEvent for T { - fn clone_value(&self) -> Box { - Box::new(self.clone()) - } -} - -/// The data that will be used to trigger an animation event. -#[derive(TypePath)] -pub(crate) struct AnimationEventData(pub(crate) Box); - -impl AnimationEventData { - pub(crate) fn new(event: impl AnimationEvent) -> Self { - Self(Box::new(event)) - } -} - -impl Debug for AnimationEventData { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.write_str("AnimationEventData(")?; - PartialReflect::debug(self.0.as_ref(), f)?; - f.write_str(")")?; - Ok(()) - } -} - -impl Clone for AnimationEventData { - fn clone(&self) -> Self { - Self(CloneableAnimationEvent::clone_value(self.0.as_ref())) - } -} - -// We have to implement `GetTypeRegistration` manually because of the embedded -// `Box`, which can't be automatically derived yet. -impl GetTypeRegistration for AnimationEventData { - fn get_type_registration() -> TypeRegistration { - let mut registration = TypeRegistration::of::(); - registration.insert::(FromType::::from_type()); - registration - } -} - -// We have to implement `Typed` manually because of the embedded -// `Box`, which can't be automatically derived yet. -impl Typed for AnimationEventData { - fn type_info() -> &'static TypeInfo { - static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); - CELL.get_or_set(|| { - TypeInfo::TupleStruct(TupleStructInfo::new::(&[UnnamedField::new::<()>(0)])) - }) - } -} - -// We have to implement `TupleStruct` manually because of the embedded -// `Box`, which can't be automatically derived yet. -impl TupleStruct for AnimationEventData { - fn field(&self, index: usize) -> Option<&dyn PartialReflect> { - match index { - 0 => Some(self.0.as_partial_reflect()), - _ => None, - } - } - - fn field_mut(&mut self, index: usize) -> Option<&mut dyn PartialReflect> { - match index { - 0 => Some(self.0.as_partial_reflect_mut()), - _ => None, - } - } - - fn field_len(&self) -> usize { - 1 - } - - fn iter_fields(&self) -> TupleStructFieldIter { - TupleStructFieldIter::new(self) - } - - fn clone_dynamic(&self) -> DynamicTupleStruct { - DynamicTupleStruct::from_iter([PartialReflect::clone_value(&*self.0)]) - } -} - -// We have to implement `PartialReflect` manually because of the embedded -// `Box`, which can't be automatically derived yet. -impl PartialReflect for AnimationEventData { - #[inline] - fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { - Some(::type_info()) - } - - #[inline] - fn into_partial_reflect(self: Box) -> Box { - self - } - - #[inline] - fn as_partial_reflect(&self) -> &dyn PartialReflect { - self - } - - #[inline] - fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { - self - } - - fn try_into_reflect(self: Box) -> Result, Box> { - Ok(self) - } - - #[inline] - fn try_as_reflect(&self) -> Option<&dyn Reflect> { - Some(self) - } - - #[inline] - fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { - Some(self) - } - - fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { - if let ReflectRef::TupleStruct(struct_value) = value.reflect_ref() { - for (i, value) in struct_value.iter_fields().enumerate() { - if let Some(v) = self.field_mut(i) { - v.try_apply(value)?; - } - } - } else { - return Err(ApplyError::MismatchedKinds { - from_kind: value.reflect_kind(), - to_kind: ReflectKind::TupleStruct, - }); - } - Ok(()) - } - - fn reflect_ref(&self) -> ReflectRef { - ReflectRef::TupleStruct(self) - } - - fn reflect_mut(&mut self) -> ReflectMut { - ReflectMut::TupleStruct(self) - } - - fn reflect_owned(self: Box) -> ReflectOwned { - ReflectOwned::TupleStruct(self) - } - - fn clone_value(&self) -> Box { - Box::new(Clone::clone(self)) - } -} - -// We have to implement `Reflect` manually because of the embedded -// `Box`, which can't be automatically derived yet. -impl Reflect for AnimationEventData { - #[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 - } - - #[inline] - fn as_reflect(&self) -> &dyn Reflect { - self - } - - #[inline] - fn as_reflect_mut(&mut self) -> &mut dyn Reflect { - self - } - - #[inline] - fn set(&mut self, value: Box) -> Result<(), Box> { - *self = value.take()?; - Ok(()) - } -} - -// We have to implement `FromReflect` manually because of the embedded -// `Box`, which can't be automatically derived yet. -impl FromReflect for AnimationEventData { - fn from_reflect(reflect: &dyn PartialReflect) -> Option { - Some(reflect.try_downcast_ref::()?.clone()) - } -} diff --git a/crates/bevy_animation/src/graph.rs b/crates/bevy_animation/src/graph.rs index d3bf1cc52db87..eb0589c53979b 100644 --- a/crates/bevy_animation/src/graph.rs +++ b/crates/bevy_animation/src/graph.rs @@ -217,9 +217,8 @@ pub enum AnimationNodeType { /// additively. /// /// The weights of all the children of this node are *not* normalized to - /// 1.0. Rather, the first child is used as a base, ignoring its weight, - /// while the others are multiplied by their respective weights and then - /// added in sequence to the base. + /// 1.0. Rather, each child is multiplied by its respective weight and + /// added in sequence. /// /// Add nodes are primarily useful for superimposing an animation for a /// portion of a rig on top of the main animation. For example, an add node diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs old mode 100755 new mode 100644 index 8d55cb7ea5c81..69d036415ecbd --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -11,15 +11,13 @@ extern crate alloc; pub mod animatable; pub mod animation_curves; -pub mod animation_event; pub mod gltf_curves; pub mod graph; pub mod transition; mod util; -use animation_event::{trigger_animation_event, AnimationEvent, AnimationEventData}; use core::{ - any::{Any, TypeId}, + any::TypeId, cell::RefCell, fmt::Debug, hash::{Hash, Hasher}, @@ -28,9 +26,12 @@ use core::{ use graph::AnimationNodeType; use prelude::AnimationCurveEvaluator; -use crate::graph::{AnimationGraphHandle, ThreadedAnimationGraphs}; +use crate::{ + graph::{AnimationGraphHandle, ThreadedAnimationGraphs}, + prelude::EvaluatorId, +}; -use bevy_app::{App, Plugin, PostUpdate}; +use bevy_app::{Animation, App, Plugin, PostUpdate}; use bevy_asset::{Asset, AssetApp, Assets}; use bevy_core::Name; use bevy_ecs::{ @@ -40,18 +41,13 @@ use bevy_ecs::{ world::EntityMutExcept, }; use bevy_math::FloatOrd; -use bevy_reflect::{ - prelude::ReflectDefault, utility::NonGenericTypeInfoCell, ApplyError, DynamicTupleStruct, - FromReflect, FromType, GetTypeRegistration, PartialReflect, Reflect, ReflectFromPtr, - ReflectKind, ReflectMut, ReflectOwned, ReflectRef, TupleStruct, TupleStructFieldIter, - TupleStructInfo, TypeInfo, TypePath, TypeRegistration, Typed, UnnamedField, -}; +use bevy_reflect::{prelude::ReflectDefault, Reflect, TypePath}; use bevy_time::Time; -use bevy_transform::{prelude::Transform, TransformSystem}; +use bevy_transform::TransformSystem; use bevy_utils::{ hashbrown::HashMap, tracing::{trace, warn}, - NoOpHash, TypeIdMap, + NoOpHash, PreHashMap, PreHashMapExt, TypeIdMap, }; use petgraph::graph::NodeIndex; use serde::{Deserialize, Serialize}; @@ -64,12 +60,8 @@ use uuid::Uuid; pub mod prelude { #[doc(hidden)] pub use crate::{ - animatable::*, - animation_curves::*, - animation_event::{AnimationEvent, ReflectAnimationEvent}, - graph::*, - transition::*, - AnimationClip, AnimationPlayer, AnimationPlugin, VariableCurve, + animatable::*, animation_curves::*, graph::*, transition::*, AnimationClip, + AnimationPlayer, AnimationPlugin, VariableCurve, }; } @@ -78,6 +70,7 @@ use crate::{ graph::{AnimationGraph, AnimationGraphAssetLoader, AnimationNodeIndex}, transition::{advance_transitions, expire_completed_transitions, AnimationTransitions}, }; +use alloc::sync::Arc; /// The [UUID namespace] of animation targets (e.g. bones). /// @@ -105,175 +98,6 @@ impl VariableCurve { } } -// We have to implement `PartialReflect` manually because of the embedded -// `Box`, which can't be automatically derived yet. -impl PartialReflect for VariableCurve { - #[inline] - fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { - Some(::type_info()) - } - - #[inline] - fn into_partial_reflect(self: Box) -> Box { - self - } - - #[inline] - fn as_partial_reflect(&self) -> &dyn PartialReflect { - self - } - - #[inline] - fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { - self - } - - fn try_into_reflect(self: Box) -> Result, Box> { - Ok(self) - } - - #[inline] - fn try_as_reflect(&self) -> Option<&dyn Reflect> { - Some(self) - } - - #[inline] - fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { - Some(self) - } - - fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { - if let ReflectRef::TupleStruct(tuple_value) = value.reflect_ref() { - for (i, value) in tuple_value.iter_fields().enumerate() { - if let Some(v) = self.field_mut(i) { - v.try_apply(value)?; - } - } - } else { - return Err(ApplyError::MismatchedKinds { - from_kind: value.reflect_kind(), - to_kind: ReflectKind::TupleStruct, - }); - } - Ok(()) - } - - fn reflect_ref(&self) -> ReflectRef { - ReflectRef::TupleStruct(self) - } - - fn reflect_mut(&mut self) -> ReflectMut { - ReflectMut::TupleStruct(self) - } - - fn reflect_owned(self: Box) -> ReflectOwned { - ReflectOwned::TupleStruct(self) - } - - fn clone_value(&self) -> Box { - Box::new((*self).clone()) - } -} - -// We have to implement `Reflect` manually because of the embedded `Box`, which can't be automatically derived yet. -impl Reflect for VariableCurve { - #[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 - } - - #[inline] - fn as_reflect(&self) -> &dyn Reflect { - self - } - - #[inline] - fn as_reflect_mut(&mut self) -> &mut dyn Reflect { - self - } - - #[inline] - fn set(&mut self, value: Box) -> Result<(), Box> { - *self = value.take()?; - Ok(()) - } -} - -// We have to implement `TupleStruct` manually because of the embedded `Box`, which can't be automatically derived yet. -impl TupleStruct for VariableCurve { - fn field(&self, index: usize) -> Option<&dyn PartialReflect> { - match index { - 0 => Some(self.0.as_partial_reflect()), - _ => None, - } - } - - fn field_mut(&mut self, index: usize) -> Option<&mut dyn PartialReflect> { - match index { - 0 => Some(self.0.as_partial_reflect_mut()), - _ => None, - } - } - - fn field_len(&self) -> usize { - 1 - } - - fn iter_fields(&self) -> TupleStructFieldIter { - TupleStructFieldIter::new(self) - } - - fn clone_dynamic(&self) -> DynamicTupleStruct { - DynamicTupleStruct::from_iter([PartialReflect::clone_value(&*self.0)]) - } -} - -// We have to implement `FromReflect` manually because of the embedded `Box`, which can't be automatically derived yet. -impl FromReflect for VariableCurve { - fn from_reflect(reflect: &dyn PartialReflect) -> Option { - Some(reflect.try_downcast_ref::()?.clone()) - } -} - -// We have to implement `GetTypeRegistration` manually because of the embedded -// `Box`, which can't be automatically derived yet. -impl GetTypeRegistration for VariableCurve { - fn get_type_registration() -> TypeRegistration { - let mut registration = TypeRegistration::of::(); - registration.insert::(FromType::::from_type()); - registration - } -} - -// We have to implement `Typed` manually because of the embedded `Box`, which can't be automatically derived yet. -impl Typed for VariableCurve { - fn type_info() -> &'static TypeInfo { - static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); - CELL.get_or_set(|| { - TypeInfo::TupleStruct(TupleStructInfo::new::(&[UnnamedField::new::<()>(0)])) - }) - } -} - /// A list of [`VariableCurve`]s and the [`AnimationTargetId`]s to which they /// apply. /// @@ -281,6 +105,8 @@ impl Typed for VariableCurve { /// [`AnimationTarget`] with that ID. #[derive(Asset, Reflect, Clone, Debug, Default)] pub struct AnimationClip { + // This field is ignored by reflection because AnimationCurves can contain things that are not reflect-able + #[reflect(ignore)] curves: AnimationCurves, events: AnimationEvents, duration: f32, @@ -289,7 +115,35 @@ pub struct AnimationClip { #[derive(Reflect, Debug, Clone)] struct TimedAnimationEvent { time: f32, - event: AnimationEventData, + event: AnimationEvent, +} + +#[derive(Reflect, Debug, Clone)] +struct AnimationEvent { + #[reflect(ignore)] + trigger: AnimationEventFn, +} + +impl AnimationEvent { + fn trigger(&self, commands: &mut Commands, entity: Entity, time: f32, weight: f32) { + (self.trigger.0)(commands, entity, time, weight); + } +} + +#[derive(Reflect, Clone)] +#[reflect(opaque)] +struct AnimationEventFn(Arc); + +impl Default for AnimationEventFn { + fn default() -> Self { + Self(Arc::new(|_commands, _entity, _time, _weight| {})) + } +} + +impl Debug for AnimationEventFn { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_tuple("AnimationEventFn").finish() + } } #[derive(Reflect, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)] @@ -472,9 +326,24 @@ impl AnimationClip { .push(variable_curve); } - /// Add an [`AnimationEvent`] to an [`AnimationTarget`] named by an [`AnimationTargetId`]. + /// Add a untargeted [`Event`] to this [`AnimationClip`]. + /// + /// The `event` will be cloned and triggered on the [`AnimationPlayer`] entity once the `time` (in seconds) + /// is reached in the animation. + /// + /// See also [`add_event_to_target`](Self::add_event_to_target). + pub fn add_event(&mut self, time: f32, event: impl Event + Clone) { + self.add_event_fn( + time, + move |commands: &mut Commands, entity: Entity, _time: f32, _weight: f32| { + commands.entity(entity).trigger(event.clone()); + }, + ); + } + + /// Add an [`Event`] to an [`AnimationTarget`] named by an [`AnimationTargetId`]. /// - /// The `event` will trigger on the entity matching the target once the `time` (in seconds) + /// The `event` will be cloned and triggered on the entity matching the target once the `time` (in seconds) /// is reached in the animation. /// /// Use [`add_event`](Self::add_event) instead if you don't have a specific target. @@ -482,26 +351,69 @@ impl AnimationClip { &mut self, target_id: AnimationTargetId, time: f32, - event: impl AnimationEvent, + event: impl Event + Clone, ) { - self.add_event_to_target_inner(AnimationEventTarget::Node(target_id), time, event); + self.add_event_fn_to_target( + target_id, + time, + move |commands: &mut Commands, entity: Entity, _time: f32, _weight: f32| { + commands.entity(entity).trigger(event.clone()); + }, + ); } - /// Add a untargeted [`AnimationEvent`] to this [`AnimationClip`]. + /// Add a untargeted event function to this [`AnimationClip`]. /// - /// The `event` will trigger on the [`AnimationPlayer`] entity once the `time` (in seconds) + /// The `func` will trigger on the [`AnimationPlayer`] entity once the `time` (in seconds) /// is reached in the animation. /// + /// For a simpler [`Event`]-based alternative, see [`AnimationClip::add_event`]. /// See also [`add_event_to_target`](Self::add_event_to_target). - pub fn add_event(&mut self, time: f32, event: impl AnimationEvent) { - self.add_event_to_target_inner(AnimationEventTarget::Root, time, event); + /// + /// ``` + /// # use bevy_animation::AnimationClip; + /// # let mut clip = AnimationClip::default(); + /// clip.add_event_fn(1.0, |commands, entity, time, weight| { + /// println!("Animation Event Triggered {entity:#?} at time {time} with weight {weight}"); + /// }) + /// ``` + pub fn add_event_fn( + &mut self, + time: f32, + func: impl Fn(&mut Commands, Entity, f32, f32) + Send + Sync + 'static, + ) { + self.add_event_internal(AnimationEventTarget::Root, time, func); + } + + /// Add an event function to an [`AnimationTarget`] named by an [`AnimationTargetId`]. + /// + /// The `func` will trigger on the entity matching the target once the `time` (in seconds) + /// is reached in the animation. + /// + /// For a simpler [`Event`]-based alternative, see [`AnimationClip::add_event_to_target`]. + /// Use [`add_event`](Self::add_event) instead if you don't have a specific target. + /// + /// ``` + /// # use bevy_animation::{AnimationClip, AnimationTargetId}; + /// # let mut clip = AnimationClip::default(); + /// clip.add_event_fn_to_target(AnimationTargetId::from_iter(["Arm", "Hand"]), 1.0, |commands, entity, time, weight| { + /// println!("Animation Event Triggered {entity:#?} at time {time} with weight {weight}"); + /// }) + /// ``` + pub fn add_event_fn_to_target( + &mut self, + target_id: AnimationTargetId, + time: f32, + func: impl Fn(&mut Commands, Entity, f32, f32) + Send + Sync + 'static, + ) { + self.add_event_internal(AnimationEventTarget::Node(target_id), time, func); } - fn add_event_to_target_inner( + fn add_event_internal( &mut self, target: AnimationEventTarget, time: f32, - event: impl AnimationEvent, + trigger_fn: impl Fn(&mut Commands, Entity, f32, f32) + Send + Sync + 'static, ) { self.duration = self.duration.max(time); let triggers = self.events.entry(target).or_default(); @@ -510,7 +422,9 @@ impl AnimationClip { index, TimedAnimationEvent { time, - event: AnimationEventData::new(event), + event: AnimationEvent { + trigger: AnimationEventFn(Arc::new(trigger_fn)), + }, }, ), } @@ -795,7 +709,7 @@ pub struct AnimationEvaluationState { /// Stores all [`AnimationCurveEvaluator`]s corresponding to properties that /// we've seen so far. /// - /// This is a mapping from the type ID of an animation curve evaluator to + /// This is a mapping from the id of an animation curve evaluator to /// the animation curve evaluator itself. /// /// For efficiency's sake, the [`AnimationCurveEvaluator`]s are cached from @@ -803,15 +717,98 @@ pub struct AnimationEvaluationState { /// there may be entries in this list corresponding to properties that the /// current [`AnimationPlayer`] doesn't animate. To iterate only over the /// properties that are currently being animated, consult the - /// [`Self::current_curve_evaluator_types`] set. - curve_evaluators: TypeIdMap>, + /// [`Self::current_evaluators`] set. + evaluators: AnimationCurveEvaluators, /// The set of [`AnimationCurveEvaluator`] types that the current /// [`AnimationPlayer`] is animating. /// /// This is built up as new curve evaluators are encountered during graph /// traversal. - current_curve_evaluator_types: TypeIdMap<()>, + current_evaluators: CurrentEvaluators, +} + +#[derive(Default)] +struct AnimationCurveEvaluators { + component_property_curve_evaluators: + PreHashMap<(TypeId, usize), Box>, + type_id_curve_evaluators: TypeIdMap>, +} + +impl AnimationCurveEvaluators { + #[inline] + pub(crate) fn get_mut(&mut self, id: EvaluatorId) -> Option<&mut dyn AnimationCurveEvaluator> { + match id { + EvaluatorId::ComponentField(component_property) => self + .component_property_curve_evaluators + .get_mut(component_property), + EvaluatorId::Type(type_id) => self.type_id_curve_evaluators.get_mut(&type_id), + } + .map(|e| &mut **e) + } + + #[inline] + pub(crate) fn get_or_insert_with( + &mut self, + id: EvaluatorId, + func: impl FnOnce() -> Box, + ) -> &mut dyn AnimationCurveEvaluator { + match id { + EvaluatorId::ComponentField(component_property) => &mut **self + .component_property_curve_evaluators + .get_or_insert_with(component_property, func), + EvaluatorId::Type(type_id) => match self.type_id_curve_evaluators.entry(type_id) { + bevy_utils::hashbrown::hash_map::Entry::Occupied(occupied_entry) => { + &mut **occupied_entry.into_mut() + } + bevy_utils::hashbrown::hash_map::Entry::Vacant(vacant_entry) => { + &mut **vacant_entry.insert(func()) + } + }, + } + } +} + +#[derive(Default)] +struct CurrentEvaluators { + component_properties: PreHashMap<(TypeId, usize), ()>, + type_ids: TypeIdMap<()>, +} + +impl CurrentEvaluators { + pub(crate) fn keys(&self) -> impl Iterator { + self.component_properties + .keys() + .map(EvaluatorId::ComponentField) + .chain(self.type_ids.keys().copied().map(EvaluatorId::Type)) + } + + pub(crate) fn clear( + &mut self, + mut visit: impl FnMut(EvaluatorId) -> Result<(), AnimationEvaluationError>, + ) -> Result<(), AnimationEvaluationError> { + for (key, _) in self.component_properties.drain() { + (visit)(EvaluatorId::ComponentField(&key))?; + } + + for (key, _) in self.type_ids.drain() { + (visit)(EvaluatorId::Type(key))?; + } + + Ok(()) + } + + #[inline] + pub(crate) fn insert(&mut self, id: EvaluatorId) { + match id { + EvaluatorId::ComponentField(component_property) => { + self.component_properties.insert(*component_property, ()); + } + EvaluatorId::Type(type_id) => { + self.type_ids.insert(type_id, ()); + } + } + } } impl AnimationPlayer { @@ -988,12 +985,7 @@ fn trigger_untargeted_animation_events( }; for TimedAnimationEvent { time, event } in triggered_events.iter() { - commands.queue(trigger_animation_event( - entity, - *time, - active_animation.weight, - event.clone().0, - )); + event.trigger(&mut commands, entity, *time, active_animation.weight); } } } @@ -1039,15 +1031,8 @@ pub fn advance_animations( } /// A type alias for [`EntityMutExcept`] as used in animation. -pub type AnimationEntityMut<'w> = EntityMutExcept< - 'w, - ( - AnimationTarget, - Transform, - AnimationPlayer, - AnimationGraphHandle, - ), ->; +pub type AnimationEntityMut<'w> = + EntityMutExcept<'w, (AnimationTarget, AnimationPlayer, AnimationGraphHandle)>; /// A system that modifies animation targets (e.g. bones in a skinned mesh) /// according to the currently-playing animations. @@ -1057,18 +1042,13 @@ pub fn animate_targets( graphs: Res>, threaded_animation_graphs: Res, players: Query<(&AnimationPlayer, &AnimationGraphHandle)>, - mut targets: Query<( - Entity, - &AnimationTarget, - Option<&mut Transform>, - AnimationEntityMut, - )>, + mut targets: Query<(Entity, &AnimationTarget, AnimationEntityMut)>, animation_evaluation_state: Local>>, ) { // Evaluate all animation targets in parallel. targets .par_iter_mut() - .for_each(|(entity, target, transform, entity_mut)| { + .for_each(|(entity, target, entity_mut)| { let &AnimationTarget { id: target_id, player: player_id, @@ -1195,12 +1175,12 @@ pub fn animate_targets( for TimedAnimationEvent { time, event } in triggered_events.iter() { - commands.queue(trigger_animation_event( + event.trigger( + &mut commands, entity, *time, active_animation.weight, - event.clone().0, - )); + ); } }); } @@ -1222,19 +1202,20 @@ pub fn animate_targets( // will both yield a `RotationCurveEvaluator` and // therefore will share the same evaluator in this // table. - let curve_evaluator_type_id = (*curve.0).evaluator_type(); + let curve_evaluator_id = (*curve.0).evaluator_id(); let curve_evaluator = evaluation_state - .curve_evaluators - .entry(curve_evaluator_type_id) - .or_insert_with(|| curve.0.create_evaluator()); + .evaluators + .get_or_insert_with(curve_evaluator_id.clone(), || { + curve.0.create_evaluator() + }); evaluation_state - .current_curve_evaluator_types - .insert(curve_evaluator_type_id, ()); + .current_evaluators + .insert(curve_evaluator_id); if let Err(err) = AnimationCurve::apply( &*curve.0, - &mut **curve_evaluator, + curve_evaluator, seek_time, weight, animation_graph_node_index, @@ -1246,16 +1227,12 @@ pub fn animate_targets( } } - if let Err(err) = evaluation_state.commit_all(transform, entity_mut) { + if let Err(err) = evaluation_state.commit_all(entity_mut) { warn!("Animation application failed: {:?}", err); } }); } -/// Animation system set -#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)] -pub struct Animation; - /// Adds animation support to an app #[derive(Default)] pub struct AnimationPlugin; @@ -1351,8 +1328,8 @@ impl AnimationEvaluationState { &mut self, node_index: AnimationNodeIndex, ) -> Result<(), AnimationEvaluationError> { - for curve_evaluator_type in self.current_curve_evaluator_types.keys() { - self.curve_evaluators + for curve_evaluator_type in self.current_evaluators.keys() { + self.evaluators .get_mut(curve_evaluator_type) .unwrap() .blend(node_index)?; @@ -1365,8 +1342,8 @@ impl AnimationEvaluationState { /// /// The given `node_index` is the node that we're evaluating. fn add_all(&mut self, node_index: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { - for curve_evaluator_type in self.current_curve_evaluator_types.keys() { - self.curve_evaluators + for curve_evaluator_type in self.current_evaluators.keys() { + self.evaluators .get_mut(curve_evaluator_type) .unwrap() .add(node_index)?; @@ -1385,8 +1362,8 @@ impl AnimationEvaluationState { weight: f32, node_index: AnimationNodeIndex, ) -> Result<(), AnimationEvaluationError> { - for curve_evaluator_type in self.current_curve_evaluator_types.keys() { - self.curve_evaluators + for curve_evaluator_type in self.current_evaluators.keys() { + self.evaluators .get_mut(curve_evaluator_type) .unwrap() .push_blend_register(weight, node_index)?; @@ -1401,19 +1378,14 @@ impl AnimationEvaluationState { /// components being animated. fn commit_all( &mut self, - mut transform: Option>, mut entity_mut: AnimationEntityMut, ) -> Result<(), AnimationEvaluationError> { - for (curve_evaluator_type, _) in self.current_curve_evaluator_types.drain() { - self.curve_evaluators - .get_mut(&curve_evaluator_type) + self.current_evaluators.clear(|id| { + self.evaluators + .get_mut(id) .unwrap() - .commit( - transform.as_mut().map(|transform| transform.reborrow()), - entity_mut.reborrow(), - )?; - } - Ok(()) + .commit(entity_mut.reborrow()) + }) } } @@ -1573,12 +1545,6 @@ mod tests { #[derive(Event, Reflect, Clone)] struct A; - impl AnimationEvent for A { - fn trigger(&self, _time: f32, _weight: f32, target: Entity, world: &mut World) { - world.entity_mut(target).trigger(self.clone()); - } - } - #[track_caller] fn assert_triggered_events_with( active_animation: &ActiveAnimation, diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index 4cdc3a2473d55..70a2f7456d950 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -1008,12 +1008,18 @@ impl App { .try_register_required_components_with::(constructor) } - /// Returns a reference to the [`World`]. + /// Returns a reference to the main [`SubApp`]'s [`World`]. This is the same as calling + /// [`app.main().world()`]. + /// + /// [`app.main().world()`]: SubApp::world pub fn world(&self) -> &World { self.main().world() } - /// Returns a mutable reference to the [`World`]. + /// Returns a mutable reference to the main [`SubApp`]'s [`World`]. This is the same as calling + /// [`app.main_mut().world_mut()`]. + /// + /// [`app.main_mut().world_mut()`]: SubApp::world_mut pub fn world_mut(&mut self) -> &mut World { self.main_mut().world_mut() } diff --git a/crates/bevy_app/src/main_schedule.rs b/crates/bevy_app/src/main_schedule.rs index f2b549443769c..834205cf5347f 100644 --- a/crates/bevy_app/src/main_schedule.rs +++ b/crates/bevy_app/src/main_schedule.rs @@ -181,6 +181,10 @@ pub struct PostUpdate; #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] pub struct Last; +/// Animation system set. This exists in [`PostUpdate`]. +#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)] +pub struct Animation; + /// Defines the schedules to be run for the [`Main`] schedule, including /// their order. #[derive(Resource, Debug)] diff --git a/crates/bevy_audio/src/audio.rs b/crates/bevy_audio/src/audio.rs index 58dc0e49a1e6b..7c020a0333fa2 100644 --- a/crates/bevy_audio/src/audio.rs +++ b/crates/bevy_audio/src/audio.rs @@ -264,6 +264,17 @@ where } } +impl AudioPlayer { + /// Creates a new [`AudioPlayer`] with the given [`Handle`]. + /// + /// For convenience reasons, this hard-codes the [`AudioSource`] type. If you want to + /// initialize an [`AudioPlayer`] with a different type, just initialize it directly using normal + /// tuple struct syntax. + pub fn new(source: Handle) -> Self { + Self(source) + } +} + /// Bundle for playing a sound. /// /// Insert this bundle onto an entity to trigger a sound source to begin playing. diff --git a/crates/bevy_audio/src/lib.rs b/crates/bevy_audio/src/lib.rs index 5120a341a1e91..1519987a4b155 100644 --- a/crates/bevy_audio/src/lib.rs +++ b/crates/bevy_audio/src/lib.rs @@ -21,7 +21,7 @@ //! //! fn play_background_audio(asset_server: Res, mut commands: Commands) { //! commands.spawn(( -//! AudioPlayer::(asset_server.load("background_audio.ogg")), +//! AudioPlayer::new(asset_server.load("background_audio.ogg")), //! PlaybackSettings::LOOP, //! )); //! } diff --git a/crates/bevy_color/Cargo.toml b/crates/bevy_color/Cargo.toml index 7c8bde7aea6f6..3c1f0d9c33a7a 100644 --- a/crates/bevy_color/Cargo.toml +++ b/crates/bevy_color/Cargo.toml @@ -7,7 +7,7 @@ homepage = "https://bevyengine.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy", "color"] -rust-version = "1.76.0" +rust-version = "1.82.0" [dependencies] bevy_math = { path = "../bevy_math", version = "0.15.0-dev", default-features = false, features = [ diff --git a/crates/bevy_core/src/task_pool_options.rs b/crates/bevy_core/src/task_pool_options.rs index 276902fb499da..cdb0418a35338 100644 --- a/crates/bevy_core/src/task_pool_options.rs +++ b/crates/bevy_core/src/task_pool_options.rs @@ -1,9 +1,12 @@ use bevy_tasks::{AsyncComputeTaskPool, ComputeTaskPool, IoTaskPool, TaskPoolBuilder}; use bevy_utils::tracing::trace; +use alloc::sync::Arc; +use core::fmt::Debug; + /// Defines a simple way to determine how many threads to use given the number of remaining cores /// and number of total cores -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct TaskPoolThreadAssignmentPolicy { /// Force using at least this many threads pub min_threads: usize, @@ -12,6 +15,22 @@ pub struct TaskPoolThreadAssignmentPolicy { /// Target using this percentage of total cores, clamped by `min_threads` and `max_threads`. It is /// permitted to use 1.0 to try to use all remaining threads pub percent: f32, + /// Callback that is invoked once for every created thread as it starts. + /// This configuration will be ignored under wasm platform. + pub on_thread_spawn: Option>, + /// Callback that is invoked once for every created thread as it terminates + /// This configuration will be ignored under wasm platform. + pub on_thread_destroy: Option>, +} + +impl Debug for TaskPoolThreadAssignmentPolicy { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("TaskPoolThreadAssignmentPolicy") + .field("min_threads", &self.min_threads) + .field("max_threads", &self.max_threads) + .field("percent", &self.percent) + .finish() + } } impl TaskPoolThreadAssignmentPolicy { @@ -61,6 +80,8 @@ impl Default for TaskPoolOptions { min_threads: 1, max_threads: 4, percent: 0.25, + on_thread_spawn: None, + on_thread_destroy: None, }, // Use 25% of cores for async compute, at least 1, no more than 4 @@ -68,6 +89,8 @@ impl Default for TaskPoolOptions { min_threads: 1, max_threads: 4, percent: 0.25, + on_thread_spawn: None, + on_thread_destroy: None, }, // Use all remaining cores for compute (at least 1) @@ -75,6 +98,8 @@ impl Default for TaskPoolOptions { min_threads: 1, max_threads: usize::MAX, percent: 1.0, // This 1.0 here means "whatever is left over" + on_thread_spawn: None, + on_thread_destroy: None, }, } } @@ -108,10 +133,21 @@ impl TaskPoolOptions { remaining_threads = remaining_threads.saturating_sub(io_threads); IoTaskPool::get_or_init(|| { - TaskPoolBuilder::default() + let mut builder = TaskPoolBuilder::default() .num_threads(io_threads) - .thread_name("IO Task Pool".to_string()) - .build() + .thread_name("IO Task Pool".to_string()); + + #[cfg(not(target_arch = "wasm32"))] + { + if let Some(f) = self.io.on_thread_spawn.clone() { + builder = builder.on_thread_spawn(move || f()); + } + if let Some(f) = self.io.on_thread_destroy.clone() { + builder = builder.on_thread_destroy(move || f()); + } + } + + builder.build() }); } @@ -125,10 +161,21 @@ impl TaskPoolOptions { remaining_threads = remaining_threads.saturating_sub(async_compute_threads); AsyncComputeTaskPool::get_or_init(|| { - TaskPoolBuilder::default() + let mut builder = TaskPoolBuilder::default() .num_threads(async_compute_threads) - .thread_name("Async Compute Task Pool".to_string()) - .build() + .thread_name("Async Compute Task Pool".to_string()); + + #[cfg(not(target_arch = "wasm32"))] + { + if let Some(f) = self.async_compute.on_thread_spawn.clone() { + builder = builder.on_thread_spawn(move || f()); + } + if let Some(f) = self.async_compute.on_thread_destroy.clone() { + builder = builder.on_thread_destroy(move || f()); + } + } + + builder.build() }); } @@ -142,10 +189,21 @@ impl TaskPoolOptions { trace!("Compute Threads: {}", compute_threads); ComputeTaskPool::get_or_init(|| { - TaskPoolBuilder::default() + let mut builder = TaskPoolBuilder::default() .num_threads(compute_threads) - .thread_name("Compute Task Pool".to_string()) - .build() + .thread_name("Compute Task Pool".to_string()); + + #[cfg(not(target_arch = "wasm32"))] + { + if let Some(f) = self.compute.on_thread_spawn.clone() { + builder = builder.on_thread_spawn(move || f()); + } + if let Some(f) = self.compute.on_thread_destroy.clone() { + builder = builder.on_thread_destroy(move || f()); + } + } + + builder.build() }); } } diff --git a/crates/bevy_core_pipeline/src/auto_exposure/pipeline.rs b/crates/bevy_core_pipeline/src/auto_exposure/pipeline.rs index 410b7deb6d47e..87d6abd8cf8c5 100644 --- a/crates/bevy_core_pipeline/src/auto_exposure/pipeline.rs +++ b/crates/bevy_core_pipeline/src/auto_exposure/pipeline.rs @@ -3,11 +3,11 @@ use super::compensation_curve::{ }; use bevy_asset::prelude::*; use bevy_ecs::prelude::*; +use bevy_image::Image; use bevy_render::{ globals::GlobalsUniform, render_resource::{binding_types::*, *}, renderer::RenderDevice, - texture::Image, view::ViewUniform, }; use core::num::NonZero; @@ -89,6 +89,7 @@ impl SpecializedComputePipeline for AutoExposurePipeline { AutoExposurePass::Average => "compute_average".into(), }, push_constant_ranges: vec![], + zero_initialize_workgroup_memory: false, } } } diff --git a/crates/bevy_core_pipeline/src/auto_exposure/settings.rs b/crates/bevy_core_pipeline/src/auto_exposure/settings.rs index 066e8d3c8867b..91bdf836eebee 100644 --- a/crates/bevy_core_pipeline/src/auto_exposure/settings.rs +++ b/crates/bevy_core_pipeline/src/auto_exposure/settings.rs @@ -3,8 +3,9 @@ use core::ops::RangeInclusive; use super::compensation_curve::AutoExposureCompensationCurve; use bevy_asset::Handle; use bevy_ecs::{prelude::Component, reflect::ReflectComponent}; +use bevy_image::Image; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -use bevy_render::{extract_component::ExtractComponent, texture::Image}; +use bevy_render::extract_component::ExtractComponent; use bevy_utils::default; /// Component that enables auto exposure for an HDR-enabled 2d or 3d camera. diff --git a/crates/bevy_core_pipeline/src/blit/mod.rs b/crates/bevy_core_pipeline/src/blit/mod.rs index d11a99cbb329d..96c0394f3034a 100644 --- a/crates/bevy_core_pipeline/src/blit/mod.rs +++ b/crates/bevy_core_pipeline/src/blit/mod.rs @@ -98,6 +98,7 @@ impl SpecializedRenderPipeline for BlitPipeline { ..Default::default() }, push_constant_ranges: Vec::new(), + zero_initialize_workgroup_memory: false, } } } diff --git a/crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs b/crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs index 28d55d360a95f..e3efe5cad8946 100644 --- a/crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs +++ b/crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs @@ -127,6 +127,7 @@ impl SpecializedRenderPipeline for BloomDownsamplingPipeline { depth_stencil: None, multisample: MultisampleState::default(), push_constant_ranges: Vec::new(), + zero_initialize_workgroup_memory: false, } } } diff --git a/crates/bevy_core_pipeline/src/bloom/upsampling_pipeline.rs b/crates/bevy_core_pipeline/src/bloom/upsampling_pipeline.rs index 91a23310ef81c..b63a3eb633485 100644 --- a/crates/bevy_core_pipeline/src/bloom/upsampling_pipeline.rs +++ b/crates/bevy_core_pipeline/src/bloom/upsampling_pipeline.rs @@ -124,6 +124,7 @@ impl SpecializedRenderPipeline for BloomUpsamplingPipeline { depth_stencil: None, multisample: MultisampleState::default(), push_constant_ranges: Vec::new(), + zero_initialize_workgroup_memory: false, } } } diff --git a/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/mod.rs b/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/mod.rs index 71c9cc2bb2a61..fbc3ecfec3f75 100644 --- a/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/mod.rs +++ b/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/mod.rs @@ -6,6 +6,7 @@ use crate::{ use bevy_app::prelude::*; use bevy_asset::{load_internal_asset, Handle}; use bevy_ecs::{prelude::*, query::QueryItem}; +use bevy_image::BevyDefault as _; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ extract_component::{ExtractComponent, ExtractComponentPlugin, UniformComponentPlugin}, @@ -16,7 +17,6 @@ use bevy_render::{ *, }, renderer::RenderDevice, - texture::BevyDefault, view::{ExtractedView, ViewTarget}, Render, RenderApp, RenderSet, }; @@ -233,6 +233,7 @@ impl SpecializedRenderPipeline for CasPipeline { depth_stencil: None, multisample: MultisampleState::default(), push_constant_ranges: Vec::new(), + zero_initialize_workgroup_memory: false, } } } @@ -242,14 +243,22 @@ fn prepare_cas_pipelines( pipeline_cache: Res, mut pipelines: ResMut>, sharpening_pipeline: Res, - views: Query<(Entity, &ExtractedView, &DenoiseCas), With>, + views: Query< + (Entity, &ExtractedView, &DenoiseCas), + Or<(Added, Changed)>, + >, + mut removals: RemovedComponents, ) { - for (entity, view, cas) in &views { + for entity in removals.read() { + commands.entity(entity).remove::(); + } + + for (entity, view, denoise_cas) in &views { let pipeline_id = pipelines.specialize( &pipeline_cache, &sharpening_pipeline, CasPipelineKey { - denoise: cas.0, + denoise: denoise_cas.0, texture_format: if view.hdr { ViewTarget::TEXTURE_FORMAT_HDR } else { diff --git a/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/robust_contrast_adaptive_sharpening.wgsl b/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/robust_contrast_adaptive_sharpening.wgsl index 252d97c9d6c3e..03b29976e7ed7 100644 --- a/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/robust_contrast_adaptive_sharpening.wgsl +++ b/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/robust_contrast_adaptive_sharpening.wgsl @@ -65,7 +65,7 @@ fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { let b = textureSample(screenTexture, samp, in.uv, vec2(0, -1)).rgb; let d = textureSample(screenTexture, samp, in.uv, vec2(-1, 0)).rgb; // We need the alpha value of the pixel we're working on for the output - let e = textureSample(screenTexture, samp, in.uv).rgbw; + let e = textureSample(screenTexture, samp, in.uv).rgba; let f = textureSample(screenTexture, samp, in.uv, vec2(1, 0)).rgb; let h = textureSample(screenTexture, samp, in.uv, vec2(0, 1)).rgb; // Min and max of ring. diff --git a/crates/bevy_core_pipeline/src/core_3d/mod.rs b/crates/bevy_core_pipeline/src/core_3d/mod.rs index ae88905bacd49..4a616226d150f 100644 --- a/crates/bevy_core_pipeline/src/core_3d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_3d/mod.rs @@ -65,14 +65,15 @@ pub const DEPTH_TEXTURE_SAMPLING_SUPPORTED: bool = true; use core::ops::Range; -use bevy_asset::{AssetId, UntypedAssetId}; -use bevy_color::LinearRgba; pub use camera_3d::*; pub use main_opaque_pass_3d_node::*; pub use main_transparent_pass_3d_node::*; use bevy_app::{App, Plugin, PostUpdate}; +use bevy_asset::{AssetId, UntypedAssetId}; +use bevy_color::LinearRgba; use bevy_ecs::{entity::EntityHashSet, prelude::*}; +use bevy_image::{BevyDefault, Image}; use bevy_math::FloatOrd; use bevy_render::sync_world::MainEntity; use bevy_render::{ @@ -91,7 +92,7 @@ use bevy_render::{ }, renderer::RenderDevice, sync_world::RenderEntity, - texture::{BevyDefault, ColorAttachment, Image, TextureCache}, + texture::{ColorAttachment, TextureCache}, view::{ExtractedView, ViewDepthTexture, ViewTarget}, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; diff --git a/crates/bevy_core_pipeline/src/deferred/copy_lighting_id.rs b/crates/bevy_core_pipeline/src/deferred/copy_lighting_id.rs index 4f5462d1f52ba..f645d22092bfa 100644 --- a/crates/bevy_core_pipeline/src/deferred/copy_lighting_id.rs +++ b/crates/bevy_core_pipeline/src/deferred/copy_lighting_id.rs @@ -160,6 +160,7 @@ impl FromWorld for CopyDeferredLightingIdPipeline { }), multisample: MultisampleState::default(), push_constant_ranges: vec![], + zero_initialize_workgroup_memory: false, }); Self { diff --git a/crates/bevy_core_pipeline/src/dof/mod.rs b/crates/bevy_core_pipeline/src/dof/mod.rs index ccf54dd4d5055..06cbbe3e9d312 100644 --- a/crates/bevy_core_pipeline/src/dof/mod.rs +++ b/crates/bevy_core_pipeline/src/dof/mod.rs @@ -26,6 +26,7 @@ use bevy_ecs::{ system::{lifetimeless::Read, Commands, Query, Res, ResMut, Resource}, world::{FromWorld, World}, }; +use bevy_image::BevyDefault as _; use bevy_math::ops; use bevy_reflect::{prelude::ReflectDefault, Reflect}; use bevy_render::{ @@ -48,7 +49,7 @@ use bevy_render::{ renderer::{RenderContext, RenderDevice}, sync_component::SyncComponentPlugin, sync_world::RenderEntity, - texture::{BevyDefault, CachedTexture, TextureCache}, + texture::{CachedTexture, TextureCache}, view::{ prepare_view_targets, ExtractedView, Msaa, ViewDepthTexture, ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms, @@ -806,6 +807,7 @@ impl SpecializedRenderPipeline for DepthOfFieldPipeline { }, targets, }), + zero_initialize_workgroup_memory: false, } } } diff --git a/crates/bevy_core_pipeline/src/fxaa/mod.rs b/crates/bevy_core_pipeline/src/fxaa/mod.rs index a448444bafc4b..547b59762442f 100644 --- a/crates/bevy_core_pipeline/src/fxaa/mod.rs +++ b/crates/bevy_core_pipeline/src/fxaa/mod.rs @@ -6,6 +6,7 @@ use crate::{ use bevy_app::prelude::*; use bevy_asset::{load_internal_asset, Handle}; use bevy_ecs::prelude::*; +use bevy_image::BevyDefault as _; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ extract_component::{ExtractComponent, ExtractComponentPlugin}, @@ -16,7 +17,6 @@ use bevy_render::{ *, }, renderer::RenderDevice, - texture::BevyDefault, view::{ExtractedView, ViewTarget}, Render, RenderApp, RenderSet, }; @@ -196,6 +196,7 @@ impl SpecializedRenderPipeline for FxaaPipeline { depth_stencil: None, multisample: MultisampleState::default(), push_constant_ranges: Vec::new(), + zero_initialize_workgroup_memory: false, } } } diff --git a/crates/bevy_core_pipeline/src/motion_blur/pipeline.rs b/crates/bevy_core_pipeline/src/motion_blur/pipeline.rs index cff26a9e22f17..8109beeb4eb3a 100644 --- a/crates/bevy_core_pipeline/src/motion_blur/pipeline.rs +++ b/crates/bevy_core_pipeline/src/motion_blur/pipeline.rs @@ -5,6 +5,7 @@ use bevy_ecs::{ system::{Commands, Query, Res, ResMut, Resource}, world::FromWorld, }; +use bevy_image::BevyDefault as _; use bevy_render::{ globals::GlobalsUniform, render_resource::{ @@ -19,7 +20,6 @@ use bevy_render::{ TextureFormat, TextureSampleType, }, renderer::RenderDevice, - texture::BevyDefault, view::{ExtractedView, Msaa, ViewTarget}, }; @@ -141,6 +141,7 @@ impl SpecializedRenderPipeline for MotionBlurPipeline { depth_stencil: None, multisample: MultisampleState::default(), push_constant_ranges: vec![], + zero_initialize_workgroup_memory: false, } } } diff --git a/crates/bevy_core_pipeline/src/oit/resolve/mod.rs b/crates/bevy_core_pipeline/src/oit/resolve/mod.rs index 2fdb3944ac64c..101f7b1ed941e 100644 --- a/crates/bevy_core_pipeline/src/oit/resolve/mod.rs +++ b/crates/bevy_core_pipeline/src/oit/resolve/mod.rs @@ -9,6 +9,7 @@ use bevy_ecs::{ entity::{EntityHashMap, EntityHashSet}, prelude::*, }; +use bevy_image::BevyDefault as _; use bevy_render::{ render_resource::{ binding_types::{storage_buffer_sized, texture_depth_2d, uniform_buffer}, @@ -18,7 +19,6 @@ use bevy_render::{ Shader, ShaderDefVal, ShaderStages, TextureFormat, }, renderer::{RenderAdapter, RenderDevice}, - texture::BevyDefault, view::{ExtractedView, ViewTarget, ViewUniform, ViewUniforms}, Render, RenderApp, RenderSet, }; @@ -208,6 +208,7 @@ fn specialize_oit_resolve_pipeline( depth_stencil: None, multisample: MultisampleState::default(), push_constant_ranges: vec![], + zero_initialize_workgroup_memory: false, } } diff --git a/crates/bevy_core_pipeline/src/post_process/mod.rs b/crates/bevy_core_pipeline/src/post_process/mod.rs index 79c41f990b33c..a633134b276d2 100644 --- a/crates/bevy_core_pipeline/src/post_process/mod.rs +++ b/crates/bevy_core_pipeline/src/post_process/mod.rs @@ -14,6 +14,7 @@ use bevy_ecs::{ system::{lifetimeless::Read, Commands, Query, Res, ResMut, Resource}, world::{FromWorld, World}, }; +use bevy_image::{BevyDefault, Image}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ camera::Camera, @@ -32,7 +33,7 @@ use bevy_render::{ TextureDimension, TextureFormat, TextureSampleType, }, renderer::{RenderContext, RenderDevice, RenderQueue}, - texture::{BevyDefault, GpuImage, Image}, + texture::GpuImage, view::{ExtractedView, ViewTarget}, Render, RenderApp, RenderSet, }; @@ -344,6 +345,7 @@ impl SpecializedRenderPipeline for PostProcessingPipeline { depth_stencil: None, multisample: default(), push_constant_ranges: vec![], + zero_initialize_workgroup_memory: false, } } } diff --git a/crates/bevy_core_pipeline/src/skybox/mod.rs b/crates/bevy_core_pipeline/src/skybox/mod.rs index 59cfa908863c2..5ca7c3fce2d26 100644 --- a/crates/bevy_core_pipeline/src/skybox/mod.rs +++ b/crates/bevy_core_pipeline/src/skybox/mod.rs @@ -6,6 +6,7 @@ use bevy_ecs::{ schedule::IntoSystemConfigs, system::{Commands, Query, Res, ResMut, Resource}, }; +use bevy_image::{BevyDefault, Image}; use bevy_math::{Mat4, Quat}; use bevy_render::{ camera::Exposure, @@ -19,7 +20,7 @@ use bevy_render::{ *, }, renderer::RenderDevice, - texture::{BevyDefault, GpuImage, Image}, + texture::GpuImage, view::{ExtractedView, Msaa, ViewTarget, ViewUniform, ViewUniforms}, Render, RenderApp, RenderSet, }; @@ -233,6 +234,7 @@ impl SpecializedRenderPipeline for SkyboxPipeline { write_mask: ColorWrites::ALL, })], }), + zero_initialize_workgroup_memory: false, } } } diff --git a/crates/bevy_core_pipeline/src/skybox/prepass.rs b/crates/bevy_core_pipeline/src/skybox/prepass.rs index fb8df89b4b602..c51e707808e93 100644 --- a/crates/bevy_core_pipeline/src/skybox/prepass.rs +++ b/crates/bevy_core_pipeline/src/skybox/prepass.rs @@ -105,6 +105,7 @@ impl SpecializedRenderPipeline for SkyboxPrepassPipeline { entry_point: "fragment".into(), targets: prepass_target_descriptors(key.normal_prepass, true, false), }), + zero_initialize_workgroup_memory: false, } } } diff --git a/crates/bevy_core_pipeline/src/smaa/mod.rs b/crates/bevy_core_pipeline/src/smaa/mod.rs index a41a77c806844..7471cdb09ab76 100644 --- a/crates/bevy_core_pipeline/src/smaa/mod.rs +++ b/crates/bevy_core_pipeline/src/smaa/mod.rs @@ -29,7 +29,12 @@ //! * Compatibility with SSAA and MSAA. //! //! [SMAA]: https://www.iryoku.com/smaa/ - +#[cfg(not(feature = "smaa_luts"))] +use crate::tonemapping::lut_placeholder; +use crate::{ + core_2d::graph::{Core2d, Node2d}, + core_3d::graph::{Core3d, Node3d}, +}; use bevy_app::{App, Plugin}; #[cfg(feature = "smaa_luts")] use bevy_asset::load_internal_binary_asset; @@ -44,6 +49,7 @@ use bevy_ecs::{ system::{lifetimeless::Read, Commands, Query, Res, ResMut, Resource}, world::{FromWorld, World}, }; +use bevy_image::{BevyDefault, Image}; use bevy_math::{vec4, Vec4}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ @@ -67,24 +73,12 @@ use bevy_render::{ VertexState, }, renderer::{RenderContext, RenderDevice, RenderQueue}, - texture::{BevyDefault, CachedTexture, GpuImage, Image, TextureCache}, + texture::{CachedTexture, GpuImage, TextureCache}, view::{ExtractedView, ViewTarget}, Render, RenderApp, RenderSet, }; -#[cfg(feature = "smaa_luts")] -use bevy_render::{ - render_asset::RenderAssetUsages, - texture::{CompressedImageFormats, ImageFormat, ImageSampler, ImageType}, -}; use bevy_utils::prelude::default; -#[cfg(not(feature = "smaa_luts"))] -use crate::tonemapping::lut_placeholder; -use crate::{ - core_2d::graph::{Core2d, Node2d}, - core_3d::graph::{Core3d, Node3d}, -}; - /// The handle of the `smaa.wgsl` shader. const SMAA_SHADER_HANDLE: Handle = Handle::weak_from_u128(12247928498010601081); /// The handle of the area LUT, a KTX2 format texture that SMAA uses internally. @@ -306,11 +300,11 @@ impl Plugin for SmaaPlugin { #[cfg(all(debug_assertions, feature = "dds"))] "SMAAAreaLUT".to_owned(), bytes, - ImageType::Format(ImageFormat::Ktx2), - CompressedImageFormats::NONE, + bevy_image::ImageType::Format(bevy_image::ImageFormat::Ktx2), + bevy_image::CompressedImageFormats::NONE, false, - ImageSampler::Default, - RenderAssetUsages::RENDER_WORLD, + bevy_image::ImageSampler::Default, + bevy_asset::RenderAssetUsages::RENDER_WORLD, ) .expect("Failed to load SMAA area LUT") ); @@ -324,11 +318,11 @@ impl Plugin for SmaaPlugin { #[cfg(all(debug_assertions, feature = "dds"))] "SMAASearchLUT".to_owned(), bytes, - ImageType::Format(ImageFormat::Ktx2), - CompressedImageFormats::NONE, + bevy_image::ImageType::Format(bevy_image::ImageFormat::Ktx2), + bevy_image::CompressedImageFormats::NONE, false, - ImageSampler::Default, - RenderAssetUsages::RENDER_WORLD, + bevy_image::ImageSampler::Default, + bevy_asset::RenderAssetUsages::RENDER_WORLD, ) .expect("Failed to load SMAA search LUT") ); @@ -512,6 +506,7 @@ impl SpecializedRenderPipeline for SmaaEdgeDetectionPipeline { bias: default(), }), multisample: MultisampleState::default(), + zero_initialize_workgroup_memory: false, } } } @@ -571,6 +566,7 @@ impl SpecializedRenderPipeline for SmaaBlendingWeightCalculationPipeline { bias: default(), }), multisample: MultisampleState::default(), + zero_initialize_workgroup_memory: false, } } } @@ -607,6 +603,7 @@ impl SpecializedRenderPipeline for SmaaNeighborhoodBlendingPipeline { primitive: PrimitiveState::default(), depth_stencil: None, multisample: MultisampleState::default(), + zero_initialize_workgroup_memory: false, } } } diff --git a/crates/bevy_core_pipeline/src/taa/mod.rs b/crates/bevy_core_pipeline/src/taa/mod.rs index f8333fc53827e..e9f8f1ac3d6a2 100644 --- a/crates/bevy_core_pipeline/src/taa/mod.rs +++ b/crates/bevy_core_pipeline/src/taa/mod.rs @@ -16,6 +16,7 @@ use bevy_ecs::{ system::{Commands, Query, Res, ResMut, Resource}, world::{FromWorld, World}, }; +use bevy_image::BevyDefault as _; use bevy_math::vec2; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ @@ -34,7 +35,7 @@ use bevy_render::{ renderer::{RenderContext, RenderDevice}, sync_component::SyncComponentPlugin, sync_world::RenderEntity, - texture::{BevyDefault, CachedTexture, TextureCache}, + texture::{CachedTexture, TextureCache}, view::{ExtractedView, Msaa, ViewTarget}, ExtractSchedule, MainWorld, Render, RenderApp, RenderSet, }; @@ -355,6 +356,7 @@ impl SpecializedRenderPipeline for TaaPipeline { depth_stencil: None, multisample: MultisampleState::default(), push_constant_ranges: Vec::new(), + zero_initialize_workgroup_memory: false, } } } diff --git a/crates/bevy_core_pipeline/src/tonemapping/mod.rs b/crates/bevy_core_pipeline/src/tonemapping/mod.rs index 972fd7836a3d7..c6fb3217253f9 100644 --- a/crates/bevy_core_pipeline/src/tonemapping/mod.rs +++ b/crates/bevy_core_pipeline/src/tonemapping/mod.rs @@ -2,6 +2,7 @@ use crate::fullscreen_vertex_shader::fullscreen_shader_vertex_state; use bevy_app::prelude::*; use bevy_asset::{load_internal_asset, Assets, Handle}; use bevy_ecs::prelude::*; +use bevy_image::{CompressedImageFormats, Image, ImageSampler, ImageType}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ camera::Camera, @@ -13,7 +14,7 @@ use bevy_render::{ *, }, renderer::RenderDevice, - texture::{CompressedImageFormats, FallbackImage, GpuImage, Image, ImageSampler, ImageType}, + texture::{FallbackImage, GpuImage}, view::{ExtractedView, ViewTarget, ViewUniform}, Render, RenderApp, RenderSet, }; @@ -307,6 +308,7 @@ impl SpecializedRenderPipeline for TonemappingPipeline { depth_stencil: None, multisample: MultisampleState::default(), push_constant_ranges: Vec::new(), + zero_initialize_workgroup_memory: false, } } } @@ -432,14 +434,14 @@ pub fn get_lut_bind_group_layout_entries() -> [BindGroupLayoutEntryBuilder; 2] { // allow(dead_code) so it doesn't complain when the tonemapping_luts feature is disabled #[allow(dead_code)] fn setup_tonemapping_lut_image(bytes: &[u8], image_type: ImageType) -> Image { - let image_sampler = ImageSampler::Descriptor(bevy_render::texture::ImageSamplerDescriptor { + let image_sampler = ImageSampler::Descriptor(bevy_image::ImageSamplerDescriptor { label: Some("Tonemapping LUT sampler".to_string()), - address_mode_u: bevy_render::texture::ImageAddressMode::ClampToEdge, - address_mode_v: bevy_render::texture::ImageAddressMode::ClampToEdge, - address_mode_w: bevy_render::texture::ImageAddressMode::ClampToEdge, - mag_filter: bevy_render::texture::ImageFilterMode::Linear, - min_filter: bevy_render::texture::ImageFilterMode::Linear, - mipmap_filter: bevy_render::texture::ImageFilterMode::Linear, + address_mode_u: bevy_image::ImageAddressMode::ClampToEdge, + address_mode_v: bevy_image::ImageAddressMode::ClampToEdge, + address_mode_w: bevy_image::ImageAddressMode::ClampToEdge, + mag_filter: bevy_image::ImageFilterMode::Linear, + min_filter: bevy_image::ImageFilterMode::Linear, + mipmap_filter: bevy_image::ImageFilterMode::Linear, ..default() }); Image::from_buffer( diff --git a/crates/bevy_dev_tools/Cargo.toml b/crates/bevy_dev_tools/Cargo.toml index 0120ab9184425..322c924f81f34 100644 --- a/crates/bevy_dev_tools/Cargo.toml +++ b/crates/bevy_dev_tools/Cargo.toml @@ -33,9 +33,7 @@ bevy_render = { path = "../bevy_render", version = "0.15.0-dev" } bevy_time = { path = "../bevy_time", version = "0.15.0-dev" } bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" } bevy_text = { path = "../bevy_text", version = "0.15.0-dev" } -bevy_ui = { path = "../bevy_ui", version = "0.15.0-dev", features = [ - "bevy_text", -] } +bevy_ui = { path = "../bevy_ui", version = "0.15.0-dev" } bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } bevy_window = { path = "../bevy_window", version = "0.15.0-dev" } bevy_state = { path = "../bevy_state", version = "0.15.0-dev" } diff --git a/crates/bevy_dev_tools/src/ci_testing/mod.rs b/crates/bevy_dev_tools/src/ci_testing/mod.rs index 18eeb6ba611b0..9f31db2140892 100644 --- a/crates/bevy_dev_tools/src/ci_testing/mod.rs +++ b/crates/bevy_dev_tools/src/ci_testing/mod.rs @@ -17,7 +17,7 @@ use core::time::Duration; /// (`ci_testing_config.ron` by default) and executes its specified actions. For a reference of the /// allowed configuration, see [`CiTestingConfig`]. /// -/// This plugin is included within `DefaultPlugins`, `HeadlessPlugins` and `MinimalPlugins` +/// This plugin is included within `DefaultPlugins` and `MinimalPlugins` /// when the `bevy_ci_testing` feature is enabled. /// It is recommended to only used this plugin during testing (manual or /// automatic), and disable it during regular development and for production builds. diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml index 27695005840d9..30117f71277d5 100644 --- a/crates/bevy_ecs/Cargo.toml +++ b/crates/bevy_ecs/Cargo.toml @@ -8,7 +8,7 @@ repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["ecs", "game", "bevy"] categories = ["game-engines", "data-structures"] -rust-version = "1.77.0" +rust-version = "1.81.0" [features] default = ["bevy_reflect"] @@ -18,6 +18,7 @@ bevy_debug_stepping = [] serialize = ["dep:serde"] track_change_detection = [] reflect_functions = ["bevy_reflect", "bevy_reflect/functions"] +detailed_trace = [] [dependencies] bevy_ptr = { path = "../bevy_ptr", version = "0.15.0-dev" } diff --git a/crates/bevy_ecs/macros/src/component.rs b/crates/bevy_ecs/macros/src/component.rs index 08b4e73056635..7797011019fb1 100644 --- a/crates/bevy_ecs/macros/src/component.rs +++ b/crates/bevy_ecs/macros/src/component.rs @@ -82,7 +82,7 @@ pub fn derive_component(input: TokenStream) -> TokenStream { for require in requires { let ident = &require.path; register_recursive_requires.push(quote! { - <#ident as Component>::register_required_components( + <#ident as #bevy_ecs_path::component::Component>::register_required_components( requiree, components, storages, diff --git a/crates/bevy_ecs/src/component.rs b/crates/bevy_ecs/src/component.rs index 4c0323f2a5844..cdf93adb6d060 100644 --- a/crates/bevy_ecs/src/component.rs +++ b/crates/bevy_ecs/src/component.rs @@ -226,9 +226,9 @@ use derive_more::derive::{Display, Error}; /// assert_eq!(2, world.entity(id).get::().unwrap().0); /// ``` /// -/// In general, this shouldn't happen often, but when it does the algorithm is simple and predictable: -/// 1. Use all of the constructors (including default constructors) directly defined in the spawned component's require list -/// 2. In the order the requires are defined in `#[require()]`, recursively visit the require list of each of the components in the list (this is a Depth First Search). When a constructor is found, it will only be used if one has not already been found. +/// In general, this shouldn't happen often, but when it does the algorithm for choosing the constructor from the tree is simple and predictable: +/// 1. A constructor from a direct `#[require()]`, if one exists, is selected with priority. +/// 2. Otherwise, perform a Depth First Search on the tree of requirements and select the first one found. /// /// From a user perspective, just think about this as the following: /// 1. Specifying a required component constructor for Foo directly on a spawned component Bar will result in that constructor being used (and overriding existing constructors lower in the inheritance tree). This is the classic "inheritance override" behavior people expect. @@ -1029,8 +1029,8 @@ impl Components { /// registration will be used. pub(crate) unsafe fn register_required_components( &mut self, - required: ComponentId, requiree: ComponentId, + required: ComponentId, constructor: fn() -> R, ) -> Result<(), RequiredComponentsError> { // SAFETY: The caller ensures that the `requiree` is valid. @@ -1065,6 +1065,10 @@ impl Components { // Propagate the new required components up the chain to all components that require the requiree. if let Some(required_by) = self.get_required_by(requiree).cloned() { + // `required` is now required by anything that `requiree` was required by. + self.get_required_by_mut(required) + .unwrap() + .extend(required_by.iter().copied()); for &required_by_id in required_by.iter() { // SAFETY: The component is in the list of required components, so it must exist already. let required_components = unsafe { @@ -1072,20 +1076,24 @@ impl Components { .debug_checked_unwrap() }; - // Register the original required component for the requiree. - // The inheritance depth is `1` since this is a component required by the original requiree. - required_components.register_by_id(required, constructor, 1); + // Register the original required component in the "parent" of the requiree. + // The inheritance depth is 1 deeper than the `requiree` wrt `required_by_id`. + let depth = required_components.0.get(&requiree).expect("requiree is required by required_by_id, so its required_components must include requiree").inheritance_depth; + required_components.register_by_id(required, constructor, depth + 1); for (component_id, component) in inherited_requirements.iter() { // Register the required component. - // The inheritance depth is increased by `1` since this is a component required by the original required component. + // The inheritance depth of inherited components is whatever the requiree's + // depth is relative to `required_by_id`, plus the inheritance depth of the + // inherited component relative to the requiree, plus 1 to account for the + // requiree in between. // SAFETY: Component ID and constructor match the ones on the original requiree. // The original requiree is responsible for making sure the registration is safe. unsafe { required_components.register_dynamic( *component_id, component.constructor.clone(), - component.inheritance_depth + 1, + component.inheritance_depth + depth + 1, ); }; } @@ -1159,15 +1167,14 @@ impl Components { // NOTE: This should maybe be private, but it is currently public so that `bevy_ecs_macros` can use it. // We can't directly move this there either, because this uses `Components::get_required_by_mut`, // which is private, and could be equally risky to expose to users. - /// Registers the given component `R` as a [required component] for `T`, - /// and adds `T` to the list of requirees for `R`. + /// Registers the given component `R` and [required components] inherited from it as required by `T`, + /// and adds `T` to their lists of requirees. /// /// The given `inheritance_depth` determines how many levels of inheritance deep the requirement is. /// A direct requirement has a depth of `0`, and each level of inheritance increases the depth by `1`. /// Lower depths are more specific requirements, and can override existing less specific registrations. /// - /// This method does *not* recursively register required components for components required by `R`, - /// nor does it register them for components that require `T`. + /// This method does *not* register any components as required by components that require `T`. /// /// Only use this method if you know what you are doing. In most cases, you should instead use [`World::register_required_components`], /// or the equivalent method in `bevy_app::App`. @@ -1196,15 +1203,14 @@ impl Components { } } - /// Registers the given component `R` as a [required component] for `T`, - /// and adds `T` to the list of requirees for `R`. + /// Registers the given component `R` and [required components] inherited from it as required by `T`, + /// and adds `T` to their lists of requirees. /// /// The given `inheritance_depth` determines how many levels of inheritance deep the requirement is. /// A direct requirement has a depth of `0`, and each level of inheritance increases the depth by `1`. /// Lower depths are more specific requirements, and can override existing less specific registrations. /// - /// This method does *not* recursively register required components for components required by `R`, - /// nor does it register them for components that require `T`. + /// This method does *not* register any components as required by components that require `T`. /// /// [required component]: Component#required-components /// @@ -1232,6 +1238,27 @@ impl Components { // Assuming it is valid, the component is in the list of required components, so it must exist already. let required_by = unsafe { self.get_required_by_mut(required).debug_checked_unwrap() }; required_by.insert(requiree); + + // Register the inherited required components for the requiree. + let required: Vec<(ComponentId, RequiredComponent)> = self + .get_info(required) + .unwrap() + .required_components() + .0 + .iter() + .map(|(id, component)| (*id, component.clone())) + .collect(); + + for (id, component) in required { + // Register the inherited required components for the requiree. + // The inheritance depth is increased by `1` since this is a component required by the original required component. + required_components.register_dynamic( + id, + component.constructor.clone(), + component.inheritance_depth + 1, + ); + self.get_required_by_mut(id).unwrap().insert(requiree); + } } #[inline] @@ -1512,8 +1539,11 @@ impl<'a> TickCells<'a> { #[derive(Copy, Clone, Debug)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] pub struct ComponentTicks { - pub(crate) added: Tick, - pub(crate) changed: Tick, + /// Tick recording the time this component or resource was added. + pub added: Tick, + + /// Tick recording the time this component or resource was most recently changed. + pub changed: Tick, } impl ComponentTicks { @@ -1531,19 +1561,8 @@ impl ComponentTicks { self.changed.is_newer_than(last_run, this_run) } - /// Returns the tick recording the time this component or resource was most recently changed. - #[inline] - pub fn last_changed_tick(&self) -> Tick { - self.changed - } - - /// Returns the tick recording the time this component or resource was added. - #[inline] - pub fn added_tick(&self) -> Tick { - self.added - } - - pub(crate) fn new(change_tick: Tick) -> Self { + /// Creates a new instance with the same change tick for `added` and `changed`. + pub fn new(change_tick: Tick) -> Self { Self { added: change_tick, changed: change_tick, diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 67254d6298f11..3a2e24d904a32 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -2321,6 +2321,118 @@ mod tests { ); } + #[test] + fn runtime_required_components_propagate_up() { + // `A` requires `B` directly. + #[derive(Component)] + #[require(B)] + struct A; + + #[derive(Component, Default)] + struct B; + + #[derive(Component, Default)] + struct C; + + let mut world = World::new(); + + // `B` requires `C` with a runtime registration. + // `A` should also require `C` because it requires `B`. + world.register_required_components::(); + + let id = world.spawn(A).id(); + + assert!(world.entity(id).get::().is_some()); + } + + #[test] + fn runtime_required_components_propagate_up_even_more() { + #[derive(Component)] + struct A; + + #[derive(Component, Default)] + struct B; + + #[derive(Component, Default)] + struct C; + + #[derive(Component, Default)] + struct D; + + let mut world = World::new(); + + world.register_required_components::(); + world.register_required_components::(); + world.register_required_components::(); + + let id = world.spawn(A).id(); + + assert!(world.entity(id).get::().is_some()); + } + + #[test] + fn runtime_required_components_deep_require_does_not_override_shallow_require() { + #[derive(Component)] + struct A; + #[derive(Component, Default)] + struct B; + #[derive(Component, Default)] + struct C; + #[derive(Component)] + struct Counter(i32); + #[derive(Component, Default)] + struct D; + + let mut world = World::new(); + + world.register_required_components::(); + world.register_required_components::(); + world.register_required_components::(); + world.register_required_components_with::(|| Counter(2)); + // This should replace the require constructor in A since it is + // shallower. + world.register_required_components_with::(|| Counter(1)); + + let id = world.spawn(A).id(); + + // The "shallower" of the two components is used. + assert_eq!(world.entity(id).get::().unwrap().0, 1); + } + + #[test] + fn runtime_required_components_deep_require_does_not_override_shallow_require_deep_subtree_after_shallow( + ) { + #[derive(Component)] + struct A; + #[derive(Component, Default)] + struct B; + #[derive(Component, Default)] + struct C; + #[derive(Component, Default)] + struct D; + #[derive(Component, Default)] + struct E; + #[derive(Component)] + struct Counter(i32); + #[derive(Component, Default)] + struct F; + + let mut world = World::new(); + + world.register_required_components::(); + world.register_required_components::(); + world.register_required_components::(); + world.register_required_components::(); + world.register_required_components_with::(|| Counter(1)); + world.register_required_components_with::(|| Counter(2)); + world.register_required_components::(); + + let id = world.spawn(A).id(); + + // The "shallower" of the two components is used. + assert_eq!(world.entity(id).get::().unwrap().0, 1); + } + #[test] fn runtime_required_components_existing_archetype() { #[derive(Component)] diff --git a/crates/bevy_ecs/src/query/access.rs b/crates/bevy_ecs/src/query/access.rs index 28dba05f87f6a..00167383fe803 100644 --- a/crates/bevy_ecs/src/query/access.rs +++ b/crates/bevy_ecs/src/query/access.rs @@ -772,7 +772,7 @@ impl Access { /// `Access`, it's not recommended. Prefer to manage your own lists of /// accessible components if your application needs to do that. #[doc(hidden)] - #[deprecated] + // TODO: this should be deprecated and removed, see https://github.com/bevyengine/bevy/issues/16339 pub fn component_reads_and_writes(&self) -> (impl Iterator + '_, bool) { ( self.component_read_and_writes diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index de3796190f172..77f40555c28ef 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -272,7 +272,8 @@ use smallvec::SmallVec; /// [`ReadOnly`]: Self::ReadOnly #[diagnostic::on_unimplemented( message = "`{Self}` is not valid to request as data in a `Query`", - label = "invalid `Query` data" + label = "invalid `Query` data", + note = "if `{Self}` is a component type, try using `&{Self}` or `&mut {Self}`" )] pub unsafe trait QueryData: WorldQuery { /// The read-only variant of this [`QueryData`], which satisfies the [`ReadOnlyQueryData`] trait. diff --git a/crates/bevy_ecs/src/query/iter.rs b/crates/bevy_ecs/src/query/iter.rs index bf4c5432546f9..4de38258e9edc 100644 --- a/crates/bevy_ecs/src/query/iter.rs +++ b/crates/bevy_ecs/src/query/iter.rs @@ -16,6 +16,7 @@ use core::{ }; use super::{QueryData, QueryFilter, ReadOnlyQueryData}; +use alloc::vec::IntoIter; /// An [`Iterator`] over query results of a [`Query`](crate::system::Query). /// @@ -774,7 +775,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { ) }; let mut keyed_query: Vec<_> = query_lens.collect(); - keyed_query.sort_by(|(key_1, _), (key_2, _)| compare(key_1, key_2)); + keyed_query.sort_unstable_by(|(key_1, _), (key_2, _)| compare(key_1, key_2)); let entity_iter = keyed_query.into_iter().map(|(.., entity)| entity); // SAFETY: // `self.world` has permission to access the required components. @@ -1251,7 +1252,11 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> Debug /// Entities that don't match the query are skipped. /// /// This struct is created by the [`Query::iter_many`](crate::system::Query::iter_many) and [`Query::iter_many_mut`](crate::system::Query::iter_many_mut) methods. -pub struct QueryManyIter<'w, 's, D: QueryData, F: QueryFilter, I: Iterator>> { +pub struct QueryManyIter<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> +where + I::Item: Borrow, +{ + world: UnsafeWorldCell<'w>, entity_iter: I, entities: &'w Entities, tables: &'w Tables, @@ -1277,6 +1282,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator>> let fetch = D::init_fetch(world, &query_state.fetch_state, last_run, this_run); let filter = F::init_fetch(world, &query_state.filter_state, last_run, this_run); QueryManyIter { + world, query_state, entities: world.entities(), archetypes: world.archetypes(), @@ -1334,37 +1340,663 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator>> F::set_archetype(filter, &query_state.filter_state, archetype, table); } - // SAFETY: set_archetype was called prior. - // `location.archetype_row` is an archetype index row in range of the current archetype, because if it was not, the match above would have `continue`d - if unsafe { F::filter_fetch(filter, entity, location.table_row) } { - // SAFETY: - // - set_archetype was called prior, `location.archetype_row` is an archetype index in range of the current archetype - // - fetch is only called once for each entity. - return Some(unsafe { D::fetch(fetch, entity, location.table_row) }); - } - } - None - } + // SAFETY: set_archetype was called prior. + // `location.archetype_row` is an archetype index row in range of the current archetype, because if it was not, the match above would have `continue`d + if unsafe { F::filter_fetch(filter, entity, location.table_row) } { + // SAFETY: + // - set_archetype was called prior, `location.archetype_row` is an archetype index in range of the current archetype + // - fetch is only called once for each entity. + return Some(unsafe { D::fetch(fetch, entity, location.table_row) }); + } + } + None + } + + /// Get next result from the query + #[inline(always)] + pub fn fetch_next(&mut self) -> Option> { + // SAFETY: + // All arguments stem from self. + // We are limiting the returned reference to self, + // making sure this method cannot be called multiple times without getting rid + // of any previously returned unique references first, thus preventing aliasing. + unsafe { + Self::fetch_next_aliased_unchecked( + &mut self.entity_iter, + self.entities, + self.tables, + self.archetypes, + &mut self.fetch, + &mut self.filter, + self.query_state, + ) + .map(D::shrink) + } + } + + /// Sorts all query items into a new iterator, using the query lens as a key. + /// + /// This sort is stable (i.e., does not reorder equal elements). + /// + /// This uses [`slice::sort`] internally. + /// + /// Defining the lens works like [`transmute_lens`](crate::system::Query::transmute_lens). + /// This includes the allowed parameter type changes listed under [allowed transmutes]. + /// However, the lens uses the filter of the original query when present. + /// + /// The sort is not cached across system runs. + /// + /// [allowed transmutes]: crate::system::Query#allowed-transmutes + /// + /// Unlike the sort methods on [`QueryIter`], this does NOT panic if `next`/`fetch_next` has been + /// called on [`QueryManyIter`] before. + /// + /// # Examples + /// ```rust + /// # use bevy_ecs::prelude::*; + /// # use std::{ops::{Deref, DerefMut}, iter::Sum}; + /// # + /// # #[derive(Component)] + /// # struct PartMarker; + /// # + /// # #[derive(Component, PartialEq, Eq, PartialOrd, Ord)] + /// # struct PartIndex(usize); + /// # + /// # #[derive(Component, Clone, Copy)] + /// # struct PartValue(usize); + /// # + /// # impl Deref for PartValue { + /// # type Target = usize; + /// # + /// # fn deref(&self) -> &Self::Target { + /// # &self.0 + /// # } + /// # } + /// # + /// # impl DerefMut for PartValue { + /// # fn deref_mut(&mut self) -> &mut Self::Target { + /// # &mut self.0 + /// # } + /// # } + /// # + /// # #[derive(Component, Debug, PartialEq, Eq, PartialOrd, Ord)] + /// # struct Length(usize); + /// # + /// # #[derive(Component, Debug, PartialEq, Eq, PartialOrd, Ord)] + /// # struct Width(usize); + /// # + /// # #[derive(Component, Debug, PartialEq, Eq, PartialOrd, Ord)] + /// # struct Height(usize); + /// # + /// # #[derive(Component, PartialEq, Eq, PartialOrd, Ord)] + /// # struct ParentEntity(Entity); + /// # + /// # let mut world = World::new(); + /// // We can ensure that a query always returns in the same order. + /// fn system_1(query: Query<(Entity, &PartIndex)>) { + /// # let entity_list: Vec = Vec::new(); + /// let parts: Vec<(Entity, &PartIndex)> = query.iter_many(entity_list).sort::<&PartIndex>().collect(); + /// } + /// + /// // We can freely rearrange query components in the key. + /// fn system_2(query: Query<(&Length, &Width, &Height), With>) { + /// # let entity_list: Vec = Vec::new(); + /// for (length, width, height) in query.iter_many(entity_list).sort::<(&Height, &Length, &Width)>() { + /// println!("height: {height:?}, width: {width:?}, length: {length:?}") + /// } + /// } + /// + /// // You can use `fetch_next_back` to obtain mutable references in reverse order. + /// fn system_3( + /// mut query: Query<&mut PartValue>, + /// ) { + /// # let entity_list: Vec = Vec::new(); + /// // We need to collect the internal iterator before iterating mutably + /// let mut parent_query_iter = query.iter_many_mut(entity_list) + /// .sort::() + /// .collect_inner(); + /// + /// let mut scratch_value = 0; + /// while let Some(mut part_value) = parent_query_iter.fetch_next_back() + /// { + /// // some order-dependent operation, here bitwise XOR + /// **part_value ^= scratch_value; + /// scratch_value = **part_value; + /// } + /// } + /// # + /// # let mut schedule = Schedule::default(); + /// # schedule.add_systems((system_1, system_2, system_3)); + /// # schedule.run(&mut world); + /// ``` + pub fn sort( + self, + ) -> QuerySortedManyIter< + 'w, + 's, + D, + F, + impl ExactSizeIterator + DoubleEndedIterator + FusedIterator + 'w, + > + where + L::Item<'w>: Ord, + { + let world = self.world; + + let query_lens_state = self.query_state.transmute_filtered::<(L, Entity), F>(world); + + // SAFETY: + // `self.world` has permission to access the required components. + // The original query iter has not been iterated on, so no items are aliased from it. + let query_lens = unsafe { + query_lens_state.iter_many_unchecked_manual( + self.entity_iter, + world, + world.last_change_tick(), + world.change_tick(), + ) + }; + let mut keyed_query: Vec<_> = query_lens + .map(|(key, entity)| (key, NeutralOrd(entity))) + .collect(); + keyed_query.sort(); + let entity_iter = keyed_query.into_iter().map(|(.., entity)| entity.0); + // SAFETY: + // `self.world` has permission to access the required components. + // Each lens query item is dropped before the respective actual query item is accessed. + unsafe { + QuerySortedManyIter::new( + world, + self.query_state, + entity_iter, + world.last_change_tick(), + world.change_tick(), + ) + } + } + + /// Sorts all query items into a new iterator, using the query lens as a key. + /// + /// This sort is unstable (i.e., may reorder equal elements). + /// + /// This uses [`slice::sort_unstable`] internally. + /// + /// Defining the lens works like [`transmute_lens`](crate::system::Query::transmute_lens). + /// This includes the allowed parameter type changes listed under [allowed transmutes].. + /// However, the lens uses the filter of the original query when present. + /// + /// The sort is not cached across system runs. + /// + /// [allowed transmutes]: crate::system::Query#allowed-transmutes + /// + /// Unlike the sort methods on [`QueryIter`], this does NOT panic if `next`/`fetch_next` has been + /// called on [`QueryManyIter`] before. + /// + /// # Example + /// ``` + /// # use bevy_ecs::prelude::*; + /// # + /// # let mut world = World::new(); + /// # + /// # #[derive(Component)] + /// # struct PartMarker; + /// # + /// # let entity_list: Vec = Vec::new(); + /// #[derive(Component, PartialEq, Eq, PartialOrd, Ord)] + /// enum Flying { + /// Enabled, + /// Disabled + /// }; + /// + /// // We perform an unstable sort by a Component with few values. + /// fn system_1(query: Query<&Flying, With>) { + /// # let entity_list: Vec = Vec::new(); + /// let part_values: Vec<&Flying> = query.iter_many(entity_list).sort_unstable::<&Flying>().collect(); + /// } + /// # + /// # let mut schedule = Schedule::default(); + /// # schedule.add_systems((system_1)); + /// # schedule.run(&mut world); + /// ``` + pub fn sort_unstable( + self, + ) -> QuerySortedManyIter< + 'w, + 's, + D, + F, + impl ExactSizeIterator + DoubleEndedIterator + FusedIterator + 'w, + > + where + L::Item<'w>: Ord, + { + let world = self.world; + + let query_lens_state = self.query_state.transmute_filtered::<(L, Entity), F>(world); + + // SAFETY: + // `self.world` has permission to access the required components. + // The original query iter has not been iterated on, so no items are aliased from it. + let query_lens = unsafe { + query_lens_state.iter_many_unchecked_manual( + self.entity_iter, + world, + world.last_change_tick(), + world.change_tick(), + ) + }; + let mut keyed_query: Vec<_> = query_lens + .map(|(key, entity)| (key, NeutralOrd(entity))) + .collect(); + keyed_query.sort_unstable(); + let entity_iter = keyed_query.into_iter().map(|(.., entity)| entity.0); + // SAFETY: + // `self.world` has permission to access the required components. + // Each lens query item is dropped before the respective actual query item is accessed. + unsafe { + QuerySortedManyIter::new( + world, + self.query_state, + entity_iter, + world.last_change_tick(), + world.change_tick(), + ) + } + } + + /// Sorts all query items into a new iterator with a comparator function over the query lens. + /// + /// This sort is stable (i.e., does not reorder equal elements). + /// + /// This uses [`slice::sort_by`] internally. + /// + /// Defining the lens works like [`transmute_lens`](crate::system::Query::transmute_lens). + /// This includes the allowed parameter type changes listed under [allowed transmutes]. + /// However, the lens uses the filter of the original query when present. + /// + /// The sort is not cached across system runs. + /// + /// [allowed transmutes]: crate::system::Query#allowed-transmutes + /// + /// Unlike the sort methods on [`QueryIter`], this does NOT panic if `next`/`fetch_next` has been + /// called on [`QueryManyIter`] before. + /// + /// # Example + /// ``` + /// # use bevy_ecs::prelude::*; + /// # use std::ops::Deref; + /// # + /// # impl Deref for PartValue { + /// # type Target = f32; + /// # + /// # fn deref(&self) -> &Self::Target { + /// # &self.0 + /// # } + /// # } + /// # + /// # let mut world = World::new(); + /// # let entity_list: Vec = Vec::new(); + /// # + /// #[derive(Component)] + /// struct PartValue(f32); + /// + /// // We can use a cmp function on components do not implement Ord. + /// fn system_1(query: Query<&PartValue>) { + /// # let entity_list: Vec = Vec::new(); + /// // Sort part values according to `f32::total_comp`. + /// let part_values: Vec<&PartValue> = query + /// .iter_many(entity_list) + /// .sort_by::<&PartValue>(|value_1, value_2| value_1.total_cmp(*value_2)) + /// .collect(); + /// } + /// # + /// # let mut schedule = Schedule::default(); + /// # schedule.add_systems((system_1)); + /// # schedule.run(&mut world); + /// ``` + pub fn sort_by( + self, + mut compare: impl FnMut(&L::Item<'w>, &L::Item<'w>) -> Ordering, + ) -> QuerySortedManyIter< + 'w, + 's, + D, + F, + impl ExactSizeIterator + DoubleEndedIterator + FusedIterator + 'w, + > { + let world = self.world; + + let query_lens_state = self.query_state.transmute_filtered::<(L, Entity), F>(world); + + // SAFETY: + // `self.world` has permission to access the required components. + // The original query iter has not been iterated on, so no items are aliased from it. + let query_lens = unsafe { + query_lens_state.iter_many_unchecked_manual( + self.entity_iter, + world, + world.last_change_tick(), + world.change_tick(), + ) + }; + let mut keyed_query: Vec<_> = query_lens.collect(); + keyed_query.sort_by(|(key_1, _), (key_2, _)| compare(key_1, key_2)); + let entity_iter = keyed_query.into_iter().map(|(.., entity)| entity); + // SAFETY: + // `self.world` has permission to access the required components. + // Each lens query item is dropped before the respective actual query item is accessed. + unsafe { + QuerySortedManyIter::new( + world, + self.query_state, + entity_iter, + world.last_change_tick(), + world.change_tick(), + ) + } + } + + /// Sorts all query items into a new iterator with a comparator function over the query lens. + /// + /// This sort is unstable (i.e., may reorder equal elements). + /// + /// This uses [`slice::sort_unstable_by`] internally. + /// + /// Defining the lens works like [`transmute_lens`](crate::system::Query::transmute_lens). + /// This includes the allowed parameter type changes listed under [allowed transmutes]. + /// However, the lens uses the filter of the original query when present. + /// + /// The sort is not cached across system runs. + /// + /// [allowed transmutes]: crate::system::Query#allowed-transmutes + /// + /// Unlike the sort methods on [`QueryIter`], this does NOT panic if `next`/`fetch_next` has been + /// called on [`QueryManyIter`] before. + pub fn sort_unstable_by( + self, + mut compare: impl FnMut(&L::Item<'w>, &L::Item<'w>) -> Ordering, + ) -> QuerySortedManyIter< + 'w, + 's, + D, + F, + impl ExactSizeIterator + DoubleEndedIterator + FusedIterator + 'w, + > { + let world = self.world; + + let query_lens_state = self.query_state.transmute_filtered::<(L, Entity), F>(world); + + // SAFETY: + // `self.world` has permission to access the required components. + // The original query iter has not been iterated on, so no items are aliased from it. + let query_lens = unsafe { + query_lens_state.iter_many_unchecked_manual( + self.entity_iter, + world, + world.last_change_tick(), + world.change_tick(), + ) + }; + let mut keyed_query: Vec<_> = query_lens.collect(); + keyed_query.sort_unstable_by(|(key_1, _), (key_2, _)| compare(key_1, key_2)); + let entity_iter = keyed_query.into_iter().map(|(.., entity)| entity); + // SAFETY: + // `self.world` has permission to access the required components. + // Each lens query item is dropped before the respective actual query item is accessed. + unsafe { + QuerySortedManyIter::new( + world, + self.query_state, + entity_iter, + world.last_change_tick(), + world.change_tick(), + ) + } + } + + /// Sorts all query items into a new iterator with a key extraction function over the query lens. + /// + /// This sort is stable (i.e., does not reorder equal elements). + /// + /// This uses [`slice::sort_by_key`] internally. + /// + /// Defining the lens works like [`transmute_lens`](crate::system::Query::transmute_lens). + /// This includes the allowed parameter type changes listed under [allowed transmutes]. + /// However, the lens uses the filter of the original query when present. + /// + /// The sort is not cached across system runs. + /// + /// [allowed transmutes]: crate::system::Query#allowed-transmutes + /// + /// Unlike the sort methods on [`QueryIter`], this does NOT panic if `next`/`fetch_next` has been + /// called on [`QueryManyIter`] before. + /// + /// # Example + /// ``` + /// # use bevy_ecs::prelude::*; + /// # use std::ops::Deref; + /// # + /// # #[derive(Component)] + /// # struct PartMarker; + /// # + /// # impl Deref for PartValue { + /// # type Target = f32; + /// # + /// # fn deref(&self) -> &Self::Target { + /// # &self.0 + /// # } + /// # } + /// # + /// # let mut world = World::new(); + /// # let entity_list: Vec = Vec::new(); + /// # + /// #[derive(Component)] + /// struct AvailableMarker; + /// + /// #[derive(Component, PartialEq, Eq, PartialOrd, Ord)] + /// enum Rarity { + /// Common, + /// Rare, + /// Epic, + /// Legendary + /// }; + /// + /// #[derive(Component)] + /// struct PartValue(f32); + /// + /// // We can sort with the internals of components that do not implement Ord. + /// fn system_1(query: Query<(Entity, &PartValue)>) { + /// # let entity_list: Vec = Vec::new(); + /// // Sort by the sines of the part values. + /// let parts: Vec<(Entity, &PartValue)> = query + /// .iter_many(entity_list) + /// .sort_by_key::<&PartValue, _>(|value| value.sin() as usize) + /// .collect(); + /// } + /// + /// // We can define our own custom comparison functions over an EntityRef. + /// fn system_2(query: Query>) { + /// # let entity_list: Vec = Vec::new(); + /// // Sort by whether parts are available and their rarity. + /// // We want the available legendaries to come first, so we reverse the iterator. + /// let parts: Vec = query.iter_many(entity_list) + /// .sort_by_key::(|entity_ref| { + /// ( + /// entity_ref.contains::(), + /// entity_ref.get::() + /// ) + /// }) + /// .rev() + /// .collect(); + /// } + /// # let mut schedule = Schedule::default(); + /// # schedule.add_systems((system_1, system_2)); + /// # schedule.run(&mut world); + /// ``` + pub fn sort_by_key( + self, + mut f: impl FnMut(&L::Item<'w>) -> K, + ) -> QuerySortedManyIter< + 'w, + 's, + D, + F, + impl ExactSizeIterator + DoubleEndedIterator + FusedIterator + 'w, + > + where + K: Ord, + { + let world = self.world; + + let query_lens_state = self.query_state.transmute_filtered::<(L, Entity), F>(world); + + // SAFETY: + // `self.world` has permission to access the required components. + // The original query iter has not been iterated on, so no items are aliased from it. + let query_lens = unsafe { + query_lens_state.iter_many_unchecked_manual( + self.entity_iter, + world, + world.last_change_tick(), + world.change_tick(), + ) + }; + let mut keyed_query: Vec<_> = query_lens.collect(); + keyed_query.sort_by_key(|(lens, _)| f(lens)); + let entity_iter = keyed_query.into_iter().map(|(.., entity)| entity); + // SAFETY: + // `self.world` has permission to access the required components. + // Each lens query item is dropped before the respective actual query item is accessed. + unsafe { + QuerySortedManyIter::new( + world, + self.query_state, + entity_iter, + world.last_change_tick(), + world.change_tick(), + ) + } + } + + /// Sorts all query items into a new iterator with a key extraction function over the query lens. + /// + /// This sort is unstable (i.e., may reorder equal elements). + /// + /// This uses [`slice::sort_unstable_by_key`] internally. + /// + /// Defining the lens works like [`transmute_lens`](crate::system::Query::transmute_lens). + /// This includes the allowed parameter type changes listed under [allowed transmutes]. + /// However, the lens uses the filter of the original query when present. + /// + /// The sort is not cached across system runs. + /// + /// [allowed transmutes]: crate::system::Query#allowed-transmutes + /// + /// Unlike the sort methods on [`QueryIter`], this does NOT panic if `next`/`fetch_next` has been + /// called on [`QueryManyIter`] before. + pub fn sort_unstable_by_key( + self, + mut f: impl FnMut(&L::Item<'w>) -> K, + ) -> QuerySortedManyIter< + 'w, + 's, + D, + F, + impl ExactSizeIterator + DoubleEndedIterator + FusedIterator + 'w, + > + where + K: Ord, + { + let world = self.world; + + let query_lens_state = self.query_state.transmute_filtered::<(L, Entity), F>(world); + + // SAFETY: + // `self.world` has permission to access the required components. + // The original query iter has not been iterated on, so no items are aliased from it. + let query_lens = unsafe { + query_lens_state.iter_many_unchecked_manual( + self.entity_iter, + world, + world.last_change_tick(), + world.change_tick(), + ) + }; + let mut keyed_query: Vec<_> = query_lens.collect(); + keyed_query.sort_unstable_by_key(|(lens, _)| f(lens)); + let entity_iter = keyed_query.into_iter().map(|(.., entity)| entity); + // SAFETY: + // `self.world` has permission to access the required components. + // Each lens query item is dropped before the respective actual query item is accessed. + unsafe { + QuerySortedManyIter::new( + world, + self.query_state, + entity_iter, + world.last_change_tick(), + world.change_tick(), + ) + } + } + + /// Sort all query items into a new iterator with a key extraction function over the query lens. + /// + /// This sort is stable (i.e., does not reorder equal elements). + /// + /// This uses [`slice::sort_by_cached_key`] internally. + /// + /// Defining the lens works like [`transmute_lens`](crate::system::Query::transmute_lens). + /// This includes the allowed parameter type changes listed under [allowed transmutes]. + /// However, the lens uses the filter of the original query when present. + /// + /// The sort is not cached across system runs. + /// + /// [allowed transmutes]: crate::system::Query#allowed-transmutes + /// + /// Unlike the sort methods on [`QueryIter`], this does NOT panic if `next`/`fetch_next` has been + /// called on [`QueryManyIter`] before. + pub fn sort_by_cached_key( + self, + mut f: impl FnMut(&L::Item<'w>) -> K, + ) -> QuerySortedManyIter< + 'w, + 's, + D, + F, + impl ExactSizeIterator + DoubleEndedIterator + FusedIterator + 'w, + > + where + K: Ord, + { + let world = self.world; + + let query_lens_state = self.query_state.transmute_filtered::<(L, Entity), F>(world); - /// Get next result from the query - #[inline(always)] - pub fn fetch_next(&mut self) -> Option> { // SAFETY: - // All arguments stem from self. - // We are limiting the returned reference to self, - // making sure this method cannot be called multiple times without getting rid - // of any previously returned unique references first, thus preventing aliasing. + // `self.world` has permission to access the required components. + // The original query iter has not been iterated on, so no items are aliased from it. + let query_lens = unsafe { + query_lens_state.iter_many_unchecked_manual( + self.entity_iter, + world, + world.last_change_tick(), + world.change_tick(), + ) + }; + let mut keyed_query: Vec<_> = query_lens.collect(); + keyed_query.sort_by_cached_key(|(lens, _)| f(lens)); + let entity_iter = keyed_query.into_iter().map(|(.., entity)| entity); + // SAFETY: + // `self.world` has permission to access the required components. + // Each lens query item is dropped before the respective actual query item is accessed. unsafe { - Self::fetch_next_aliased_unchecked( - &mut self.entity_iter, - self.entities, - self.tables, - self.archetypes, - &mut self.fetch, - &mut self.filter, + QuerySortedManyIter::new( + world, self.query_state, + entity_iter, + world.last_change_tick(), + world.change_tick(), ) - .map(D::shrink) } } } @@ -1465,6 +2097,183 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator>> De } } +/// An [`Iterator`] over sorted query results of a [`QueryManyIter`]. +/// +/// This struct is created by the [`sort`](QueryManyIter), [`sort_unstable`](QueryManyIter), +/// [`sort_by`](QueryManyIter), [`sort_unstable_by`](QueryManyIter), [`sort_by_key`](QueryManyIter), +/// [`sort_unstable_by_key`](QueryManyIter), and [`sort_by_cached_key`](QueryManyIter) methods of [`QueryManyIter`]. +pub struct QuerySortedManyIter<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> { + entity_iter: I, + entities: &'w Entities, + tables: &'w Tables, + archetypes: &'w Archetypes, + fetch: D::Fetch<'w>, + query_state: &'s QueryState, +} + +impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> + QuerySortedManyIter<'w, 's, D, F, I> +{ + /// # Safety + /// - `world` must have permission to access any of the components registered in `query_state`. + /// - `world` must be the same one used to initialize `query_state`. + /// - `entity_list` must only contain unique entities or be empty. + pub(crate) unsafe fn new>( + world: UnsafeWorldCell<'w>, + query_state: &'s QueryState, + entity_list: EntityList, + last_run: Tick, + this_run: Tick, + ) -> QuerySortedManyIter<'w, 's, D, F, I> { + let fetch = D::init_fetch(world, &query_state.fetch_state, last_run, this_run); + QuerySortedManyIter { + query_state, + entities: world.entities(), + archetypes: world.archetypes(), + // SAFETY: We only access table data that has been registered in `query_state`. + // This means `world` has permission to access the data we use. + tables: &world.storages().tables, + fetch, + entity_iter: entity_list.into_iter(), + } + } + + /// # Safety + /// The lifetime here is not restrictive enough for Fetch with &mut access, + /// as calling `fetch_next_aliased_unchecked` multiple times can produce multiple + /// references to the same component, leading to unique reference aliasing. + /// + /// It is always safe for shared access. + /// `entity` must stem from `self.entity_iter`, and not have been passed before. + #[inline(always)] + unsafe fn fetch_next_aliased_unchecked(&mut self, entity: Entity) -> D::Item<'w> { + let (location, archetype, table); + // SAFETY: + // `tables` and `archetypes` belong to the same world that the [`QueryIter`] + // was initialized for. + unsafe { + location = self.entities.get(entity).debug_checked_unwrap(); + archetype = self + .archetypes + .get(location.archetype_id) + .debug_checked_unwrap(); + table = self.tables.get(location.table_id).debug_checked_unwrap(); + } + + // SAFETY: `archetype` is from the world that `fetch` was created for, + // `fetch_state` is the state that `fetch` was initialized with + unsafe { + D::set_archetype( + &mut self.fetch, + &self.query_state.fetch_state, + archetype, + table, + ); + } + + // The entity list has already been filtered by the query lens, so we forego filtering here. + // SAFETY: + // - set_archetype was called prior, `location.archetype_row` is an archetype index in range of the current archetype + // - fetch is only called once for each entity. + unsafe { D::fetch(&mut self.fetch, entity, location.table_row) } + } + + /// Collects the internal [`I`](QuerySortedManyIter) once. + /// [`fetch_next`](QuerySortedManyIter) and [`fetch_next_back`](QuerySortedManyIter) require this to be called first. + #[inline(always)] + pub fn collect_inner(self) -> QuerySortedManyIter<'w, 's, D, F, IntoIter> { + QuerySortedManyIter { + entity_iter: self.entity_iter.collect::>().into_iter(), + entities: self.entities, + tables: self.tables, + archetypes: self.archetypes, + fetch: self.fetch, + query_state: self.query_state, + } + } +} + +impl<'w, 's, D: QueryData, F: QueryFilter> QuerySortedManyIter<'w, 's, D, F, IntoIter> { + /// Get next result from the query + /// [`collect_inner`](QuerySortedManyIter) needs to be called before this method becomes available. + /// This is done to prevent mutable aliasing. + #[inline(always)] + pub fn fetch_next(&mut self) -> Option> { + let entity = self.entity_iter.next()?; + + // SAFETY: + // We have collected the entity_iter once to drop all internal lens query item + // references. + // We are limiting the returned reference to self, + // making sure this method cannot be called multiple times without getting rid + // of any previously returned unique references first, thus preventing aliasing. + // `entity` is passed from `entity_iter` the first time. + unsafe { D::shrink(self.fetch_next_aliased_unchecked(entity)).into() } + } + + /// Get next result from the query + /// [`collect_inner`](QuerySortedManyIter) needs to be called before this method becomes available. + /// This is done to prevent mutable aliasing. + #[inline(always)] + pub fn fetch_next_back(&mut self) -> Option> { + let entity = self.entity_iter.next_back()?; + + // SAFETY: + // We have collected the entity_iter once to drop all internal lens query item + // references. + // We are limiting the returned reference to self, + // making sure this method cannot be called multiple times without getting rid + // of any previously returned unique references first, thus preventing aliasing. + // `entity` is passed from `entity_iter` the first time. + unsafe { D::shrink(self.fetch_next_aliased_unchecked(entity)).into() } + } +} + +impl<'w, 's, D: ReadOnlyQueryData, F: QueryFilter, I: Iterator> Iterator + for QuerySortedManyIter<'w, 's, D, F, I> +{ + type Item = D::Item<'w>; + + #[inline(always)] + fn next(&mut self) -> Option { + let entity = self.entity_iter.next()?; + // SAFETY: + // It is safe to alias for ReadOnlyWorldQuery. + // `entity` is passed from `entity_iter` the first time. + unsafe { self.fetch_next_aliased_unchecked(entity).into() } + } + + fn size_hint(&self) -> (usize, Option) { + self.entity_iter.size_hint() + } +} + +impl<'w, 's, D: ReadOnlyQueryData, F: QueryFilter, I: DoubleEndedIterator> + DoubleEndedIterator for QuerySortedManyIter<'w, 's, D, F, I> +{ + #[inline(always)] + fn next_back(&mut self) -> Option { + let entity = self.entity_iter.next_back()?; + // SAFETY: + // It is safe to alias for ReadOnlyWorldQuery. + // `entity` is passed from `entity_iter` the first time. + unsafe { self.fetch_next_aliased_unchecked(entity).into() } + } +} + +impl<'w, 's, D: ReadOnlyQueryData, F: QueryFilter, I: ExactSizeIterator> + ExactSizeIterator for QuerySortedManyIter<'w, 's, D, F, I> +{ +} + +impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> Debug + for QuerySortedManyIter<'w, 's, D, F, I> +{ + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("QuerySortedManyIter").finish() + } +} + /// An iterator over `K`-sized combinations of query items without repetition. /// /// A combination is an arrangement of a collection of items where order does not matter. @@ -1841,7 +2650,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { } // NOTE: If you are changing query iteration code, remember to update the following places, where relevant: - // QueryIter, QueryIterationCursor, QuerySortedIter, QueryManyIter, QueryCombinationIter, QueryState::par_fold_init_unchecked_manual + // QueryIter, QueryIterationCursor, QuerySortedIter, QueryManyIter, QuerySortedManyIter, QueryCombinationIter, QueryState::par_fold_init_unchecked_manual /// # Safety /// `tables` and `archetypes` must belong to the same world that the [`QueryIterationCursor`] /// was initialized for. @@ -1998,7 +2807,7 @@ mod tests { #[allow(clippy::unnecessary_sort_by)] #[test] - fn query_sorts() { + fn query_iter_sorts() { let mut world = World::new(); let mut query = world.query::(); @@ -2067,7 +2876,7 @@ mod tests { #[test] #[should_panic] - fn query_sort_after_next() { + fn query_iter_sort_after_next() { let mut world = World::new(); world.spawn((A(0.),)); world.spawn((A(1.1),)); @@ -2097,7 +2906,7 @@ mod tests { #[test] #[should_panic] - fn query_sort_after_next_dense() { + fn query_iter_sort_after_next_dense() { let mut world = World::new(); world.spawn((Sparse(11),)); world.spawn((Sparse(22),)); @@ -2126,7 +2935,7 @@ mod tests { } #[test] - fn empty_query_sort_after_next_does_not_panic() { + fn empty_query_iter_sort_after_next_does_not_panic() { let mut world = World::new(); { let mut query = world.query::<(&A, &Sparse)>(); @@ -2183,4 +2992,103 @@ mod tests { ); } } + + #[allow(clippy::unnecessary_sort_by)] + #[test] + fn query_iter_many_sorts() { + let mut world = World::new(); + + let entity_list: &Vec<_> = &world + .spawn_batch([A(0.), A(1.), A(2.), A(3.), A(4.)]) + .collect(); + + let mut query = world.query::(); + + let sort = query + .iter_many(&world, entity_list) + .sort::() + .collect::>(); + + let sort_unstable = query + .iter_many(&world, entity_list) + .sort_unstable::() + .collect::>(); + + let sort_by = query + .iter_many(&world, entity_list) + .sort_by::(Ord::cmp) + .collect::>(); + + let sort_unstable_by = query + .iter_many(&world, entity_list) + .sort_unstable_by::(Ord::cmp) + .collect::>(); + + let sort_by_key = query + .iter_many(&world, entity_list) + .sort_by_key::(|&e| e) + .collect::>(); + + let sort_unstable_by_key = query + .iter_many(&world, entity_list) + .sort_unstable_by_key::(|&e| e) + .collect::>(); + + let sort_by_cached_key = query + .iter_many(&world, entity_list) + .sort_by_cached_key::(|&e| e) + .collect::>(); + + let mut sort_v2 = query.iter_many(&world, entity_list).collect::>(); + sort_v2.sort(); + + let mut sort_unstable_v2 = query.iter_many(&world, entity_list).collect::>(); + sort_unstable_v2.sort_unstable(); + + let mut sort_by_v2 = query.iter_many(&world, entity_list).collect::>(); + sort_by_v2.sort_by(Ord::cmp); + + let mut sort_unstable_by_v2 = query.iter_many(&world, entity_list).collect::>(); + sort_unstable_by_v2.sort_unstable_by(Ord::cmp); + + let mut sort_by_key_v2 = query.iter_many(&world, entity_list).collect::>(); + sort_by_key_v2.sort_by_key(|&e| e); + + let mut sort_unstable_by_key_v2 = query.iter_many(&world, entity_list).collect::>(); + sort_unstable_by_key_v2.sort_unstable_by_key(|&e| e); + + let mut sort_by_cached_key_v2 = query.iter_many(&world, entity_list).collect::>(); + sort_by_cached_key_v2.sort_by_cached_key(|&e| e); + + assert_eq!(sort, sort_v2); + assert_eq!(sort_unstable, sort_unstable_v2); + assert_eq!(sort_by, sort_by_v2); + assert_eq!(sort_unstable_by, sort_unstable_by_v2); + assert_eq!(sort_by_key, sort_by_key_v2); + assert_eq!(sort_unstable_by_key, sort_unstable_by_key_v2); + assert_eq!(sort_by_cached_key, sort_by_cached_key_v2); + } + + #[test] + fn query_iter_many_sort_doesnt_panic_after_next() { + let mut world = World::new(); + + let entity_list: &Vec<_> = &world + .spawn_batch([A(0.), A(1.), A(2.), A(3.), A(4.)]) + .collect(); + + let mut query = world.query::(); + let mut iter = query.iter_many(&world, entity_list); + + _ = iter.next(); + + iter.sort::(); + + let mut query_2 = world.query::<&mut A>(); + let mut iter_2 = query_2.iter_many_mut(&mut world, entity_list); + + _ = iter_2.fetch_next(); + + iter_2.sort::(); + } } diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index 69b67368fe4f3..f493ba7c97ef9 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -6,6 +6,7 @@ use crate::{ prelude::FromWorld, query::{ Access, DebugCheckedUnwrap, FilteredAccess, QueryCombinationIter, QueryIter, QueryParIter, + WorldQuery, }, storage::{SparseSetIndex, TableId}, world::{unsafe_world_cell::UnsafeWorldCell, World, WorldId}, @@ -160,6 +161,16 @@ impl QueryState { state } + /// Creates a new [`QueryState`] from an immutable [`World`] reference and inherits the result of `world.id()`. + /// + /// This function may fail if, for example, + /// the components that make up this query have not been registered into the world. + pub fn try_new(world: &World) -> Option { + let mut state = Self::try_new_uninitialized(world)?; + state.update_archetypes(world); + Some(state) + } + /// Identical to `new`, but it populates the provided `access` with the matched results. pub(crate) fn new_with_access( world: &mut World, @@ -186,7 +197,32 @@ impl QueryState { fn new_uninitialized(world: &mut World) -> Self { let fetch_state = D::init_state(world); let filter_state = F::init_state(world); + Self::from_states_uninitialized(world.id(), fetch_state, filter_state) + } + + /// Creates a new [`QueryState`] but does not populate it with the matched results from the World yet + /// + /// `new_archetype` and its variants must be called on all of the World's archetypes before the + /// state can return valid query results. + fn try_new_uninitialized(world: &World) -> Option { + let fetch_state = D::get_state(world.components())?; + let filter_state = F::get_state(world.components())?; + Some(Self::from_states_uninitialized( + world.id(), + fetch_state, + filter_state, + )) + } + /// Creates a new [`QueryState`] but does not populate it with the matched results from the World yet + /// + /// `new_archetype` and its variants must be called on all of the World's archetypes before the + /// state can return valid query results. + fn from_states_uninitialized( + world_id: WorldId, + fetch_state: ::State, + filter_state: ::State, + ) -> Self { let mut component_access = FilteredAccess::default(); D::update_component_access(&fetch_state, &mut component_access); @@ -205,7 +241,7 @@ impl QueryState { let is_dense = D::IS_DENSE && F::IS_DENSE; Self { - world_id: world.id(), + world_id, archetype_generation: ArchetypeGeneration::initial(), matched_storage_ids: Vec::new(), is_dense, diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index 5ec6621572de2..b00304ae1462d 100644 --- a/crates/bevy_ecs/src/schedule/schedule.rs +++ b/crates/bevy_ecs/src/schedule/schedule.rs @@ -1961,13 +1961,13 @@ pub enum ScheduleBuildError { #[display("System dependencies contain cycle(s).\n{_0}")] DependencyCycle(String), /// Tried to order a system (set) relative to a system set it belongs to. - #[display("`{0}` and `{_1}` have both `in_set` and `before`-`after` relationships (these might be transitive). This combination is unsolvable as a system cannot run before or after a set it belongs to.")] + #[display("`{_0}` and `{_1}` have both `in_set` and `before`-`after` relationships (these might be transitive). This combination is unsolvable as a system cannot run before or after a set it belongs to.")] CrossDependency(String, String), /// Tried to order system sets that share systems. - #[display("`{0}` and `{_1}` have a `before`-`after` relationship (which may be transitive) but share systems.")] + #[display("`{_0}` and `{_1}` have a `before`-`after` relationship (which may be transitive) but share systems.")] SetsHaveOrderButIntersect(String, String), /// Tried to order a system (set) relative to all instances of some system function. - #[display("Tried to order against `{0}` in a schedule that has more than one `{0}` instance. `{_0}` is a `SystemTypeSet` and cannot be used for ordering if ambiguous. Use a different set without this restriction.")] + #[display("Tried to order against `{_0}` in a schedule that has more than one `{_0}` instance. `{_0}` is a `SystemTypeSet` and cannot be used for ordering if ambiguous. Use a different set without this restriction.")] SystemTypeSetAmbiguity(String), /// Systems with conflicting access have indeterminate run order. /// diff --git a/crates/bevy_ecs/src/storage/blob_array.rs b/crates/bevy_ecs/src/storage/blob_array.rs index e6aefdb4b7dd4..c508b78c9853c 100644 --- a/crates/bevy_ecs/src/storage/blob_array.rs +++ b/crates/bevy_ecs/src/storage/blob_array.rs @@ -413,7 +413,7 @@ impl BlobArray { self.get_unchecked_mut(index_to_keep).promote() } - /// This method will can [`Self::swap_remove_unchecked`] and drop the result. + /// This method will call [`Self::swap_remove_unchecked`] and drop the result. /// /// # Safety /// - `index_to_keep` must be safe to access (within the bounds of the length of the array). diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 08eda1e5d59fc..289e37e3b460d 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -4,6 +4,7 @@ use core::{marker::PhantomData, panic::Location}; use super::{ Deferred, IntoObserverSystem, IntoSystem, RegisterSystem, Resource, RunSystemCachedWith, + UnregisterSystem, }; use crate::{ self as bevy_ecs, @@ -13,6 +14,7 @@ use crate::{ entity::{Entities, Entity}, event::{Event, SendEvent}, observer::{Observer, TriggerEvent, TriggerTargets}, + schedule::ScheduleLabel, system::{input::SystemInput, RunSystemWithInput, SystemId}, world::{ command_queue::RawCommandQueue, unsafe_world_cell::UnsafeWorldCell, Command, CommandQueue, @@ -890,6 +892,17 @@ impl<'w, 's> Commands<'w, 's> { SystemId::from_entity(entity) } + /// Removes a system previously registered with [`Commands::register_system`] or [`World::register_system`]. + /// + /// See [`World::unregister_system`] for more information. + pub fn unregister_system(&mut self, system_id: SystemId) + where + I: SystemInput + Send + 'static, + O: Send + 'static, + { + self.queue(UnregisterSystem::new(system_id)); + } + /// Similar to [`Self::run_system`], but caching the [`SystemId`] in a /// [`CachedSystemId`](crate::system::CachedSystemId) resource. /// @@ -961,6 +974,53 @@ impl<'w, 's> Commands<'w, 's> { self.queue(SendEvent { event }); self } + + /// Runs the schedule corresponding to the given [`ScheduleLabel`]. + /// + /// Calls [`World::try_run_schedule`](World::try_run_schedule). + /// + /// This will log an error if the schedule is not available to be run. + /// + /// # Examples + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # use bevy_ecs::schedule::ScheduleLabel; + /// # + /// # #[derive(Default, Resource)] + /// # struct Counter(u32); + /// # + /// #[derive(ScheduleLabel, Hash, Debug, PartialEq, Eq, Clone, Copy)] + /// struct FooSchedule; + /// + /// # fn foo_system(mut counter: ResMut) { + /// # counter.0 += 1; + /// # } + /// # + /// # let mut schedule = Schedule::new(FooSchedule); + /// # schedule.add_systems(foo_system); + /// # + /// # let mut world = World::default(); + /// # + /// # world.init_resource::(); + /// # world.add_schedule(schedule); + /// # + /// # assert_eq!(world.resource::().0, 0); + /// # + /// # let mut commands = world.commands(); + /// commands.run_schedule(FooSchedule); + /// # + /// # world.flush(); + /// # + /// # assert_eq!(world.resource::().0, 1); + /// ``` + pub fn run_schedule(&mut self, label: impl ScheduleLabel) { + self.queue(|world: &mut World| { + if let Err(error) = world.try_run_schedule(label) { + panic!("Failed to run schedule: {error}"); + } + }); + } } /// A [`Command`] which gets executed for a given [`Entity`]. diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index 78f310ccc7986..43f6a3350251b 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -395,9 +395,11 @@ impl SystemState { ) -> FunctionSystem { FunctionSystem { func, - param_state: Some(self.param_state), + state: Some(FunctionSystemState { + param: self.param_state, + world_id: self.world_id, + }), system_meta: self.meta, - world_id: Some(self.world_id), archetype_generation: self.archetype_generation, marker: PhantomData, } @@ -602,14 +604,25 @@ where F: SystemParamFunction, { func: F, - pub(crate) param_state: Option<::State>, - pub(crate) system_meta: SystemMeta, - world_id: Option, + state: Option>, + system_meta: SystemMeta, archetype_generation: ArchetypeGeneration, // NOTE: PhantomData T> gives this safe Send/Sync impls marker: PhantomData Marker>, } +/// The state of a [`FunctionSystem`], which must be initialized with +/// [`System::initialize`] before the system can be run. A panic will occur if +/// the system is run without being initialized. +struct FunctionSystemState { + /// The cached state of the system's [`SystemParam`]s. + param: P::State, + /// The id of the [`World`] this system was initialized with. If the world + /// passed to [`System::update_archetype_component_access`] does not match + /// this id, a panic will occur. + world_id: WorldId, +} + impl FunctionSystem where F: SystemParamFunction, @@ -631,9 +644,8 @@ where fn clone(&self) -> Self { Self { func: self.func.clone(), - param_state: None, + state: None, system_meta: SystemMeta::new::(), - world_id: None, archetype_generation: ArchetypeGeneration::initial(), marker: PhantomData, } @@ -653,9 +665,8 @@ where fn into_system(func: Self) -> Self::System { FunctionSystem { func, - param_state: None, + state: None, system_meta: SystemMeta::new::(), - world_id: None, archetype_generation: ArchetypeGeneration::initial(), marker: PhantomData, } @@ -669,7 +680,8 @@ where /// Message shown when a system isn't initialized // When lines get too long, rustfmt can sometimes refuse to format them. // Work around this by storing the message separately. - const PARAM_MESSAGE: &'static str = "System's param_state was not found. Did you forget to initialize this system before running it?"; + const ERROR_UNINITIALIZED: &'static str = + "System's state was not found. Did you forget to initialize this system before running it?"; } impl System for FunctionSystem @@ -721,19 +733,14 @@ where let change_tick = world.increment_change_tick(); + let param_state = &mut self.state.as_mut().expect(Self::ERROR_UNINITIALIZED).param; // SAFETY: // - The caller has invoked `update_archetype_component_access`, which will panic // if the world does not match. // - All world accesses used by `F::Param` have been registered, so the caller // will ensure that there are no data access conflicts. - let params = unsafe { - F::Param::get_param( - self.param_state.as_mut().expect(Self::PARAM_MESSAGE), - &self.system_meta, - world, - change_tick, - ) - }; + let params = + unsafe { F::Param::get_param(param_state, &self.system_meta, world, change_tick) }; let out = self.func.run(input, params); self.system_meta.last_run = change_tick; out @@ -741,19 +748,19 @@ where #[inline] fn apply_deferred(&mut self, world: &mut World) { - let param_state = self.param_state.as_mut().expect(Self::PARAM_MESSAGE); + let param_state = &mut self.state.as_mut().expect(Self::ERROR_UNINITIALIZED).param; F::Param::apply(param_state, &self.system_meta, world); } #[inline] fn queue_deferred(&mut self, world: DeferredWorld) { - let param_state = self.param_state.as_mut().expect(Self::PARAM_MESSAGE); + let param_state = &mut self.state.as_mut().expect(Self::ERROR_UNINITIALIZED).param; F::Param::queue(param_state, &self.system_meta, world); } #[inline] unsafe fn validate_param_unsafe(&mut self, world: UnsafeWorldCell) -> bool { - let param_state = self.param_state.as_ref().expect(Self::PARAM_MESSAGE); + let param_state = &self.state.as_ref().expect(Self::ERROR_UNINITIALIZED).param; // SAFETY: // - The caller has invoked `update_archetype_component_access`, which will panic // if the world does not match. @@ -768,29 +775,32 @@ where #[inline] fn initialize(&mut self, world: &mut World) { - if let Some(id) = self.world_id { + if let Some(state) = &self.state { assert_eq!( - id, + state.world_id, world.id(), "System built with a different world than the one it was added to.", ); } else { - self.world_id = Some(world.id()); - self.param_state = Some(F::Param::init_state(world, &mut self.system_meta)); + self.state = Some(FunctionSystemState { + param: F::Param::init_state(world, &mut self.system_meta), + world_id: world.id(), + }); } self.system_meta.last_run = world.change_tick().relative_to(Tick::MAX); } fn update_archetype_component_access(&mut self, world: UnsafeWorldCell) { - assert_eq!(self.world_id, Some(world.id()), "Encountered a mismatched World. A System cannot be used with Worlds other than the one it was initialized with."); + let state = self.state.as_mut().expect(Self::ERROR_UNINITIALIZED); + assert_eq!(state.world_id, world.id(), "Encountered a mismatched World. A System cannot be used with Worlds other than the one it was initialized with."); + let archetypes = world.archetypes(); let old_generation = core::mem::replace(&mut self.archetype_generation, archetypes.generation()); for archetype in &archetypes[old_generation..] { - let param_state = self.param_state.as_mut().unwrap(); // SAFETY: The assertion above ensures that the param_state was initialized from `world`. - unsafe { F::Param::new_archetype(param_state, archetype, &mut self.system_meta) }; + unsafe { F::Param::new_archetype(&mut state.param, archetype, &mut self.system_meta) }; } } diff --git a/crates/bevy_ecs/src/system/query.rs b/crates/bevy_ecs/src/system/query.rs index b6a30b14aeb30..26440c0c643f4 100644 --- a/crates/bevy_ecs/src/system/query.rs +++ b/crates/bevy_ecs/src/system/query.rs @@ -352,6 +352,8 @@ use core::{ /// [`par_iter`]: Self::par_iter /// [`par_iter_mut`]: Self::par_iter_mut /// [performance]: #performance +/// [`Single`]: Single +/// [`Option`]: Single /// [`single`]: Self::single /// [`single_mut`]: Self::single_mut /// [`SparseSet`]: crate::storage::SparseSet @@ -649,7 +651,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// Returns an iterator over the 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. /// /// # Examples /// diff --git a/crates/bevy_ecs/src/system/system_registry.rs b/crates/bevy_ecs/src/system/system_registry.rs index 1aa17709345e3..573194713f8ba 100644 --- a/crates/bevy_ecs/src/system/system_registry.rs +++ b/crates/bevy_ecs/src/system/system_registry.rs @@ -30,7 +30,7 @@ pub struct SystemIdMarker; /// A system that has been removed from the registry. /// It contains the system and whether or not it has been initialized. /// -/// This struct is returned by [`World::remove_system`]. +/// This struct is returned by [`World::unregister_system`]. pub struct RemovedSystem { initialized: bool, system: BoxedSystem, @@ -172,7 +172,7 @@ impl World { /// /// If no system corresponds to the given [`SystemId`], this method returns an error. /// Systems are also not allowed to remove themselves, this returns an error too. - pub fn remove_system( + pub fn unregister_system( &mut self, id: SystemId, ) -> Result, RegisteredSystemError> @@ -412,7 +412,7 @@ impl World { /// Removes a cached system and its [`CachedSystemId`] resource. /// /// See [`World::register_system_cached`] for more information. - pub fn remove_system_cached( + pub fn unregister_system_cached( &mut self, _system: S, ) -> Result, RegisteredSystemError> @@ -424,7 +424,7 @@ impl World { let id = self .remove_resource::>() .ok_or(RegisteredSystemError::SystemNotCached)?; - self.remove_system(id.0) + self.unregister_system(id.0) } /// Runs a cached system, registering it if necessary. @@ -544,6 +544,32 @@ where } } +/// The [`Command`] type for unregistering one-shot systems from [`Commands`](crate::system::Commands). +pub struct UnregisterSystem { + system_id: SystemId, +} + +impl UnregisterSystem +where + I: SystemInput + 'static, + O: 'static, +{ + /// Creates a new [`Command`] struct, which can be added to [`Commands`](crate::system::Commands). + pub fn new(system_id: SystemId) -> Self { + Self { system_id } + } +} + +impl Command for UnregisterSystem +where + I: SystemInput + 'static, + O: 'static, +{ + fn apply(self, world: &mut World) { + let _ = world.unregister_system(self.system_id); + } +} + /// The [`Command`] type for running a cached one-shot system from /// [`Commands`](crate::system::Commands). /// @@ -834,7 +860,7 @@ mod tests { let new = world.register_system_cached(four); assert_eq!(old, new); - let result = world.remove_system_cached(four); + let result = world.unregister_system_cached(four); assert!(result.is_ok()); let new = world.register_system_cached(four); assert_ne!(old, new); diff --git a/crates/bevy_ecs/src/traversal.rs b/crates/bevy_ecs/src/traversal.rs index 16a92802fd524..3795883dfdb1b 100644 --- a/crates/bevy_ecs/src/traversal.rs +++ b/crates/bevy_ecs/src/traversal.rs @@ -9,7 +9,7 @@ use crate::{entity::Entity, query::ReadOnlyQueryData}; /// /// Infinite loops are possible, and are not checked for. While looping can be desirable in some contexts /// (for example, an observer that triggers itself multiple times before stopping), following an infinite -/// traversal loop without an eventual exit will can your application to hang. Each implementer of `Traversal` +/// traversal loop without an eventual exit will cause your application to hang. Each implementer of `Traversal` /// for documenting possible looping behavior, and consumers of those implementations are responsible for /// avoiding infinite loops in their code. /// diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 7b8dc9465b57e..b5e66974d89e1 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -730,6 +730,34 @@ impl<'w> EntityMut<'w> { unsafe { component_ids.fetch_mut(self.0) } } + /// Returns [untyped mutable reference](MutUntyped) to component for + /// the current entity, based on the given [`ComponentId`]. + /// + /// Unlike [`EntityMut::get_mut_by_id`], this method borrows &self instead of + /// &mut self, allowing the caller to access multiple components simultaneously. + /// + /// # Errors + /// + /// - Returns [`EntityComponentError::MissingComponent`] if the entity does + /// not have a component. + /// - Returns [`EntityComponentError::AliasedMutability`] if a component + /// is requested multiple times. + /// + /// # Safety + /// It is the callers responsibility to ensure that + /// - the [`UnsafeEntityCell`] has permission to access the component mutably + /// - no other references to the component exist at the same time + #[inline] + pub unsafe fn get_mut_by_id_unchecked( + &self, + component_ids: F, + ) -> Result, EntityComponentError> { + // SAFETY: + // - The caller must ensure simultaneous access is limited + // - to components that are mutually independent. + unsafe { component_ids.fetch_mut(self.0) } + } + /// Consumes `self` and returns [untyped mutable reference(s)](MutUntyped) /// to component(s) with lifetime `'w` for the current entity, based on the /// given [`ComponentId`]s. @@ -2774,13 +2802,19 @@ impl<'a> From<&'a mut EntityWorldMut<'_>> for FilteredEntityMut<'a> { } } +/// Error type returned by [`TryFrom`] conversions from filtered entity types +/// ([`FilteredEntityRef`]/[`FilteredEntityMut`]) to full-access entity types +/// ([`EntityRef`]/[`EntityMut`]). #[derive(Error, Display, Debug)] pub enum TryFromFilteredError { + /// Error indicating that the filtered entity does not have read access to + /// all components. #[display( "Conversion failed, filtered entity ref does not have read access to all components" )] MissingReadAllAccess, - + /// Error indicating that the filtered entity does not have write access to + /// all components. #[display( "Conversion failed, filtered entity ref does not have write access to all components" )] @@ -4036,7 +4070,6 @@ mod tests { } #[test] - #[ignore] // This should pass, but it currently fails due to limitations in our access model. fn ref_compatible_with_resource_mut() { fn borrow_system(_: Query, _: ResMut) {} @@ -4067,7 +4100,6 @@ mod tests { } #[test] - #[ignore] // This should pass, but it currently fails due to limitations in our access model. fn mut_compatible_with_resource() { fn borrow_mut_system(_: Res, _: Query) {} @@ -4075,7 +4107,6 @@ mod tests { } #[test] - #[ignore] // This should pass, but it currently fails due to limitations in our access model. fn mut_compatible_with_resource_mut() { fn borrow_mut_system(_: ResMut, _: Query) {} @@ -4428,4 +4459,27 @@ mod tests { .map(|_| { unreachable!() }) ); } + + #[test] + fn get_mut_by_id_unchecked() { + let mut world = World::default(); + let e1 = world.spawn((X(7), Y(10))).id(); + let x_id = world.register_component::(); + let y_id = world.register_component::(); + + let e1_mut = &world.get_entity_mut([e1]).unwrap()[0]; + // SAFETY: The entity e1 contains component X. + let x_ptr = unsafe { e1_mut.get_mut_by_id_unchecked(x_id) }.unwrap(); + // SAFETY: The entity e1 contains component Y, with components X and Y being mutually independent. + let y_ptr = unsafe { e1_mut.get_mut_by_id_unchecked(y_id) }.unwrap(); + + // SAFETY: components match the id they were fetched with + let x_component = unsafe { x_ptr.into_inner().deref_mut::() }; + x_component.0 += 1; + // SAFETY: components match the id they were fetched with + let y_component = unsafe { y_ptr.into_inner().deref_mut::() }; + y_component.0 -= 1; + + assert_eq!((&mut X(8), &mut Y(9)), (x_component, y_component)); + } } diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index d50f8f421cfc2..6eb715af56e8a 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -22,8 +22,8 @@ pub use component_constants::*; pub use deferred_world::DeferredWorld; pub use entity_fetch::WorldEntityFetch; pub use entity_ref::{ - EntityMut, EntityMutExcept, EntityRef, EntityRefExcept, EntityWorldMut, Entry, - FilteredEntityMut, FilteredEntityRef, OccupiedEntry, VacantEntry, + DynamicComponentFetch, EntityMut, EntityMutExcept, EntityRef, EntityRefExcept, EntityWorldMut, + Entry, FilteredEntityMut, FilteredEntityRef, OccupiedEntry, TryFromFilteredError, VacantEntry, }; pub use filtered_resource::*; pub use identifier::WorldId; @@ -515,7 +515,7 @@ impl World { // SAFETY: We just created the `required` and `requiree` components. unsafe { self.components - .register_required_components::(required, requiree, constructor) + .register_required_components::(requiree, required, constructor) } } @@ -1670,6 +1670,84 @@ impl World { QueryState::new(self) } + /// Returns [`QueryState`] for the given [`QueryData`], which is used to efficiently + /// run queries on the [`World`] by storing and reusing the [`QueryState`]. + /// ``` + /// use bevy_ecs::{component::Component, entity::Entity, world::World}; + /// + /// #[derive(Component, Debug, PartialEq)] + /// struct Position { + /// x: f32, + /// y: f32, + /// } + /// + /// let mut world = World::new(); + /// world.spawn_batch(vec![ + /// Position { x: 0.0, y: 0.0 }, + /// Position { x: 1.0, y: 1.0 }, + /// ]); + /// + /// fn get_positions(world: &World) -> Vec<(Entity, &Position)> { + /// let mut query = world.try_query::<(Entity, &Position)>().unwrap(); + /// query.iter(world).collect() + /// } + /// + /// let positions = get_positions(&world); + /// + /// assert_eq!(world.get::(positions[0].0).unwrap(), positions[0].1); + /// assert_eq!(world.get::(positions[1].0).unwrap(), positions[1].1); + /// ``` + /// + /// Requires only an immutable world reference, but may fail if, for example, + /// the components that make up this query have not been registered into the world. + /// ``` + /// use bevy_ecs::{component::Component, entity::Entity, world::World}; + /// + /// #[derive(Component)] + /// struct A; + /// + /// let mut world = World::new(); + /// + /// let none_query = world.try_query::<&A>(); + /// assert!(none_query.is_none()); + /// + /// world.register_component::(); + /// + /// let some_query = world.try_query::<&A>(); + /// assert!(some_query.is_some()); + /// ``` + #[inline] + pub fn try_query(&self) -> Option> { + self.try_query_filtered::() + } + + /// Returns [`QueryState`] for the given filtered [`QueryData`], which is used to efficiently + /// run queries on the [`World`] by storing and reusing the [`QueryState`]. + /// ``` + /// use bevy_ecs::{component::Component, entity::Entity, world::World, query::With}; + /// + /// #[derive(Component)] + /// struct A; + /// #[derive(Component)] + /// struct B; + /// + /// let mut world = World::new(); + /// let e1 = world.spawn(A).id(); + /// let e2 = world.spawn((A, B)).id(); + /// + /// let mut query = world.try_query_filtered::>().unwrap(); + /// let matching_entities = query.iter(&world).collect::>(); + /// + /// assert_eq!(matching_entities, vec![e2]); + /// ``` + /// + /// Requires only an immutable world reference, but may fail if, for example, + /// the components that make up this query have not been registered into the world. + #[inline] + pub fn try_query_filtered(&self) -> Option> { + QueryState::try_new(self) + } + /// Returns an iterator of entities that had components of type `T` removed /// since the last call to [`World::clear_trackers`]. pub fn removed(&self) -> impl Iterator + '_ { diff --git a/crates/bevy_gizmos/Cargo.toml b/crates/bevy_gizmos/Cargo.toml index 5f79b93d3a266..e880e985fff52 100644 --- a/crates/bevy_gizmos/Cargo.toml +++ b/crates/bevy_gizmos/Cargo.toml @@ -20,6 +20,7 @@ bevy_sprite = { path = "../bevy_sprite", version = "0.15.0-dev", optional = true bevy_app = { path = "../bevy_app", version = "0.15.0-dev" } bevy_color = { path = "../bevy_color", version = "0.15.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.15.0-dev" } bevy_math = { path = "../bevy_math", version = "0.15.0-dev" } bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" } bevy_render = { path = "../bevy_render", version = "0.15.0-dev", optional = true } diff --git a/crates/bevy_gizmos/src/curves.rs b/crates/bevy_gizmos/src/curves.rs index 4a7b1aec1e045..be747626decbc 100644 --- a/crates/bevy_gizmos/src/curves.rs +++ b/crates/bevy_gizmos/src/curves.rs @@ -33,7 +33,7 @@ where /// # use bevy_color::palettes::basic::{RED}; /// fn system(mut gizmos: Gizmos) { /// let domain = Interval::UNIT; - /// let curve = function_curve(domain, |t| Vec2::from(t.sin_cos())); + /// let curve = FunctionCurve::new(domain, |t| Vec2::from(t.sin_cos())); /// gizmos.curve_2d(curve, (0..=100).map(|n| n as f32 / 100.0), RED); /// } /// # bevy_ecs::system::assert_is_system(system); @@ -67,7 +67,7 @@ where /// # use bevy_color::palettes::basic::{RED}; /// fn system(mut gizmos: Gizmos) { /// let domain = Interval::UNIT; - /// let curve = function_curve(domain, |t| { + /// let curve = FunctionCurve::new(domain, |t| { /// let (x,y) = t.sin_cos(); /// Vec3::new(x, y, t) /// }); @@ -104,7 +104,7 @@ where /// # use bevy_color::{Mix, palettes::basic::{GREEN, RED}}; /// fn system(mut gizmos: Gizmos) { /// let domain = Interval::UNIT; - /// let curve = function_curve(domain, |t| Vec2::from(t.sin_cos())); + /// let curve = FunctionCurve::new(domain, |t| Vec2::from(t.sin_cos())); /// gizmos.curve_gradient_2d( /// curve, /// (0..=100).map(|n| n as f32 / 100.0) @@ -147,7 +147,7 @@ where /// # use bevy_color::{Mix, palettes::basic::{GREEN, RED}}; /// fn system(mut gizmos: Gizmos) { /// let domain = Interval::UNIT; - /// let curve = function_curve(domain, |t| { + /// let curve = FunctionCurve::new(domain, |t| { /// let (x,y) = t.sin_cos(); /// Vec3::new(x, y, t) /// }); diff --git a/crates/bevy_gizmos/src/pipeline_2d.rs b/crates/bevy_gizmos/src/pipeline_2d.rs index 995b59af2243b..9e122782f7fc5 100644 --- a/crates/bevy_gizmos/src/pipeline_2d.rs +++ b/crates/bevy_gizmos/src/pipeline_2d.rs @@ -13,6 +13,7 @@ use bevy_ecs::{ system::{Query, Res, ResMut, Resource}, world::{FromWorld, World}, }; +use bevy_image::BevyDefault as _; use bevy_math::FloatOrd; use bevy_render::sync_world::MainEntity; use bevy_render::{ @@ -22,7 +23,6 @@ use bevy_render::{ ViewSortedRenderPhases, }, render_resource::*, - texture::BevyDefault, view::{ExtractedView, Msaa, RenderLayers, ViewTarget}, Render, RenderApp, RenderSet, }; @@ -161,6 +161,7 @@ impl SpecializedRenderPipeline for LineGizmoPipeline { }, label: Some("LineGizmo Pipeline 2D".into()), push_constant_ranges: vec![], + zero_initialize_workgroup_memory: false, } } } @@ -261,6 +262,7 @@ impl SpecializedRenderPipeline for LineJointGizmoPipeline { }, label: Some("LineJointGizmo Pipeline 2D".into()), push_constant_ranges: vec![], + zero_initialize_workgroup_memory: false, } } } diff --git a/crates/bevy_gizmos/src/pipeline_3d.rs b/crates/bevy_gizmos/src/pipeline_3d.rs index 6d16db4711622..2d3f2bbd8d583 100644 --- a/crates/bevy_gizmos/src/pipeline_3d.rs +++ b/crates/bevy_gizmos/src/pipeline_3d.rs @@ -17,6 +17,7 @@ use bevy_ecs::{ system::{Query, Res, ResMut, Resource}, world::{FromWorld, World}, }; +use bevy_image::BevyDefault as _; use bevy_pbr::{MeshPipeline, MeshPipelineKey, SetMeshViewBindGroup}; use bevy_render::sync_world::MainEntity; use bevy_render::{ @@ -26,7 +27,6 @@ use bevy_render::{ ViewSortedRenderPhases, }, render_resource::*, - texture::BevyDefault, view::{ExtractedView, Msaa, RenderLayers, ViewTarget}, Render, RenderApp, RenderSet, }; @@ -158,6 +158,7 @@ impl SpecializedRenderPipeline for LineGizmoPipeline { }, label: Some("LineGizmo Pipeline".into()), push_constant_ranges: vec![], + zero_initialize_workgroup_memory: false, } } } @@ -256,6 +257,7 @@ impl SpecializedRenderPipeline for LineJointGizmoPipeline { }, label: Some("LineJointGizmo Pipeline".into()), push_constant_ranges: vec![], + zero_initialize_workgroup_memory: false, } } } diff --git a/crates/bevy_gltf/src/lib.rs b/crates/bevy_gltf/src/lib.rs index 05d4c596f8469..46f9ad3ae90fb 100644 --- a/crates/bevy_gltf/src/lib.rs +++ b/crates/bevy_gltf/src/lib.rs @@ -103,12 +103,12 @@ pub use loader::*; use bevy_app::prelude::*; use bevy_asset::{Asset, AssetApp, AssetPath, Handle}; use bevy_ecs::{prelude::Component, reflect::ReflectComponent}; +use bevy_image::CompressedImageFormats; use bevy_pbr::StandardMaterial; use bevy_reflect::{std_traits::ReflectDefault, Reflect, TypePath}; use bevy_render::{ mesh::{skinning::SkinnedMeshInverseBindposes, Mesh, MeshVertexAttribute}, renderer::RenderDevice, - texture::CompressedImageFormats, }; use bevy_scene::Scene; diff --git a/crates/bevy_gltf/src/loader.rs b/crates/bevy_gltf/src/loader.rs index 1cc747264e7a7..84c565bd8e5ec 100644 --- a/crates/bevy_gltf/src/loader.rs +++ b/crates/bevy_gltf/src/loader.rs @@ -15,6 +15,10 @@ use bevy_ecs::{ world::World, }; use bevy_hierarchy::{BuildChildren, ChildBuild, WorldChildBuilder}; +use bevy_image::{ + CompressedImageFormats, Image, ImageAddressMode, ImageFilterMode, ImageLoaderSettings, + ImageSampler, ImageSamplerDescriptor, ImageType, TextureError, +}; use bevy_math::{Affine2, Mat4, Vec3}; use bevy_pbr::{ DirectionalLight, MeshMaterial3d, PointLight, SpotLight, StandardMaterial, UvChannel, @@ -31,10 +35,6 @@ use bevy_render::{ primitives::Aabb, render_asset::RenderAssetUsages, render_resource::{Face, PrimitiveTopology}, - texture::{ - CompressedImageFormats, Image, ImageAddressMode, ImageFilterMode, ImageLoaderSettings, - ImageSampler, ImageSamplerDescriptor, ImageType, TextureError, - }, view::Visibility, }; use bevy_scene::Scene; @@ -274,8 +274,8 @@ async fn load_gltf<'a, 'b, 'c>( #[cfg(feature = "bevy_animation")] let (animations, named_animations, animation_roots) = { - use bevy_animation::{animation_curves::*, gltf_curves::*, VariableCurve}; - use bevy_math::curve::{constant_curve, Interval, UnevenSampleAutoCurve}; + use bevy_animation::{animated_field, animation_curves::*, gltf_curves::*, VariableCurve}; + use bevy_math::curve::{ConstantCurve, Interval, UnevenSampleAutoCurve}; use bevy_math::{Quat, Vec4}; use gltf::animation::util::ReadOutputs; let mut animations = vec![]; @@ -310,12 +310,13 @@ async fn load_gltf<'a, 'b, 'c>( { match outputs { ReadOutputs::Translations(tr) => { + let translation_property = animated_field!(Transform::translation); let translations: Vec = tr.map(Vec3::from).collect(); if keyframe_timestamps.len() == 1 { - #[allow(clippy::unnecessary_map_on_constructor)] - Some(constant_curve(Interval::EVERYWHERE, translations[0])) - .map(TranslationCurve) - .map(VariableCurve::new) + Some(VariableCurve::new(AnimatableCurve::new( + translation_property, + ConstantCurve::new(Interval::EVERYWHERE, translations[0]), + ))) } else { match interpolation { gltf::animation::Interpolation::Linear => { @@ -323,34 +324,47 @@ async fn load_gltf<'a, 'b, 'c>( keyframe_timestamps.into_iter().zip(translations), ) .ok() - .map(TranslationCurve) - .map(VariableCurve::new) + .map(|curve| { + VariableCurve::new(AnimatableCurve::new( + translation_property, + curve, + )) + }) } gltf::animation::Interpolation::Step => { SteppedKeyframeCurve::new( keyframe_timestamps.into_iter().zip(translations), ) .ok() - .map(TranslationCurve) - .map(VariableCurve::new) + .map(|curve| { + VariableCurve::new(AnimatableCurve::new( + translation_property, + curve, + )) + }) } gltf::animation::Interpolation::CubicSpline => { CubicKeyframeCurve::new(keyframe_timestamps, translations) .ok() - .map(TranslationCurve) - .map(VariableCurve::new) + .map(|curve| { + VariableCurve::new(AnimatableCurve::new( + translation_property, + curve, + )) + }) } } } } ReadOutputs::Rotations(rots) => { + let rotation_property = animated_field!(Transform::rotation); let rotations: Vec = rots.into_f32().map(Quat::from_array).collect(); if keyframe_timestamps.len() == 1 { - #[allow(clippy::unnecessary_map_on_constructor)] - Some(constant_curve(Interval::EVERYWHERE, rotations[0])) - .map(RotationCurve) - .map(VariableCurve::new) + Some(VariableCurve::new(AnimatableCurve::new( + rotation_property, + ConstantCurve::new(Interval::EVERYWHERE, rotations[0]), + ))) } else { match interpolation { gltf::animation::Interpolation::Linear => { @@ -358,16 +372,24 @@ async fn load_gltf<'a, 'b, 'c>( keyframe_timestamps.into_iter().zip(rotations), ) .ok() - .map(RotationCurve) - .map(VariableCurve::new) + .map(|curve| { + VariableCurve::new(AnimatableCurve::new( + rotation_property, + curve, + )) + }) } gltf::animation::Interpolation::Step => { SteppedKeyframeCurve::new( keyframe_timestamps.into_iter().zip(rotations), ) .ok() - .map(RotationCurve) - .map(VariableCurve::new) + .map(|curve| { + VariableCurve::new(AnimatableCurve::new( + rotation_property, + curve, + )) + }) } gltf::animation::Interpolation::CubicSpline => { CubicRotationCurve::new( @@ -375,19 +397,24 @@ async fn load_gltf<'a, 'b, 'c>( rotations.into_iter().map(Vec4::from), ) .ok() - .map(RotationCurve) - .map(VariableCurve::new) + .map(|curve| { + VariableCurve::new(AnimatableCurve::new( + rotation_property, + curve, + )) + }) } } } } ReadOutputs::Scales(scale) => { + let scale_property = animated_field!(Transform::scale); let scales: Vec = scale.map(Vec3::from).collect(); if keyframe_timestamps.len() == 1 { - #[allow(clippy::unnecessary_map_on_constructor)] - Some(constant_curve(Interval::EVERYWHERE, scales[0])) - .map(ScaleCurve) - .map(VariableCurve::new) + Some(VariableCurve::new(AnimatableCurve::new( + scale_property, + ConstantCurve::new(Interval::EVERYWHERE, scales[0]), + ))) } else { match interpolation { gltf::animation::Interpolation::Linear => { @@ -395,22 +422,34 @@ async fn load_gltf<'a, 'b, 'c>( keyframe_timestamps.into_iter().zip(scales), ) .ok() - .map(ScaleCurve) - .map(VariableCurve::new) + .map(|curve| { + VariableCurve::new(AnimatableCurve::new( + scale_property, + curve, + )) + }) } gltf::animation::Interpolation::Step => { SteppedKeyframeCurve::new( keyframe_timestamps.into_iter().zip(scales), ) .ok() - .map(ScaleCurve) - .map(VariableCurve::new) + .map(|curve| { + VariableCurve::new(AnimatableCurve::new( + scale_property, + curve, + )) + }) } gltf::animation::Interpolation::CubicSpline => { CubicKeyframeCurve::new(keyframe_timestamps, scales) .ok() - .map(ScaleCurve) - .map(VariableCurve::new) + .map(|curve| { + VariableCurve::new(AnimatableCurve::new( + scale_property, + curve, + )) + }) } } } @@ -419,7 +458,7 @@ async fn load_gltf<'a, 'b, 'c>( let weights: Vec = weights.into_f32().collect(); if keyframe_timestamps.len() == 1 { #[allow(clippy::unnecessary_map_on_constructor)] - Some(constant_curve(Interval::EVERYWHERE, weights)) + Some(ConstantCurve::new(Interval::EVERYWHERE, weights)) .map(WeightsCurve) .map(VariableCurve::new) } else { diff --git a/crates/bevy_gltf/src/vertex_attributes.rs b/crates/bevy_gltf/src/vertex_attributes.rs index c08d072f4c4fd..1ecf477f7b55f 100644 --- a/crates/bevy_gltf/src/vertex_attributes.rs +++ b/crates/bevy_gltf/src/vertex_attributes.rs @@ -248,7 +248,7 @@ pub(crate) enum ConvertAttributeError { "Vertex attribute {_0} has format {_1:?} but expected {_3:?} for target attribute {_2}" )] WrongFormat(String, VertexFormat, String, VertexFormat), - #[display("{0} in accessor {_1}")] + #[display("{_0} in accessor {_1}")] AccessFailed(AccessFailed, usize), #[display("Unknown vertex attribute {_0}")] UnknownName(String), diff --git a/crates/bevy_hierarchy/src/events.rs b/crates/bevy_hierarchy/src/events.rs index c397488c08434..5a667fef7789b 100644 --- a/crates/bevy_hierarchy/src/events.rs +++ b/crates/bevy_hierarchy/src/events.rs @@ -1,4 +1,5 @@ use bevy_ecs::{event::Event, prelude::Entity}; +#[cfg(feature = "reflect")] use bevy_reflect::Reflect; /// An [`Event`] that is fired whenever there is a change in the world's hierarchy. diff --git a/crates/bevy_hierarchy/src/lib.rs b/crates/bevy_hierarchy/src/lib.rs old mode 100755 new mode 100644 diff --git a/crates/bevy_image/Cargo.toml b/crates/bevy_image/Cargo.toml index 3583b21a79bb1..666598723bac5 100644 --- a/crates/bevy_image/Cargo.toml +++ b/crates/bevy_image/Cargo.toml @@ -49,7 +49,9 @@ image = { version = "0.25.2", default-features = false } # misc bitflags = { version = "2.3", features = ["serde"] } bytemuck = { version = "1.5" } -wgpu = { version = "23", default-features = false } +wgpu-types = { version = "23", default-features = false } +# TODO: remove dependency on wgpu once https://github.com/gfx-rs/wgpu/pull/6648, 6649 and 6650 have been released +wgpu = { version = "23.0.1", default-features = false } serde = { version = "1", features = ["derive"] } derive_more = { version = "1", default-features = false, features = [ "error", diff --git a/crates/bevy_image/src/basis.rs b/crates/bevy_image/src/basis.rs index e91c40e840d47..7772f38c0b952 100644 --- a/crates/bevy_image/src/basis.rs +++ b/crates/bevy_image/src/basis.rs @@ -1,7 +1,7 @@ use basis_universal::{ BasisTextureType, DecodeFlags, TranscodeParameters, Transcoder, TranscoderTextureFormat, }; -use wgpu::{AstcBlock, AstcChannel, Extent3d, TextureDimension, TextureFormat}; +use wgpu_types::{AstcBlock, AstcChannel, Extent3d, TextureDimension, TextureFormat}; use super::{CompressedImageFormats, Image, TextureError}; diff --git a/crates/bevy_image/src/dds.rs b/crates/bevy_image/src/dds.rs index 4ba311377d280..f36ec25894c02 100644 --- a/crates/bevy_image/src/dds.rs +++ b/crates/bevy_image/src/dds.rs @@ -1,10 +1,11 @@ +//! [DirectDraw Surface](https://en.wikipedia.org/wiki/DirectDraw_Surface) functionality. + #[cfg(debug_assertions)] use bevy_utils::warn_once; use ddsfile::{Caps2, D3DFormat, Dds, DxgiFormat}; use std::io::Cursor; -use wgpu::{ - Extent3d, TextureDimension, TextureFormat, TextureViewDescriptor, TextureViewDimension, -}; +use wgpu::TextureViewDescriptor; +use wgpu_types::{Extent3d, TextureDimension, TextureFormat, TextureViewDimension}; use super::{CompressedImageFormats, Image, TextureError}; @@ -16,7 +17,8 @@ pub fn dds_buffer_to_image( is_srgb: bool, ) -> Result { let mut cursor = Cursor::new(buffer); - let dds = Dds::read(&mut cursor).expect("Failed to parse DDS file"); + let dds = Dds::read(&mut cursor) + .map_err(|error| TextureError::InvalidData(format!("Failed to parse DDS file: {error}")))?; let texture_format = dds_format_to_texture_format(&dds, is_srgb)?; if !supported_compressed_formats.supports(texture_format) { return Err(TextureError::UnsupportedTextureFormat(format!( @@ -281,14 +283,18 @@ pub fn dds_format_to_texture_format( #[cfg(test)] mod test { - use wgpu::{util::TextureDataOrder, TextureDescriptor, TextureDimension}; + use wgpu::util::TextureDataOrder; + use wgpu_types::{TextureDescriptor, TextureDimension, TextureFormat}; use crate::CompressedImageFormats; use super::dds_buffer_to_image; /// `wgpu::create_texture_with_data` that reads from data structure but doesn't actually talk to your GPU - fn fake_wgpu_create_texture_with_data(desc: &TextureDescriptor<'_>, data: &[u8]) { + fn fake_wgpu_create_texture_with_data( + desc: &TextureDescriptor, &'_ [TextureFormat]>, + data: &[u8], + ) { // Will return None only if it's a combined depth-stencil format // If so, default to 4, validation will fail later anyway since the depth or stencil // aspect needs to be written to individually diff --git a/crates/bevy_image/src/exr_texture_loader.rs b/crates/bevy_image/src/exr_texture_loader.rs index 6e40c088ea94a..658fcccafd970 100644 --- a/crates/bevy_image/src/exr_texture_loader.rs +++ b/crates/bevy_image/src/exr_texture_loader.rs @@ -3,7 +3,7 @@ use bevy_asset::{io::Reader, AssetLoader, LoadContext, RenderAssetUsages}; use derive_more::derive::{Display, Error, From}; use image::ImageDecoder; use serde::{Deserialize, Serialize}; -use wgpu::{Extent3d, TextureDimension, TextureFormat}; +use wgpu_types::{Extent3d, TextureDimension, TextureFormat}; /// Loads EXR textures as Texture assets #[derive(Clone, Default)] diff --git a/crates/bevy_image/src/hdr_texture_loader.rs b/crates/bevy_image/src/hdr_texture_loader.rs index 91dd9dcc640aa..64032c04306a9 100644 --- a/crates/bevy_image/src/hdr_texture_loader.rs +++ b/crates/bevy_image/src/hdr_texture_loader.rs @@ -4,7 +4,7 @@ use bevy_asset::{io::Reader, AssetLoader, LoadContext}; use derive_more::derive::{Display, Error, From}; use image::DynamicImage; use serde::{Deserialize, Serialize}; -use wgpu::{Extent3d, TextureDimension, TextureFormat}; +use wgpu_types::{Extent3d, TextureDimension, TextureFormat}; /// Loads HDR textures as Texture assets #[derive(Clone, Default)] diff --git a/crates/bevy_image/src/image.rs b/crates/bevy_image/src/image.rs index abf504536e2e7..a0953b74f078b 100644 --- a/crates/bevy_image/src/image.rs +++ b/crates/bevy_image/src/image.rs @@ -13,7 +13,12 @@ use bevy_reflect::Reflect; use core::hash::Hash; use derive_more::derive::{Display, Error, From}; use serde::{Deserialize, Serialize}; -use wgpu::{Extent3d, TextureDimension, TextureFormat, TextureViewDescriptor}; +use wgpu::{SamplerDescriptor, TextureViewDescriptor}; +use wgpu_types::{ + AddressMode, CompareFunction, Extent3d, Features, FilterMode, SamplerBorderColor, + TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, +}; + pub trait BevyDefault { fn bevy_default() -> Self; } @@ -283,7 +288,7 @@ impl ImageFormat { pub struct Image { pub data: Vec, // TODO: this nesting makes accessing Image metadata verbose. Either flatten out descriptor or add accessors - pub texture_descriptor: wgpu::TextureDescriptor<'static>, + pub texture_descriptor: TextureDescriptor, &'static [TextureFormat]>, /// The [`ImageSampler`] to use during rendering. pub sampler: ImageSampler, pub texture_view_descriptor: Option>, @@ -337,7 +342,7 @@ impl ImageSampler { /// /// See [`ImageSamplerDescriptor`] for information how to configure this. /// -/// This type mirrors [`wgpu::AddressMode`]. +/// This type mirrors [`AddressMode`]. #[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)] pub enum ImageAddressMode { /// Clamp the value to the edge of the texture. @@ -357,7 +362,7 @@ pub enum ImageAddressMode { /// 1.25 -> 0.75 MirrorRepeat, /// Clamp the value to the border of the texture - /// Requires the wgpu feature [`wgpu::Features::ADDRESS_MODE_CLAMP_TO_BORDER`]. + /// Requires the wgpu feature [`Features::ADDRESS_MODE_CLAMP_TO_BORDER`]. /// /// -0.25 -> border /// 1.25 -> border @@ -366,7 +371,7 @@ pub enum ImageAddressMode { /// Texel mixing mode when sampling between texels. /// -/// This type mirrors [`wgpu::FilterMode`]. +/// This type mirrors [`FilterMode`]. #[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)] pub enum ImageFilterMode { /// Nearest neighbor sampling. @@ -382,7 +387,7 @@ pub enum ImageFilterMode { /// Comparison function used for depth and stencil operations. /// -/// This type mirrors [`wgpu::CompareFunction`]. +/// This type mirrors [`CompareFunction`]. #[derive(Clone, Copy, Debug, Serialize, Deserialize)] pub enum ImageCompareFunction { /// Function never passes @@ -409,7 +414,7 @@ pub enum ImageCompareFunction { /// Color variation to use when the sampler addressing mode is [`ImageAddressMode::ClampToBorder`]. /// -/// This type mirrors [`wgpu::SamplerBorderColor`]. +/// This type mirrors [`SamplerBorderColor`]. #[derive(Clone, Copy, Debug, Serialize, Deserialize)] pub enum ImageSamplerBorderColor { /// RGBA color `[0, 0, 0, 0]`. @@ -422,7 +427,7 @@ pub enum ImageSamplerBorderColor { /// textures that have an alpha component, and equivalent to [`Self::OpaqueBlack`] /// for textures that do not have an alpha component. On other backends, /// this is equivalent to [`Self::TransparentBlack`]. Requires - /// [`wgpu::Features::ADDRESS_MODE_CLAMP_TO_ZERO`]. Not supported on the web. + /// [`Features::ADDRESS_MODE_CLAMP_TO_ZERO`]. Not supported on the web. Zero, } @@ -432,7 +437,7 @@ pub enum ImageSamplerBorderColor { /// it will be serialized to an image asset `.meta` file which might require a migration in case of /// a breaking change. /// -/// This types mirrors [`wgpu::SamplerDescriptor`], but that might change in future versions. +/// This types mirrors [`SamplerDescriptor`], but that might change in future versions. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ImageSamplerDescriptor { pub label: Option, @@ -502,8 +507,8 @@ impl ImageSamplerDescriptor { } } - pub fn as_wgpu(&self) -> wgpu::SamplerDescriptor { - wgpu::SamplerDescriptor { + pub fn as_wgpu(&self) -> SamplerDescriptor { + SamplerDescriptor { label: self.label.as_deref(), address_mode_u: self.address_mode_u.into(), address_mode_v: self.address_mode_v.into(), @@ -520,100 +525,100 @@ impl ImageSamplerDescriptor { } } -impl From for wgpu::AddressMode { +impl From for AddressMode { fn from(value: ImageAddressMode) -> Self { match value { - ImageAddressMode::ClampToEdge => wgpu::AddressMode::ClampToEdge, - ImageAddressMode::Repeat => wgpu::AddressMode::Repeat, - ImageAddressMode::MirrorRepeat => wgpu::AddressMode::MirrorRepeat, - ImageAddressMode::ClampToBorder => wgpu::AddressMode::ClampToBorder, + ImageAddressMode::ClampToEdge => AddressMode::ClampToEdge, + ImageAddressMode::Repeat => AddressMode::Repeat, + ImageAddressMode::MirrorRepeat => AddressMode::MirrorRepeat, + ImageAddressMode::ClampToBorder => AddressMode::ClampToBorder, } } } -impl From for wgpu::FilterMode { +impl From for FilterMode { fn from(value: ImageFilterMode) -> Self { match value { - ImageFilterMode::Nearest => wgpu::FilterMode::Nearest, - ImageFilterMode::Linear => wgpu::FilterMode::Linear, + ImageFilterMode::Nearest => FilterMode::Nearest, + ImageFilterMode::Linear => FilterMode::Linear, } } } -impl From for wgpu::CompareFunction { +impl From for CompareFunction { fn from(value: ImageCompareFunction) -> Self { match value { - ImageCompareFunction::Never => wgpu::CompareFunction::Never, - ImageCompareFunction::Less => wgpu::CompareFunction::Less, - ImageCompareFunction::Equal => wgpu::CompareFunction::Equal, - ImageCompareFunction::LessEqual => wgpu::CompareFunction::LessEqual, - ImageCompareFunction::Greater => wgpu::CompareFunction::Greater, - ImageCompareFunction::NotEqual => wgpu::CompareFunction::NotEqual, - ImageCompareFunction::GreaterEqual => wgpu::CompareFunction::GreaterEqual, - ImageCompareFunction::Always => wgpu::CompareFunction::Always, + ImageCompareFunction::Never => CompareFunction::Never, + ImageCompareFunction::Less => CompareFunction::Less, + ImageCompareFunction::Equal => CompareFunction::Equal, + ImageCompareFunction::LessEqual => CompareFunction::LessEqual, + ImageCompareFunction::Greater => CompareFunction::Greater, + ImageCompareFunction::NotEqual => CompareFunction::NotEqual, + ImageCompareFunction::GreaterEqual => CompareFunction::GreaterEqual, + ImageCompareFunction::Always => CompareFunction::Always, } } } -impl From for wgpu::SamplerBorderColor { +impl From for SamplerBorderColor { fn from(value: ImageSamplerBorderColor) -> Self { match value { - ImageSamplerBorderColor::TransparentBlack => wgpu::SamplerBorderColor::TransparentBlack, - ImageSamplerBorderColor::OpaqueBlack => wgpu::SamplerBorderColor::OpaqueBlack, - ImageSamplerBorderColor::OpaqueWhite => wgpu::SamplerBorderColor::OpaqueWhite, - ImageSamplerBorderColor::Zero => wgpu::SamplerBorderColor::Zero, + ImageSamplerBorderColor::TransparentBlack => SamplerBorderColor::TransparentBlack, + ImageSamplerBorderColor::OpaqueBlack => SamplerBorderColor::OpaqueBlack, + ImageSamplerBorderColor::OpaqueWhite => SamplerBorderColor::OpaqueWhite, + ImageSamplerBorderColor::Zero => SamplerBorderColor::Zero, } } } -impl From for ImageAddressMode { - fn from(value: wgpu::AddressMode) -> Self { +impl From for ImageAddressMode { + fn from(value: AddressMode) -> Self { match value { - wgpu::AddressMode::ClampToEdge => ImageAddressMode::ClampToEdge, - wgpu::AddressMode::Repeat => ImageAddressMode::Repeat, - wgpu::AddressMode::MirrorRepeat => ImageAddressMode::MirrorRepeat, - wgpu::AddressMode::ClampToBorder => ImageAddressMode::ClampToBorder, + AddressMode::ClampToEdge => ImageAddressMode::ClampToEdge, + AddressMode::Repeat => ImageAddressMode::Repeat, + AddressMode::MirrorRepeat => ImageAddressMode::MirrorRepeat, + AddressMode::ClampToBorder => ImageAddressMode::ClampToBorder, } } } -impl From for ImageFilterMode { - fn from(value: wgpu::FilterMode) -> Self { +impl From for ImageFilterMode { + fn from(value: FilterMode) -> Self { match value { - wgpu::FilterMode::Nearest => ImageFilterMode::Nearest, - wgpu::FilterMode::Linear => ImageFilterMode::Linear, + FilterMode::Nearest => ImageFilterMode::Nearest, + FilterMode::Linear => ImageFilterMode::Linear, } } } -impl From for ImageCompareFunction { - fn from(value: wgpu::CompareFunction) -> Self { +impl From for ImageCompareFunction { + fn from(value: CompareFunction) -> Self { match value { - wgpu::CompareFunction::Never => ImageCompareFunction::Never, - wgpu::CompareFunction::Less => ImageCompareFunction::Less, - wgpu::CompareFunction::Equal => ImageCompareFunction::Equal, - wgpu::CompareFunction::LessEqual => ImageCompareFunction::LessEqual, - wgpu::CompareFunction::Greater => ImageCompareFunction::Greater, - wgpu::CompareFunction::NotEqual => ImageCompareFunction::NotEqual, - wgpu::CompareFunction::GreaterEqual => ImageCompareFunction::GreaterEqual, - wgpu::CompareFunction::Always => ImageCompareFunction::Always, + CompareFunction::Never => ImageCompareFunction::Never, + CompareFunction::Less => ImageCompareFunction::Less, + CompareFunction::Equal => ImageCompareFunction::Equal, + CompareFunction::LessEqual => ImageCompareFunction::LessEqual, + CompareFunction::Greater => ImageCompareFunction::Greater, + CompareFunction::NotEqual => ImageCompareFunction::NotEqual, + CompareFunction::GreaterEqual => ImageCompareFunction::GreaterEqual, + CompareFunction::Always => ImageCompareFunction::Always, } } } -impl From for ImageSamplerBorderColor { - fn from(value: wgpu::SamplerBorderColor) -> Self { +impl From for ImageSamplerBorderColor { + fn from(value: SamplerBorderColor) -> Self { match value { - wgpu::SamplerBorderColor::TransparentBlack => ImageSamplerBorderColor::TransparentBlack, - wgpu::SamplerBorderColor::OpaqueBlack => ImageSamplerBorderColor::OpaqueBlack, - wgpu::SamplerBorderColor::OpaqueWhite => ImageSamplerBorderColor::OpaqueWhite, - wgpu::SamplerBorderColor::Zero => ImageSamplerBorderColor::Zero, + SamplerBorderColor::TransparentBlack => ImageSamplerBorderColor::TransparentBlack, + SamplerBorderColor::OpaqueBlack => ImageSamplerBorderColor::OpaqueBlack, + SamplerBorderColor::OpaqueWhite => ImageSamplerBorderColor::OpaqueWhite, + SamplerBorderColor::Zero => ImageSamplerBorderColor::Zero, } } } -impl<'a> From> for ImageSamplerDescriptor { - fn from(value: wgpu::SamplerDescriptor) -> Self { +impl<'a> From> for ImageSamplerDescriptor { + fn from(value: SamplerDescriptor) -> Self { ImageSamplerDescriptor { label: value.label.map(ToString::to_string), address_mode_u: value.address_mode_u.into(), @@ -638,7 +643,7 @@ impl Default for Image { let data = vec![255; format.pixel_size()]; Image { data, - texture_descriptor: wgpu::TextureDescriptor { + texture_descriptor: TextureDescriptor { size: Extent3d { width: 1, height: 1, @@ -649,7 +654,7 @@ impl Default for Image { label: None, mip_level_count: 1, sample_count: 1, - usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, view_formats: &[], }, sampler: ImageSampler::Default, @@ -700,7 +705,7 @@ impl Image { let data = vec![255, 255, 255, 0]; Image { data, - texture_descriptor: wgpu::TextureDescriptor { + texture_descriptor: TextureDescriptor { size: Extent3d { width: 1, height: 1, @@ -711,7 +716,7 @@ impl Image { label: None, mip_level_count: 1, sample_count: 1, - usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, view_formats: &[], }, sampler: ImageSampler::Default, @@ -920,13 +925,13 @@ impl Image { let format_description = self.texture_descriptor.format; format_description .required_features() - .contains(wgpu::Features::TEXTURE_COMPRESSION_ASTC) + .contains(Features::TEXTURE_COMPRESSION_ASTC) || format_description .required_features() - .contains(wgpu::Features::TEXTURE_COMPRESSION_BC) + .contains(Features::TEXTURE_COMPRESSION_BC) || format_description .required_features() - .contains(wgpu::Features::TEXTURE_COMPRESSION_ETC2) + .contains(Features::TEXTURE_COMPRESSION_ETC2) } /// Compute the byte offset where the data of a specific pixel is stored @@ -943,19 +948,19 @@ impl Image { let pixel_size = self.texture_descriptor.format.pixel_size(); let pixel_offset = match self.texture_descriptor.dimension { TextureDimension::D3 => { - if coords.x > width || coords.y > height || coords.z > depth { + if coords.x >= width || coords.y >= height || coords.z >= depth { return None; } coords.z * height * width + coords.y * width + coords.x } TextureDimension::D2 => { - if coords.x > width || coords.y > height { + if coords.x >= width || coords.y >= height { return None; } coords.y * width + coords.x } TextureDimension::D1 => { - if coords.x > width { + if coords.x >= width { return None; } coords.x @@ -1496,15 +1501,15 @@ bitflags::bitflags! { } impl CompressedImageFormats { - pub fn from_features(features: wgpu::Features) -> Self { + pub fn from_features(features: Features) -> Self { let mut supported_compressed_formats = Self::default(); - if features.contains(wgpu::Features::TEXTURE_COMPRESSION_ASTC) { + if features.contains(Features::TEXTURE_COMPRESSION_ASTC) { supported_compressed_formats |= Self::ASTC_LDR; } - if features.contains(wgpu::Features::TEXTURE_COMPRESSION_BC) { + if features.contains(Features::TEXTURE_COMPRESSION_BC) { supported_compressed_formats |= Self::BC; } - if features.contains(wgpu::Features::TEXTURE_COMPRESSION_ETC2) { + if features.contains(Features::TEXTURE_COMPRESSION_ETC2) { supported_compressed_formats |= Self::ETC2; } supported_compressed_formats @@ -1572,4 +1577,28 @@ mod test { assert_eq!(UVec2::ONE, image.size()); assert_eq!(Vec2::ONE, image.size_f32()); } + + #[test] + fn on_edge_pixel_is_invalid() { + let image = Image::new_fill( + Extent3d { + width: 5, + height: 10, + depth_or_array_layers: 1, + }, + TextureDimension::D2, + &[0, 0, 0, 255], + TextureFormat::Rgba8Unorm, + RenderAssetUsages::MAIN_WORLD, + ); + assert!(matches!(image.get_color_at(4, 9), Ok(Color::BLACK))); + assert!(matches!( + image.get_color_at(0, 10), + Err(TextureAccessError::OutOfBounds { x: 0, y: 10, z: 0 }) + )); + assert!(matches!( + image.get_color_at(5, 10), + Err(TextureAccessError::OutOfBounds { x: 5, y: 10, z: 0 }) + )); + } } diff --git a/crates/bevy_image/src/image_texture_conversion.rs b/crates/bevy_image/src/image_texture_conversion.rs index c4d7fa426987e..fc3531a50d1f3 100644 --- a/crates/bevy_image/src/image_texture_conversion.rs +++ b/crates/bevy_image/src/image_texture_conversion.rs @@ -2,7 +2,7 @@ use crate::{Image, TextureFormatPixelInfo}; use bevy_asset::RenderAssetUsages; use derive_more::derive::{Display, Error}; use image::{DynamicImage, ImageBuffer}; -use wgpu::{Extent3d, TextureDimension, TextureFormat}; +use wgpu_types::{Extent3d, TextureDimension, TextureFormat}; impl Image { /// Converts a [`DynamicImage`] to an [`Image`]. diff --git a/crates/bevy_image/src/ktx2.rs b/crates/bevy_image/src/ktx2.rs index 7ba3bb6106d7d..27c5007f432c0 100644 --- a/crates/bevy_image/src/ktx2.rs +++ b/crates/bevy_image/src/ktx2.rs @@ -13,9 +13,9 @@ use ktx2::{ BasicDataFormatDescriptor, ChannelTypeQualifiers, ColorModel, DataFormatDescriptorHeader, Header, SampleInformation, }; -use wgpu::{ - AstcBlock, AstcChannel, Extent3d, TextureDimension, TextureFormat, TextureViewDescriptor, - TextureViewDimension, +use wgpu::TextureViewDescriptor; +use wgpu_types::{ + AstcBlock, AstcChannel, Extent3d, TextureDimension, TextureFormat, TextureViewDimension, }; use super::{CompressedImageFormats, DataFormat, Image, TextureError, TranscodeFormat}; diff --git a/crates/bevy_image/src/lib.rs b/crates/bevy_image/src/lib.rs index ac5510e5e335b..ce2e932826067 100644 --- a/crates/bevy_image/src/lib.rs +++ b/crates/bevy_image/src/lib.rs @@ -2,6 +2,10 @@ #![allow(missing_docs, reason = "Not all docs are written yet, see #3492.")] #![allow(unsafe_code)] +pub mod prelude { + pub use crate::{BevyDefault as _, Image, ImageFormat, TextureError}; +} + mod image; pub use self::image::*; #[cfg(feature = "basis-universal")] diff --git a/crates/bevy_input/src/button_input.rs b/crates/bevy_input/src/button_input.rs index d329470d8be70..c781fafe49ac6 100644 --- a/crates/bevy_input/src/button_input.rs +++ b/crates/bevy_input/src/button_input.rs @@ -210,7 +210,17 @@ where /// Returns `true` if any item in `inputs` has just been released. pub fn any_just_released(&self, inputs: impl IntoIterator) -> bool { - inputs.into_iter().any(|it| self.just_released(it)) + inputs.into_iter().any(|input| self.just_released(input)) + } + + /// Returns `true` if all items in `inputs` have just been released. + pub fn all_just_released(&self, inputs: impl IntoIterator) -> bool { + inputs.into_iter().all(|input| self.just_released(input)) + } + + /// Returns `true` if all items in `inputs` have been just pressed. + pub fn all_just_pressed(&self, inputs: impl IntoIterator) -> bool { + inputs.into_iter().all(|input| self.just_pressed(input)) } /// Clears the `just_released` state of the `input` and returns `true` if the `input` has just been released. diff --git a/crates/bevy_input/src/gamepad.rs b/crates/bevy_input/src/gamepad.rs index a356e525b4b20..5493103a61805 100644 --- a/crates/bevy_input/src/gamepad.rs +++ b/crates/bevy_input/src/gamepad.rs @@ -307,7 +307,7 @@ pub enum ButtonSettingsError { }, } -/// Stores a connected gamepad's state and any metadata such as the device name. +/// Stores a connected gamepad's metadata such as the name and its [`GamepadButton`] and [`GamepadAxis`]. /// /// An entity with this component is spawned automatically after [`GamepadConnectionEvent`] /// and updated by [`gamepad_event_processing_system`]. @@ -325,11 +325,11 @@ pub enum ButtonSettingsError { /// for (name, gamepad) in &gamepads { /// println!("{name}"); /// -/// if gamepad.digital.just_pressed(GamepadButton::North) { -/// println!("{name} just pressed North") +/// if gamepad.just_pressed(GamepadButton::North) { +/// println!("{} just pressed North", name) /// } /// -/// if let Some(left_stick_x) = gamepad.analog.get(GamepadAxis::LeftStickX) { +/// if let Some(left_stick_x) = gamepad.get(GamepadAxis::LeftStickX) { /// println!("left stick X: {}", left_stick_x) /// } /// } @@ -340,46 +340,175 @@ pub enum ButtonSettingsError { #[require(GamepadSettings)] pub struct Gamepad { /// The USB vendor ID as assigned by the USB-IF, if available. - pub vendor_id: Option, + pub(crate) vendor_id: Option, /// The USB product ID as assigned by the [vendor], if available. /// /// [vendor]: Self::vendor_id - pub product_id: Option, + pub(crate) product_id: Option, /// [`ButtonInput`] of [`GamepadButton`] representing their digital state - pub digital: ButtonInput, + pub(crate) digital: ButtonInput, /// [`Axis`] of [`GamepadButton`] representing their analog state. - pub analog: Axis, + pub(crate) analog: Axis, } impl Gamepad { + /// Returns the USB vendor ID as assigned by the USB-IF, if available. + pub fn vendor_id(&self) -> Option { + self.vendor_id + } + + /// Returns the USB product ID as assigned by the [vendor], if available. + /// + /// [vendor]: Self::vendor_id + pub fn product_id(&self) -> Option { + self.product_id + } + + /// Returns the analog data of the provided [`GamepadAxis`] or [`GamepadButton`]. + /// + /// This will be clamped between [[`Axis::MIN`],[`Axis::MAX`]]. + pub fn get(&self, input: impl Into) -> Option { + self.analog.get(input.into()) + } + + /// Returns the unclamped analog data of the provided [`GamepadAxis`] or [`GamepadButton`]. + /// + /// This value may be outside the [`Axis::MIN`] and [`Axis::MAX`] range. + pub fn get_unclamped(&self, input: impl Into) -> Option { + self.analog.get_unclamped(input.into()) + } + /// Returns the left stick as a [`Vec2`] pub fn left_stick(&self) -> Vec2 { Vec2 { - x: self.analog.get(GamepadAxis::LeftStickX).unwrap_or(0.0), - y: self.analog.get(GamepadAxis::LeftStickY).unwrap_or(0.0), + x: self.get(GamepadAxis::LeftStickX).unwrap_or(0.0), + y: self.get(GamepadAxis::LeftStickY).unwrap_or(0.0), } } /// Returns the right stick as a [`Vec2`] pub fn right_stick(&self) -> Vec2 { Vec2 { - x: self.analog.get(GamepadAxis::RightStickX).unwrap_or(0.0), - y: self.analog.get(GamepadAxis::RightStickY).unwrap_or(0.0), + x: self.get(GamepadAxis::RightStickX).unwrap_or(0.0), + y: self.get(GamepadAxis::RightStickY).unwrap_or(0.0), } } /// Returns the directional pad as a [`Vec2`] pub fn dpad(&self) -> Vec2 { Vec2 { - x: self.analog.get(GamepadButton::DPadRight).unwrap_or(0.0) - - self.analog.get(GamepadButton::DPadLeft).unwrap_or(0.0), - y: self.analog.get(GamepadButton::DPadUp).unwrap_or(0.0) - - self.analog.get(GamepadButton::DPadDown).unwrap_or(0.0), + x: self.get(GamepadButton::DPadRight).unwrap_or(0.0) + - self.get(GamepadButton::DPadLeft).unwrap_or(0.0), + y: self.get(GamepadButton::DPadUp).unwrap_or(0.0) + - self.get(GamepadButton::DPadDown).unwrap_or(0.0), } } + + /// Returns `true` if the [`GamepadButton`] has been pressed. + pub fn pressed(&self, button_type: GamepadButton) -> bool { + self.digital.pressed(button_type) + } + + /// Returns `true` if any item in the [`GamepadButton`] iterator has been pressed. + pub fn any_pressed(&self, button_inputs: impl IntoIterator) -> bool { + self.digital.any_pressed(button_inputs) + } + + /// Returns `true` if all items in the [`GamepadButton`] iterator have been pressed. + pub fn all_pressed(&self, button_inputs: impl IntoIterator) -> bool { + self.digital.all_pressed(button_inputs) + } + + /// Returns `true` if the [`GamepadButton`] 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, button_type: GamepadButton) -> bool { + self.digital.just_pressed(button_type) + } + + /// Returns `true` if any item in the [`GamepadButton`] iterator has been pressed during the current frame. + pub fn any_just_pressed(&self, button_inputs: impl IntoIterator) -> bool { + self.digital.any_just_pressed(button_inputs) + } + + /// Returns `true` if all items in the [`GamepadButton`] iterator have been just pressed. + pub fn all_just_pressed(&self, button_inputs: impl IntoIterator) -> bool { + self.digital.all_just_pressed(button_inputs) + } + + /// Returns `true` if the [`GamepadButton`] 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, button_type: GamepadButton) -> bool { + self.digital.just_released(button_type) + } + + /// Returns `true` if any item in the [`GamepadButton`] iterator has just been released. + pub fn any_just_released( + &self, + button_inputs: impl IntoIterator, + ) -> bool { + self.digital.any_just_released(button_inputs) + } + + /// Returns `true` if all items in the [`GamepadButton`] iterator have just been released. + pub fn all_just_released( + &self, + button_inputs: impl IntoIterator, + ) -> bool { + self.digital.all_just_released(button_inputs) + } + + /// Returns an iterator over all digital [button]s that are pressed. + /// + /// [button]: GamepadButton + pub fn get_pressed(&self) -> impl Iterator { + self.digital.get_pressed() + } + + /// Returns an iterator over all digital [button]s that were just pressed. + /// + /// [button]: GamepadButton + pub fn get_just_pressed(&self) -> impl Iterator { + self.digital.get_just_pressed() + } + + /// Returns an iterator over all digital [button]s that were just released. + /// + /// [button]: GamepadButton + pub fn get_just_released(&self) -> impl Iterator { + self.digital.get_just_released() + } + + /// Returns an iterator over all analog [axes]. + /// + /// [axes]: GamepadInput + pub fn get_analog_axes(&self) -> impl Iterator { + self.analog.all_axes() + } + + /// [`ButtonInput`] of [`GamepadButton`] representing their digital state + pub fn digital(&self) -> &ButtonInput { + &self.digital + } + + /// Mutable [`ButtonInput`] of [`GamepadButton`] representing their digital state. Useful for mocking inputs. + pub fn digital_mut(&mut self) -> &mut ButtonInput { + &mut self.digital + } + + /// [`Axis`] of [`GamepadButton`] representing their analog state. + pub fn analog(&self) -> &Axis { + &self.analog + } + + /// Mutable [`Axis`] of [`GamepadButton`] representing their analog state. Useful for mocking inputs. + pub fn analog_mut(&mut self) -> &mut Axis { + &mut self.analog + } } impl Default for Gamepad { @@ -1307,7 +1436,7 @@ pub fn gamepad_event_processing_system( }; let Some(filtered_value) = gamepad_settings .get_axis_settings(axis) - .filter(value, gamepad_axis.analog.get(axis)) + .filter(value, gamepad_axis.get(axis)) else { continue; }; @@ -1328,7 +1457,7 @@ pub fn gamepad_event_processing_system( }; let Some(filtered_value) = settings .get_button_axis_settings(button) - .filter(value, gamepad_buttons.analog.get(button)) + .filter(value, gamepad_buttons.get(button)) else { continue; }; @@ -1337,7 +1466,7 @@ pub fn gamepad_event_processing_system( if button_settings.is_released(filtered_value) { // Check if button was previously pressed - if gamepad_buttons.digital.pressed(button) { + if gamepad_buttons.pressed(button) { processed_digital_events.send(GamepadButtonStateChangedEvent::new( gamepad, button, @@ -1349,7 +1478,7 @@ pub fn gamepad_event_processing_system( gamepad_buttons.digital.release(button); } else if button_settings.is_pressed(filtered_value) { // Check if button was previously not pressed - if !gamepad_buttons.digital.pressed(button) { + if !gamepad_buttons.pressed(button) { processed_digital_events.send(GamepadButtonStateChangedEvent::new( gamepad, button, @@ -2403,8 +2532,13 @@ mod tests { assert_eq!(event.button, GamepadButton::DPadDown); assert_eq!(event.state, ButtonState::Pressed); } - let gamepad = ctx.app.world_mut().get::(entity).unwrap(); - assert!(gamepad.digital.pressed(GamepadButton::DPadDown)); + assert!(ctx + .app + .world_mut() + .query::<&Gamepad>() + .get(ctx.app.world(), entity) + .unwrap() + .pressed(GamepadButton::DPadDown)); ctx.app .world_mut() @@ -2419,8 +2553,13 @@ mod tests { .len(), 0 ); - let gamepad = ctx.app.world_mut().get::(entity).unwrap(); - assert!(gamepad.digital.pressed(GamepadButton::DPadDown)); + assert!(ctx + .app + .world_mut() + .query::<&Gamepad>() + .get(ctx.app.world(), entity) + .unwrap() + .pressed(GamepadButton::DPadDown)); } #[test] @@ -2439,13 +2578,23 @@ mod tests { ctx.update(); // Check it is flagged for this frame - let gamepad = ctx.app.world_mut().get::(entity).unwrap(); - assert!(gamepad.digital.just_pressed(GamepadButton::DPadDown)); + assert!(ctx + .app + .world_mut() + .query::<&Gamepad>() + .get(ctx.app.world(), entity) + .unwrap() + .just_pressed(GamepadButton::DPadDown)); ctx.update(); //Check it clears next frame - let gamepad = ctx.app.world_mut().get::(entity).unwrap(); - assert!(!gamepad.digital.just_pressed(GamepadButton::DPadDown)); + assert!(!ctx + .app + .world_mut() + .query::<&Gamepad>() + .get(ctx.app.world(), entity) + .unwrap() + .just_pressed(GamepadButton::DPadDown)); } #[test] fn gamepad_buttons_released() { @@ -2488,8 +2637,13 @@ mod tests { assert_eq!(event.button, GamepadButton::DPadDown); assert_eq!(event.state, ButtonState::Released); } - let gamepad = ctx.app.world_mut().get::(entity).unwrap(); - assert!(!gamepad.digital.pressed(GamepadButton::DPadDown)); + assert!(!ctx + .app + .world_mut() + .query::<&Gamepad>() + .get(ctx.app.world(), entity) + .unwrap() + .pressed(GamepadButton::DPadDown)); ctx.app .world_mut() .resource_mut::>() @@ -2528,13 +2682,23 @@ mod tests { ctx.update(); // Check it is flagged for this frame - let gamepad = ctx.app.world_mut().get::(entity).unwrap(); - assert!(gamepad.digital.just_released(GamepadButton::DPadDown)); + assert!(ctx + .app + .world_mut() + .query::<&Gamepad>() + .get(ctx.app.world(), entity) + .unwrap() + .just_released(GamepadButton::DPadDown)); ctx.update(); - // Check it clears next frame - let gamepad = ctx.app.world_mut().get::(entity).unwrap(); - assert!(!gamepad.digital.just_released(GamepadButton::DPadDown)); + //Check it clears next frame + assert!(!ctx + .app + .world_mut() + .query::<&Gamepad>() + .get(ctx.app.world(), entity) + .unwrap() + .just_released(GamepadButton::DPadDown)); } #[test] diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 74267a4797356..fc1d9a3d5d9d5 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -24,7 +24,7 @@ trace = [ trace_chrome = ["bevy_log/tracing-chrome"] trace_tracy = ["bevy_render?/tracing-tracy", "bevy_log/tracing-tracy"] trace_tracy_memory = ["bevy_log/trace_tracy_memory"] -detailed_trace = ["bevy_utils/detailed_trace"] +detailed_trace = ["bevy_ecs/detailed_trace", "bevy_render?/detailed_trace"] sysinfo_plugin = ["bevy_diagnostic/sysinfo_plugin"] @@ -97,7 +97,7 @@ serialize = [ "bevy_time/serialize", "bevy_transform/serialize", "bevy_ui?/serialize", - "bevy_window/serialize", + "bevy_window?/serialize", "bevy_winit?/serialize", ] multi_threaded = [ @@ -135,7 +135,7 @@ pbr_anisotropy_texture = [ ] # Percentage-closer soft shadows -pbr_pcss = ["bevy_pbr?/pbr_pcss"] +experimental_pbr_pcss = ["bevy_pbr?/experimental_pbr_pcss"] # Optimise for WebGL2 webgl = [ @@ -162,6 +162,7 @@ animation = ["bevy_animation", "bevy_gltf?/bevy_animation"] bevy_sprite = ["dep:bevy_sprite", "bevy_gizmos?/bevy_sprite"] bevy_pbr = ["dep:bevy_pbr", "bevy_gizmos?/bevy_pbr"] +bevy_window = ["dep:bevy_window", "dep:bevy_a11y"] # Used to disable code that is unsupported when Bevy is dynamically linked dynamic_linking = ["bevy_diagnostic/dynamic_linking"] @@ -173,7 +174,7 @@ android_shared_stdcxx = ["bevy_audio/android_shared_stdcxx"] # screen readers and forks.) accesskit_unix = ["bevy_winit/accesskit_unix"] -bevy_text = ["dep:bevy_text", "bevy_ui?/bevy_text"] +bevy_text = ["dep:bevy_text"] bevy_render = [ "dep:bevy_render", @@ -217,13 +218,23 @@ bevy_dev_tools = ["dep:bevy_dev_tools"] bevy_remote = ["dep:bevy_remote"] # Provides picking functionality -bevy_picking = [ - "dep:bevy_picking", - "bevy_picking/bevy_mesh", - "bevy_ui?/bevy_picking", - "bevy_sprite?/bevy_picking", +bevy_picking = ["dep:bevy_picking"] + +# Provides a mesh picking backend +bevy_mesh_picking_backend = [ + "bevy_picking", + "bevy_picking/bevy_mesh_picking_backend", +] + +# Provides a sprite picking backend +bevy_sprite_picking_backend = [ + "bevy_picking", + "bevy_sprite/bevy_sprite_picking_backend", ] +# Provides a UI picking backend +bevy_ui_picking_backend = ["bevy_picking", "bevy_ui/bevy_ui_picking_backend"] + # Enable support for the ios_simulator by downgrading some rendering capabilities ios_simulator = ["bevy_pbr?/ios_simulator", "bevy_render?/ios_simulator"] @@ -248,7 +259,7 @@ ghost_nodes = ["bevy_ui/ghost_nodes"] [dependencies] # bevy -bevy_a11y = { path = "../bevy_a11y", version = "0.15.0-dev" } +bevy_a11y = { path = "../bevy_a11y", version = "0.15.0-dev", optional = true } bevy_app = { path = "../bevy_app", version = "0.15.0-dev" } bevy_core = { path = "../bevy_core", version = "0.15.0-dev" } bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" } @@ -266,7 +277,7 @@ bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [ bevy_time = { path = "../bevy_time", version = "0.15.0-dev" } bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" } bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } -bevy_window = { path = "../bevy_window", version = "0.15.0-dev" } +bevy_window = { path = "../bevy_window", version = "0.15.0-dev", optional = true } bevy_tasks = { path = "../bevy_tasks", version = "0.15.0-dev" } # bevy (optional) bevy_animation = { path = "../bevy_animation", optional = true, version = "0.15.0-dev" } diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index 41754882c926a..dee5e5960f3d2 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -13,7 +13,11 @@ plugin_group! { bevy_hierarchy:::HierarchyPlugin, bevy_diagnostic:::DiagnosticsPlugin, bevy_input:::InputPlugin, + #[custom(cfg(not(feature = "bevy_window")))] + bevy_app:::ScheduleRunnerPlugin, + #[cfg(feature = "bevy_window")] bevy_window:::WindowPlugin, + #[cfg(feature = "bevy_window")] bevy_a11y:::AccessibilityPlugin, #[custom(cfg(not(target_arch = "wasm32")))] bevy_app:::TerminalCtrlCHandlerPlugin, @@ -72,55 +76,6 @@ plugin_group! { /// /// [`DefaultPlugins`] contains all the plugins typically required to build /// a *Bevy* application which includes a *window* and presentation components. - /// For *headless* cases – without a *window* or presentation, see [`HeadlessPlugins`]. - /// For the absolute minimum number of plugins needed to run a Bevy application, see [`MinimalPlugins`]. -} - -plugin_group! { - /// This plugin group will add all the default plugins for a headless (no *window* or rendering) *Bevy* application: - pub struct HeadlessPlugins { - bevy_app:::PanicHandlerPlugin, - bevy_log:::LogPlugin, - bevy_core:::TaskPoolPlugin, - bevy_core:::TypeRegistrationPlugin, - bevy_core:::FrameCountPlugin, - bevy_time:::TimePlugin, - bevy_transform:::TransformPlugin, - bevy_hierarchy:::HierarchyPlugin, - bevy_diagnostic:::DiagnosticsPlugin, - bevy_app:::ScheduleRunnerPlugin, - #[custom(cfg(not(target_arch = "wasm32")))] - bevy_app:::TerminalCtrlCHandlerPlugin, - #[cfg(feature = "bevy_asset")] - bevy_asset:::AssetPlugin, - #[cfg(feature = "bevy_scene")] - bevy_scene:::ScenePlugin, - #[cfg(feature = "bevy_animation")] - bevy_animation:::AnimationPlugin, - #[cfg(feature = "bevy_state")] - bevy_state::app:::StatesPlugin, - #[cfg(feature = "bevy_ci_testing")] - bevy_dev_tools::ci_testing:::CiTestingPlugin, - #[doc(hidden)] - :IgnoreAmbiguitiesPlugin, - } - /// This group of plugins is intended for use for *headless* programs, for example: dedicated game servers. - /// See the [*Bevy* *headless* example](https://github.com/bevyengine/bevy/blob/main/examples/app/headless.rs) - /// - /// [`HeadlessPlugins`] obeys *Cargo* *feature* flags. Users may exert control over this plugin group - /// by disabling `default-features` in their `Cargo.toml` and enabling only those features - /// that they wish to use. - /// - /// [`HeadlessPlugins`] contains all the plugins typically required to build - /// a *Bevy* application. In contrast with [`DefaultPlugins`], it leaves out *window* and presentation components. - /// This allows applications built using this plugin group to run on devices that do not have a screen or rendering - /// capabilities. - /// It includes a [schedule runner (`ScheduleRunnerPlugin`)](crate::app::ScheduleRunnerPlugin) - /// to provide functionality that would otherwise be driven by a windowed application's - /// *event loop* or *message loop*. - /// - /// Windowed applications that wish to use a reduced set of plugins should consider the - /// [`DefaultPlugins`] plugin group which can be controlled with *Cargo* *feature* flags. /// For the absolute minimum number of plugins needed to run a Bevy application, see [`MinimalPlugins`]. } @@ -162,8 +117,6 @@ plugin_group! { } /// This plugin group represents the absolute minimum, bare-bones, bevy application. /// Use this if you want to have absolute control over the plugins used. - /// If you are looking to make a *headless* application - without a *window* or rendering, - /// it is usually best to use [`HeadlessPlugins`]. /// /// It includes a [schedule runner (`ScheduleRunnerPlugin`)](crate::app::ScheduleRunnerPlugin) /// to provide functionality that would otherwise be driven by a windowed application's diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index bc553af972223..dd7c3517732d7 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -13,6 +13,7 @@ pub mod prelude; mod default_plugins; pub use default_plugins::*; +#[cfg(feature = "bevy_window")] pub use bevy_a11y as a11y; #[cfg(feature = "bevy_animation")] pub use bevy_animation as animation; @@ -37,6 +38,8 @@ pub use bevy_gizmos as gizmos; #[cfg(feature = "bevy_gltf")] pub use bevy_gltf as gltf; pub use bevy_hierarchy as hierarchy; +#[cfg(feature = "bevy_image")] +pub use bevy_image as image; pub use bevy_input as input; pub use bevy_log as log; pub use bevy_math as math; @@ -64,6 +67,7 @@ pub use bevy_transform as transform; #[cfg(feature = "bevy_ui")] pub use bevy_ui as ui; pub use bevy_utils as utils; +#[cfg(feature = "bevy_window")] pub use bevy_window as window; #[cfg(feature = "bevy_winit")] pub use bevy_winit as winit; diff --git a/crates/bevy_internal/src/prelude.rs b/crates/bevy_internal/src/prelude.rs index bec80694fd490..a523488f65309 100644 --- a/crates/bevy_internal/src/prelude.rs +++ b/crates/bevy_internal/src/prelude.rs @@ -2,10 +2,17 @@ pub use crate::{ app::prelude::*, core::prelude::*, ecs::prelude::*, hierarchy::prelude::*, input::prelude::*, log::prelude::*, math::prelude::*, reflect::prelude::*, time::prelude::*, - transform::prelude::*, utils::prelude::*, window::prelude::*, DefaultPlugins, HeadlessPlugins, - MinimalPlugins, + transform::prelude::*, utils::prelude::*, DefaultPlugins, MinimalPlugins, }; +#[doc(hidden)] +#[cfg(feature = "bevy_window")] +pub use crate::window::prelude::*; + +#[doc(hidden)] +#[cfg(feature = "bevy_image")] +pub use crate::image::prelude::*; + pub use bevy_derive::{bevy_main, Deref, DerefMut}; #[doc(hidden)] diff --git a/crates/bevy_math/Cargo.toml b/crates/bevy_math/Cargo.toml index 74ed8dee85abc..9da2e9c045d51 100644 --- a/crates/bevy_math/Cargo.toml +++ b/crates/bevy_math/Cargo.toml @@ -7,23 +7,23 @@ homepage = "https://bevyengine.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] -rust-version = "1.68.2" +rust-version = "1.81.0" [dependencies] -glam = { version = "0.29", features = ["bytemuck"] } +glam = { version = "0.29", default-features = false, features = ["bytemuck"] } derive_more = { version = "1", default-features = false, features = [ "error", "from", "display", "into", ] } -itertools = "0.13.0" -serde = { version = "1", features = ["derive"], optional = true } +itertools = { version = "0.13.0", default-features = false } +serde = { version = "1", default-features = false, 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 } +approx = { version = "0.5", default-features = false, optional = true } +rand = { version = "0.8", default-features = false, optional = true } rand_distr = { version = "0.4.3", optional = true } smallvec = { version = "1.11" } bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [ @@ -36,11 +36,30 @@ approx = "0.5" rand = "0.8" rand_chacha = "0.3" # Enable the approx feature when testing. -bevy_math = { path = ".", version = "0.15.0-dev", features = ["approx"] } -glam = { version = "0.29", features = ["approx"] } +bevy_math = { path = ".", version = "0.15.0-dev", default-features = false, features = [ + "approx", +] } +glam = { version = "0.29", default-features = false, features = ["approx"] } [features] -default = ["rand", "bevy_reflect", "curve"] +default = ["std", "rand", "bevy_reflect", "curve"] +std = [ + "alloc", + "glam/std", + "derive_more/std", + "itertools/use_std", + "serde?/std", + "approx?/std", + "rand?/std", + "rand_distr?/std", +] +alloc = [ + "itertools/use_alloc", + "serde?/alloc", + "rand?/alloc", + "rand_distr?/alloc", +] + serialize = ["dep:serde", "glam/serde"] # Enable approx for glam types to approximate floating point equality comparisons and assertions approx = ["dep:approx", "glam/approx"] @@ -57,6 +76,8 @@ debug_glam_assert = ["glam/debug-glam-assert"] rand = ["dep:rand", "dep:rand_distr", "glam/rand"] # Include code related to the Curve trait curve = [] +# Enable bevy_reflect (requires std) +bevy_reflect = ["dep:bevy_reflect", "std"] [lints] workspace = true diff --git a/crates/bevy_math/clippy.toml b/crates/bevy_math/clippy.toml index bad8801c01736..0fb122e4dcef6 100644 --- a/crates/bevy_math/clippy.toml +++ b/crates/bevy_math/clippy.toml @@ -26,4 +26,13 @@ disallowed-methods = [ { path = "f32::asinh", reason = "use ops::asinh instead for libm determinism" }, { path = "f32::acosh", reason = "use ops::acosh instead for libm determinism" }, { path = "f32::atanh", reason = "use ops::atanh instead for libm determinism" }, + # These methods have defined precision, but are only available from the standard library, + # not in core. Using these substitutes allows for no_std compatibility. + { path = "f32::rem_euclid", reason = "use ops::rem_euclid instead for no_std compatibility" }, + { path = "f32::abs", reason = "use ops::abs instead for no_std compatibility" }, + { path = "f32::sqrt", reason = "use ops::sqrt instead for no_std compatibility" }, + { path = "f32::copysign", reason = "use ops::copysign instead for no_std compatibility" }, + { path = "f32::round", reason = "use ops::round instead for no_std compatibility" }, + { path = "f32::floor", reason = "use ops::floor instead for no_std compatibility" }, + { path = "f32::fract", reason = "use ops::fract instead for no_std compatibility" }, ] diff --git a/crates/bevy_math/src/aspect_ratio.rs b/crates/bevy_math/src/aspect_ratio.rs index a318ff0296b1c..1a07de91d3438 100644 --- a/crates/bevy_math/src/aspect_ratio.rs +++ b/crates/bevy_math/src/aspect_ratio.rs @@ -45,31 +45,31 @@ impl AspectRatio { /// Returns the aspect ratio as a f32 value. #[inline] - pub fn ratio(&self) -> f32 { + pub const fn ratio(&self) -> f32 { self.0 } /// Returns the inverse of this aspect ratio (height/width). #[inline] - pub fn inverse(&self) -> Self { + pub const fn inverse(&self) -> Self { Self(1.0 / self.0) } /// Returns true if the aspect ratio represents a landscape orientation. #[inline] - pub fn is_landscape(&self) -> bool { + pub const fn is_landscape(&self) -> bool { self.0 > 1.0 } /// Returns true if the aspect ratio represents a portrait orientation. #[inline] - pub fn is_portrait(&self) -> bool { + pub const fn is_portrait(&self) -> bool { self.0 < 1.0 } /// Returns true if the aspect ratio is exactly square. #[inline] - pub fn is_square(&self) -> bool { + pub const fn is_square(&self) -> bool { self.0 == 1.0 } } diff --git a/crates/bevy_math/src/bounding/bounded2d/mod.rs b/crates/bevy_math/src/bounding/bounded2d/mod.rs index 7e824101bc790..c5be831a86f86 100644 --- a/crates/bevy_math/src/bounding/bounded2d/mod.rs +++ b/crates/bevy_math/src/bounding/bounded2d/mod.rs @@ -2,6 +2,7 @@ mod primitive_impls; use super::{BoundingVolume, IntersectsVolume}; use crate::{ + ops, prelude::{Mat2, Rot2, Vec2}, FloatPow, Isometry2d, }; @@ -296,12 +297,12 @@ mod aabb2d_tests { min: Vec2::new(-1., -1.), max: Vec2::new(1., 1.), }; - assert!((aabb.visible_area() - 4.).abs() < f32::EPSILON); + assert!(ops::abs(aabb.visible_area() - 4.) < f32::EPSILON); let aabb = Aabb2d { min: Vec2::new(0., 0.), max: Vec2::new(1., 0.5), }; - assert!((aabb.visible_area() - 0.5).abs() < f32::EPSILON); + assert!(ops::abs(aabb.visible_area() - 0.5) < f32::EPSILON); } #[test] @@ -488,7 +489,7 @@ impl BoundingCircle { } } - BoundingCircle::new(isometry * center, radius_squared.sqrt()) + BoundingCircle::new(isometry * center, ops::sqrt(radius_squared)) } /// Get the radius of the bounding circle @@ -539,7 +540,7 @@ impl BoundingVolume for BoundingCircle { #[inline(always)] fn contains(&self, other: &Self) -> bool { let diff = self.radius() - other.radius(); - self.center.distance_squared(other.center) <= diff.squared().copysign(diff) + self.center.distance_squared(other.center) <= ops::copysign(diff.squared(), diff) } #[inline(always)] @@ -614,14 +615,14 @@ mod bounding_circle_tests { use super::BoundingCircle; use crate::{ bounding::{BoundingVolume, IntersectsVolume}, - Vec2, + ops, Vec2, }; #[test] fn area() { let circle = BoundingCircle::new(Vec2::ONE, 5.); // Since this number is messy we check it with a higher threshold - assert!((circle.visible_area() - 78.5398).abs() < 0.001); + assert!(ops::abs(circle.visible_area() - 78.5398) < 0.001); } #[test] @@ -647,7 +648,7 @@ mod bounding_circle_tests { let b = BoundingCircle::new(Vec2::new(1., -4.), 1.); let merged = a.merge(&b); assert!((merged.center - Vec2::new(1., 0.5)).length() < f32::EPSILON); - assert!((merged.radius() - 5.5).abs() < f32::EPSILON); + assert!(ops::abs(merged.radius() - 5.5) < f32::EPSILON); assert!(merged.contains(&a)); assert!(merged.contains(&b)); assert!(!a.contains(&merged)); @@ -679,7 +680,7 @@ mod bounding_circle_tests { fn grow() { let a = BoundingCircle::new(Vec2::ONE, 5.); let padded = a.grow(1.25); - assert!((padded.radius() - 6.25).abs() < f32::EPSILON); + assert!(ops::abs(padded.radius() - 6.25) < f32::EPSILON); assert!(padded.contains(&a)); assert!(!a.contains(&padded)); } @@ -688,7 +689,7 @@ mod bounding_circle_tests { fn shrink() { let a = BoundingCircle::new(Vec2::ONE, 5.); let shrunk = a.shrink(0.5); - assert!((shrunk.radius() - 4.5).abs() < f32::EPSILON); + assert!(ops::abs(shrunk.radius() - 4.5) < f32::EPSILON); assert!(a.contains(&shrunk)); assert!(!shrunk.contains(&a)); } @@ -697,7 +698,7 @@ mod bounding_circle_tests { fn scale_around_center() { let a = BoundingCircle::new(Vec2::ONE, 5.); let scaled = a.scale_around_center(2.); - assert!((scaled.radius() - 10.).abs() < f32::EPSILON); + assert!(ops::abs(scaled.radius() - 10.) < f32::EPSILON); assert!(!a.contains(&scaled)); assert!(scaled.contains(&a)); } diff --git a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs index d4d211cd127a5..aeee66f51e919 100644 --- a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs +++ b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs @@ -3,14 +3,16 @@ use crate::{ ops, primitives::{ - Annulus, Arc2d, BoxedPolygon, BoxedPolyline2d, Capsule2d, Circle, CircularSector, - CircularSegment, Ellipse, Line2d, Plane2d, Polygon, Polyline2d, Rectangle, RegularPolygon, - Rhombus, Segment2d, Triangle2d, + Annulus, Arc2d, Capsule2d, Circle, CircularSector, CircularSegment, Ellipse, Line2d, + Plane2d, Polygon, Polyline2d, Rectangle, RegularPolygon, Rhombus, Segment2d, Triangle2d, }, Dir2, Isometry2d, Mat2, Rot2, Vec2, }; use core::f32::consts::{FRAC_PI_2, PI, TAU}; +#[cfg(feature = "alloc")] +use crate::primitives::{BoxedPolygon, BoxedPolyline2d}; + use smallvec::SmallVec; use super::{Aabb2d, Bounded2d, BoundingCircle}; @@ -41,11 +43,11 @@ fn arc_bounding_points(arc: Arc2d, rotation: impl Into) -> SmallVec<[Vec2; // The half-angles are measured from a starting point of π/2, being the angle of Vec2::Y. // Compute the normalized angles of the endpoints with the rotation taken into account, and then // check if we are looking for an angle that is between or outside them. - let left_angle = (FRAC_PI_2 + arc.half_angle + rotation.as_radians()).rem_euclid(TAU); - let right_angle = (FRAC_PI_2 - arc.half_angle + rotation.as_radians()).rem_euclid(TAU); + let left_angle = ops::rem_euclid(FRAC_PI_2 + arc.half_angle + rotation.as_radians(), TAU); + let right_angle = ops::rem_euclid(FRAC_PI_2 - arc.half_angle + rotation.as_radians(), TAU); let inverted = left_angle < right_angle; for extremum in [Vec2::X, Vec2::Y, Vec2::NEG_X, Vec2::NEG_Y] { - let angle = extremum.to_angle().rem_euclid(TAU); + let angle = ops::rem_euclid(extremum.to_angle(), TAU); // If inverted = true, then right_angle > left_angle, so we are looking for an angle that is not between them. // There's a chance that this condition fails due to rounding error, if the endpoint angle is juuuust shy of the axis. // But in that case, the endpoint itself is within rounding error of the axis and will define the bounds just fine. @@ -286,6 +288,7 @@ impl Bounded2d for Polyline2d { } } +#[cfg(feature = "alloc")] impl Bounded2d for BoxedPolyline2d { fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { Aabb2d::from_point_cloud(isometry, &self.vertices) @@ -348,7 +351,8 @@ impl Bounded2d for Rectangle { // Compute the AABB of the rotated rectangle by transforming the half-extents // by an absolute rotation matrix. let (sin, cos) = isometry.rotation.sin_cos(); - let abs_rot_mat = Mat2::from_cols_array(&[cos.abs(), sin.abs(), sin.abs(), cos.abs()]); + let abs_rot_mat = + Mat2::from_cols_array(&[ops::abs(cos), ops::abs(sin), ops::abs(sin), ops::abs(cos)]); let half_size = abs_rot_mat * self.half_size; Aabb2d::new(isometry.translation, half_size) @@ -371,6 +375,7 @@ impl Bounded2d for Polygon { } } +#[cfg(feature = "alloc")] impl Bounded2d for BoxedPolygon { fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { Aabb2d::from_point_cloud(isometry, &self.vertices) @@ -470,6 +475,7 @@ mod tests { // Arcs and circular segments have the same bounding shapes so they share test cases. fn arc_and_segment() { struct TestCase { + #[allow(unused)] name: &'static str, arc: Arc2d, translation: Vec2, @@ -487,7 +493,7 @@ mod tests { } // The apothem of an arc covering 1/6th of a circle. - let apothem = f32::sqrt(3.0) / 2.0; + let apothem = ops::sqrt(3.0) / 2.0; let tests = [ // Test case: a basic minor arc TestCase { @@ -558,7 +564,7 @@ mod tests { aabb_min: Vec2::ZERO, aabb_max: Vec2::splat(1.0), bounding_circle_center: Vec2::splat(0.5), - bounding_circle_radius: f32::sqrt(2.0) / 2.0, + bounding_circle_radius: ops::sqrt(2.0) / 2.0, }, // Test case: a basic major arc TestCase { @@ -597,6 +603,7 @@ mod tests { ]; for test in tests { + #[cfg(feature = "std")] println!("subtest case: {}", test.name); let segment: CircularSegment = test.arc.into(); @@ -622,6 +629,7 @@ mod tests { #[test] fn circular_sector() { struct TestCase { + #[allow(unused)] name: &'static str, arc: Arc2d, translation: Vec2, @@ -639,8 +647,8 @@ mod tests { } // The apothem of an arc covering 1/6th of a circle. - let apothem = f32::sqrt(3.0) / 2.0; - let inv_sqrt_3 = f32::sqrt(3.0).recip(); + let apothem = ops::sqrt(3.0) / 2.0; + let inv_sqrt_3 = ops::sqrt(3.0).recip(); let tests = [ // Test case: An sector whose arc is minor, but whose bounding circle is not the circumcircle of the endpoints and center TestCase { @@ -717,7 +725,7 @@ mod tests { aabb_min: Vec2::ZERO, aabb_max: Vec2::splat(1.0), bounding_circle_center: Vec2::splat(0.5), - bounding_circle_radius: f32::sqrt(2.0) / 2.0, + bounding_circle_radius: ops::sqrt(2.0) / 2.0, }, TestCase { name: "5/6th circle untransformed", @@ -753,6 +761,7 @@ mod tests { ]; for test in tests { + #[cfg(feature = "std")] println!("subtest case: {}", test.name); let sector: CircularSector = test.arc.into(); diff --git a/crates/bevy_math/src/bounding/bounded3d/extrusion.rs b/crates/bevy_math/src/bounding/bounded3d/extrusion.rs index 8eb3eb9791795..f70101bdd0594 100644 --- a/crates/bevy_math/src/bounding/bounded3d/extrusion.rs +++ b/crates/bevy_math/src/bounding/bounded3d/extrusion.rs @@ -6,12 +6,15 @@ use crate::{ bounding::{BoundingCircle, BoundingVolume}, ops, primitives::{ - BoxedPolygon, BoxedPolyline2d, Capsule2d, Cuboid, Cylinder, Ellipse, Extrusion, Line2d, - Polygon, Polyline2d, Primitive2d, Rectangle, RegularPolygon, Segment2d, Triangle2d, + Capsule2d, Cuboid, Cylinder, Ellipse, Extrusion, Line2d, Polygon, Polyline2d, Primitive2d, + Rectangle, RegularPolygon, Segment2d, Triangle2d, }, Isometry2d, Isometry3d, Quat, Rot2, }; +#[cfg(feature = "alloc")] +use crate::primitives::{BoxedPolygon, BoxedPolyline2d}; + use crate::{bounding::Bounded2d, primitives::Circle}; use super::{Aabb3d, Bounded3d, BoundingSphere}; @@ -26,7 +29,7 @@ impl BoundedExtrusion for Circle { let top = (segment_dir * half_depth).abs(); let e = (Vec3A::ONE - segment_dir * segment_dir).max(Vec3A::ZERO); - let half_size = self.radius * Vec3A::new(e.x.sqrt(), e.y.sqrt(), e.z.sqrt()); + let half_size = self.radius * Vec3A::new(ops::sqrt(e.x), ops::sqrt(e.y), ops::sqrt(e.z)); Aabb3d { min: isometry.translation - half_size - top, @@ -56,8 +59,8 @@ impl BoundedExtrusion for Ellipse { let m = -axis.x / axis.y; let signum = axis.signum(); - let y = signum.y * b * b / (b * b + m * m * a * a).sqrt(); - let x = signum.x * a * (1. - y * y / b / b).sqrt(); + let y = signum.y * b * b / ops::sqrt(b * b + m * m * a * a); + let x = signum.x * a * ops::sqrt(1. - y * y / b / b); isometry.rotation * Vec3A::new(x, y, 0.) }); @@ -104,6 +107,7 @@ impl BoundedExtrusion for Polyline2d { } } +#[cfg(feature = "alloc")] impl BoundedExtrusion for BoxedPolyline2d { fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into) -> Aabb3d { let isometry = isometry.into(); @@ -144,6 +148,7 @@ impl BoundedExtrusion for Polygon { } } +#[cfg(feature = "alloc")] impl BoundedExtrusion for BoxedPolygon { fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into) -> Aabb3d { let isometry = isometry.into(); @@ -301,7 +306,7 @@ mod tests { let bounding_sphere = extrusion.bounding_sphere(isometry); assert_eq!(bounding_sphere.center, translation.into()); - assert_eq!(bounding_sphere.radius(), 8f32.sqrt()); + assert_eq!(bounding_sphere.radius(), ops::sqrt(8f32)); } #[test] @@ -444,7 +449,7 @@ mod tests { let bounding_sphere = extrusion.bounding_sphere(isometry); assert_eq!(bounding_sphere.center, translation.into()); - assert_eq!(bounding_sphere.radius(), 8f32.sqrt()); + assert_eq!(bounding_sphere.radius(), ops::sqrt(8f32)); } #[test] diff --git a/crates/bevy_math/src/bounding/bounded3d/mod.rs b/crates/bevy_math/src/bounding/bounded3d/mod.rs index 38460fc08e09f..c4f3c979f67cb 100644 --- a/crates/bevy_math/src/bounding/bounded3d/mod.rs +++ b/crates/bevy_math/src/bounding/bounded3d/mod.rs @@ -4,7 +4,10 @@ mod primitive_impls; use glam::Mat3; use super::{BoundingVolume, IntersectsVolume}; -use crate::{ops::FloatPow, Isometry3d, Quat, Vec3A}; +use crate::{ + ops::{self, FloatPow}, + Isometry3d, Quat, Vec3A, +}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; @@ -297,12 +300,12 @@ mod aabb3d_tests { min: Vec3A::new(-1., -1., -1.), max: Vec3A::new(1., 1., 1.), }; - assert!((aabb.visible_area() - 12.).abs() < f32::EPSILON); + assert!(ops::abs(aabb.visible_area() - 12.) < f32::EPSILON); let aabb = Aabb3d { min: Vec3A::new(0., 0., 0.), max: Vec3A::new(1., 0.5, 0.25), }; - assert!((aabb.visible_area() - 0.875).abs() < f32::EPSILON); + assert!(ops::abs(aabb.visible_area() - 0.875) < f32::EPSILON); } #[test] @@ -494,7 +497,7 @@ impl BoundingSphere { } } - BoundingSphere::new(isometry * center, radius_squared.sqrt()) + BoundingSphere::new(isometry * center, ops::sqrt(radius_squared)) } /// Get the radius of the bounding sphere @@ -528,7 +531,7 @@ impl BoundingSphere { } else { // The point is outside the sphere. // Find the closest point on the surface of the sphere. - let dir_to_point = point / distance_squared.sqrt(); + let dir_to_point = point / ops::sqrt(distance_squared); self.center + radius * dir_to_point } } @@ -557,7 +560,7 @@ impl BoundingVolume for BoundingSphere { #[inline(always)] fn contains(&self, other: &Self) -> bool { let diff = self.radius() - other.radius(); - self.center.distance_squared(other.center) <= diff.squared().copysign(diff) + self.center.distance_squared(other.center) <= ops::copysign(diff.squared(), diff) } #[inline(always)] @@ -644,14 +647,14 @@ mod bounding_sphere_tests { use super::BoundingSphere; use crate::{ bounding::{BoundingVolume, IntersectsVolume}, - Quat, Vec3, Vec3A, + ops, Quat, Vec3, Vec3A, }; #[test] fn area() { let sphere = BoundingSphere::new(Vec3::ONE, 5.); // Since this number is messy we check it with a higher threshold - assert!((sphere.visible_area() - 157.0796).abs() < 0.001); + assert!(ops::abs(sphere.visible_area() - 157.0796) < 0.001); } #[test] @@ -677,7 +680,7 @@ mod bounding_sphere_tests { let b = BoundingSphere::new(Vec3::new(1., 1., -4.), 1.); let merged = a.merge(&b); assert!((merged.center - Vec3A::new(1., 1., 0.5)).length() < f32::EPSILON); - assert!((merged.radius() - 5.5).abs() < f32::EPSILON); + assert!(ops::abs(merged.radius() - 5.5) < f32::EPSILON); assert!(merged.contains(&a)); assert!(merged.contains(&b)); assert!(!a.contains(&merged)); @@ -709,7 +712,7 @@ mod bounding_sphere_tests { fn grow() { let a = BoundingSphere::new(Vec3::ONE, 5.); let padded = a.grow(1.25); - assert!((padded.radius() - 6.25).abs() < f32::EPSILON); + assert!(ops::abs(padded.radius() - 6.25) < f32::EPSILON); assert!(padded.contains(&a)); assert!(!a.contains(&padded)); } @@ -718,7 +721,7 @@ mod bounding_sphere_tests { fn shrink() { let a = BoundingSphere::new(Vec3::ONE, 5.); let shrunk = a.shrink(0.5); - assert!((shrunk.radius() - 4.5).abs() < f32::EPSILON); + assert!(ops::abs(shrunk.radius() - 4.5) < f32::EPSILON); assert!(a.contains(&shrunk)); assert!(!shrunk.contains(&a)); } @@ -727,7 +730,7 @@ mod bounding_sphere_tests { fn scale_around_center() { let a = BoundingSphere::new(Vec3::ONE, 5.); let scaled = a.scale_around_center(2.); - assert!((scaled.radius() - 10.).abs() < f32::EPSILON); + assert!(ops::abs(scaled.radius() - 10.) < f32::EPSILON); assert!(!a.contains(&scaled)); assert!(scaled.contains(&a)); } diff --git a/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs b/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs index 3871ac07490ae..679d8577f0d41 100644 --- a/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs +++ b/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs @@ -1,17 +1,18 @@ //! Contains [`Bounded3d`] implementations for [geometric primitives](crate::primitives). -use glam::Vec3A; - use crate::{ bounding::{Bounded2d, BoundingCircle}, ops, primitives::{ - BoxedPolyline3d, Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, InfinitePlane3d, - Line3d, Polyline3d, Segment3d, Sphere, Torus, Triangle2d, Triangle3d, + Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, InfinitePlane3d, Line3d, Polyline3d, + Segment3d, Sphere, Torus, Triangle2d, Triangle3d, }, - Isometry2d, Isometry3d, Mat3, Vec2, Vec3, + Isometry2d, Isometry3d, Mat3, Vec2, Vec3, Vec3A, }; +#[cfg(feature = "alloc")] +use crate::primitives::BoxedPolyline3d; + use super::{Aabb3d, Bounded3d, BoundingSphere}; impl Bounded3d for Sphere { @@ -100,6 +101,7 @@ impl Bounded3d for Polyline3d { } } +#[cfg(feature = "alloc")] impl Bounded3d for BoxedPolyline3d { fn aabb_3d(&self, isometry: impl Into) -> Aabb3d { Aabb3d::from_point_cloud(isometry, self.vertices.iter().copied()) @@ -144,7 +146,7 @@ impl Bounded3d for Cylinder { let bottom = -top; let e = (Vec3A::ONE - segment_dir * segment_dir).max(Vec3A::ZERO); - let half_size = self.radius * Vec3A::new(e.x.sqrt(), e.y.sqrt(), e.z.sqrt()); + let half_size = self.radius * Vec3A::new(ops::sqrt(e.x), ops::sqrt(e.y), ops::sqrt(e.z)); Aabb3d { min: isometry.translation + (top - half_size).min(bottom - half_size), @@ -195,7 +197,7 @@ impl Bounded3d for Cone { let bottom = -top; let e = (Vec3A::ONE - segment_dir * segment_dir).max(Vec3A::ZERO); - let half_extents = Vec3A::new(e.x.sqrt(), e.y.sqrt(), e.z.sqrt()); + let half_extents = Vec3A::new(ops::sqrt(e.x), ops::sqrt(e.y), ops::sqrt(e.z)); Aabb3d { min: isometry.translation + top.min(bottom - self.radius * half_extents), @@ -236,7 +238,7 @@ impl Bounded3d for ConicalFrustum { let bottom = -top; let e = (Vec3A::ONE - segment_dir * segment_dir).max(Vec3A::ZERO); - let half_extents = Vec3A::new(e.x.sqrt(), e.y.sqrt(), e.z.sqrt()); + let half_extents = Vec3A::new(ops::sqrt(e.x), ops::sqrt(e.y), ops::sqrt(e.z)); Aabb3d { min: isometry.translation @@ -319,7 +321,8 @@ impl Bounded3d for Torus { // Reference: http://iquilezles.org/articles/diskbbox/ let normal = isometry.rotation * Vec3A::Y; let e = (Vec3A::ONE - normal * normal).max(Vec3A::ZERO); - let disc_half_size = self.major_radius * Vec3A::new(e.x.sqrt(), e.y.sqrt(), e.z.sqrt()); + let disc_half_size = + self.major_radius * Vec3A::new(ops::sqrt(e.x), ops::sqrt(e.y), ops::sqrt(e.z)); // Expand the disc by the minor radius to get the torus half-size let half_size = disc_half_size + Vec3A::splat(self.minor_radius); diff --git a/crates/bevy_math/src/bounding/raycast2d.rs b/crates/bevy_math/src/bounding/raycast2d.rs index 745e53de8267c..3b46bcfba62c3 100644 --- a/crates/bevy_math/src/bounding/raycast2d.rs +++ b/crates/bevy_math/src/bounding/raycast2d.rs @@ -1,5 +1,8 @@ use super::{Aabb2d, BoundingCircle, IntersectsVolume}; -use crate::{ops::FloatPow, Dir2, Ray2d, Vec2}; +use crate::{ + ops::{self, FloatPow}, + Dir2, Ray2d, Vec2, +}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; @@ -77,10 +80,12 @@ impl RayCast2d { let projected = offset.dot(*self.ray.direction); let closest_point = offset - projected * *self.ray.direction; let distance_squared = circle.radius().squared() - closest_point.length_squared(); - if distance_squared < 0. || projected.squared().copysign(-projected) < -distance_squared { + if distance_squared < 0. + || ops::copysign(projected.squared(), -projected) < -distance_squared + { None } else { - let toi = -projected - distance_squared.sqrt(); + let toi = -projected - ops::sqrt(distance_squared); if toi > self.max { None } else { @@ -224,21 +229,21 @@ mod tests { 4.996, ), ] { - let case = format!( - "Case:\n Test: {:?}\n Volume: {:?}\n Expected distance: {:?}", - test, volume, expected_distance + assert!( + test.intersects(volume), + "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}", ); - assert!(test.intersects(volume), "{}", case); let actual_distance = test.circle_intersection_at(volume).unwrap(); assert!( - (actual_distance - expected_distance).abs() < EPSILON, - "{}\n Actual distance: {}", - case, - actual_distance + ops::abs(actual_distance - expected_distance) < EPSILON, + "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}", ); let inverted_ray = RayCast2d::new(test.ray.origin, -test.ray.direction, test.max); - assert!(!inverted_ray.intersects(volume), "{}", case); + assert!( + !inverted_ray.intersects(volume), + "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}", + ); } } @@ -263,9 +268,7 @@ mod tests { ] { assert!( !test.intersects(volume), - "Case:\n Test: {:?}\n Volume: {:?}", - test, - volume, + "Case:\n Test: {test:?}\n Volume: {volume:?}", ); } } @@ -278,14 +281,17 @@ mod tests { for max in &[0., 1., 900.] { let test = RayCast2d::new(*origin, *direction, *max); - let case = format!( - "Case:\n origin: {:?}\n Direction: {:?}\n Max: {}", - origin, direction, max, + assert!( + test.intersects(&volume), + "Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}", ); - assert!(test.intersects(&volume), "{}", case); let actual_distance = test.circle_intersection_at(&volume); - assert_eq!(actual_distance, Some(0.), "{}", case); + assert_eq!( + actual_distance, + Some(0.), + "Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}", + ); } } } @@ -331,21 +337,21 @@ mod tests { 1.414, ), ] { - let case = format!( - "Case:\n Test: {:?}\n Volume: {:?}\n Expected distance: {:?}", - test, volume, expected_distance + assert!( + test.intersects(volume), + "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}", ); - assert!(test.intersects(volume), "{}", case); let actual_distance = test.aabb_intersection_at(volume).unwrap(); assert!( - (actual_distance - expected_distance).abs() < EPSILON, - "{}\n Actual distance: {}", - case, - actual_distance + ops::abs(actual_distance - expected_distance) < EPSILON, + "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}", ); let inverted_ray = RayCast2d::new(test.ray.origin, -test.ray.direction, test.max); - assert!(!inverted_ray.intersects(volume), "{}", case); + assert!( + !inverted_ray.intersects(volume), + "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}", + ); } } @@ -370,9 +376,7 @@ mod tests { ] { assert!( !test.intersects(volume), - "Case:\n Test: {:?}\n Volume: {:?}", - test, - volume, + "Case:\n Test: {test:?}\n Volume: {volume:?}", ); } } @@ -385,14 +389,17 @@ mod tests { for max in &[0., 1., 900.] { let test = RayCast2d::new(*origin, *direction, *max); - let case = format!( - "Case:\n origin: {:?}\n Direction: {:?}\n Max: {}", - origin, direction, max, + assert!( + test.intersects(&volume), + "Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}", ); - assert!(test.intersects(&volume), "{}", case); let actual_distance = test.aabb_intersection_at(&volume); - assert_eq!(actual_distance, Some(0.), "{}", case,); + assert_eq!( + actual_distance, + Some(0.), + "Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}", + ); } } } @@ -441,22 +448,22 @@ mod tests { 3., ), ] { - let case = format!( - "Case:\n Test: {:?}\n Volume: {:?}\n Expected distance: {:?}", - test, volume, expected_distance + assert!( + test.intersects(volume), + "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}", ); - assert!(test.intersects(volume), "{}", case); let actual_distance = test.aabb_collision_at(*volume).unwrap(); assert!( - (actual_distance - expected_distance).abs() < EPSILON, - "{}\n Actual distance: {}", - case, - actual_distance + ops::abs(actual_distance - expected_distance) < EPSILON, + "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}", ); let inverted_ray = RayCast2d::new(test.ray.ray.origin, -test.ray.ray.direction, test.ray.max); - assert!(!inverted_ray.intersects(volume), "{}", case); + assert!( + !inverted_ray.intersects(volume), + "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}", + ); } } @@ -508,22 +515,22 @@ mod tests { 3.677, ), ] { - let case = format!( - "Case:\n Test: {:?}\n Volume: {:?}\n Expected distance: {:?}", - test, volume, expected_distance + assert!( + test.intersects(volume), + "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}", ); - assert!(test.intersects(volume), "{}", case); let actual_distance = test.circle_collision_at(*volume).unwrap(); assert!( - (actual_distance - expected_distance).abs() < EPSILON, - "{}\n Actual distance: {}", - case, - actual_distance + ops::abs(actual_distance - expected_distance) < EPSILON, + "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}", ); let inverted_ray = RayCast2d::new(test.ray.ray.origin, -test.ray.ray.direction, test.ray.max); - assert!(!inverted_ray.intersects(volume), "{}", case); + assert!( + !inverted_ray.intersects(volume), + "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}", + ); } } } diff --git a/crates/bevy_math/src/bounding/raycast3d.rs b/crates/bevy_math/src/bounding/raycast3d.rs index 3a369d9fe142a..bfd5d17a0dd8c 100644 --- a/crates/bevy_math/src/bounding/raycast3d.rs +++ b/crates/bevy_math/src/bounding/raycast3d.rs @@ -1,5 +1,8 @@ use super::{Aabb3d, BoundingSphere, IntersectsVolume}; -use crate::{ops::FloatPow, Dir3A, Ray3d, Vec3A}; +use crate::{ + ops::{self, FloatPow}, + Dir3A, Ray3d, Vec3A, +}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; @@ -74,10 +77,12 @@ impl RayCast3d { let projected = offset.dot(*self.direction); let closest_point = offset - projected * *self.direction; let distance_squared = sphere.radius().squared() - closest_point.length_squared(); - if distance_squared < 0. || projected.squared().copysign(-projected) < -distance_squared { + if distance_squared < 0. + || ops::copysign(projected.squared(), -projected) < -distance_squared + { None } else { - let toi = -projected - distance_squared.sqrt(); + let toi = -projected - ops::sqrt(distance_squared); if toi > self.max { None } else { @@ -236,21 +241,21 @@ mod tests { 4.996, ), ] { - let case = format!( - "Case:\n Test: {:?}\n Volume: {:?}\n Expected distance: {:?}", - test, volume, expected_distance + assert!( + test.intersects(volume), + "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}", ); - assert!(test.intersects(volume), "{}", case); let actual_distance = test.sphere_intersection_at(volume).unwrap(); assert!( - (actual_distance - expected_distance).abs() < EPSILON, - "{}\n Actual distance: {}", - case, - actual_distance + ops::abs(actual_distance - expected_distance) < EPSILON, + "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}", ); let inverted_ray = RayCast3d::new(test.origin, -test.direction, test.max); - assert!(!inverted_ray.intersects(volume), "{}", case); + assert!( + !inverted_ray.intersects(volume), + "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}", + ); } } @@ -275,9 +280,7 @@ mod tests { ] { assert!( !test.intersects(volume), - "Case:\n Test: {:?}\n Volume: {:?}", - test, - volume, + "Case:\n Test: {test:?}\n Volume: {volume:?}", ); } } @@ -290,14 +293,17 @@ mod tests { for max in &[0., 1., 900.] { let test = RayCast3d::new(*origin, *direction, *max); - let case = format!( - "Case:\n origin: {:?}\n Direction: {:?}\n Max: {}", - origin, direction, max, + assert!( + test.intersects(&volume), + "Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}", ); - assert!(test.intersects(&volume), "{}", case); let actual_distance = test.sphere_intersection_at(&volume); - assert_eq!(actual_distance, Some(0.), "{}", case,); + assert_eq!( + actual_distance, + Some(0.), + "Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}", + ); } } } @@ -343,21 +349,21 @@ mod tests { 1.732, ), ] { - let case = format!( - "Case:\n Test: {:?}\n Volume: {:?}\n Expected distance: {:?}", - test, volume, expected_distance + assert!( + test.intersects(volume), + "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}", ); - assert!(test.intersects(volume), "{}", case); let actual_distance = test.aabb_intersection_at(volume).unwrap(); assert!( - (actual_distance - expected_distance).abs() < EPSILON, - "{}\n Actual distance: {}", - case, - actual_distance + ops::abs(actual_distance - expected_distance) < EPSILON, + "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}", ); let inverted_ray = RayCast3d::new(test.origin, -test.direction, test.max); - assert!(!inverted_ray.intersects(volume), "{}", case); + assert!( + !inverted_ray.intersects(volume), + "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}", + ); } } @@ -382,9 +388,7 @@ mod tests { ] { assert!( !test.intersects(volume), - "Case:\n Test: {:?}\n Volume: {:?}", - test, - volume, + "Case:\n Test: {test:?}\n Volume: {volume:?}", ); } } @@ -397,14 +401,17 @@ mod tests { for max in &[0., 1., 900.] { let test = RayCast3d::new(*origin, *direction, *max); - let case = format!( - "Case:\n origin: {:?}\n Direction: {:?}\n Max: {}", - origin, direction, max, + assert!( + test.intersects(&volume), + "Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}", ); - assert!(test.intersects(&volume), "{}", case); let actual_distance = test.aabb_intersection_at(&volume); - assert_eq!(actual_distance, Some(0.), "{}", case,); + assert_eq!( + actual_distance, + Some(0.), + "Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}", + ); } } } @@ -453,21 +460,21 @@ mod tests { 3., ), ] { - let case = format!( - "Case:\n Test: {:?}\n Volume: {:?}\n Expected distance: {:?}", - test, volume, expected_distance + assert!( + test.intersects(volume), + "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}", ); - assert!(test.intersects(volume), "{}", case); let actual_distance = test.aabb_collision_at(*volume).unwrap(); assert!( - (actual_distance - expected_distance).abs() < EPSILON, - "{}\n Actual distance: {}", - case, - actual_distance + ops::abs(actual_distance - expected_distance) < EPSILON, + "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}", ); let inverted_ray = RayCast3d::new(test.ray.origin, -test.ray.direction, test.ray.max); - assert!(!inverted_ray.intersects(volume), "{}", case); + assert!( + !inverted_ray.intersects(volume), + "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}", + ); } } @@ -519,21 +526,21 @@ mod tests { 3.677, ), ] { - let case = format!( - "Case:\n Test: {:?}\n Volume: {:?}\n Expected distance: {:?}", - test, volume, expected_distance + assert!( + test.intersects(volume), + "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}", ); - assert!(test.intersects(volume), "{}", case); let actual_distance = test.sphere_collision_at(*volume).unwrap(); assert!( - (actual_distance - expected_distance).abs() < EPSILON, - "{}\n Actual distance: {}", - case, - actual_distance + ops::abs(actual_distance - expected_distance) < EPSILON, + "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}", ); let inverted_ray = RayCast3d::new(test.ray.origin, -test.ray.direction, test.ray.max); - assert!(!inverted_ray.intersects(volume), "{}", case); + assert!( + !inverted_ray.intersects(volume), + "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}", + ); } } } diff --git a/crates/bevy_math/src/common_traits.rs b/crates/bevy_math/src/common_traits.rs index c6bc43ae9ce9a..455f3dd62854f 100644 --- a/crates/bevy_math/src/common_traits.rs +++ b/crates/bevy_math/src/common_traits.rs @@ -157,7 +157,7 @@ impl NormedVectorSpace for Vec2 { impl NormedVectorSpace for f32 { #[inline] fn norm(self) -> f32 { - self.abs() + ops::abs(self) } #[inline] diff --git a/crates/bevy_math/src/compass.rs b/crates/bevy_math/src/compass.rs index 244c37962eb4d..5ee224df4b118 100644 --- a/crates/bevy_math/src/compass.rs +++ b/crates/bevy_math/src/compass.rs @@ -140,7 +140,7 @@ mod test_compass_quadrant { #[test] fn test_cardinal_directions() { - let tests = vec![ + let tests = [ ( Dir2::new(Vec2::new(1.0, 0.0)).unwrap(), CompassQuadrant::East, @@ -166,7 +166,7 @@ mod test_compass_quadrant { #[test] fn test_north_pie_slice() { - let tests = vec![ + let tests = [ ( Dir2::new(Vec2::new(-0.1, 0.9)).unwrap(), CompassQuadrant::North, @@ -184,7 +184,7 @@ mod test_compass_quadrant { #[test] fn test_east_pie_slice() { - let tests = vec![ + let tests = [ ( Dir2::new(Vec2::new(0.9, 0.1)).unwrap(), CompassQuadrant::East, @@ -202,7 +202,7 @@ mod test_compass_quadrant { #[test] fn test_south_pie_slice() { - let tests = vec![ + let tests = [ ( Dir2::new(Vec2::new(-0.1, -0.9)).unwrap(), CompassQuadrant::South, @@ -220,7 +220,7 @@ mod test_compass_quadrant { #[test] fn test_west_pie_slice() { - let tests = vec![ + let tests = [ ( Dir2::new(Vec2::new(-0.9, -0.1)).unwrap(), CompassQuadrant::West, @@ -243,7 +243,7 @@ mod test_compass_octant { #[test] fn test_cardinal_directions() { - let tests = vec![ + let tests = [ ( Dir2::new(Vec2::new(-0.5, 0.5)).unwrap(), CompassOctant::NorthWest, @@ -282,7 +282,7 @@ mod test_compass_octant { #[test] fn test_north_pie_slice() { - let tests = vec![ + let tests = [ ( Dir2::new(Vec2::new(-0.1, 0.9)).unwrap(), CompassOctant::North, @@ -300,7 +300,7 @@ mod test_compass_octant { #[test] fn test_north_east_pie_slice() { - let tests = vec![ + let tests = [ ( Dir2::new(Vec2::new(0.4, 0.6)).unwrap(), CompassOctant::NorthEast, @@ -318,7 +318,7 @@ mod test_compass_octant { #[test] fn test_east_pie_slice() { - let tests = vec![ + let tests = [ (Dir2::new(Vec2::new(0.9, 0.1)).unwrap(), CompassOctant::East), ( Dir2::new(Vec2::new(0.9, -0.1)).unwrap(), @@ -333,7 +333,7 @@ mod test_compass_octant { #[test] fn test_south_east_pie_slice() { - let tests = vec![ + let tests = [ ( Dir2::new(Vec2::new(0.4, -0.6)).unwrap(), CompassOctant::SouthEast, @@ -351,7 +351,7 @@ mod test_compass_octant { #[test] fn test_south_pie_slice() { - let tests = vec![ + let tests = [ ( Dir2::new(Vec2::new(-0.1, -0.9)).unwrap(), CompassOctant::South, @@ -369,7 +369,7 @@ mod test_compass_octant { #[test] fn test_south_west_pie_slice() { - let tests = vec![ + let tests = [ ( Dir2::new(Vec2::new(-0.4, -0.6)).unwrap(), CompassOctant::SouthWest, @@ -387,7 +387,7 @@ mod test_compass_octant { #[test] fn test_west_pie_slice() { - let tests = vec![ + let tests = [ ( Dir2::new(Vec2::new(-0.9, -0.1)).unwrap(), CompassOctant::West, @@ -405,7 +405,7 @@ mod test_compass_octant { #[test] fn test_north_west_pie_slice() { - let tests = vec![ + let tests = [ ( Dir2::new(Vec2::new(-0.4, 0.6)).unwrap(), CompassOctant::NorthWest, diff --git a/crates/bevy_math/src/cubic_splines.rs b/crates/bevy_math/src/cubic_splines.rs index 87d95b0fc2d85..8e6e8801085f7 100644 --- a/crates/bevy_math/src/cubic_splines.rs +++ b/crates/bevy_math/src/cubic_splines.rs @@ -1,11 +1,16 @@ //! Provides types for building cubic splines for rendering curves and use with animation easing. -use core::{fmt::Debug, iter::once}; +use core::fmt::Debug; -use crate::{ops::FloatPow, Vec2, VectorSpace}; +use crate::{ + ops::{self, FloatPow}, + Vec2, VectorSpace, +}; use derive_more::derive::{Display, Error}; -use itertools::Itertools; + +#[cfg(feature = "alloc")] +use {alloc::vec, alloc::vec::Vec, core::iter::once, itertools::Itertools}; #[cfg(feature = "curve")] use crate::curve::{Curve, Interval}; @@ -47,6 +52,7 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; /// let bezier = CubicBezier::new(points).to_curve().unwrap(); /// let positions: Vec<_> = bezier.iter_positions(100).collect(); /// ``` +#[cfg(feature = "alloc")] #[derive(Clone, Debug)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] pub struct CubicBezier { @@ -54,6 +60,7 @@ pub struct CubicBezier { pub control_points: Vec<[P; 4]>, } +#[cfg(feature = "alloc")] impl CubicBezier

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

{ } } } + +#[cfg(feature = "alloc")] impl CubicGenerator

for CubicBezier

{ type Error = CubicBezierError; @@ -140,12 +149,15 @@ pub struct CubicBezierError; /// ``` /// /// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic +#[cfg(feature = "alloc")] #[derive(Clone, Debug)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] pub struct CubicHermite { /// The control points of the Hermite curve. pub control_points: Vec<(P, P)>, } + +#[cfg(feature = "alloc")] impl CubicHermite

{ /// Create a new Hermite curve from sets of control points. pub fn new( @@ -172,6 +184,8 @@ impl CubicHermite

{ ] } } + +#[cfg(feature = "alloc")] impl CubicGenerator

for CubicHermite

{ type Error = InsufficientDataError; @@ -196,6 +210,8 @@ impl CubicGenerator

for CubicHermite

{ } } } + +#[cfg(feature = "alloc")] impl CyclicCubicGenerator

for CubicHermite

{ type Error = InsufficientDataError; @@ -258,6 +274,7 @@ impl CyclicCubicGenerator

for CubicHermite

{ /// ``` /// /// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic +#[cfg(feature = "alloc")] #[derive(Clone, Debug)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] pub struct CubicCardinalSpline { @@ -267,6 +284,7 @@ pub struct CubicCardinalSpline { pub control_points: Vec

, } +#[cfg(feature = "alloc")] impl CubicCardinalSpline

{ /// Build a new Cardinal spline. pub fn new(tension: f32, control_points: impl Into>) -> Self { @@ -299,6 +317,8 @@ impl CubicCardinalSpline

{ ] } } + +#[cfg(feature = "alloc")] impl CubicGenerator

for CubicCardinalSpline

{ type Error = InsufficientDataError; @@ -335,6 +355,8 @@ impl CubicGenerator

for CubicCardinalSpline

{ Ok(CubicCurve { segments }) } } + +#[cfg(feature = "alloc")] impl CyclicCubicGenerator

for CubicCardinalSpline

{ type Error = InsufficientDataError; @@ -411,12 +433,15 @@ impl CyclicCubicGenerator

for CubicCardinalSpline

{ /// ``` /// /// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic +#[cfg(feature = "alloc")] #[derive(Clone, Debug)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] pub struct CubicBSpline { /// The control points of the spline pub control_points: Vec

, } + +#[cfg(feature = "alloc")] impl CubicBSpline

{ /// Build a new B-Spline. pub fn new(control_points: impl Into>) -> Self { @@ -448,6 +473,8 @@ impl CubicBSpline

{ char_matrix } } + +#[cfg(feature = "alloc")] impl CubicGenerator

for CubicBSpline

{ type Error = InsufficientDataError; @@ -470,6 +497,7 @@ impl CubicGenerator

for CubicBSpline

{ } } +#[cfg(feature = "alloc")] impl CyclicCubicGenerator

for CubicBSpline

{ type Error = InsufficientDataError; @@ -578,6 +606,7 @@ pub enum CubicNurbsError { /// .unwrap(); /// let positions: Vec<_> = nurbs.iter_positions(100).collect(); /// ``` +#[cfg(feature = "alloc")] #[derive(Clone, Debug)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] pub struct CubicNurbs { @@ -588,6 +617,8 @@ pub struct CubicNurbs { /// Knots pub knots: Vec, } + +#[cfg(feature = "alloc")] impl CubicNurbs

{ /// Build a Non-Uniform Rational B-Spline. /// @@ -748,6 +779,8 @@ impl CubicNurbs

{ ] } } + +#[cfg(feature = "alloc")] impl RationalGenerator

for CubicNurbs

{ type Error = InsufficientDataError; @@ -802,12 +835,15 @@ impl RationalGenerator

for CubicNurbs

{ /// formed with [`to_curve_cyclic`], the final segment connects the last control point with the first. /// /// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic +#[cfg(feature = "alloc")] #[derive(Clone, Debug)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] pub struct LinearSpline { /// The control points of the linear spline. pub points: Vec

, } + +#[cfg(feature = "alloc")] impl LinearSpline

{ /// Create a new linear spline from a list of points to be interpolated. pub fn new(points: impl Into>) -> Self { @@ -816,6 +852,8 @@ impl LinearSpline

{ } } } + +#[cfg(feature = "alloc")] impl CubicGenerator

for LinearSpline

{ type Error = InsufficientDataError; @@ -843,6 +881,8 @@ impl CubicGenerator

for LinearSpline

{ } } } + +#[cfg(feature = "alloc")] impl CyclicCubicGenerator

for LinearSpline

{ type Error = InsufficientDataError; @@ -877,6 +917,7 @@ pub struct InsufficientDataError { } /// Implement this on cubic splines that can generate a cubic curve from their spline parameters. +#[cfg(feature = "alloc")] pub trait CubicGenerator { /// An error type indicating why construction might fail. type Error; @@ -888,6 +929,7 @@ pub trait CubicGenerator { /// Implement this on cubic splines that can generate a cyclic cubic curve from their spline parameters. /// /// This makes sense only when the control data can be interpreted cyclically. +#[cfg(feature = "alloc")] pub trait CyclicCubicGenerator { /// An error type indicating why construction might fail. type Error; @@ -937,6 +979,7 @@ impl CubicSegment

{ } /// Calculate polynomial coefficients for the cubic curve using a characteristic matrix. + #[allow(unused)] #[inline] fn coefficients(p: [P; 4], char_matrix: [[f32; 4]; 4]) -> Self { let [c0, c1, c2, c3] = char_matrix; @@ -964,6 +1007,7 @@ impl CubicSegment { /// /// This is a very common tool for UI animations that accelerate and decelerate smoothly. For /// example, the ubiquitous "ease-in-out" is defined as `(0.25, 0.1), (0.25, 1.0)`. + #[cfg(feature = "alloc")] pub fn new_bezier(p1: impl Into, p2: impl Into) -> Self { let (p0, p3) = (Vec2::ZERO, Vec2::ONE); let bezier = CubicBezier::new([[p0, p1.into(), p2.into(), p3]]) @@ -984,9 +1028,12 @@ impl CubicSegment { /// /// ``` /// # use bevy_math::prelude::*; + /// # #[cfg(feature = "alloc")] + /// # { /// let cubic_bezier = CubicSegment::new_bezier((0.25, 0.1), (0.25, 1.0)); /// assert_eq!(cubic_bezier.ease(0.0), 0.0); /// assert_eq!(cubic_bezier.ease(1.0), 1.0); + /// # } /// ``` /// /// # How cubic easing works @@ -1050,7 +1097,7 @@ impl CubicSegment { for _ in 0..Self::MAX_ITERS { pos_guess = self.position(t_guess); let error = pos_guess.x - x; - if error.abs() <= Self::MAX_ERROR { + if ops::abs(error) <= Self::MAX_ERROR { break; } // Using Newton's method, use the tangent line to estimate a better guess value. @@ -1079,6 +1126,7 @@ impl Curve

for CubicSegment

{ /// /// Use any struct that implements the [`CubicGenerator`] trait to create a new curve, such as /// [`CubicBezier`]. +#[cfg(feature = "alloc")] #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] @@ -1087,6 +1135,7 @@ pub struct CubicCurve { segments: Vec>, } +#[cfg(feature = "alloc")] impl CubicCurve

{ /// Create a new curve from a collection of segments. If the collection of segments is empty, /// a curve cannot be built and `None` will be returned instead. @@ -1193,13 +1242,13 @@ impl CubicCurve

{ if self.segments.len() == 1 { (&self.segments[0], t) } else { - let i = (t.floor() as usize).clamp(0, self.segments.len() - 1); + let i = (ops::floor(t) as usize).clamp(0, self.segments.len() - 1); (&self.segments[i], t - i as f32) } } } -#[cfg(feature = "curve")] +#[cfg(all(feature = "curve", feature = "alloc"))] impl Curve

for CubicCurve

{ #[inline] fn domain(&self) -> Interval { @@ -1214,12 +1263,14 @@ impl Curve

for CubicCurve

{ } } +#[cfg(feature = "alloc")] impl Extend> for CubicCurve

{ fn extend>>(&mut self, iter: T) { self.segments.extend(iter); } } +#[cfg(feature = "alloc")] impl IntoIterator for CubicCurve

{ type IntoIter = > as IntoIterator>::IntoIter; @@ -1231,6 +1282,7 @@ impl IntoIterator for CubicCurve

{ } /// Implement this on cubic splines that can generate a rational cubic curve from their spline parameters. +#[cfg(feature = "alloc")] pub trait RationalGenerator { /// An error type indicating why construction might fail. type Error; @@ -1334,6 +1386,7 @@ impl RationalSegment

{ } /// Calculate polynomial coefficients for the cubic polynomials using a characteristic matrix. + #[allow(unused)] #[inline] fn coefficients( control_points: [P; 4], @@ -1389,6 +1442,7 @@ impl Curve

for RationalSegment

{ /// /// Use any struct that implements the [`RationalGenerator`] trait to create a new curve, such as /// [`CubicNurbs`], or convert [`CubicCurve`] using `into/from`. +#[cfg(feature = "alloc")] #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] @@ -1397,6 +1451,7 @@ pub struct RationalCurve { segments: Vec>, } +#[cfg(feature = "alloc")] impl RationalCurve

{ /// Create a new curve from a collection of segments. If the collection of segments is empty, /// a curve cannot be built and `None` will be returned instead. @@ -1528,7 +1583,7 @@ impl RationalCurve

{ } } -#[cfg(feature = "curve")] +#[cfg(all(feature = "curve", feature = "alloc"))] impl Curve

for RationalCurve

{ #[inline] fn domain(&self) -> Interval { @@ -1543,12 +1598,14 @@ impl Curve

for RationalCurve

{ } } +#[cfg(feature = "alloc")] impl Extend> for RationalCurve

{ fn extend>>(&mut self, iter: T) { self.segments.extend(iter); } } +#[cfg(feature = "alloc")] impl IntoIterator for RationalCurve

{ type IntoIter = > as IntoIterator>::IntoIter; @@ -1569,6 +1626,7 @@ impl From> for RationalSegment

{ } } +#[cfg(feature = "alloc")] impl From> for RationalCurve

{ fn from(value: CubicCurve

) -> Self { Self { @@ -1577,10 +1635,9 @@ impl From> for RationalCurve

{ } } +#[cfg(feature = "alloc")] #[cfg(test)] mod tests { - use glam::{vec2, Vec2}; - use crate::{ cubic_splines::{ CubicBSpline, CubicBezier, CubicGenerator, CubicNurbs, CubicSegment, RationalCurve, @@ -1588,6 +1645,8 @@ mod tests { }, ops::{self, FloatPow}, }; + use alloc::vec::Vec; + use glam::{vec2, Vec2}; /// How close two floats can be and still be considered equal const FLOAT_EQ: f32 = 1e-5; @@ -1760,7 +1819,7 @@ mod tests { let curve = spline.to_curve().unwrap(); for (i, point) in curve.iter_positions(10).enumerate() { assert!( - f32::abs(point.length() - 1.0) < EPSILON, + ops::abs(point.length() - 1.0) < EPSILON, "Point {i} is not on the unit circle: {point:?} has length {}", point.length() ); diff --git a/crates/bevy_math/src/curve/adaptors.rs b/crates/bevy_math/src/curve/adaptors.rs index 1d186706f264d..1dfdecdbbce3e 100644 --- a/crates/bevy_math/src/curve/adaptors.rs +++ b/crates/bevy_math/src/curve/adaptors.rs @@ -3,6 +3,7 @@ use super::interval::*; use super::Curve; +use crate::ops; use crate::VectorSpace; use core::any::type_name; use core::fmt::{self, Debug}; @@ -621,7 +622,7 @@ where fn sample_unchecked(&self, t: f32) -> T { // the domain is bounded by construction let d = self.curve.domain(); - let cyclic_t = (t - d.start()).rem_euclid(d.length()); + let cyclic_t = ops::rem_euclid(t - d.start(), d.length()); let t = if t != d.start() && cyclic_t == 0.0 { d.end() } else { @@ -669,7 +670,7 @@ where fn sample_unchecked(&self, t: f32) -> T { // the domain is bounded by construction let d = self.curve.domain(); - let cyclic_t = (t - d.start()).rem_euclid(d.length()); + let cyclic_t = ops::rem_euclid(t - d.start(), d.length()); let t = if t != d.start() && cyclic_t == 0.0 { d.end() } else { diff --git a/crates/bevy_math/src/curve/cores.rs b/crates/bevy_math/src/curve/cores.rs index 6c63eabb29cfa..237ee47f58771 100644 --- a/crates/bevy_math/src/curve/cores.rs +++ b/crates/bevy_math/src/curve/cores.rs @@ -6,10 +6,14 @@ //! provided methods all maintain the invariants, so this is only a concern if you manually mutate //! the fields. +use crate::ops; + use super::interval::Interval; use core::fmt::Debug; use derive_more::derive::{Display, Error}; -use itertools::Itertools; + +#[cfg(feature = "alloc")] +use {alloc::vec::Vec, itertools::Itertools}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; @@ -112,6 +116,7 @@ impl InterpolationDatum { /// } /// } /// ``` +#[cfg(feature = "alloc")] #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] @@ -146,6 +151,7 @@ pub enum EvenCoreError { UnboundedDomain, } +#[cfg(feature = "alloc")] impl EvenCore { /// Create a new [`EvenCore`] from the specified `domain` and `samples`. The samples are /// regarded to be evenly spaced within the given domain interval, so that the outermost @@ -243,11 +249,11 @@ pub fn even_interp(domain: Interval, samples: usize, t: f32) -> InterpolationDat // To the right side of all the samples InterpolationDatum::RightTail(samples - 1) } else { - let lower_index = steps_taken.floor() as usize; + let lower_index = ops::floor(steps_taken) as usize; // This upper index is always valid because `steps_taken` is a finite value // strictly less than `samples - 1`, so its floor is at most `samples - 2` let upper_index = lower_index + 1; - let s = steps_taken.fract(); + let s = ops::fract(steps_taken); InterpolationDatum::Between(lower_index, upper_index, s) } } @@ -314,6 +320,7 @@ pub fn even_interp(domain: Interval, samples: usize, t: f32) -> InterpolationDat /// [`domain`]: UnevenCore::domain /// [`sample_with`]: UnevenCore::sample_with /// [the provided constructor]: UnevenCore::new +#[cfg(feature = "alloc")] #[derive(Debug, Clone)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] @@ -346,6 +353,7 @@ pub enum UnevenCoreError { }, } +#[cfg(feature = "alloc")] impl UnevenCore { /// Create a new [`UnevenCore`]. The given samples are filtered to finite times and /// sorted internally; if there are not at least 2 valid timed samples, an error will be @@ -453,6 +461,7 @@ impl UnevenCore { /// if the sample type can effectively be encoded as a fixed-length slice of values. /// /// [sampling width]: ChunkedUnevenCore::width +#[cfg(feature = "alloc")] #[derive(Debug, Clone)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] @@ -507,6 +516,7 @@ pub enum ChunkedUnevenCoreError { }, } +#[cfg(feature = "alloc")] impl ChunkedUnevenCore { /// Create a new [`ChunkedUnevenCore`]. The given `times` are sorted, filtered to finite times, /// and deduplicated. See the [type-level documentation] for more information about this type. @@ -644,6 +654,7 @@ impl ChunkedUnevenCore { } /// Sort the given times, deduplicate them, and filter them to only finite times. +#[cfg(feature = "alloc")] fn filter_sort_dedup_times(times: impl IntoIterator) -> Vec { // Filter before sorting/deduplication so that NAN doesn't interfere with them. let mut times = times.into_iter().filter(|t| t.is_finite()).collect_vec(); @@ -682,7 +693,7 @@ pub fn uneven_interp(times: &[f32], t: f32) -> InterpolationDatum { } } -#[cfg(test)] +#[cfg(all(test, feature = "alloc"))] mod tests { use super::{ChunkedUnevenCore, EvenCore, UnevenCore}; use crate::curve::{cores::InterpolationDatum, interval}; diff --git a/crates/bevy_math/src/curve/easing.rs b/crates/bevy_math/src/curve/easing.rs index 689ffb24f5730..0e5406fa732c7 100644 --- a/crates/bevy_math/src/curve/easing.rs +++ b/crates/bevy_math/src/curve/easing.rs @@ -3,9 +3,10 @@ //! //! [easing functions]: EaseFunction -use crate::{Dir2, Dir3, Dir3A, Quat, Rot2, VectorSpace}; - -use super::{function_curve, Curve, Interval}; +use crate::{ + curve::{FunctionCurve, Interval}, + Curve, Dir2, Dir3, Dir3A, Quat, Rot2, VectorSpace, +}; // TODO: Think about merging `Ease` with `StableInterpolate` @@ -28,13 +29,13 @@ pub trait Ease: Sized { impl Ease for V { fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve { - function_curve(Interval::EVERYWHERE, move |t| V::lerp(start, end, t)) + FunctionCurve::new(Interval::EVERYWHERE, move |t| V::lerp(start, end, t)) } } impl Ease for Rot2 { fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve { - function_curve(Interval::EVERYWHERE, move |t| Rot2::slerp(start, end, t)) + FunctionCurve::new(Interval::EVERYWHERE, move |t| Rot2::slerp(start, end, t)) } } @@ -44,7 +45,7 @@ impl Ease for Quat { let end_adjusted = if dot < 0.0 { -end } else { end }; let difference = end_adjusted * start.inverse(); let (axis, angle) = difference.to_axis_angle(); - function_curve(Interval::EVERYWHERE, move |s| { + FunctionCurve::new(Interval::EVERYWHERE, move |s| { Quat::from_axis_angle(axis, angle * s) * start }) } @@ -52,7 +53,7 @@ impl Ease for Quat { impl Ease for Dir2 { fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve { - function_curve(Interval::EVERYWHERE, move |t| Dir2::slerp(start, end, t)) + FunctionCurve::new(Interval::EVERYWHERE, move |t| Dir2::slerp(start, end, t)) } } @@ -71,20 +72,6 @@ impl Ease for Dir3A { } } -/// Given a `start` and `end` value, create a curve parametrized over [the unit interval] -/// that connects them, using the given [ease function] to determine the form of the -/// curve in between. -/// -/// [the unit interval]: Interval::UNIT -/// [ease function]: EaseFunction -pub fn easing_curve(start: T, end: T, ease_fn: EaseFunction) -> EasingCurve { - EasingCurve { - start, - end, - ease_fn, - } -} - /// A [`Curve`] that is defined by /// /// - an initial `start` sample value at `t = 0` @@ -104,6 +91,22 @@ pub struct EasingCurve { ease_fn: EaseFunction, } +impl EasingCurve { + /// Given a `start` and `end` value, create a curve parametrized over [the unit interval] + /// that connects them, using the given [ease function] to determine the form of the + /// curve in between. + /// + /// [the unit interval]: Interval::UNIT + /// [ease function]: EaseFunction + pub fn new(start: T, end: T, ease_fn: EaseFunction) -> Self { + Self { + start, + end, + ease_fn, + } + } +} + impl Curve for EasingCurve where T: Ease + Clone, @@ -306,18 +309,18 @@ mod easing_functions { #[inline] pub(crate) fn circular_in(t: f32) -> f32 { - 1.0 - (1.0 - t.squared()).sqrt() + 1.0 - ops::sqrt(1.0 - t.squared()) } #[inline] pub(crate) fn circular_out(t: f32) -> f32 { - (1.0 - (t - 1.0).squared()).sqrt() + ops::sqrt(1.0 - (t - 1.0).squared()) } #[inline] pub(crate) fn circular_in_out(t: f32) -> f32 { if t < 0.5 { - (1.0 - (1.0 - (2.0 * t).squared()).sqrt()) / 2.0 + (1.0 - ops::sqrt(1.0 - (2.0 * t).squared())) / 2.0 } else { - ((1.0 - (-2.0 * t + 2.0).squared()).sqrt() + 1.0) / 2.0 + (ops::sqrt(1.0 - (-2.0 * t + 2.0).squared()) + 1.0) / 2.0 } } @@ -408,7 +411,7 @@ mod easing_functions { #[inline] pub(crate) fn steps(num_steps: usize, t: f32) -> f32 { - (t * num_steps as f32).round() / num_steps.max(1) as f32 + ops::round(t * num_steps as f32) / num_steps.max(1) as f32 } #[inline] diff --git a/crates/bevy_math/src/curve/interval.rs b/crates/bevy_math/src/curve/interval.rs index 291d0499c0af1..f15b21e1cb1d7 100644 --- a/crates/bevy_math/src/curve/interval.rs +++ b/crates/bevy_math/src/curve/interval.rs @@ -198,6 +198,8 @@ pub fn interval(start: f32, end: f32) -> Result #[cfg(test)] mod tests { + use crate::ops; + use super::*; use approx::{assert_abs_diff_eq, AbsDiffEq}; @@ -237,10 +239,10 @@ mod tests { #[test] fn lengths() { let ivl = interval(-5.0, 10.0).unwrap(); - assert!((ivl.length() - 15.0).abs() <= f32::EPSILON); + assert!(ops::abs(ivl.length() - 15.0) <= f32::EPSILON); let ivl = interval(5.0, 100.0).unwrap(); - assert!((ivl.length() - 95.0).abs() <= f32::EPSILON); + assert!(ops::abs(ivl.length() - 95.0) <= f32::EPSILON); let ivl = interval(0.0, f32::INFINITY).unwrap(); assert_eq!(ivl.length(), f32::INFINITY); diff --git a/crates/bevy_math/src/curve/iterable.rs b/crates/bevy_math/src/curve/iterable.rs index b8adcd30d5c45..ee29372dd41b8 100644 --- a/crates/bevy_math/src/curve/iterable.rs +++ b/crates/bevy_math/src/curve/iterable.rs @@ -1,7 +1,10 @@ //! Iterable curves, which sample in the form of an iterator in order to support `Vec`-like //! output whose length cannot be known statically. -use super::{ConstantCurve, Interval}; +use super::Interval; + +#[cfg(feature = "alloc")] +use {super::ConstantCurve, alloc::vec::Vec}; /// A curve which provides samples in the form of [`Iterator`]s. /// @@ -39,6 +42,7 @@ pub trait IterableCurve { } } +#[cfg(feature = "alloc")] impl IterableCurve for ConstantCurve> where T: Clone, diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index 851dddc564716..a1d24301aaf24 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -56,13 +56,13 @@ //! # use bevy_math::vec3; //! # use bevy_math::curve::*; //! // A sinusoid: -//! let sine_curve = function_curve(Interval::EVERYWHERE, f32::sin); +//! let sine_curve = FunctionCurve::new(Interval::EVERYWHERE, f32::sin); //! //! // A sawtooth wave: -//! let sawtooth_curve = function_curve(Interval::EVERYWHERE, |t| t % 1.0); +//! let sawtooth_curve = FunctionCurve::new(Interval::EVERYWHERE, |t| t % 1.0); //! //! // A helix: -//! let helix_curve = function_curve(Interval::EVERYWHERE, |theta| vec3(theta.sin(), theta, theta.cos())); +//! let helix_curve = FunctionCurve::new(Interval::EVERYWHERE, |theta| vec3(theta.sin(), theta, theta.cos())); //! ``` //! //! Sample-interpolated curves commonly arises in both rasterization and in animation, and this library @@ -127,7 +127,7 @@ //! # use bevy_math::{vec2, prelude::*}; //! # use std::f32::consts::TAU; //! // Our original curve, which may look something like this: -//! let ellipse_curve = function_curve( +//! let ellipse_curve = FunctionCurve::new( //! interval(0.0, TAU).unwrap(), //! |t| vec2(t.cos(), t.sin() * 2.0) //! ); @@ -141,7 +141,7 @@ //! ```rust //! # use bevy_math::{vec2, prelude::*}; //! # use std::f32::consts::TAU; -//! # let ellipse_curve = function_curve(interval(0.0, TAU).unwrap(), |t| vec2(t.cos(), t.sin() * 2.0)); +//! # let ellipse_curve = FunctionCurve::new(interval(0.0, TAU).unwrap(), |t| vec2(t.cos(), t.sin() * 2.0)); //! # let ellipse_motion_curve = ellipse_curve.map(|pos| pos.extend(0.0)); //! // Change the domain to `[0, 1]` instead of `[0, TAU]`: //! let final_curve = ellipse_motion_curve.reparametrize_linear(Interval::UNIT).unwrap(); @@ -155,7 +155,7 @@ //! // A line segment curve connecting two points in the plane: //! let start = vec2(-1.0, 1.0); //! let end = vec2(1.0, 1.0); -//! let segment = function_curve(Interval::UNIT, |t| start.lerp(end, t)); +//! let segment = FunctionCurve::new(Interval::UNIT, |t| start.lerp(end, t)); //! //! // Let's make a curve that goes back and forth along this line segment forever. //! // @@ -177,13 +177,13 @@ //! # use bevy_math::{vec2, prelude::*}; //! # use std::f32::consts::PI; //! // A line segment connecting `(-1, 0)` to `(0, 0)`: -//! let line_curve = function_curve( +//! let line_curve = FunctionCurve::new( //! Interval::UNIT, //! |t| vec2(-1.0, 0.0).lerp(vec2(0.0, 0.0), t) //! ); //! //! // A half-circle curve starting at `(0, 0)`: -//! let half_circle_curve = function_curve( +//! let half_circle_curve = FunctionCurve::new( //! interval(0.0, PI).unwrap(), //! |t| vec2(t.cos() * -1.0 + 1.0, t.sin()) //! ); @@ -198,10 +198,10 @@ //! ```rust //! # use bevy_math::{vec2, prelude::*}; //! // Some entity's position in 2D: -//! let position_curve = function_curve(Interval::UNIT, |t| vec2(t.cos(), t.sin())); +//! let position_curve = FunctionCurve::new(Interval::UNIT, |t| vec2(t.cos(), t.sin())); //! //! // The same entity's orientation, described as a rotation. (In this case it will be spinning.) -//! let orientation_curve = function_curve(Interval::UNIT, |t| Rot2::radians(5.0 * t)); +//! let orientation_curve = FunctionCurve::new(Interval::UNIT, |t| Rot2::radians(5.0 * t)); //! //! // Both in one curve with `(Vec2, Rot2)` output: //! let position_and_orientation = position_curve.zip(orientation_curve).unwrap(); @@ -220,7 +220,7 @@ //! ```rust //! # use bevy_math::{vec2, prelude::*}; //! // A curve that is not easily transported because it relies on evaluating a function: -//! let interesting_curve = function_curve(Interval::UNIT, |t| vec2(t * 3.0, t.exp())); +//! let interesting_curve = FunctionCurve::new(Interval::UNIT, |t| vec2(t * 3.0, t.exp())); //! //! // A rasterized form of the preceding curve which is just a `SampleAutoCurve`. Inside, this //! // just stores an `Interval` along with a buffer of sample data, so it's easy to serialize @@ -250,7 +250,7 @@ //! Here is a demonstration: //! ```rust //! # use bevy_math::prelude::*; -//! # let some_magic_constructor = || easing_curve(0.0, 1.0, EaseFunction::ElasticInOut).graph(); +//! # let some_magic_constructor = || EasingCurve::new(0.0, 1.0, EaseFunction::ElasticInOut).graph(); //! //`my_curve` is obtained somehow. It is a `Curve<(f32, f32)>`. //! let my_curve = some_magic_constructor(); //! @@ -272,7 +272,7 @@ //! [changing parametrizations]: Curve::reparametrize //! [mapping output]: Curve::map //! [rasterization]: Curve::resample -//! [functions]: function_curve +//! [functions]: FunctionCurve //! [sample interpolation]: SampleCurve //! [splines]: crate::cubic_splines //! [easings]: easing @@ -282,7 +282,7 @@ //! [`zip`]: Curve::zip //! [`resample`]: Curve::resample //! -//! [^footnote]: In fact, universal as well, in some sense: if `curve` is any curve, then `function_curve +//! [^footnote]: In fact, universal as well, in some sense: if `curve` is any curve, then `FunctionCurve::new //! (curve.domain(), |t| curve.sample_unchecked(t))` is an equivalent function curve. pub mod adaptors; @@ -290,21 +290,27 @@ pub mod cores; pub mod easing; pub mod interval; pub mod iterable; + +#[cfg(feature = "alloc")] pub mod sample_curves; // bevy_math::curve re-exports all commonly-needed curve-related items. pub use adaptors::*; pub use easing::*; pub use interval::{interval, Interval}; -pub use sample_curves::*; -use cores::{EvenCore, UnevenCore}; +#[cfg(feature = "alloc")] +pub use { + crate::StableInterpolate, + cores::{EvenCore, UnevenCore}, + itertools::Itertools, + sample_curves::*, +}; -use crate::{StableInterpolate, VectorSpace}; +use crate::VectorSpace; use core::{marker::PhantomData, ops::Deref}; use derive_more::derive::{Display, Error}; use interval::InvalidIntervalError; -use itertools::Itertools; /// A trait for a type that can represent values of type `T` parametrized over a fixed interval. /// @@ -414,7 +420,7 @@ pub trait Curve { /// factor rather than multiplying: /// ``` /// # use bevy_math::curve::*; - /// let my_curve = constant_curve(Interval::UNIT, 1.0); + /// let my_curve = ConstantCurve::new(Interval::UNIT, 1.0); /// let scaled_curve = my_curve.reparametrize(interval(0.0, 2.0).unwrap(), |t| t / 2.0); /// ``` /// This kind of linear remapping is provided by the convenience method @@ -425,12 +431,12 @@ pub trait Curve { /// // Reverse a curve: /// # use bevy_math::curve::*; /// # use bevy_math::vec2; - /// let my_curve = constant_curve(Interval::UNIT, 1.0); + /// let my_curve = ConstantCurve::new(Interval::UNIT, 1.0); /// let domain = my_curve.domain(); /// let reversed_curve = my_curve.reparametrize(domain, |t| domain.end() - (t - domain.start())); /// /// // Take a segment of a curve: - /// # let my_curve = constant_curve(Interval::UNIT, 1.0); + /// # let my_curve = ConstantCurve::new(Interval::UNIT, 1.0); /// let curve_segment = my_curve.reparametrize(interval(0.0, 0.5).unwrap(), |t| 0.5 + t); /// ``` #[must_use] @@ -712,10 +718,11 @@ pub trait Curve { /// ``` /// # use bevy_math::*; /// # use bevy_math::curve::*; - /// let quarter_rotation = function_curve(interval(0.0, 90.0).unwrap(), |t| Rot2::degrees(t)); + /// let quarter_rotation = FunctionCurve::new(interval(0.0, 90.0).unwrap(), |t| Rot2::degrees(t)); /// // A curve which only stores three data points and uses `nlerp` to interpolate them: /// let resampled_rotation = quarter_rotation.resample(3, |x, y, t| x.nlerp(*y, t)); /// ``` + #[cfg(feature = "alloc")] fn resample( &self, segments: usize, @@ -743,6 +750,7 @@ pub trait Curve { /// domain, then a [`ResamplingError`] is returned. /// /// [automatic interpolation]: crate::common_traits::StableInterpolate + #[cfg(feature = "alloc")] fn resample_auto(&self, segments: usize) -> Result, ResamplingError> where Self: Sized, @@ -794,6 +802,7 @@ pub trait Curve { /// The interpolation takes two values by reference together with a scalar parameter and /// produces an owned value. The expectation is that `interpolation(&x, &y, 0.0)` and /// `interpolation(&x, &y, 1.0)` are equivalent to `x` and `y` respectively. + #[cfg(feature = "alloc")] fn resample_uneven( &self, sample_times: impl IntoIterator, @@ -832,6 +841,7 @@ pub trait Curve { /// sample times of the iterator. /// /// [automatic interpolation]: crate::common_traits::StableInterpolate + #[cfg(feature = "alloc")] fn resample_uneven_auto( &self, sample_times: impl IntoIterator, @@ -863,7 +873,7 @@ pub trait Curve { /// # Example /// ``` /// # use bevy_math::curve::*; - /// let my_curve = function_curve(Interval::UNIT, |t| t * t + 1.0); + /// let my_curve = FunctionCurve::new(Interval::UNIT, |t| t * t + 1.0); /// /// // Borrow `my_curve` long enough to resample a mapped version. Note that `map` takes /// // ownership of its input. @@ -976,27 +986,8 @@ pub enum ResamplingError { UnboundedDomain, } -/// Create a [`Curve`] that constantly takes the given `value` over the given `domain`. -pub fn constant_curve(domain: Interval, value: T) -> ConstantCurve { - ConstantCurve { domain, value } -} - -/// Convert the given function `f` into a [`Curve`] with the given `domain`, sampled by -/// evaluating the function. -pub fn function_curve(domain: Interval, f: F) -> FunctionCurve -where - F: Fn(f32) -> T, -{ - FunctionCurve { - domain, - f, - _phantom: PhantomData, - } -} - #[cfg(test)] mod tests { - use super::easing::*; use super::*; use crate::{ops, Quat}; use approx::{assert_abs_diff_eq, AbsDiffEq}; @@ -1005,7 +996,7 @@ mod tests { #[test] fn curve_can_be_made_into_an_object() { - let curve = constant_curve(Interval::UNIT, 42.0); + let curve = ConstantCurve::new(Interval::UNIT, 42.0); let curve: &dyn Curve = &curve; assert_eq!(curve.sample(1.0), Some(42.0)); @@ -1014,21 +1005,21 @@ mod tests { #[test] fn constant_curves() { - let curve = constant_curve(Interval::EVERYWHERE, 5.0); + let curve = ConstantCurve::new(Interval::EVERYWHERE, 5.0); assert!(curve.sample_unchecked(-35.0) == 5.0); - let curve = constant_curve(Interval::UNIT, true); + let curve = ConstantCurve::new(Interval::UNIT, true); assert!(curve.sample_unchecked(2.0)); assert!(curve.sample(2.0).is_none()); } #[test] fn function_curves() { - let curve = function_curve(Interval::EVERYWHERE, |t| t * t); + let curve = FunctionCurve::new(Interval::EVERYWHERE, |t| t * t); assert!(curve.sample_unchecked(2.0).abs_diff_eq(&4.0, f32::EPSILON)); assert!(curve.sample_unchecked(-3.0).abs_diff_eq(&9.0, f32::EPSILON)); - let curve = function_curve(interval(0.0, f32::INFINITY).unwrap(), ops::log2); + let curve = FunctionCurve::new(interval(0.0, f32::INFINITY).unwrap(), ops::log2); assert_eq!(curve.sample_unchecked(3.5), ops::log2(3.5)); assert!(curve.sample_unchecked(-1.0).is_nan()); assert!(curve.sample(-1.0).is_none()); @@ -1038,7 +1029,7 @@ mod tests { fn linear_curve() { let start = Vec2::ZERO; let end = Vec2::new(1.0, 2.0); - let curve = easing_curve(start, end, EaseFunction::Linear); + let curve = EasingCurve::new(start, end, EaseFunction::Linear); let mid = (start + end) / 2.0; @@ -1054,7 +1045,7 @@ mod tests { let start = Vec2::ZERO; let end = Vec2::new(1.0, 2.0); - let curve = easing_curve(start, end, EaseFunction::Steps(4)); + let curve = EasingCurve::new(start, end, EaseFunction::Steps(4)); [ (0.0, start), (0.124, start), @@ -1078,7 +1069,7 @@ mod tests { let start = Vec2::ZERO; let end = Vec2::new(1.0, 2.0); - let curve = easing_curve(start, end, EaseFunction::QuadraticIn); + let curve = EasingCurve::new(start, end, EaseFunction::QuadraticIn); [ (0.0, start), (0.25, Vec2::new(0.0625, 0.125)), @@ -1093,7 +1084,7 @@ mod tests { #[test] fn mapping() { - let curve = function_curve(Interval::EVERYWHERE, |t| t * 3.0 + 1.0); + let curve = FunctionCurve::new(Interval::EVERYWHERE, |t| t * 3.0 + 1.0); let mapped_curve = curve.map(|x| x / 7.0); assert_eq!(mapped_curve.sample_unchecked(3.5), (3.5 * 3.0 + 1.0) / 7.0); assert_eq!( @@ -1102,7 +1093,7 @@ mod tests { ); assert_eq!(mapped_curve.domain(), Interval::EVERYWHERE); - let curve = function_curve(Interval::UNIT, |t| t * TAU); + let curve = FunctionCurve::new(Interval::UNIT, |t| t * TAU); let mapped_curve = curve.map(Quat::from_rotation_z); assert_eq!(mapped_curve.sample_unchecked(0.0), Quat::IDENTITY); assert!(mapped_curve.sample_unchecked(1.0).is_near_identity()); @@ -1111,7 +1102,7 @@ mod tests { #[test] fn reverse() { - let curve = function_curve(Interval::new(0.0, 1.0).unwrap(), |t| t * 3.0 + 1.0); + let curve = FunctionCurve::new(Interval::new(0.0, 1.0).unwrap(), |t| t * 3.0 + 1.0); let rev_curve = curve.reverse().unwrap(); assert_eq!(rev_curve.sample(-0.1), None); assert_eq!(rev_curve.sample(0.0), Some(1.0 * 3.0 + 1.0)); @@ -1119,7 +1110,7 @@ mod tests { assert_eq!(rev_curve.sample(1.0), Some(0.0 * 3.0 + 1.0)); assert_eq!(rev_curve.sample(1.1), None); - let curve = function_curve(Interval::new(-2.0, 1.0).unwrap(), |t| t * 3.0 + 1.0); + let curve = FunctionCurve::new(Interval::new(-2.0, 1.0).unwrap(), |t| t * 3.0 + 1.0); let rev_curve = curve.reverse().unwrap(); assert_eq!(rev_curve.sample(-2.1), None); assert_eq!(rev_curve.sample(-2.0), Some(1.0 * 3.0 + 1.0)); @@ -1130,7 +1121,7 @@ mod tests { #[test] fn repeat() { - let curve = function_curve(Interval::new(0.0, 1.0).unwrap(), |t| t * 3.0 + 1.0); + let curve = FunctionCurve::new(Interval::new(0.0, 1.0).unwrap(), |t| t * 3.0 + 1.0); let repeat_curve = curve.by_ref().repeat(1).unwrap(); assert_eq!(repeat_curve.sample(-0.1), None); assert_eq!(repeat_curve.sample(0.0), Some(0.0 * 3.0 + 1.0)); @@ -1159,7 +1150,7 @@ mod tests { #[test] fn ping_pong() { - let curve = function_curve(Interval::new(0.0, 1.0).unwrap(), |t| t * 3.0 + 1.0); + let curve = FunctionCurve::new(Interval::new(0.0, 1.0).unwrap(), |t| t * 3.0 + 1.0); let ping_pong_curve = curve.ping_pong().unwrap(); assert_eq!(ping_pong_curve.sample(-0.1), None); assert_eq!(ping_pong_curve.sample(0.0), Some(0.0 * 3.0 + 1.0)); @@ -1169,7 +1160,7 @@ mod tests { assert_eq!(ping_pong_curve.sample(2.0), Some(0.0 * 3.0 + 1.0)); assert_eq!(ping_pong_curve.sample(2.1), None); - let curve = function_curve(Interval::new(-2.0, 2.0).unwrap(), |t| t * 3.0 + 1.0); + let curve = FunctionCurve::new(Interval::new(-2.0, 2.0).unwrap(), |t| t * 3.0 + 1.0); let ping_pong_curve = curve.ping_pong().unwrap(); assert_eq!(ping_pong_curve.sample(-2.1), None); assert_eq!(ping_pong_curve.sample(-2.0), Some(-2.0 * 3.0 + 1.0)); @@ -1182,8 +1173,8 @@ mod tests { #[test] fn continue_chain() { - let first = function_curve(Interval::new(0.0, 1.0).unwrap(), |t| t * 3.0 + 1.0); - let second = function_curve(Interval::new(0.0, 1.0).unwrap(), |t| t * t); + let first = FunctionCurve::new(Interval::new(0.0, 1.0).unwrap(), |t| t * 3.0 + 1.0); + let second = FunctionCurve::new(Interval::new(0.0, 1.0).unwrap(), |t| t * t); let c0_chain_curve = first.chain_continue(second).unwrap(); assert_eq!(c0_chain_curve.sample(-0.1), None); assert_eq!(c0_chain_curve.sample(0.0), Some(0.0 * 3.0 + 1.0)); @@ -1196,7 +1187,7 @@ mod tests { #[test] fn reparameterization() { - let curve = function_curve(interval(1.0, f32::INFINITY).unwrap(), ops::log2); + let curve = FunctionCurve::new(interval(1.0, f32::INFINITY).unwrap(), ops::log2); let reparametrized_curve = curve .by_ref() .reparametrize(interval(0.0, f32::INFINITY).unwrap(), ops::exp2); @@ -1216,7 +1207,7 @@ mod tests { #[test] fn multiple_maps() { // Make sure these actually happen in the right order. - let curve = function_curve(Interval::UNIT, ops::exp2); + let curve = FunctionCurve::new(Interval::UNIT, ops::exp2); let first_mapped = curve.map(ops::log2); let second_mapped = first_mapped.map(|x| x * -2.0); assert_abs_diff_eq!(second_mapped.sample_unchecked(0.0), 0.0); @@ -1227,7 +1218,7 @@ mod tests { #[test] fn multiple_reparams() { // Make sure these happen in the right order too. - let curve = function_curve(Interval::UNIT, ops::exp2); + let curve = FunctionCurve::new(Interval::UNIT, ops::exp2); let first_reparam = curve.reparametrize(interval(1.0, 2.0).unwrap(), ops::log2); let second_reparam = first_reparam.reparametrize(Interval::UNIT, |t| t + 1.0); assert_abs_diff_eq!(second_reparam.sample_unchecked(0.0), 1.0); @@ -1237,7 +1228,7 @@ mod tests { #[test] fn resampling() { - let curve = function_curve(interval(1.0, 4.0).unwrap(), ops::log2); + let curve = FunctionCurve::new(interval(1.0, 4.0).unwrap(), ops::log2); // Need at least one segment to sample. let nice_try = curve.by_ref().resample_auto(0); @@ -1257,7 +1248,7 @@ mod tests { } // Another example. - let curve = function_curve(interval(0.0, TAU).unwrap(), ops::cos); + let curve = FunctionCurve::new(interval(0.0, TAU).unwrap(), ops::cos); let resampled_curve = curve.by_ref().resample_auto(1000).unwrap(); for test_pt in curve.domain().spaced_points(1001).unwrap() { let expected = curve.sample_unchecked(test_pt); @@ -1271,7 +1262,7 @@ mod tests { #[test] fn uneven_resampling() { - let curve = function_curve(interval(0.0, f32::INFINITY).unwrap(), ops::exp); + let curve = FunctionCurve::new(interval(0.0, f32::INFINITY).unwrap(), ops::exp); // Need at least two points to resample. let nice_try = curve.by_ref().resample_uneven_auto([1.0; 1]); @@ -1290,7 +1281,7 @@ mod tests { assert_abs_diff_eq!(resampled_curve.domain().end(), 9.9, epsilon = 1e-6); // Another example. - let curve = function_curve(interval(1.0, f32::INFINITY).unwrap(), ops::log2); + let curve = FunctionCurve::new(interval(1.0, f32::INFINITY).unwrap(), ops::log2); let sample_points = (0..10).map(|idx| ops::exp2(idx as f32)); let resampled_curve = curve.by_ref().resample_uneven_auto(sample_points).unwrap(); for idx in 0..10 { @@ -1306,7 +1297,7 @@ mod tests { fn sample_iterators() { let times = [-0.5, 0.0, 0.5, 1.0, 1.5]; - let curve = function_curve(Interval::EVERYWHERE, |t| t * 3.0 + 1.0); + let curve = FunctionCurve::new(Interval::EVERYWHERE, |t| t * 3.0 + 1.0); let samples = curve.sample_iter_unchecked(times).collect::>(); let [y0, y1, y2, y3, y4] = samples.try_into().unwrap(); @@ -1316,7 +1307,7 @@ mod tests { assert_eq!(y3, 1.0 * 3.0 + 1.0); assert_eq!(y4, 1.5 * 3.0 + 1.0); - let finite_curve = function_curve(Interval::new(0.0, 1.0).unwrap(), |t| t * 3.0 + 1.0); + let finite_curve = FunctionCurve::new(Interval::new(0.0, 1.0).unwrap(), |t| t * 3.0 + 1.0); let samples = finite_curve.sample_iter(times).collect::>(); let [y0, y1, y2, y3, y4] = samples.try_into().unwrap(); diff --git a/crates/bevy_math/src/curve/sample_curves.rs b/crates/bevy_math/src/curve/sample_curves.rs index b1abcb0399803..7a37f55640090 100644 --- a/crates/bevy_math/src/curve/sample_curves.rs +++ b/crates/bevy_math/src/curve/sample_curves.rs @@ -356,6 +356,7 @@ impl UnevenSampleAutoCurve { } #[cfg(test)] +#[cfg(feature = "bevy_reflect")] mod tests { //! These tests should guarantee (by even compiling) that `SampleCurve` and `UnevenSampleCurve` //! can be `Reflect` under reasonable circumstances where their interpolation is defined by: diff --git a/crates/bevy_math/src/direction.rs b/crates/bevy_math/src/direction.rs index acbc5816ad3f2..d427a24261645 100644 --- a/crates/bevy_math/src/direction.rs +++ b/crates/bevy_math/src/direction.rs @@ -55,17 +55,23 @@ impl core::fmt::Display for InvalidDirectionError { /// and similarly for the error. #[cfg(debug_assertions)] fn assert_is_normalized(message: &str, length_squared: f32) { - let length_error_squared = (length_squared - 1.0).abs(); + use crate::ops; + + let length_error_squared = ops::abs(length_squared - 1.0); // Panic for large error and warn for slight error. if length_error_squared > 2e-2 || length_error_squared.is_nan() { // Length error is approximately 1e-2 or more. - panic!("Error: {message} The length is {}.", length_squared.sqrt()); + panic!( + "Error: {message} The length is {}.", + ops::sqrt(length_squared) + ); } else if length_error_squared > 2e-4 { // Length error is approximately 1e-4 or more. + #[cfg(feature = "std")] eprintln!( "Warning: {message} The length is {}.", - length_squared.sqrt() + ops::sqrt(length_squared) ); } } @@ -886,17 +892,17 @@ mod tests { fn dir2_slerp() { assert_relative_eq!( Dir2::X.slerp(Dir2::Y, 0.5), - Dir2::from_xy(0.5_f32.sqrt(), 0.5_f32.sqrt()).unwrap() + Dir2::from_xy(ops::sqrt(0.5_f32), ops::sqrt(0.5_f32)).unwrap() ); assert_eq!(Dir2::Y.slerp(Dir2::X, 0.0), Dir2::Y); assert_relative_eq!(Dir2::X.slerp(Dir2::Y, 1.0), Dir2::Y); assert_relative_eq!( Dir2::Y.slerp(Dir2::X, 1.0 / 3.0), - Dir2::from_xy(0.5, 0.75_f32.sqrt()).unwrap() + Dir2::from_xy(0.5, ops::sqrt(0.75_f32)).unwrap() ); assert_relative_eq!( Dir2::X.slerp(Dir2::Y, 2.0 / 3.0), - Dir2::from_xy(0.5, 0.75_f32.sqrt()).unwrap() + Dir2::from_xy(0.5, ops::sqrt(0.75_f32)).unwrap() ); } @@ -967,18 +973,18 @@ mod tests { fn dir3_slerp() { assert_relative_eq!( Dir3::X.slerp(Dir3::Y, 0.5), - Dir3::from_xyz(0.5f32.sqrt(), 0.5f32.sqrt(), 0.0).unwrap() + Dir3::from_xyz(ops::sqrt(0.5f32), ops::sqrt(0.5f32), 0.0).unwrap() ); assert_relative_eq!(Dir3::Y.slerp(Dir3::Z, 0.0), Dir3::Y); assert_relative_eq!(Dir3::Z.slerp(Dir3::X, 1.0), Dir3::X, epsilon = 0.000001); assert_relative_eq!( Dir3::X.slerp(Dir3::Z, 1.0 / 3.0), - Dir3::from_xyz(0.75f32.sqrt(), 0.0, 0.5).unwrap(), + Dir3::from_xyz(ops::sqrt(0.75f32), 0.0, 0.5).unwrap(), epsilon = 0.000001 ); assert_relative_eq!( Dir3::Z.slerp(Dir3::Y, 2.0 / 3.0), - Dir3::from_xyz(0.0, 0.75f32.sqrt(), 0.5).unwrap() + Dir3::from_xyz(0.0, ops::sqrt(0.75f32), 0.5).unwrap() ); } @@ -1038,18 +1044,18 @@ mod tests { fn dir3a_slerp() { assert_relative_eq!( Dir3A::X.slerp(Dir3A::Y, 0.5), - Dir3A::from_xyz(0.5f32.sqrt(), 0.5f32.sqrt(), 0.0).unwrap() + Dir3A::from_xyz(ops::sqrt(0.5f32), ops::sqrt(0.5f32), 0.0).unwrap() ); assert_relative_eq!(Dir3A::Y.slerp(Dir3A::Z, 0.0), Dir3A::Y); assert_relative_eq!(Dir3A::Z.slerp(Dir3A::X, 1.0), Dir3A::X, epsilon = 0.000001); assert_relative_eq!( Dir3A::X.slerp(Dir3A::Z, 1.0 / 3.0), - Dir3A::from_xyz(0.75f32.sqrt(), 0.0, 0.5).unwrap(), + Dir3A::from_xyz(ops::sqrt(0.75f32), 0.0, 0.5).unwrap(), epsilon = 0.000001 ); assert_relative_eq!( Dir3A::Z.slerp(Dir3A::Y, 2.0 / 3.0), - Dir3A::from_xyz(0.0, 0.75f32.sqrt(), 0.5).unwrap() + Dir3A::from_xyz(0.0, ops::sqrt(0.75f32), 0.5).unwrap() ); } diff --git a/crates/bevy_math/src/float_ord.rs b/crates/bevy_math/src/float_ord.rs index e53a083f773e6..b5f72d1c7cf5e 100644 --- a/crates/bevy_math/src/float_ord.rs +++ b/crates/bevy_math/src/float_ord.rs @@ -95,8 +95,6 @@ impl Neg for FloatOrd { #[cfg(test)] mod tests { - use std::hash::DefaultHasher; - use super::*; const NAN: FloatOrd = FloatOrd(f32::NAN); @@ -157,10 +155,11 @@ mod tests { assert!(ONE >= ZERO); } + #[cfg(feature = "std")] #[test] fn float_ord_hash() { let hash = |num| { - let mut h = DefaultHasher::new(); + let mut h = std::hash::DefaultHasher::new(); FloatOrd(num).hash(&mut h); h.finish() }; diff --git a/crates/bevy_math/src/lib.rs b/crates/bevy_math/src/lib.rs index 0f6caa30cf753..1d99edd051566 100644 --- a/crates/bevy_math/src/lib.rs +++ b/crates/bevy_math/src/lib.rs @@ -6,6 +6,7 @@ html_logo_url = "https://bevyengine.org/assets/icon.png", html_favicon_url = "https://bevyengine.org/assets/icon.png" )] +#![cfg_attr(not(feature = "std"), no_std)] //! Provides math types and functionality for the Bevy game engine. //! @@ -13,6 +14,9 @@ //! matrices like [`Mat2`], [`Mat3`] and [`Mat4`] and orientation representations //! like [`Quat`]. +#[cfg(feature = "alloc")] +extern crate alloc; + mod affine3; mod aspect_ratio; pub mod bounding; @@ -58,17 +62,15 @@ pub use sampling::{FromRng, ShapeSample}; pub mod prelude { #[doc(hidden)] pub use crate::{ - cubic_splines::{ - CubicBSpline, CubicBezier, CubicCardinalSpline, CubicCurve, CubicGenerator, - CubicHermite, CubicNurbs, CubicNurbsError, CubicSegment, CyclicCubicGenerator, - RationalCurve, RationalGenerator, RationalSegment, - }, + bvec2, bvec3, bvec3a, bvec4, bvec4a, + cubic_splines::{CubicNurbsError, CubicSegment, RationalSegment}, direction::{Dir2, Dir3, Dir3A}, - ops, + ivec2, ivec3, ivec4, mat2, mat3, mat3a, mat4, ops, primitives::*, - BVec2, BVec3, BVec4, EulerRot, FloatExt, IRect, IVec2, IVec3, IVec4, Isometry2d, - Isometry3d, Mat2, Mat3, Mat4, Quat, Ray2d, Ray3d, Rect, Rot2, StableInterpolate, URect, - UVec2, UVec3, UVec4, Vec2, Vec2Swizzles, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles, + quat, uvec2, uvec3, uvec4, vec2, vec3, vec3a, vec4, BVec2, BVec3, BVec3A, BVec4, BVec4A, + EulerRot, FloatExt, IRect, IVec2, IVec3, IVec4, Isometry2d, Isometry3d, Mat2, Mat3, Mat3A, + Mat4, Quat, Ray2d, Ray3d, Rect, Rot2, StableInterpolate, URect, UVec2, UVec3, UVec4, Vec2, + Vec2Swizzles, Vec3, Vec3A, Vec3Swizzles, Vec4, Vec4Swizzles, }; #[doc(hidden)] @@ -78,6 +80,13 @@ pub mod prelude { #[doc(hidden)] #[cfg(feature = "rand")] pub use crate::sampling::{FromRng, ShapeSample}; + + #[cfg(feature = "alloc")] + #[doc(hidden)] + pub use crate::cubic_splines::{ + CubicBSpline, CubicBezier, CubicCardinalSpline, CubicCurve, CubicGenerator, CubicHermite, + CubicNurbs, CyclicCubicGenerator, RationalCurve, RationalGenerator, + }; } pub use glam::*; diff --git a/crates/bevy_math/src/ops.rs b/crates/bevy_math/src/ops.rs index 2e9c3a0ca4a85..143f98ec0f5b8 100644 --- a/crates/bevy_math/src/ops.rs +++ b/crates/bevy_math/src/ops.rs @@ -4,6 +4,9 @@ //! //! All the functions here are named according to their versions in the standard //! library. +//! +//! It also provides `no_std` compatible alternatives to certain floating-point +//! operations which are not provided in the [`core`] library. #![allow(dead_code)] #![allow(clippy::disallowed_methods)] @@ -444,12 +447,159 @@ mod libm_ops { } } +#[cfg(all(feature = "libm", not(feature = "std")))] +mod libm_ops_for_no_std { + //! Provides standardized names for [`f32`] operations which may not be + //! supported on `no_std` platforms. + //! On `no_std` platforms, this forwards to the implementations provided + //! by [`libm`]. + + /// Calculates the least nonnegative remainder of `self (mod rhs)`. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn rem_euclid(x: f32, y: f32) -> f32 { + let result = libm::remainderf(x, y); + + // libm::remainderf has a range of -y/2 to +y/2 + if result < 0. { + result + y + } else { + result + } + } + + /// Computes the absolute value of x. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn abs(x: f32) -> f32 { + libm::fabsf(x) + } + + /// Returns the square root of a number. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn sqrt(x: f32) -> f32 { + libm::sqrtf(x) + } + + /// Returns a number composed of the magnitude of `x` and the sign of `y`. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn copysign(x: f32, y: f32) -> f32 { + libm::copysignf(x, y) + } + + /// Returns the nearest integer to `x`. If a value is half-way between two integers, round away from `0.0`. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn round(x: f32) -> f32 { + libm::roundf(x) + } + + /// Returns the largest integer less than or equal to `x`. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn floor(x: f32) -> f32 { + libm::floorf(x) + } + + /// Returns the fractional part of `x`. + /// + /// This function always returns the precise result. + #[inline(always)] + pub fn fract(x: f32) -> f32 { + libm::modff(x).0 + } +} + +#[cfg(feature = "std")] +mod std_ops_for_no_std { + //! Provides standardized names for [`f32`] operations which may not be + //! supported on `no_std` platforms. + //! On `std` platforms, this forwards directly to the implementations provided + //! by [`std`]. + + /// Calculates the least nonnegative remainder of `x (mod y)`. + /// + /// The result of this operation is guaranteed to be the rounded infinite-precision result. + #[inline(always)] + pub fn rem_euclid(x: f32, y: f32) -> f32 { + f32::rem_euclid(x, y) + } + + /// Computes the absolute value of x. + /// + /// This function always returns the precise result. + #[inline(always)] + pub fn abs(x: f32) -> f32 { + f32::abs(x) + } + + /// Returns the square root of a number. + /// + /// The result of this operation is guaranteed to be the rounded infinite-precision result. + /// It is specified by IEEE 754 as `squareRoot` and guaranteed not to change. + #[inline(always)] + pub fn sqrt(x: f32) -> f32 { + f32::sqrt(x) + } + + /// Returns a number composed of the magnitude of `x` and the sign of `y`. + /// + /// Equal to `x` if the sign of `x` and `y` are the same, otherwise equal to `-x`. If `x` is a + /// `NaN`, then a `NaN` with the sign bit of `y` is returned. Note, however, that conserving the + /// sign bit on `NaN` across arithmetical operations is not generally guaranteed. + #[inline(always)] + pub fn copysign(x: f32, y: f32) -> f32 { + f32::copysign(x, y) + } + + /// Returns the nearest integer to `x`. If a value is half-way between two integers, round away from `0.0`. + /// + /// This function always returns the precise result. + #[inline(always)] + pub fn round(x: f32) -> f32 { + f32::round(x) + } + + /// Returns the largest integer less than or equal to `x`. + /// + /// This function always returns the precise result. + #[inline(always)] + pub fn floor(x: f32) -> f32 { + f32::floor(x) + } + + /// Returns the fractional part of `x`. + /// + /// This function always returns the precise result. + #[inline(always)] + pub fn fract(x: f32) -> f32 { + f32::fract(x) + } +} + #[cfg(feature = "libm")] pub use libm_ops::*; #[cfg(not(feature = "libm"))] pub use std_ops::*; +#[cfg(feature = "std")] +pub use std_ops_for_no_std::*; + +#[cfg(all(feature = "libm", not(feature = "std")))] +pub use libm_ops_for_no_std::*; + +#[cfg(all(not(feature = "libm"), not(feature = "std")))] +compile_error!("Either the `libm` feature or the `std` feature must be enabled."); + /// This extension trait covers shortfall in determinacy from the lack of a `libm` counterpart /// to `f32::powi`. Use this for the common small exponents. pub trait FloatPow { diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index 62d1633622dde..446a94fcba81e 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -12,6 +12,9 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; #[cfg(all(feature = "serialize", feature = "bevy_reflect"))] use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; +#[cfg(feature = "alloc")] +use alloc::{boxed::Box, vec::Vec}; + /// A circle primitive, representing the set of points some distance from the origin #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -64,7 +67,7 @@ impl Circle { } else { // The point is outside the circle. // Find the closest point on the perimeter of the circle. - let dir_to_point = point / distance_squared.sqrt(); + let dir_to_point = point / ops::sqrt(distance_squared); self.radius * dir_to_point } } @@ -230,7 +233,7 @@ impl Arc2d { // used by Wolfram MathWorld, which is the distance rather than the segment. pub fn apothem(&self) -> f32 { let sign = if self.is_minor() { 1.0 } else { -1.0 }; - sign * f32::sqrt(self.radius.squared() - self.half_chord_length().squared()) + sign * ops::sqrt(self.radius.squared() - self.half_chord_length().squared()) } /// Get the length of the sagitta of this arc, that is, @@ -280,7 +283,7 @@ impl Arc2d { )] pub struct CircularSector { /// The arc defining the sector - #[cfg_attr(feature = "serialize", serde(flatten))] + #[cfg_attr(all(feature = "serialize", feature = "alloc"), serde(flatten))] pub arc: Arc2d, } impl Primitive2d for CircularSector {} @@ -423,7 +426,7 @@ impl CircularSector { )] pub struct CircularSegment { /// The arc defining the segment - #[cfg_attr(feature = "serialize", serde(flatten))] + #[cfg_attr(all(feature = "serialize", feature = "alloc"), serde(flatten))] pub arc: Arc2d, } impl Primitive2d for CircularSegment {} @@ -676,7 +679,7 @@ mod arc_tests { #[test] fn quarter_circle() { - let sqrt_half: f32 = f32::sqrt(0.5); + let sqrt_half: f32 = ops::sqrt(0.5); let tests = ArcTestCase { radius: 1.0, half_angle: FRAC_PI_4, @@ -687,7 +690,7 @@ mod arc_tests { endpoints: [Vec2::new(-sqrt_half, sqrt_half), Vec2::splat(sqrt_half)], midpoint: Vec2::Y, half_chord_length: sqrt_half, - chord_length: f32::sqrt(2.0), + chord_length: ops::sqrt(2.0), chord_midpoint: Vec2::new(0.0, sqrt_half), apothem: sqrt_half, sagitta: 1.0 - sqrt_half, @@ -822,7 +825,7 @@ impl Ellipse { let a = self.semi_major(); let b = self.semi_minor(); - (a * a - b * b).sqrt() / a + ops::sqrt(a * a - b * b) / a } #[inline(always)] @@ -833,7 +836,7 @@ impl Ellipse { let a = self.semi_major(); let b = self.semi_minor(); - (a * a - b * b).sqrt() + ops::sqrt(a * a - b * b) } /// Returns the length of the semi-major axis. This corresponds to the longest radius of the ellipse. @@ -982,13 +985,13 @@ impl Annulus { } 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(); + let dir_to_point = point / ops::sqrt(distance_squared); 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(); + let dir_to_point = point / ops::sqrt(distance_squared); self.inner_circle.radius * dir_to_point } } @@ -1301,14 +1304,18 @@ impl Polyline2d { /// in a `Box<[Vec2]>`. /// /// For a version without alloc: [`Polyline2d`] +#[cfg(feature = "alloc")] #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct BoxedPolyline2d { /// The vertices of the polyline pub vertices: Box<[Vec2]>, } + +#[cfg(feature = "alloc")] impl Primitive2d for BoxedPolyline2d {} +#[cfg(feature = "alloc")] impl FromIterator for BoxedPolyline2d { fn from_iter>(iter: I) -> Self { let vertices: Vec = iter.into_iter().collect(); @@ -1318,6 +1325,7 @@ impl FromIterator for BoxedPolyline2d { } } +#[cfg(feature = "alloc")] impl BoxedPolyline2d { /// Create a new `BoxedPolyline2d` from its vertices pub fn new(vertices: impl IntoIterator) -> Self { @@ -1480,7 +1488,7 @@ impl Measured2d for Triangle2d { #[inline(always)] fn area(&self) -> f32 { let [a, b, c] = self.vertices; - (a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y)).abs() / 2.0 + ops::abs(a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y)) / 2.0 } /// Get the perimeter of the triangle @@ -1709,14 +1717,18 @@ impl TryFrom> for ConvexPolygon { /// in a `Box<[Vec2]>`. /// /// For a version without alloc: [`Polygon`] +#[cfg(feature = "alloc")] #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct BoxedPolygon { /// The vertices of the `BoxedPolygon` pub vertices: Box<[Vec2]>, } + +#[cfg(feature = "alloc")] impl Primitive2d for BoxedPolygon {} +#[cfg(feature = "alloc")] impl FromIterator for BoxedPolygon { fn from_iter>(iter: I) -> Self { let vertices: Vec = iter.into_iter().collect(); @@ -1726,6 +1738,7 @@ impl FromIterator for BoxedPolygon { } } +#[cfg(feature = "alloc")] impl BoxedPolygon { /// Create a new `BoxedPolygon` from its vertices pub fn new(vertices: impl IntoIterator) -> Self { diff --git a/crates/bevy_math/src/primitives/dim3.rs b/crates/bevy_math/src/primitives/dim3.rs index cd1b5013e6b72..8d25df5b83d0a 100644 --- a/crates/bevy_math/src/primitives/dim3.rs +++ b/crates/bevy_math/src/primitives/dim3.rs @@ -1,7 +1,10 @@ use core::f32::consts::{FRAC_PI_3, PI}; use super::{Circle, Measured2d, Measured3d, Primitive2d, Primitive3d}; -use crate::{ops, ops::FloatPow, Dir3, InvalidDirectionError, Isometry3d, Mat3, Vec2, Vec3}; +use crate::{ + ops::{self, FloatPow}, + Dir3, InvalidDirectionError, Isometry3d, Mat3, Vec2, Vec3, +}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::{std_traits::ReflectDefault, Reflect}; @@ -9,6 +12,9 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; use glam::Quat; +#[cfg(feature = "alloc")] +use alloc::{boxed::Box, vec::Vec}; + /// A sphere primitive, representing the set of all points some distance from the origin #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -61,7 +67,7 @@ impl Sphere { } else { // The point is outside the sphere. // Find the closest point on the surface of the sphere. - let dir_to_point = point / distance_squared.sqrt(); + let dir_to_point = point / ops::sqrt(distance_squared); self.radius * dir_to_point } } @@ -440,14 +446,18 @@ impl Polyline3d { /// in a `Box<[Vec3]>`. /// /// For a version without alloc: [`Polyline3d`] +#[cfg(feature = "alloc")] #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct BoxedPolyline3d { /// The vertices of the polyline pub vertices: Box<[Vec3]>, } + +#[cfg(feature = "alloc")] impl Primitive3d for BoxedPolyline3d {} +#[cfg(feature = "alloc")] impl FromIterator for BoxedPolyline3d { fn from_iter>(iter: I) -> Self { let vertices: Vec = iter.into_iter().collect(); @@ -457,6 +467,7 @@ impl FromIterator for BoxedPolyline3d { } } +#[cfg(feature = "alloc")] impl BoxedPolyline3d { /// Create a new `BoxedPolyline3d` from its vertices pub fn new(vertices: impl IntoIterator) -> Self { @@ -1246,7 +1257,7 @@ impl Measured3d for Tetrahedron { /// Get the volume of the tetrahedron. #[inline(always)] fn volume(&self) -> f32 { - self.signed_volume().abs() + ops::abs(self.signed_volume()) } } @@ -1573,7 +1584,7 @@ mod tests { assert_eq!(default_triangle.area(), 0.5, "incorrect area"); assert_relative_eq!( default_triangle.perimeter(), - 1.0 + 2.0 * 1.25_f32.sqrt(), + 1.0 + 2.0 * ops::sqrt(1.25_f32), epsilon = 10e-9 ); assert_eq!(default_triangle.normal(), Ok(Dir3::Z), "incorrect normal"); diff --git a/crates/bevy_math/src/primitives/serde.rs b/crates/bevy_math/src/primitives/serde.rs index bdbf72f69e737..7db6be9700114 100644 --- a/crates/bevy_math/src/primitives/serde.rs +++ b/crates/bevy_math/src/primitives/serde.rs @@ -31,7 +31,7 @@ pub(crate) mod array { type Value = [T; N]; fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result { - formatter.write_str(&format!("an array of length {}", N)) + formatter.write_fmt(format_args!("an array of length {}", N)) } #[inline] @@ -39,17 +39,21 @@ pub(crate) mod array { where A: SeqAccess<'de>, { - let mut data = Vec::with_capacity(N); - for _ in 0..N { + let mut data = [const { Option::::None }; N]; + + for element in data.iter_mut() { match (seq.next_element())? { - Some(val) => data.push(val), + Some(val) => *element = Some(val), None => return Err(serde::de::Error::invalid_length(N, &self)), } } - match data.try_into() { - Ok(arr) => Ok(arr), - Err(_) => unreachable!(), - } + + let data = data.map(|value| match value { + Some(value) => value, + None => unreachable!(), + }); + + Ok(data) } } diff --git a/crates/bevy_math/src/ray.rs b/crates/bevy_math/src/ray.rs index e083d7b4a1b3d..273ed61fa4b97 100644 --- a/crates/bevy_math/src/ray.rs +++ b/crates/bevy_math/src/ray.rs @@ -1,4 +1,5 @@ use crate::{ + ops, primitives::{InfinitePlane3d, Plane2d}, Dir2, Dir3, Vec2, Vec3, }; @@ -40,7 +41,7 @@ impl Ray2d { #[inline] pub fn intersect_plane(&self, plane_origin: Vec2, plane: Plane2d) -> Option { let denominator = plane.normal.dot(*self.direction); - if denominator.abs() > f32::EPSILON { + if ops::abs(denominator) > f32::EPSILON { let distance = (plane_origin - self.origin).dot(*plane.normal) / denominator; if distance > f32::EPSILON { return Some(distance); @@ -82,7 +83,7 @@ impl Ray3d { #[inline] pub fn intersect_plane(&self, plane_origin: Vec3, plane: InfinitePlane3d) -> Option { let denominator = plane.normal.dot(*self.direction); - if denominator.abs() > f32::EPSILON { + if ops::abs(denominator) > f32::EPSILON { let distance = (plane_origin - self.origin).dot(*plane.normal) / denominator; if distance > f32::EPSILON { return Some(distance); diff --git a/crates/bevy_math/src/rects/rect.rs b/crates/bevy_math/src/rects/rect.rs index 8e804dcd085f0..901a569a71f4e 100644 --- a/crates/bevy_math/src/rects/rect.rs +++ b/crates/bevy_math/src/rects/rect.rs @@ -371,6 +371,8 @@ impl Rect { #[cfg(test)] mod tests { + use crate::ops; + use super::*; #[test] @@ -382,8 +384,8 @@ mod tests { assert!(r.center().abs_diff_eq(Vec2::new(3., -5.), 1e-5)); - assert!((r.width() - 8.).abs() <= 1e-5); - assert!((r.height() - 11.).abs() <= 1e-5); + assert!(ops::abs(r.width() - 8.) <= 1e-5); + assert!(ops::abs(r.height() - 11.) <= 1e-5); assert!(r.size().abs_diff_eq(Vec2::new(8., 11.), 1e-5)); assert!(r.half_size().abs_diff_eq(Vec2::new(4., 5.5), 1e-5)); diff --git a/crates/bevy_math/src/rotation2d.rs b/crates/bevy_math/src/rotation2d.rs index d4df9548f5741..5b0bc816bc5ca 100644 --- a/crates/bevy_math/src/rotation2d.rs +++ b/crates/bevy_math/src/rotation2d.rs @@ -318,7 +318,7 @@ impl Rot2 { // The allowed length is 1 +/- 1e-4, so the largest allowed // squared length is (1 + 1e-4)^2 = 1.00020001, which makes // the threshold for the squared length approximately 2e-4. - (self.length_squared() - 1.0).abs() <= 2e-4 + ops::abs(self.length_squared() - 1.0) <= 2e-4 } /// Returns `true` if the rotation is near [`Rot2::IDENTITY`]. @@ -326,12 +326,22 @@ impl Rot2 { pub fn is_near_identity(self) -> bool { // Same as `Quat::is_near_identity`, but using sine and cosine let threshold_angle_sin = 0.000_049_692_047; // let threshold_angle = 0.002_847_144_6; - self.cos > 0.0 && self.sin.abs() < threshold_angle_sin + self.cos > 0.0 && ops::abs(self.sin) < threshold_angle_sin } /// Returns the angle in radians needed to make `self` and `other` coincide. #[inline] + #[deprecated( + since = "0.15.0", + note = "Use `angle_to` instead, the semantics of `angle_between` will change in the future." + )] pub fn angle_between(self, other: Self) -> f32 { + self.angle_to(other) + } + + /// Returns the angle in radians needed to make `self` and `other` coincide. + #[inline] + pub fn angle_to(self, other: Self) -> f32 { (other * self.inverse()).as_radians() } @@ -340,7 +350,7 @@ impl Rot2 { #[inline] #[must_use] #[doc(alias = "conjugate")] - pub fn inverse(self) -> Self { + pub const fn inverse(self) -> Self { Self { cos: self.cos, sin: -self.sin, @@ -424,7 +434,7 @@ impl Rot2 { /// ``` #[inline] pub fn slerp(self, end: Self, s: f32) -> Self { - self * Self::radians(self.angle_between(end) * s) + self * Self::radians(self.angle_to(end) * s) } } @@ -510,7 +520,7 @@ mod tests { use approx::assert_relative_eq; - use crate::{Dir2, Rot2, Vec2}; + use crate::{ops, Dir2, Rot2, Vec2}; #[test] fn creation() { @@ -567,10 +577,7 @@ mod tests { assert_relative_eq!((rotation1 * rotation2.inverse()).as_degrees(), 45.0); // This should be equivalent to the above - assert_relative_eq!( - rotation2.angle_between(rotation1), - core::f32::consts::FRAC_PI_4 - ); + assert_relative_eq!(rotation2.angle_to(rotation1), core::f32::consts::FRAC_PI_4); } #[test] @@ -582,7 +589,7 @@ mod tests { assert_eq!(rotation.length_squared(), 125.0); assert_eq!(rotation.length(), 11.18034); - assert!((rotation.normalize().length() - 1.0).abs() < 10e-7); + assert!(ops::abs(rotation.normalize().length() - 1.0) < 10e-7); } #[test] @@ -695,7 +702,7 @@ mod tests { assert!(rot1.nlerp(rot2, 0.0).is_near_identity()); // At 0.5, there is no valid rotation, so the fallback is the original angle. assert_eq!(rot1.nlerp(rot2, 0.5).as_degrees(), 0.0); - assert_eq!(rot1.nlerp(rot2, 1.0).as_degrees().abs(), 180.0); + assert_eq!(ops::abs(rot1.nlerp(rot2, 1.0).as_degrees()), 180.0); } #[test] @@ -711,9 +718,9 @@ mod tests { let rot1 = Rot2::IDENTITY; let rot2 = Rot2::from_sin_cos(0.0, -1.0); - assert!((rot1.slerp(rot2, 1.0 / 3.0).as_degrees() - 60.0).abs() < 10e-6); + assert!(ops::abs(rot1.slerp(rot2, 1.0 / 3.0).as_degrees() - 60.0) < 10e-6); assert!(rot1.slerp(rot2, 0.0).is_near_identity()); assert_eq!(rot1.slerp(rot2, 0.5).as_degrees(), 90.0); - assert_eq!(rot1.slerp(rot2, 1.0).as_degrees().abs(), 180.0); + assert_eq!(ops::abs(rot1.slerp(rot2, 1.0).as_degrees()), 180.0); } } diff --git a/crates/bevy_math/src/sampling/mesh_sampling.rs b/crates/bevy_math/src/sampling/mesh_sampling.rs index e5163c0148367..898198dea4261 100644 --- a/crates/bevy_math/src/sampling/mesh_sampling.rs +++ b/crates/bevy_math/src/sampling/mesh_sampling.rs @@ -4,6 +4,7 @@ use crate::{ primitives::{Measured2d, Triangle3d}, ShapeSample, Vec3, }; +use alloc::vec::Vec; use rand::Rng; use rand_distr::{Distribution, WeightedAliasIndex, WeightedError}; diff --git a/crates/bevy_math/src/sampling/mod.rs b/crates/bevy_math/src/sampling/mod.rs index c4e482acbb39d..78db92588b1f6 100644 --- a/crates/bevy_math/src/sampling/mod.rs +++ b/crates/bevy_math/src/sampling/mod.rs @@ -2,10 +2,12 @@ //! //! To use this, the "rand" feature must be enabled. +#[cfg(feature = "alloc")] pub mod mesh_sampling; pub mod shape_sampling; pub mod standard; +#[cfg(feature = "alloc")] pub use mesh_sampling::*; pub use shape_sampling::*; pub use standard::*; diff --git a/crates/bevy_math/src/sampling/shape_sampling.rs b/crates/bevy_math/src/sampling/shape_sampling.rs index fdec36aa0b101..8489a88f19e2d 100644 --- a/crates/bevy_math/src/sampling/shape_sampling.rs +++ b/crates/bevy_math/src/sampling/shape_sampling.rs @@ -154,7 +154,7 @@ impl ShapeSample for Circle { // 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(); + let r = ops::sqrt(r_squared); let (sin, cos) = ops::sin_cos(theta); Vec2::new(r * cos, r * sin) } @@ -171,7 +171,7 @@ impl ShapeSample for Circle { fn sample_unit_sphere_boundary(rng: &mut R) -> Vec3 { let z = rng.gen_range(-1f32..=1f32); let (a_sin, a_cos) = ops::sin_cos(rng.gen_range(-PI..=PI)); - let c = (1f32 - z * z).sqrt(); + let c = ops::sqrt(1f32 - z * z); let x = a_sin * c; let y = a_cos * c; @@ -202,7 +202,7 @@ impl ShapeSample for Annulus { // Like random sampling for a circle, radius is weighted by the square. let r_squared = rng.gen_range((inner_radius * inner_radius)..(outer_radius * outer_radius)); - let r = r_squared.sqrt(); + let r = ops::sqrt(r_squared); let theta = rng.gen_range(0.0..TAU); let (sin, cos) = ops::sin_cos(theta); @@ -627,7 +627,7 @@ mod tests { let point = circle.sample_boundary(&mut rng); let angle = ops::atan(point.y / point.x) + PI / 2.0; - let wedge = (angle * 8.0 / PI).floor() as usize; + let wedge = ops::floor(angle * 8.0 / PI) as usize; wedge_hits[wedge] += 1; } diff --git a/crates/bevy_mesh/Cargo.toml b/crates/bevy_mesh/Cargo.toml index fd00c8d0417a7..9db8fde8b8ced 100644 --- a/crates/bevy_mesh/Cargo.toml +++ b/crates/bevy_mesh/Cargo.toml @@ -24,7 +24,7 @@ bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } # misc bitflags = { version = "2.3", features = ["serde"] } bytemuck = { version = "1.5" } -wgpu = { version = "23", default-features = false } +wgpu = { version = "23.0.1", default-features = false } serde = { version = "1", features = ["derive"] } hexasphere = "15.0" derive_more = { version = "1", default-features = false, features = [ diff --git a/crates/bevy_mesh/src/mesh.rs b/crates/bevy_mesh/src/mesh.rs index 8a5ac7e37d920..0303bd7878f36 100644 --- a/crates/bevy_mesh/src/mesh.rs +++ b/crates/bevy_mesh/src/mesh.rs @@ -2,10 +2,11 @@ use bevy_transform::components::Transform; pub use wgpu::PrimitiveTopology; use super::{ - face_normal, generate_tangents_for_mesh, scale_normal, FourIterators, GenerateTangentsError, - Indices, MeshAttributeData, MeshTrianglesError, MeshVertexAttribute, MeshVertexAttributeId, - MeshVertexBufferLayout, MeshVertexBufferLayoutRef, MeshVertexBufferLayouts, - MeshWindingInvertError, VertexAttributeValues, VertexBufferLayout, VertexFormatSize, + face_area_normal, face_normal, generate_tangents_for_mesh, scale_normal, FourIterators, + GenerateTangentsError, Indices, MeshAttributeData, MeshTrianglesError, MeshVertexAttribute, + MeshVertexAttributeId, MeshVertexBufferLayout, MeshVertexBufferLayoutRef, + MeshVertexBufferLayouts, MeshWindingInvertError, VertexAttributeValues, VertexBufferLayout, + VertexFormatSize, }; use alloc::collections::BTreeMap; use bevy_asset::{Asset, Handle, RenderAssetUsages}; @@ -698,7 +699,7 @@ impl Mesh { .chunks_exact(3) .for_each(|face| { let [a, b, c] = [face[0], face[1], face[2]]; - let normal = Vec3::from(face_normal(positions[a], positions[b], positions[c])); + let normal = Vec3::from(face_area_normal(positions[a], positions[b], positions[c])); [a, b, c].iter().for_each(|pos| { normals[*pos] += normal; }); @@ -1402,6 +1403,41 @@ mod tests { assert_eq!([1., 0., 0.], normals[3]); } + #[test] + fn compute_smooth_normals_proportionate() { + let mut mesh = Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ); + + // z y + // | / + // 3---2.. + // | / \ + // 0-------1---x + + mesh.insert_attribute( + Mesh::ATTRIBUTE_POSITION, + vec![[0., 0., 0.], [2., 0., 0.], [0., 1., 0.], [0., 0., 1.]], + ); + mesh.insert_indices(Indices::U16(vec![0, 1, 2, 0, 2, 3])); + mesh.compute_smooth_normals(); + let normals = mesh + .attribute(Mesh::ATTRIBUTE_NORMAL) + .unwrap() + .as_float3() + .unwrap(); + assert_eq!(4, normals.len()); + // 0 + assert_eq!(Vec3::new(1., 0., 2.).normalize().to_array(), normals[0]); + // 1 + assert_eq!([0., 0., 1.], normals[1]); + // 2 + assert_eq!(Vec3::new(1., 0., 2.).normalize().to_array(), normals[2]); + // 3 + assert_eq!([1., 0., 0.], normals[3]); + } + #[test] fn triangles_from_triangle_list() { let mut mesh = Mesh::new( diff --git a/crates/bevy_mesh/src/vertex.rs b/crates/bevy_mesh/src/vertex.rs index a6b52dac7e086..a256edc4987fa 100644 --- a/crates/bevy_mesh/src/vertex.rs +++ b/crates/bevy_mesh/src/vertex.rs @@ -138,7 +138,29 @@ pub(crate) struct MeshAttributeData { pub(crate) values: VertexAttributeValues, } -pub(crate) fn face_normal(a: [f32; 3], b: [f32; 3], c: [f32; 3]) -> [f32; 3] { +/// Compute a vector whose direction is the normal of the triangle formed by +/// points a, b, c, and whose magnitude is double the area of the triangle. This +/// is useful for computing smooth normals where the contributing normals are +/// proportionate to the areas of the triangles as [discussed +/// here](https://iquilezles.org/articles/normals/). +/// +/// Question: Why double the area? Because the area of a triangle _A_ is +/// determined by this equation: +/// +/// _A = |(b - a) x (c - a)| / 2_ +/// +/// By computing _2 A_ we avoid a division operation, and when calculating the +/// the sum of these vectors which are then normalized, a constant multiple has +/// no effect. +#[inline] +pub fn face_area_normal(a: [f32; 3], b: [f32; 3], c: [f32; 3]) -> [f32; 3] { + let (a, b, c) = (Vec3::from(a), Vec3::from(b), Vec3::from(c)); + (b - a).cross(c - a).into() +} + +/// Compute the normal of a face made of three points: a, b, and c. +#[inline] +pub fn face_normal(a: [f32; 3], b: [f32; 3], c: [f32; 3]) -> [f32; 3] { let (a, b, c) = (Vec3::from(a), Vec3::from(b), Vec3::from(c)); (b - a).cross(c - a).normalize().into() } diff --git a/crates/bevy_pbr/Cargo.toml b/crates/bevy_pbr/Cargo.toml index 5dde71084322f..571cd4082bdb2 100644 --- a/crates/bevy_pbr/Cargo.toml +++ b/crates/bevy_pbr/Cargo.toml @@ -14,7 +14,7 @@ webgpu = [] pbr_transmission_textures = [] pbr_multi_layer_material_textures = [] pbr_anisotropy_texture = [] -pbr_pcss = [] +experimental_pbr_pcss = [] shader_format_glsl = ["bevy_render/shader_format_glsl"] trace = ["bevy_render/trace"] ios_simulator = ["bevy_render/ios_simulator"] @@ -37,6 +37,7 @@ bevy_color = { path = "../bevy_color", version = "0.15.0-dev" } bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.15.0-dev" } bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.15.0-dev" } bevy_math = { path = "../bevy_math", version = "0.15.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [ "bevy", diff --git a/crates/bevy_pbr/src/deferred/mod.rs b/crates/bevy_pbr/src/deferred/mod.rs index bcb23a7048bfd..67271a916f400 100644 --- a/crates/bevy_pbr/src/deferred/mod.rs +++ b/crates/bevy_pbr/src/deferred/mod.rs @@ -5,6 +5,9 @@ use crate::{ ViewScreenSpaceReflectionsUniformOffset, TONEMAPPING_LUT_SAMPLER_BINDING_INDEX, TONEMAPPING_LUT_TEXTURE_BINDING_INDEX, }; +use crate::{ + MeshPipelineKey, ShadowFilteringMethod, ViewFogUniformOffset, ViewLightsUniformOffset, +}; use bevy_app::prelude::*; use bevy_asset::{load_internal_asset, Handle}; use bevy_core_pipeline::{ @@ -16,6 +19,7 @@ use bevy_core_pipeline::{ tonemapping::{DebandDither, Tonemapping}, }; use bevy_ecs::{prelude::*, query::QueryItem}; +use bevy_image::BevyDefault as _; use bevy_render::{ extract_component::{ ComponentUniforms, ExtractComponent, ExtractComponentPlugin, UniformComponentPlugin, @@ -23,15 +27,10 @@ use bevy_render::{ render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner}, render_resource::{binding_types::uniform_buffer, *}, renderer::{RenderContext, RenderDevice}, - texture::BevyDefault, view::{ExtractedView, ViewTarget, ViewUniformOffset}, Render, RenderApp, RenderSet, }; -use crate::{ - MeshPipelineKey, ShadowFilteringMethod, ViewFogUniformOffset, ViewLightsUniformOffset, -}; - pub struct DeferredPbrLightingPlugin; pub const DEFERRED_LIGHTING_SHADER_HANDLE: Handle = @@ -390,6 +389,7 @@ impl SpecializedRenderPipeline for DeferredLightingLayout { }), multisample: MultisampleState::default(), push_constant_ranges: vec![], + zero_initialize_workgroup_memory: false, } } } diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index 559fa99dcc2bf..7526f384e2fe5 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -116,6 +116,7 @@ use bevy_app::prelude::*; use bevy_asset::{load_internal_asset, AssetApp, Assets, Handle}; use bevy_core_pipeline::core_3d::graph::{Core3d, Node3d}; use bevy_ecs::prelude::*; +use bevy_image::Image; use bevy_render::{ alpha::AlphaMode, camera::{ @@ -128,10 +129,11 @@ use bevy_render::{ render_graph::RenderGraph, render_resource::Shader, sync_component::SyncComponentPlugin, - texture::{GpuImage, Image}, + texture::GpuImage, view::{check_visibility, VisibilitySystems}, ExtractSchedule, Render, RenderApp, RenderSet, }; + use bevy_transform::TransformSystem; pub const PBR_TYPES_SHADER_HANDLE: Handle = Handle::weak_from_u128(1708015359337029744); diff --git a/crates/bevy_pbr/src/light/directional_light.rs b/crates/bevy_pbr/src/light/directional_light.rs index 3bc8c49cd2c88..06c277aef731c 100644 --- a/crates/bevy_pbr/src/light/directional_light.rs +++ b/crates/bevy_pbr/src/light/directional_light.rs @@ -95,6 +95,7 @@ pub struct DirectionalLight { /// /// Note that soft shadows are significantly more expensive to render than /// hard shadows. + #[cfg(feature = "experimental_pbr_pcss")] pub soft_shadow_size: Option, /// A value that adjusts the tradeoff between self-shadowing artifacts and @@ -120,9 +121,10 @@ impl Default for DirectionalLight { color: Color::WHITE, illuminance: light_consts::lux::AMBIENT_DAYLIGHT, shadows_enabled: false, - soft_shadow_size: None, shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS, shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS, + #[cfg(feature = "experimental_pbr_pcss")] + soft_shadow_size: None, } } } diff --git a/crates/bevy_pbr/src/light/mod.rs b/crates/bevy_pbr/src/light/mod.rs index 65d27d51ce691..0b93c93c6792c 100644 --- a/crates/bevy_pbr/src/light/mod.rs +++ b/crates/bevy_pbr/src/light/mod.rs @@ -1,6 +1,9 @@ use core::ops::DerefMut; -use bevy_ecs::{entity::EntityHashMap, prelude::*}; +use bevy_ecs::{ + entity::{EntityHashMap, EntityHashSet}, + prelude::*, +}; use bevy_math::{ops, Mat4, Vec3A, Vec4}; use bevy_reflect::prelude::*; use bevy_render::{ @@ -836,6 +839,7 @@ pub fn check_dir_light_mesh_visibility( }); } +#[allow(clippy::too_many_arguments)] pub fn check_point_light_mesh_visibility( visible_point_lights: Query<&VisibleClusterableObjects>, mut point_lights: Query<( @@ -872,10 +876,17 @@ pub fn check_point_light_mesh_visibility( visible_entity_ranges: Option>, mut cubemap_visible_entities_queue: Local; 6]>>, mut spot_visible_entities_queue: Local>>, + mut checked_lights: Local, ) { + checked_lights.clear(); + let visible_entity_ranges = visible_entity_ranges.as_deref(); for visible_lights in &visible_point_lights { for light_entity in visible_lights.entities.iter().copied() { + if !checked_lights.insert(light_entity) { + continue; + } + // Point lights if let Ok(( point_light, diff --git a/crates/bevy_pbr/src/light/point_light.rs b/crates/bevy_pbr/src/light/point_light.rs index a351913aabd86..ee08a57ba1e39 100644 --- a/crates/bevy_pbr/src/light/point_light.rs +++ b/crates/bevy_pbr/src/light/point_light.rs @@ -61,6 +61,7 @@ pub struct PointLight { /// /// Note that soft shadows are significantly more expensive to render than /// hard shadows. + #[cfg(feature = "experimental_pbr_pcss")] pub soft_shadows_enabled: bool, /// A bias used when sampling shadow maps to avoid "shadow-acne", or false shadow occlusions @@ -95,10 +96,11 @@ impl Default for PointLight { range: 20.0, radius: 0.0, shadows_enabled: false, - soft_shadows_enabled: false, shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS, shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS, shadow_map_near_z: Self::DEFAULT_SHADOW_MAP_NEAR_Z, + #[cfg(feature = "experimental_pbr_pcss")] + soft_shadows_enabled: false, } } } diff --git a/crates/bevy_pbr/src/light/spot_light.rs b/crates/bevy_pbr/src/light/spot_light.rs index 9b78291b02c4c..845d55e697317 100644 --- a/crates/bevy_pbr/src/light/spot_light.rs +++ b/crates/bevy_pbr/src/light/spot_light.rs @@ -57,6 +57,7 @@ pub struct SpotLight { /// /// Note that soft shadows are significantly more expensive to render than /// hard shadows. + #[cfg(feature = "experimental_pbr_pcss")] pub soft_shadows_enabled: bool, /// A value that adjusts the tradeoff between self-shadowing artifacts and @@ -115,12 +116,13 @@ impl Default for SpotLight { range: 20.0, radius: 0.0, shadows_enabled: false, - soft_shadows_enabled: false, shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS, shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS, shadow_map_near_z: Self::DEFAULT_SHADOW_MAP_NEAR_Z, inner_angle: 0.0, outer_angle: core::f32::consts::FRAC_PI_4, + #[cfg(feature = "experimental_pbr_pcss")] + soft_shadows_enabled: false, } } } diff --git a/crates/bevy_pbr/src/light_probe/environment_map.rs b/crates/bevy_pbr/src/light_probe/environment_map.rs index 4fff505427642..6b47c9915a836 100644 --- a/crates/bevy_pbr/src/light_probe/environment_map.rs +++ b/crates/bevy_pbr/src/light_probe/environment_map.rs @@ -51,6 +51,7 @@ use bevy_ecs::{ bundle::Bundle, component::Component, query::QueryItem, reflect::ReflectComponent, system::lifetimeless::Read, }; +use bevy_image::Image; use bevy_math::Quat; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ @@ -63,7 +64,7 @@ use bevy_render::{ TextureSampleType, TextureView, }, renderer::RenderDevice, - texture::{FallbackImage, GpuImage, Image}, + texture::{FallbackImage, GpuImage}, }; use core::{num::NonZero, ops::Deref}; diff --git a/crates/bevy_pbr/src/light_probe/irradiance_volume.rs b/crates/bevy_pbr/src/light_probe/irradiance_volume.rs index f43d84d4e7c39..bc8fc542fb32d 100644 --- a/crates/bevy_pbr/src/light_probe/irradiance_volume.rs +++ b/crates/bevy_pbr/src/light_probe/irradiance_volume.rs @@ -133,6 +133,7 @@ //! [Why ambient cubes?]: #why-ambient-cubes use bevy_ecs::{component::Component, reflect::ReflectComponent}; +use bevy_image::Image; use bevy_render::{ render_asset::RenderAssets, render_resource::{ @@ -140,7 +141,7 @@ use bevy_render::{ TextureSampleType, TextureView, }, renderer::RenderDevice, - texture::{FallbackImage, GpuImage, Image}, + texture::{FallbackImage, GpuImage}, }; use core::{num::NonZero, ops::Deref}; diff --git a/crates/bevy_pbr/src/light_probe/mod.rs b/crates/bevy_pbr/src/light_probe/mod.rs index 0cda05f055eae..cab462c2801bf 100644 --- a/crates/bevy_pbr/src/light_probe/mod.rs +++ b/crates/bevy_pbr/src/light_probe/mod.rs @@ -12,6 +12,7 @@ use bevy_ecs::{ schedule::IntoSystemConfigs, system::{Commands, Local, Query, Res, ResMut, Resource}, }; +use bevy_image::Image; use bevy_math::{Affine3A, FloatOrd, Mat4, Vec3A, Vec4}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ @@ -22,7 +23,7 @@ use bevy_render::{ renderer::{RenderDevice, RenderQueue}, settings::WgpuFeatures, sync_world::RenderEntity, - texture::{FallbackImage, GpuImage, Image}, + texture::{FallbackImage, GpuImage}, view::{ExtractedView, Visibility}, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; diff --git a/crates/bevy_pbr/src/lightmap/mod.rs b/crates/bevy_pbr/src/lightmap/mod.rs index 982f7214a8c81..0e0debc92e3f3 100644 --- a/crates/bevy_pbr/src/lightmap/mod.rs +++ b/crates/bevy_pbr/src/lightmap/mod.rs @@ -39,6 +39,7 @@ use bevy_ecs::{ schedule::IntoSystemConfigs, system::{Query, Res, ResMut, Resource}, }; +use bevy_image::Image; use bevy_math::{uvec2, vec4, Rect, UVec2}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::sync_world::MainEntityHashMap; @@ -46,7 +47,7 @@ use bevy_render::{ mesh::{Mesh, RenderMesh}, render_asset::RenderAssets, render_resource::Shader, - texture::{GpuImage, Image}, + texture::GpuImage, view::ViewVisibility, Extract, ExtractSchedule, RenderApp, }; diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index 916b5a4676476..65d935226e7ab 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -60,8 +60,9 @@ use core::{ /// ``` /// # use bevy_pbr::{Material, MeshMaterial3d}; /// # use bevy_ecs::prelude::*; +/// # use bevy_image::Image; /// # use bevy_reflect::TypePath; -/// # use bevy_render::{mesh::{Mesh, Mesh3d}, render_resource::{AsBindGroup, ShaderRef}, texture::Image}; +/// # use bevy_render::{mesh::{Mesh, Mesh3d}, render_resource::{AsBindGroup, ShaderRef}}; /// # use bevy_color::LinearRgba; /// # use bevy_color::palettes::basic::RED; /// # use bevy_asset::{Handle, AssetServer, Assets, Asset}; diff --git a/crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs b/crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs index b1b91e8cc5c6a..06582133e1ce9 100644 --- a/crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs +++ b/crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs @@ -200,6 +200,7 @@ pub fn prepare_material_meshlet_meshes_main_opaque_pass( entry_point: material_fragment.entry_point, targets: material_fragment.targets, }), + zero_initialize_workgroup_memory: false, }; let material_id = instance_manager.get_material_id(material_id.untyped()); @@ -353,6 +354,7 @@ pub fn prepare_material_meshlet_meshes_prepass( entry_point, targets: material_fragment.targets, }), + zero_initialize_workgroup_memory: false, }; let material_id = instance_manager.get_material_id(material_id.untyped()); diff --git a/crates/bevy_pbr/src/meshlet/mod.rs b/crates/bevy_pbr/src/meshlet/mod.rs index 74194d043f3f6..70da162be4ef1 100644 --- a/crates/bevy_pbr/src/meshlet/mod.rs +++ b/crates/bevy_pbr/src/meshlet/mod.rs @@ -33,7 +33,9 @@ pub(crate) use self::{ }, }; -pub use self::asset::{MeshletMesh, MeshletMeshLoader, MeshletMeshSaver}; +pub use self::asset::{ + MeshletMesh, MeshletMeshLoader, MeshletMeshSaver, MESHLET_MESH_ASSET_VERSION, +}; #[cfg(feature = "meshlet_processor")] pub use self::from_mesh::{ MeshToMeshletMeshConversionError, MESHLET_DEFAULT_VERTEX_POSITION_QUANTIZATION_FACTOR, diff --git a/crates/bevy_pbr/src/meshlet/pipelines.rs b/crates/bevy_pbr/src/meshlet/pipelines.rs index 0ee25bde6a0d0..ab333e736c7e7 100644 --- a/crates/bevy_pbr/src/meshlet/pipelines.rs +++ b/crates/bevy_pbr/src/meshlet/pipelines.rs @@ -76,6 +76,7 @@ impl FromWorld for MeshletPipelines { shader: MESHLET_FILL_CLUSTER_BUFFERS_SHADER_HANDLE, shader_defs: vec!["MESHLET_FILL_CLUSTER_BUFFERS_PASS".into()], entry_point: "fill_cluster_buffers".into(), + zero_initialize_workgroup_memory: false, }, ), @@ -92,6 +93,7 @@ impl FromWorld for MeshletPipelines { "MESHLET_FIRST_CULLING_PASS".into(), ], entry_point: "cull_clusters".into(), + zero_initialize_workgroup_memory: false, }), cull_second: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { @@ -107,6 +109,7 @@ impl FromWorld for MeshletPipelines { "MESHLET_SECOND_CULLING_PASS".into(), ], entry_point: "cull_clusters".into(), + zero_initialize_workgroup_memory: false, }), downsample_depth_first: pipeline_cache.queue_compute_pipeline( @@ -120,6 +123,7 @@ impl FromWorld for MeshletPipelines { shader: MESHLET_DOWNSAMPLE_DEPTH_SHADER_HANDLE, shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into()], entry_point: "downsample_depth_first".into(), + zero_initialize_workgroup_memory: false, }, ), @@ -134,6 +138,7 @@ impl FromWorld for MeshletPipelines { shader: MESHLET_DOWNSAMPLE_DEPTH_SHADER_HANDLE, shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into()], entry_point: "downsample_depth_second".into(), + zero_initialize_workgroup_memory: false, }, ), @@ -148,6 +153,7 @@ impl FromWorld for MeshletPipelines { shader: MESHLET_DOWNSAMPLE_DEPTH_SHADER_HANDLE, shader_defs: vec![], entry_point: "downsample_depth_first".into(), + zero_initialize_workgroup_memory: false, }, ), @@ -162,6 +168,7 @@ impl FromWorld for MeshletPipelines { shader: MESHLET_DOWNSAMPLE_DEPTH_SHADER_HANDLE, shader_defs: vec![], entry_point: "downsample_depth_second".into(), + zero_initialize_workgroup_memory: false, }, ), @@ -182,6 +189,7 @@ impl FromWorld for MeshletPipelines { .into(), ], entry_point: "rasterize_cluster".into(), + zero_initialize_workgroup_memory: false, }, ), @@ -203,6 +211,7 @@ impl FromWorld for MeshletPipelines { .into(), ], entry_point: "rasterize_cluster".into(), + zero_initialize_workgroup_memory: false, }, ), @@ -226,6 +235,7 @@ impl FromWorld for MeshletPipelines { .into(), ], entry_point: "rasterize_cluster".into(), + zero_initialize_workgroup_memory: false, }), visibility_buffer_hardware_raster: pipeline_cache.queue_render_pipeline( @@ -269,6 +279,7 @@ impl FromWorld for MeshletPipelines { write_mask: ColorWrites::empty(), })], }), + zero_initialize_workgroup_memory: false, }, ), @@ -309,6 +320,7 @@ impl FromWorld for MeshletPipelines { write_mask: ColorWrites::empty(), })], }), + zero_initialize_workgroup_memory: false, }, ), @@ -356,6 +368,7 @@ impl FromWorld for MeshletPipelines { write_mask: ColorWrites::empty(), })], }), + zero_initialize_workgroup_memory: false, }), resolve_depth: pipeline_cache.queue_render_pipeline(RenderPipelineDescriptor { @@ -381,6 +394,7 @@ impl FromWorld for MeshletPipelines { entry_point: "resolve_depth".into(), targets: vec![], }), + zero_initialize_workgroup_memory: false, }), resolve_depth_shadow_view: pipeline_cache.queue_render_pipeline( @@ -407,6 +421,7 @@ impl FromWorld for MeshletPipelines { entry_point: "resolve_depth".into(), targets: vec![], }), + zero_initialize_workgroup_memory: false, }, ), @@ -434,6 +449,7 @@ impl FromWorld for MeshletPipelines { entry_point: "resolve_material_depth".into(), targets: vec![], }), + zero_initialize_workgroup_memory: false, }, ), @@ -448,6 +464,7 @@ impl FromWorld for MeshletPipelines { shader: MESHLET_REMAP_1D_TO_2D_DISPATCH_SHADER_HANDLE, shader_defs: vec![], entry_point: "remap_dispatch".into(), + zero_initialize_workgroup_memory: false, }) }), } diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index 6fae9a60accdc..8bd10f9678984 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -571,6 +571,7 @@ where }, push_constant_ranges: vec![], label: Some("prepass_pipeline".into()), + zero_initialize_workgroup_memory: false, }; // This is a bit risky because it's possible to change something that would diff --git a/crates/bevy_pbr/src/render/gpu_preprocess.rs b/crates/bevy_pbr/src/render/gpu_preprocess.rs index ee28005a8c5cb..2350b8c7043e5 100644 --- a/crates/bevy_pbr/src/render/gpu_preprocess.rs +++ b/crates/bevy_pbr/src/render/gpu_preprocess.rs @@ -290,6 +290,7 @@ impl SpecializedComputePipeline for PreprocessPipeline { shader: MESH_PREPROCESS_SHADER_HANDLE, shader_defs, entry_point: "main".into(), + zero_initialize_workgroup_memory: false, } } } diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index bd0248789d5b3..b307429c8ee92 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -9,6 +9,7 @@ use bevy_ecs::{ system::lifetimeless::Read, }; use bevy_math::{ops, Mat4, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; +use bevy_render::camera::SortedCameras; use bevy_render::sync_world::{MainEntity, RenderEntity, TemporaryRenderEntity}; use bevy_render::{ diagnostic::RecordDiagnostics, @@ -29,6 +30,7 @@ use bevy_utils::tracing::info_span; use bevy_utils::{ default, tracing::{error, warn}, + HashMap, }; use core::{hash::Hash, ops::Range}; @@ -41,12 +43,12 @@ pub struct ExtractedPointLight { pub radius: f32, pub transform: GlobalTransform, pub shadows_enabled: bool, - pub soft_shadows_enabled: bool, pub shadow_depth_bias: f32, pub shadow_normal_bias: f32, pub shadow_map_near_z: f32, pub spot_light_angles: Option<(f32, f32)>, pub volumetric: bool, + pub soft_shadows_enabled: bool, } #[derive(Component, Debug)] @@ -56,13 +58,13 @@ pub struct ExtractedDirectionalLight { pub transform: GlobalTransform, pub shadows_enabled: bool, pub volumetric: bool, - pub soft_shadow_size: Option, pub shadow_depth_bias: f32, pub shadow_normal_bias: f32, pub cascade_shadow_config: CascadeShadowConfig, pub cascades: EntityHashMap>, pub frusta: EntityHashMap>, pub render_layers: RenderLayers, + pub soft_shadow_size: Option, } // NOTE: These must match the bit flags in bevy_pbr/src/render/mesh_view_types.wgsl! @@ -147,10 +149,10 @@ pub const MAX_CASCADES_PER_LIGHT: usize = 1; #[derive(Resource, Clone)] pub struct ShadowSamplers { pub point_light_comparison_sampler: Sampler, - #[cfg(feature = "pbr_pcss")] + #[cfg(feature = "experimental_pbr_pcss")] pub point_light_linear_sampler: Sampler, pub directional_light_comparison_sampler: Sampler, - #[cfg(feature = "pbr_pcss")] + #[cfg(feature = "experimental_pbr_pcss")] pub directional_light_linear_sampler: Sampler, } @@ -174,7 +176,7 @@ impl FromWorld for ShadowSamplers { compare: Some(CompareFunction::GreaterEqual), ..base_sampler_descriptor }), - #[cfg(feature = "pbr_pcss")] + #[cfg(feature = "experimental_pbr_pcss")] point_light_linear_sampler: render_device.create_sampler(&base_sampler_descriptor), directional_light_comparison_sampler: render_device.create_sampler( &SamplerDescriptor { @@ -182,7 +184,7 @@ impl FromWorld for ShadowSamplers { ..base_sampler_descriptor }, ), - #[cfg(feature = "pbr_pcss")] + #[cfg(feature = "experimental_pbr_pcss")] directional_light_linear_sampler: render_device .create_sampler(&base_sampler_descriptor), } @@ -291,7 +293,6 @@ pub fn extract_lights( radius: point_light.radius, transform: *transform, shadows_enabled: point_light.shadows_enabled, - soft_shadows_enabled: point_light.soft_shadows_enabled, shadow_depth_bias: point_light.shadow_depth_bias, // The factor of SQRT_2 is for the worst-case diagonal offset shadow_normal_bias: point_light.shadow_normal_bias @@ -300,6 +301,10 @@ pub fn extract_lights( shadow_map_near_z: point_light.shadow_map_near_z, spot_light_angles: None, volumetric: volumetric_light.is_some(), + #[cfg(feature = "experimental_pbr_pcss")] + soft_shadows_enabled: point_light.soft_shadows_enabled, + #[cfg(not(feature = "experimental_pbr_pcss"))] + soft_shadows_enabled: false, }; point_lights_values.push(( render_entity, @@ -350,7 +355,6 @@ pub fn extract_lights( radius: spot_light.radius, transform: *transform, shadows_enabled: spot_light.shadows_enabled, - soft_shadows_enabled: spot_light.soft_shadows_enabled, shadow_depth_bias: spot_light.shadow_depth_bias, // The factor of SQRT_2 is for the worst-case diagonal offset shadow_normal_bias: spot_light.shadow_normal_bias @@ -359,6 +363,10 @@ pub fn extract_lights( shadow_map_near_z: spot_light.shadow_map_near_z, spot_light_angles: Some((spot_light.inner_angle, spot_light.outer_angle)), volumetric: volumetric_light.is_some(), + #[cfg(feature = "experimental_pbr_pcss")] + soft_shadows_enabled: spot_light.soft_shadows_enabled, + #[cfg(not(feature = "experimental_pbr_pcss"))] + soft_shadows_enabled: false, }, render_visible_entities, *frustum, @@ -430,7 +438,10 @@ pub fn extract_lights( illuminance: directional_light.illuminance, transform: *transform, volumetric: volumetric_light.is_some(), + #[cfg(feature = "experimental_pbr_pcss")] soft_shadow_size: directional_light.soft_shadow_size, + #[cfg(not(feature = "experimental_pbr_pcss"))] + soft_shadow_size: None, shadows_enabled: directional_light.shadows_enabled, shadow_depth_bias: directional_light.shadow_depth_bias, // The factor of SQRT_2 is for the worst-case diagonal offset @@ -677,10 +688,11 @@ pub fn prepare_lights( point_light_shadow_map: Res, directional_light_shadow_map: Res, mut shadow_render_phases: ResMut>, - (mut max_directional_lights_warning_emitted, mut max_cascades_per_light_warning_emitted): ( - Local, - Local, - ), + ( + mut max_directional_lights_warning_emitted, + mut max_cascades_per_light_warning_emitted, + mut live_shadow_mapping_lights, + ): (Local, Local, Local), point_lights: Query<( Entity, &ExtractedPointLight, @@ -688,7 +700,7 @@ pub fn prepare_lights( )>, directional_lights: Query<(Entity, &ExtractedDirectionalLight)>, mut light_view_entities: Query<&mut LightViewEntities>, - mut live_shadow_mapping_lights: Local, + sorted_cameras: Res, ) { let views_iter = views.iter(); let views_count = views_iter.len(); @@ -911,17 +923,17 @@ pub fn prepare_lights( .extend(1.0 / (light.range * light.range)), position_radius: light.transform.translation().extend(light.radius), flags: flags.bits(), - soft_shadow_size: if light.soft_shadows_enabled { - light.radius - } else { - 0.0 - }, shadow_depth_bias: light.shadow_depth_bias, shadow_normal_bias: light.shadow_normal_bias, shadow_map_near_z: light.shadow_map_near_z, spot_light_tan_angle, pad_a: 0.0, pad_b: 0.0, + soft_shadow_size: if light.soft_shadows_enabled { + light.radius + } else { + 0.0 + }, }); global_light_meta.entity_to_index.insert(entity, index); } @@ -984,50 +996,108 @@ pub fn prepare_lights( live_shadow_mapping_lights.clear(); + let mut point_light_depth_attachments = HashMap::::default(); + let mut directional_light_depth_attachments = HashMap::::default(); + + let point_light_depth_texture = texture_cache.get( + &render_device, + TextureDescriptor { + size: Extent3d { + width: point_light_shadow_map.size as u32, + height: point_light_shadow_map.size as u32, + depth_or_array_layers: point_light_shadow_maps_count.max(1) as u32 * 6, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: CORE_3D_DEPTH_FORMAT, + label: Some("point_light_shadow_map_texture"), + usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + ); + + let point_light_depth_texture_view = + point_light_depth_texture + .texture + .create_view(&TextureViewDescriptor { + label: Some("point_light_shadow_map_array_texture_view"), + format: None, + // NOTE: iOS Simulator is missing CubeArray support so we use Cube instead. + // See https://github.com/bevyengine/bevy/pull/12052 - remove if support is added. + #[cfg(all( + not(feature = "ios_simulator"), + any( + not(feature = "webgl"), + not(target_arch = "wasm32"), + feature = "webgpu" + ) + ))] + dimension: Some(TextureViewDimension::CubeArray), + #[cfg(any( + feature = "ios_simulator", + all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")) + ))] + dimension: Some(TextureViewDimension::Cube), + aspect: TextureAspect::DepthOnly, + base_mip_level: 0, + mip_level_count: None, + base_array_layer: 0, + array_layer_count: None, + }); + + let directional_light_depth_texture = texture_cache.get( + &render_device, + TextureDescriptor { + size: Extent3d { + width: (directional_light_shadow_map.size as u32) + .min(render_device.limits().max_texture_dimension_2d), + height: (directional_light_shadow_map.size as u32) + .min(render_device.limits().max_texture_dimension_2d), + depth_or_array_layers: (num_directional_cascades_enabled + + spot_light_shadow_maps_count) + .max(1) as u32, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: CORE_3D_DEPTH_FORMAT, + label: Some("directional_light_shadow_map_texture"), + usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + ); + + let directional_light_depth_texture_view = + directional_light_depth_texture + .texture + .create_view(&TextureViewDescriptor { + label: Some("directional_light_shadow_map_array_texture_view"), + format: None, + #[cfg(any( + not(feature = "webgl"), + not(target_arch = "wasm32"), + feature = "webgpu" + ))] + dimension: Some(TextureViewDimension::D2Array), + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + dimension: Some(TextureViewDimension::D2), + aspect: TextureAspect::DepthOnly, + base_mip_level: 0, + mip_level_count: None, + base_array_layer: 0, + array_layer_count: None, + }); + let mut live_views = EntityHashSet::with_capacity_and_hasher(views_count, EntityHash); // set up light data for each view - for (entity, extracted_view, clusters, maybe_layers) in views.iter() { + for (entity, extracted_view, clusters, maybe_layers) in sorted_cameras + .0 + .iter() + .filter_map(|sorted_camera| views.get(sorted_camera.entity).ok()) + { live_views.insert(entity); - - let point_light_depth_texture = texture_cache.get( - &render_device, - TextureDescriptor { - size: Extent3d { - width: point_light_shadow_map.size as u32, - height: point_light_shadow_map.size as u32, - depth_or_array_layers: point_light_shadow_maps_count.max(1) as u32 * 6, - }, - mip_level_count: 1, - sample_count: 1, - dimension: TextureDimension::D2, - format: CORE_3D_DEPTH_FORMAT, - label: Some("point_light_shadow_map_texture"), - usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }, - ); - let directional_light_depth_texture = texture_cache.get( - &render_device, - TextureDescriptor { - size: Extent3d { - width: (directional_light_shadow_map.size as u32) - .min(render_device.limits().max_texture_dimension_2d), - height: (directional_light_shadow_map.size as u32) - .min(render_device.limits().max_texture_dimension_2d), - depth_or_array_layers: (num_directional_cascades_enabled - + spot_light_shadow_maps_count) - .max(1) as u32, - }, - mip_level_count: 1, - sample_count: 1, - dimension: TextureDimension::D2, - format: CORE_3D_DEPTH_FORMAT, - label: Some("directional_light_shadow_map_texture"), - usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }, - ); let mut view_lights = Vec::new(); let is_orthographic = extracted_view.clip_from_view.w_axis.w == 1.0; @@ -1102,23 +1172,35 @@ pub fn prepare_lights( .zip(light_view_entities.iter().copied()) .enumerate() { - let depth_texture_view = - point_light_depth_texture - .texture - .create_view(&TextureViewDescriptor { - label: Some("point_light_shadow_map_texture_view"), - format: None, - dimension: Some(TextureViewDimension::D2), - aspect: TextureAspect::All, - base_mip_level: 0, - mip_level_count: None, - base_array_layer: (light_index * 6 + face_index) as u32, - array_layer_count: Some(1u32), - }); + let mut first = false; + let base_array_layer = (light_index * 6 + face_index) as u32; + + let depth_attachment = point_light_depth_attachments + .entry(base_array_layer) + .or_insert_with(|| { + first = true; + + let depth_texture_view = + point_light_depth_texture + .texture + .create_view(&TextureViewDescriptor { + label: Some("point_light_shadow_map_texture_view"), + format: None, + dimension: Some(TextureViewDimension::D2), + aspect: TextureAspect::All, + base_mip_level: 0, + mip_level_count: None, + base_array_layer, + array_layer_count: Some(1u32), + }); + + DepthAttachment::new(depth_texture_view, Some(0.0)) + }) + .clone(); commands.entity(view_light_entity).insert(( ShadowView { - depth_attachment: DepthAttachment::new(depth_texture_view, Some(0.0)), + depth_attachment, pass_name: format!( "shadow pass point light {} {}", light_index, @@ -1147,8 +1229,11 @@ pub fn prepare_lights( view_lights.push(view_light_entity); - shadow_render_phases.insert_or_clear(view_light_entity); - live_shadow_mapping_lights.insert(view_light_entity); + if first { + // Subsequent views with the same light entity will reuse the same shadow map + shadow_render_phases.insert_or_clear(view_light_entity); + live_shadow_mapping_lights.insert(view_light_entity); + } } } @@ -1177,19 +1262,30 @@ pub fn prepare_lights( [point_light_count..point_light_count + spot_light_shadow_maps_count] are spot lights").1; let spot_projection = spot_light_clip_from_view(angle, light.shadow_map_near_z); - let depth_texture_view = - directional_light_depth_texture - .texture - .create_view(&TextureViewDescriptor { - label: Some("spot_light_shadow_map_texture_view"), - format: None, - dimension: Some(TextureViewDimension::D2), - aspect: TextureAspect::All, - base_mip_level: 0, - mip_level_count: None, - base_array_layer: (num_directional_cascades_enabled + light_index) as u32, - array_layer_count: Some(1u32), - }); + let mut first = false; + let base_array_layer = (num_directional_cascades_enabled + light_index) as u32; + + let depth_attachment = directional_light_depth_attachments + .entry(base_array_layer) + .or_insert_with(|| { + first = true; + + let depth_texture_view = directional_light_depth_texture.texture.create_view( + &TextureViewDescriptor { + label: Some("spot_light_shadow_map_texture_view"), + format: None, + dimension: Some(TextureViewDimension::D2), + aspect: TextureAspect::All, + base_mip_level: 0, + mip_level_count: None, + base_array_layer, + array_layer_count: Some(1u32), + }, + ); + + DepthAttachment::new(depth_texture_view, Some(0.0)) + }) + .clone(); let light_view_entities = light_view_entities .entry(entity) @@ -1199,7 +1295,7 @@ pub fn prepare_lights( commands.entity(view_light_entity).insert(( ShadowView { - depth_attachment: DepthAttachment::new(depth_texture_view, Some(0.0)), + depth_attachment, pass_name: format!("shadow pass spot light {light_index}"), }, ExtractedView { @@ -1221,8 +1317,11 @@ pub fn prepare_lights( view_lights.push(view_light_entity); - shadow_render_phases.insert_or_clear(view_light_entity); - live_shadow_mapping_lights.insert(view_light_entity); + if first { + // Subsequent views with the same light entity will reuse the same shadow map + shadow_render_phases.insert_or_clear(view_light_entity); + live_shadow_mapping_lights.insert(view_light_entity); + } } // directional lights @@ -1307,6 +1406,12 @@ pub fn prepare_lights( base_array_layer: directional_depth_texture_array_index, array_layer_count: Some(1u32), }); + + // NOTE: For point and spotlights, we reuse the same depth attachment for all views. + // However, for directional lights, we want a new depth attachment for each view, + // so that the view is cleared for each view. + let depth_attachment = DepthAttachment::new(depth_texture_view, Some(0.0)); + directional_depth_texture_array_index += 1; let mut frustum = *frustum; @@ -1316,7 +1421,7 @@ pub fn prepare_lights( commands.entity(view_light_entity).insert(( ShadowView { - depth_attachment: DepthAttachment::new(depth_texture_view, Some(0.0)), + depth_attachment, pass_name: format!( "shadow pass directional light {light_index} cascade {cascade_index}" ), @@ -1342,65 +1447,19 @@ pub fn prepare_lights( )); view_lights.push(view_light_entity); + // Subsequent views with the same light entity will **NOT** reuse the same shadow map + // (Because the cascades are unique to each view) shadow_render_phases.insert_or_clear(view_light_entity); live_shadow_mapping_lights.insert(view_light_entity); } } - let point_light_depth_texture_view = - point_light_depth_texture - .texture - .create_view(&TextureViewDescriptor { - label: Some("point_light_shadow_map_array_texture_view"), - format: None, - // NOTE: iOS Simulator is missing CubeArray support so we use Cube instead. - // See https://github.com/bevyengine/bevy/pull/12052 - remove if support is added. - #[cfg(all( - not(feature = "ios_simulator"), - any( - not(feature = "webgl"), - not(target_arch = "wasm32"), - feature = "webgpu" - ) - ))] - dimension: Some(TextureViewDimension::CubeArray), - #[cfg(any( - feature = "ios_simulator", - all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")) - ))] - dimension: Some(TextureViewDimension::Cube), - aspect: TextureAspect::DepthOnly, - base_mip_level: 0, - mip_level_count: None, - base_array_layer: 0, - array_layer_count: None, - }); - let directional_light_depth_texture_view = directional_light_depth_texture - .texture - .create_view(&TextureViewDescriptor { - label: Some("directional_light_shadow_map_array_texture_view"), - format: None, - #[cfg(any( - not(feature = "webgl"), - not(target_arch = "wasm32"), - feature = "webgpu" - ))] - dimension: Some(TextureViewDimension::D2Array), - #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] - dimension: Some(TextureViewDimension::D2), - aspect: TextureAspect::DepthOnly, - base_mip_level: 0, - mip_level_count: None, - base_array_layer: 0, - array_layer_count: None, - }); - commands.entity(entity).insert(( ViewShadowBindings { - point_light_depth_texture: point_light_depth_texture.texture, - point_light_depth_texture_view, - directional_light_depth_texture: directional_light_depth_texture.texture, - directional_light_depth_texture_view, + point_light_depth_texture: point_light_depth_texture.texture.clone(), + point_light_depth_texture_view: point_light_depth_texture_view.clone(), + directional_light_depth_texture: directional_light_depth_texture.texture.clone(), + directional_light_depth_texture_view: directional_light_depth_texture_view.clone(), }, ViewLightEntities { lights: view_lights, diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index aea88e0246ed9..e8adff671b01e 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -14,6 +14,7 @@ use bevy_ecs::{ query::ROQueryItem, system::{lifetimeless::*, SystemParamItem, SystemState}, }; +use bevy_image::{BevyDefault, ImageSampler, TextureFormatPixelInfo}; use bevy_math::{Affine3, Rect, UVec2, Vec3, Vec4}; use bevy_render::{ batching::{ @@ -32,7 +33,7 @@ use bevy_render::{ }, render_resource::*, renderer::{RenderDevice, RenderQueue}, - texture::{BevyDefault, DefaultImageSampler, ImageSampler, TextureFormatPixelInfo}, + texture::DefaultImageSampler, view::{ prepare_view_targets, GpuCulling, RenderVisibilityRanges, ViewTarget, ViewUniformOffset, ViewVisibility, VisibilityRange, @@ -1858,7 +1859,7 @@ impl SpecializedMeshPipeline for MeshPipeline { #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] shader_defs.push("WEBGL2".into()); - #[cfg(feature = "pbr_pcss")] + #[cfg(feature = "experimental_pbr_pcss")] shader_defs.push("PCSS_SAMPLERS_AVAILABLE".into()); if key.contains(MeshPipelineKey::TONEMAP_IN_SHADER) { @@ -2020,6 +2021,7 @@ impl SpecializedMeshPipeline for MeshPipeline { alpha_to_coverage_enabled, }, label: Some(label), + zero_initialize_workgroup_memory: false, }) } } diff --git a/crates/bevy_pbr/src/render/mesh_view_bindings.rs b/crates/bevy_pbr/src/render/mesh_view_bindings.rs index 159a87bd1c25b..c3a723164e21f 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.rs +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.rs @@ -15,13 +15,14 @@ use bevy_ecs::{ system::{Commands, Query, Res, Resource}, world::{FromWorld, World}, }; +use bevy_image::BevyDefault as _; use bevy_math::Vec4; use bevy_render::{ globals::{GlobalsBuffer, GlobalsUniform}, render_asset::RenderAssets, render_resource::{binding_types::*, *}, renderer::RenderDevice, - texture::{BevyDefault, FallbackImage, FallbackImageMsaa, FallbackImageZero, GpuImage}, + texture::{FallbackImage, FallbackImageMsaa, FallbackImageZero, GpuImage}, view::{ Msaa, RenderVisibilityRanges, ViewUniform, ViewUniforms, VISIBILITY_RANGES_STORAGE_BUFFER_COUNT, @@ -227,7 +228,7 @@ fn layout_entries( // Point Shadow Texture Array Comparison Sampler (3, sampler(SamplerBindingType::Comparison)), // Point Shadow Texture Array Linear Sampler - #[cfg(feature = "pbr_pcss")] + #[cfg(feature = "experimental_pbr_pcss")] (4, sampler(SamplerBindingType::Filtering)), // Directional Shadow Texture Array ( @@ -244,7 +245,7 @@ fn layout_entries( // Directional Shadow Texture Array Comparison Sampler (6, sampler(SamplerBindingType::Comparison)), // Directional Shadow Texture Array Linear Sampler - #[cfg(feature = "pbr_pcss")] + #[cfg(feature = "experimental_pbr_pcss")] (7, sampler(SamplerBindingType::Filtering)), // PointLights ( @@ -579,11 +580,11 @@ pub fn prepare_mesh_view_bind_groups( (1, light_binding.clone()), (2, &shadow_bindings.point_light_depth_texture_view), (3, &shadow_samplers.point_light_comparison_sampler), - #[cfg(feature = "pbr_pcss")] + #[cfg(feature = "experimental_pbr_pcss")] (4, &shadow_samplers.point_light_linear_sampler), (5, &shadow_bindings.directional_light_depth_texture_view), (6, &shadow_samplers.directional_light_comparison_sampler), - #[cfg(feature = "pbr_pcss")] + #[cfg(feature = "experimental_pbr_pcss")] (7, &shadow_samplers.directional_light_linear_sampler), (8, clusterable_objects_binding.clone()), ( diff --git a/crates/bevy_pbr/src/ssao/mod.rs b/crates/bevy_pbr/src/ssao/mod.rs index 42ac9978f6192..366ac91e22acd 100644 --- a/crates/bevy_pbr/src/ssao/mod.rs +++ b/crates/bevy_pbr/src/ssao/mod.rs @@ -448,6 +448,7 @@ impl FromWorld for SsaoPipelines { shader: PREPROCESS_DEPTH_SHADER_HANDLE, shader_defs: Vec::new(), entry_point: "preprocess_depth".into(), + zero_initialize_workgroup_memory: false, }); let spatial_denoise_pipeline = @@ -461,6 +462,7 @@ impl FromWorld for SsaoPipelines { shader: SPATIAL_DENOISE_SHADER_HANDLE, shader_defs: Vec::new(), entry_point: "spatial_denoise".into(), + zero_initialize_workgroup_memory: false, }); Self { @@ -513,6 +515,7 @@ impl SpecializedComputePipeline for SsaoPipelines { shader: SSAO_SHADER_HANDLE, shader_defs, entry_point: "ssao".into(), + zero_initialize_workgroup_memory: false, } } } diff --git a/crates/bevy_pbr/src/ssr/mod.rs b/crates/bevy_pbr/src/ssr/mod.rs index 5852315f58d84..935e5f5b8f22e 100644 --- a/crates/bevy_pbr/src/ssr/mod.rs +++ b/crates/bevy_pbr/src/ssr/mod.rs @@ -23,6 +23,7 @@ use bevy_ecs::{ system::{lifetimeless::Read, Commands, Query, Res, ResMut, Resource}, world::{FromWorld, World}, }; +use bevy_image::BevyDefault as _; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ extract_component::{ExtractComponent, ExtractComponentPlugin}, @@ -36,7 +37,6 @@ use bevy_render::{ TextureFormat, TextureSampleType, }, renderer::{RenderContext, RenderDevice, RenderQueue}, - texture::BevyDefault as _, view::{ExtractedView, Msaa, ViewTarget, ViewUniformOffset}, Render, RenderApp, RenderSet, }; @@ -560,6 +560,7 @@ impl SpecializedRenderPipeline for ScreenSpaceReflectionsPipeline { primitive: default(), depth_stencil: None, multisample: default(), + zero_initialize_workgroup_memory: false, } } } diff --git a/crates/bevy_pbr/src/volumetric_fog/mod.rs b/crates/bevy_pbr/src/volumetric_fog/mod.rs index 7a94a1e135797..529127649529c 100644 --- a/crates/bevy_pbr/src/volumetric_fog/mod.rs +++ b/crates/bevy_pbr/src/volumetric_fog/mod.rs @@ -42,6 +42,7 @@ use bevy_ecs::{ bundle::Bundle, component::Component, reflect::ReflectComponent, schedule::IntoSystemConfigs as _, }; +use bevy_image::Image; use bevy_math::{ primitives::{Cuboid, Plane3d}, Vec2, Vec3, @@ -52,7 +53,6 @@ use bevy_render::{ render_graph::{RenderGraphApp, ViewNodeRunner}, render_resource::{Shader, SpecializedRenderPipelines}, sync_component::SyncComponentPlugin, - texture::Image, view::{InheritedVisibility, ViewVisibility, Visibility}, ExtractSchedule, Render, RenderApp, RenderSet, }; diff --git a/crates/bevy_pbr/src/volumetric_fog/render.rs b/crates/bevy_pbr/src/volumetric_fog/render.rs index d1dd500f44396..76039bbbe448b 100644 --- a/crates/bevy_pbr/src/volumetric_fog/render.rs +++ b/crates/bevy_pbr/src/volumetric_fog/render.rs @@ -16,6 +16,7 @@ use bevy_ecs::{ system::{lifetimeless::Read, Commands, Local, Query, Res, ResMut, Resource}, world::{FromWorld, World}, }; +use bevy_image::{BevyDefault, Image}; use bevy_math::{vec4, Mat3A, Mat4, Vec3, Vec3A, Vec4, Vec4Swizzles as _}; use bevy_render::{ mesh::{ @@ -37,7 +38,7 @@ use bevy_render::{ }, renderer::{RenderContext, RenderDevice, RenderQueue}, sync_world::RenderEntity, - texture::{BevyDefault as _, GpuImage, Image}, + texture::GpuImage, view::{ExtractedView, Msaa, ViewDepthTexture, ViewTarget, ViewUniformOffset}, Extract, }; @@ -600,6 +601,7 @@ impl SpecializedRenderPipeline for VolumetricFogPipeline { write_mask: ColorWrites::ALL, })], }), + zero_initialize_workgroup_memory: false, } } } diff --git a/crates/bevy_picking/Cargo.toml b/crates/bevy_picking/Cargo.toml index d4731e001b0cb..3deba7d21bcba 100644 --- a/crates/bevy_picking/Cargo.toml +++ b/crates/bevy_picking/Cargo.toml @@ -9,7 +9,7 @@ license = "MIT OR Apache-2.0" [features] # Provides a mesh picking backend -bevy_mesh = ["dep:bevy_mesh", "dep:crossbeam-channel"] +bevy_mesh_picking_backend = ["dep:bevy_mesh", "dep:crossbeam-channel"] [dependencies] bevy_app = { path = "../bevy_app", version = "0.15.0-dev" } diff --git a/crates/bevy_picking/src/backend.rs b/crates/bevy_picking/src/backend.rs index 49567d47d624b..6ddb1a7ffe4ce 100644 --- a/crates/bevy_picking/src/backend.rs +++ b/crates/bevy_picking/src/backend.rs @@ -6,10 +6,10 @@ //! detail. //! //! Because `bevy_picking` is very loosely coupled with its backends, you can mix and match as -//! many backends as you want. For example, You could use the `rapier` backend to raycast against +//! many backends as you want. For example, you could use the `rapier` backend to raycast against //! physics objects, a picking shader backend to pick non-physics meshes, and the `bevy_ui` backend -//! for your UI. The [`PointerHits`]s produced by these various backends will be combined, sorted, -//! and used as a homogeneous input for the picking systems that consume these events. +//! for your UI. The [`PointerHits`] instances produced by these various backends will be combined, +//! sorted, and used as a homogeneous input for the picking systems that consume these events. //! //! ## Implementation //! @@ -22,7 +22,7 @@ //! //! - Backends do not need to consider the [`PickingBehavior`](crate::PickingBehavior) component, though they may //! use it for optimization purposes. For example, a backend that traverses a spatial hierarchy -//! may want to early exit if it intersects an entity that blocks lower entities from being +//! may want to exit early if it intersects an entity that blocks lower entities from being //! picked. //! //! ### Raycasting Backends @@ -49,9 +49,8 @@ pub mod prelude { /// An event produced by a picking backend after it has run its hit tests, describing the entities /// under a pointer. /// -/// Some backends may only support providing the topmost entity; this is a valid limitation of some -/// backends. For example, a picking shader might only have data on the topmost rendered output from -/// its buffer. +/// Some backends may only support providing the topmost entity; this is a valid limitation. For +/// example, a picking shader might only have data on the topmost rendered output from its buffer. /// /// Note that systems reading these events in [`PreUpdate`](bevy_app) will not report ordering /// ambiguities with picking backends. Take care to ensure such systems are explicitly ordered diff --git a/crates/bevy_picking/src/events.rs b/crates/bevy_picking/src/events.rs index 32c34b575672e..e75d6a68196a9 100644 --- a/crates/bevy_picking/src/events.rs +++ b/crates/bevy_picking/src/events.rs @@ -321,28 +321,51 @@ pub struct PickingEventWriters<'w> { /// Dispatches interaction events to the target entities. /// /// Within a single frame, events are dispatched in the following order: -/// + The sequence [`DragEnter`], [`Over`]. +/// + [`Out`] → [`DragLeave`]. +/// + [`DragEnter`] → [`Over`]. /// + Any number of any of the following: -/// + For each movement: The sequence [`DragStart`], [`Drag`], [`DragOver`], [`Move`]. -/// + For each button press: Either [`Down`], or the sequence [`Click`], [`Up`], [`DragDrop`], [`DragEnd`], [`DragLeave`]. -/// + For each pointer cancellation: Simply [`Cancel`]. -/// + Finally the sequence [`Out`], [`DragLeave`]. +/// + For each movement: [`DragStart`] → [`Drag`] → [`DragOver`] → [`Move`]. +/// + For each button press: [`Down`] or [`Click`] → [`Up`] → [`DragDrop`] → [`DragEnd`] → [`DragLeave`]. +/// + For each pointer cancellation: [`Cancel`]. /// -/// Only the last event in a given sequence is garenteed to be present. +/// Additionally, across multiple frames, the following are also strictly +/// ordered by the interaction state machine: +/// + When a pointer moves over the target: +/// [`Over`], [`Move`], [`Out`]. +/// + When a pointer presses buttons on the target: +/// [`Down`], [`Click`], [`Up`]. +/// + When a pointer drags the target: +/// [`DragStart`], [`Drag`], [`DragEnd`]. +/// + When a pointer drags something over the target: +/// [`DragEnter`], [`DragOver`], [`DragDrop`], [`DragLeave`]. +/// + When a pointer is canceled: +/// No other events will follow the [`Cancel`] event for that pointer. /// -/// Additionally, across multiple frames, the following are also strictly ordered by the interaction state machine: -/// + When a pointer moves over the target: [`Over`], [`Move`], [`Out`]. -/// + When a pointer presses buttons on the target: [`Down`], [`Up`], [`Click`]. -/// + When a pointer drags the target: [`DragStart`], [`Drag`], [`DragEnd`]. -/// + When a pointer drags something over the target: [`DragEnter`], [`DragOver`], [`DragDrop`], [`DragLeave`]. -/// + When a pointer is canceled: No other events will follow the [`Cancel`] event for that pointer. +/// Two events -- [`Over`] and [`Out`] -- are driven only by the [`HoverMap`]. +/// The rest rely on additional data from the [`PointerInput`] event stream. To +/// receive these events for a custom pointer, you must add [`PointerInput`] +/// events. /// -/// Two events -- [`Over`] and [`Out`] -- are driven only by the [`HoverMap`]. The rest rely on additional data from the -/// [`PointerInput`] event stream. To receive these events for a custom pointer, you must add [`PointerInput`] events. +/// When the pointer goes from hovering entity A to entity B, entity A will +/// receive [`Out`] and then entity B will receive [`Over`]. No entity will ever +/// receive both an [`Over`] and and a [`Out`] event during the same frame. /// -/// Note: Though it is common for the [`PointerInput`] stream may contain multiple pointer movements and presses each frame, -/// the hover state is determined only by the pointer's *final position*. Since the hover state ultimately determines which -/// entities receive events, this may mean that an entity can receive events which occurred before it was actually hovered. +/// When we account for event bubbling, this is no longer true. When focus shifts +/// between children, parent entities may receive redundant [`Out`] → [`Over`] pairs. +/// In the context of UI, this is especially problematic. Additional hierarchy-aware +/// events will be added in a future release. +/// +/// Both [`Click`] and [`Up`] target the entity hovered in the *previous frame*, +/// rather than the current frame. This is because touch pointers hover nothing +/// on the frame they are released. The end effect is that these two events can +/// be received sequentally after an [`Out`] event (but always on the same frame +/// as the [`Out`] event). +/// +/// Note: Though it is common for the [`PointerInput`] stream may contain +/// multiple pointer movements and presses each frame, the hover state is +/// determined only by the pointer's *final position*. Since the hover state +/// ultimately determines which entities receive events, this may mean that an +/// entity can receive events from before or after it was actually hovered. #[allow(clippy::too_many_arguments)] pub fn pointer_events( // Input @@ -366,6 +389,57 @@ pub fn pointer_events( .and_then(|pointer| pointer.location.clone()) }; + // If the entity was hovered by a specific pointer last frame... + for (pointer_id, hovered_entity, hit) in previous_hover_map + .iter() + .flat_map(|(id, hashmap)| hashmap.iter().map(|data| (*id, *data.0, data.1.clone()))) + { + // ...but is now not being hovered by that same pointer... + if !hover_map + .get(&pointer_id) + .iter() + .any(|e| e.contains_key(&hovered_entity)) + { + let Some(location) = pointer_location(pointer_id) else { + debug!( + "Unable to get location for pointer {:?} during pointer out", + pointer_id + ); + continue; + }; + + // Always send Out events + let out_event = Pointer::new( + pointer_id, + location.clone(), + hovered_entity, + Out { hit: hit.clone() }, + ); + commands.trigger_targets(out_event.clone(), hovered_entity); + event_writers.out_events.send(out_event); + + // Possibly send DragLeave events + for button in PointerButton::iter() { + let state = pointer_state.get_mut(pointer_id, button); + state.dragging_over.remove(&hovered_entity); + for drag_target in state.dragging.keys() { + let drag_leave_event = Pointer::new( + pointer_id, + location.clone(), + hovered_entity, + DragLeave { + button, + dragged: *drag_target, + hit: hit.clone(), + }, + ); + commands.trigger_targets(drag_leave_event.clone(), hovered_entity); + event_writers.drag_leave_events.send(drag_leave_event); + } + } + } + } + // If the entity is hovered... for (pointer_id, hovered_entity, hit) in hover_map .iter() @@ -659,55 +733,4 @@ pub fn pointer_events( } } } - - // If the entity was hovered by a specific pointer last frame... - for (pointer_id, hovered_entity, hit) in previous_hover_map - .iter() - .flat_map(|(id, hashmap)| hashmap.iter().map(|data| (*id, *data.0, data.1.clone()))) - { - // ...but is now not being hovered by that same pointer... - if !hover_map - .get(&pointer_id) - .iter() - .any(|e| e.contains_key(&hovered_entity)) - { - let Some(location) = pointer_location(pointer_id) else { - debug!( - "Unable to get location for pointer {:?} during pointer out", - pointer_id - ); - continue; - }; - - // Always send Out events - let out_event = Pointer::new( - pointer_id, - location.clone(), - hovered_entity, - Out { hit: hit.clone() }, - ); - commands.trigger_targets(out_event.clone(), hovered_entity); - event_writers.out_events.send(out_event); - - // Possibly send DragLeave events - for button in PointerButton::iter() { - let state = pointer_state.get_mut(pointer_id, button); - state.dragging_over.remove(&hovered_entity); - for drag_target in state.dragging.keys() { - let drag_leave_event = Pointer::new( - pointer_id, - location.clone(), - hovered_entity, - DragLeave { - button, - dragged: *drag_target, - hit: hit.clone(), - }, - ); - commands.trigger_targets(drag_leave_event.clone(), hovered_entity); - event_writers.drag_leave_events.send(drag_leave_event); - } - } - } - } } diff --git a/crates/bevy_picking/src/lib.rs b/crates/bevy_picking/src/lib.rs index 6c8e05e7a42c9..6b0b95f11bf58 100644 --- a/crates/bevy_picking/src/lib.rs +++ b/crates/bevy_picking/src/lib.rs @@ -30,8 +30,8 @@ //! ## Expressive Events //! //! The events in this module (see [`events`]) cannot be listened to with normal `EventReader`s. -//! Instead, they are dispatched to *ovservers* attached to specific entities. When events are generated, they -//! bubble up the entity hierarchy starting from their target, until they reach the root or bubbling is haulted +//! Instead, they are dispatched to *observers* attached to specific entities. When events are generated, they +//! bubble up the entity hierarchy starting from their target, until they reach the root or bubbling is halted //! with a call to [`Trigger::propagate`](bevy_ecs::observer::Trigger::propagate). //! See [`Observer`] for details. //! @@ -73,8 +73,8 @@ //! //! #### Input Agnostic //! -//! Picking provides a generic Pointer abstracton, which is useful for reacting to many different -//! types of input devices. Pointers can be controlled with anything, whether its the included mouse +//! Picking provides a generic Pointer abstraction, which is useful for reacting to many different +//! types of input devices. Pointers can be controlled with anything, whether it's the included mouse //! or touch inputs, or a custom gamepad input system you write yourself to control a virtual pointer. //! //! ## Robustness @@ -156,7 +156,7 @@ pub mod backend; pub mod events; pub mod focus; pub mod input; -#[cfg(feature = "bevy_mesh")] +#[cfg(feature = "bevy_mesh_picking_backend")] pub mod mesh_picking; pub mod pointer; @@ -168,7 +168,7 @@ use bevy_reflect::prelude::*; /// /// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { - #[cfg(feature = "bevy_mesh")] + #[cfg(feature = "bevy_mesh_picking_backend")] #[doc(hidden)] pub use crate::mesh_picking::{ ray_cast::{MeshRayCast, RayCastBackfaces, RayCastSettings, RayCastVisibility}, diff --git a/crates/bevy_picking/src/pointer.rs b/crates/bevy_picking/src/pointer.rs index 23f544699a886..fd7ec7b1eacb9 100644 --- a/crates/bevy_picking/src/pointer.rs +++ b/crates/bevy_picking/src/pointer.rs @@ -9,7 +9,7 @@ //! driven by lower-level input devices and consumed by higher-level interaction systems. use bevy_ecs::prelude::*; -use bevy_math::{Rect, Vec2}; +use bevy_math::Vec2; use bevy_reflect::prelude::*; use bevy_render::camera::{Camera, NormalizedRenderTarget}; use bevy_utils::HashMap; @@ -233,13 +233,9 @@ impl Location { return false; } - let position = Vec2::new(self.position.x, self.position.y); - camera .logical_viewport_rect() - .map(|Rect { min, max }| { - (position - min).min_element() >= 0.0 && (position - max).max_element() <= 0.0 - }) + .map(|rect| rect.contains(self.position)) .unwrap_or(false) } } diff --git a/crates/bevy_ptr/Cargo.toml b/crates/bevy_ptr/Cargo.toml index aa95d6561ebbf..0aea96ebd023b 100644 --- a/crates/bevy_ptr/Cargo.toml +++ b/crates/bevy_ptr/Cargo.toml @@ -7,6 +7,7 @@ homepage = "https://bevyengine.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy", "no_std"] +rust-version = "1.81.0" [dependencies] diff --git a/crates/bevy_reflect/Cargo.toml b/crates/bevy_reflect/Cargo.toml index 01e8b9d747c76..c573042829633 100644 --- a/crates/bevy_reflect/Cargo.toml +++ b/crates/bevy_reflect/Cargo.toml @@ -7,7 +7,7 @@ homepage = "https://bevyengine.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] -rust-version = "1.76.0" +rust-version = "1.81.0" [features] default = ["std", "smallvec", "debug"] diff --git a/crates/bevy_reflect/src/lib.rs b/crates/bevy_reflect/src/lib.rs index 2e54182749445..07a3c43c12a6e 100644 --- a/crates/bevy_reflect/src/lib.rs +++ b/crates/bevy_reflect/src/lib.rs @@ -374,7 +374,7 @@ //! ``` //! //! The generated type data can be used to convert a valid `dyn Reflect` into a `dyn MyTrait`. -//! See the [trait reflection example](https://github.com/bevyengine/bevy/blob/latest/examples/reflection/trait_reflection.rs) +//! See the [dynamic types example](https://github.com/bevyengine/bevy/blob/latest/examples/reflection/dynamic_types.rs) //! for more information and usage details. //! //! # Serialization diff --git a/crates/bevy_reflect/src/serde/de/arrays.rs b/crates/bevy_reflect/src/serde/de/arrays.rs index 96d2434e076c3..f1e0d60dd6154 100644 --- a/crates/bevy_reflect/src/serde/de/arrays.rs +++ b/crates/bevy_reflect/src/serde/de/arrays.rs @@ -6,31 +6,25 @@ use alloc::{string::ToString, vec::Vec}; use core::{fmt, fmt::Formatter}; use serde::de::{Error, SeqAccess, Visitor}; +use super::ReflectDeserializerProcessor; + /// A [`Visitor`] for deserializing [`Array`] values. /// /// [`Array`]: crate::Array -pub(super) struct ArrayVisitor<'a> { - array_info: &'static ArrayInfo, - registry: &'a TypeRegistry, -} - -impl<'a> ArrayVisitor<'a> { - pub fn new(array_info: &'static ArrayInfo, registry: &'a TypeRegistry) -> Self { - Self { - array_info, - registry, - } - } +pub(super) struct ArrayVisitor<'a, P> { + pub array_info: &'static ArrayInfo, + pub registry: &'a TypeRegistry, + pub processor: Option<&'a mut P>, } -impl<'a, 'de> Visitor<'de> for ArrayVisitor<'a> { +impl<'de, P: ReflectDeserializerProcessor> Visitor<'de> for ArrayVisitor<'_, P> { type Value = DynamicArray; fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { formatter.write_str("reflected array value") } - fn visit_seq(self, mut seq: V) -> Result + fn visit_seq(mut self, mut seq: V) -> Result where V: SeqAccess<'de>, { @@ -39,6 +33,7 @@ impl<'a, 'de> Visitor<'de> for ArrayVisitor<'a> { while let Some(value) = seq.next_element_seed(TypedReflectDeserializer::new_internal( registration, self.registry, + self.processor.as_deref_mut(), ))? { vec.push(value); } diff --git a/crates/bevy_reflect/src/serde/de/deserializer.rs b/crates/bevy_reflect/src/serde/de/deserializer.rs index 9a1f918bfd508..1c20eaad07abc 100644 --- a/crates/bevy_reflect/src/serde/de/deserializer.rs +++ b/crates/bevy_reflect/src/serde/de/deserializer.rs @@ -16,6 +16,8 @@ use alloc::boxed::Box; use core::{fmt, fmt::Formatter}; use serde::de::{DeserializeSeed, Error, IgnoredAny, MapAccess, Visitor}; +use super::ReflectDeserializerProcessor; + /// A general purpose deserializer for reflected types. /// /// This is the deserializer counterpart to [`ReflectSerializer`]. @@ -43,6 +45,10 @@ use serde::de::{DeserializeSeed, Error, IgnoredAny, MapAccess, Visitor}; /// 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`]. /// +/// If you want to override deserialization for a specific [`TypeRegistration`], +/// you can pass in a reference to a [`ReflectDeserializerProcessor`] which will +/// take priority over all other deserialization methods - see [`with_processor`]. +/// /// # Example /// /// ``` @@ -95,28 +101,57 @@ use serde::de::{DeserializeSeed, Error, IgnoredAny, MapAccess, Visitor}; /// [`Box`]: crate::DynamicList /// [`FromReflect`]: crate::FromReflect /// [`ReflectFromReflect`]: crate::ReflectFromReflect -pub struct ReflectDeserializer<'a> { +/// [`with_processor`]: Self::with_processor +pub struct ReflectDeserializer<'a, P: ReflectDeserializerProcessor = ()> { registry: &'a TypeRegistry, + processor: Option<&'a mut P>, } -impl<'a> ReflectDeserializer<'a> { +impl<'a> ReflectDeserializer<'a, ()> { + /// Creates a deserializer with no processor. + /// + /// If you want to add custom logic for deserializing certain types, use + /// [`with_processor`]. + /// + /// [`with_processor`]: Self::with_processor pub fn new(registry: &'a TypeRegistry) -> Self { - Self { registry } + Self { + registry, + processor: None, + } } } -impl<'a, 'de> DeserializeSeed<'de> for ReflectDeserializer<'a> { +impl<'a, P: ReflectDeserializerProcessor> ReflectDeserializer<'a, P> { + /// Creates a deserializer with a processor. + /// + /// If you do not need any custom logic for handling certain types, use + /// [`new`]. + /// + /// [`new`]: Self::new + pub fn with_processor(registry: &'a TypeRegistry, processor: &'a mut P) -> Self { + Self { + registry, + processor: Some(processor), + } + } +} + +impl<'de, P: ReflectDeserializerProcessor> DeserializeSeed<'de> for ReflectDeserializer<'_, P> { type Value = Box; fn deserialize(self, deserializer: D) -> Result where D: serde::Deserializer<'de>, { - struct UntypedReflectDeserializerVisitor<'a> { + struct UntypedReflectDeserializerVisitor<'a, P> { registry: &'a TypeRegistry, + processor: Option<&'a mut P>, } - impl<'a, 'de> Visitor<'de> for UntypedReflectDeserializerVisitor<'a> { + impl<'de, P: ReflectDeserializerProcessor> Visitor<'de> + for UntypedReflectDeserializerVisitor<'_, P> + { type Value = Box; fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { @@ -132,10 +167,11 @@ impl<'a, 'de> DeserializeSeed<'de> for ReflectDeserializer<'a> { .next_key_seed(TypeRegistrationDeserializer::new(self.registry))? .ok_or_else(|| Error::invalid_length(0, &"a single entry"))?; - let value = map.next_value_seed(TypedReflectDeserializer { + let value = map.next_value_seed(TypedReflectDeserializer::new_internal( registration, - registry: self.registry, - })?; + self.registry, + self.processor, + ))?; if map.next_key::()?.is_some() { return Err(Error::invalid_length(2, &"a single entry")); @@ -147,6 +183,7 @@ impl<'a, 'de> DeserializeSeed<'de> for ReflectDeserializer<'a> { deserializer.deserialize_map(UntypedReflectDeserializerVisitor { registry: self.registry, + processor: self.processor, }) } } @@ -176,6 +213,10 @@ impl<'a, 'de> DeserializeSeed<'de> for ReflectDeserializer<'a> { /// 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`]. /// +/// If you want to override deserialization for a specific [`TypeRegistration`], +/// you can pass in a reference to a [`ReflectDeserializerProcessor`] which will +/// take priority over all other deserialization methods - see [`with_processor`]. +/// /// # Example /// /// ``` @@ -227,13 +268,20 @@ impl<'a, 'de> DeserializeSeed<'de> for ReflectDeserializer<'a> { /// [`Box`]: crate::DynamicList /// [`FromReflect`]: crate::FromReflect /// [`ReflectFromReflect`]: crate::ReflectFromReflect -pub struct TypedReflectDeserializer<'a> { +/// [`with_processor`]: Self::with_processor +pub struct TypedReflectDeserializer<'a, P: ReflectDeserializerProcessor = ()> { registration: &'a TypeRegistration, registry: &'a TypeRegistry, + processor: Option<&'a mut P>, } -impl<'a> TypedReflectDeserializer<'a> { - /// Creates a new [`TypedReflectDeserializer`] for the given type registration. +impl<'a> TypedReflectDeserializer<'a, ()> { + /// Creates a typed deserializer with no processor. + /// + /// If you want to add custom logic for deserializing certain types, use + /// [`with_processor`]. + /// + /// [`with_processor`]: Self::with_processor pub fn new(registration: &'a TypeRegistration, registry: &'a TypeRegistry) -> Self { #[cfg(feature = "debug_stack")] TYPE_INFO_STACK.set(crate::type_info_stack::TypeInfoStack::new()); @@ -241,10 +289,12 @@ impl<'a> TypedReflectDeserializer<'a> { Self { registration, registry, + processor: None, } } - /// Creates a new [`TypedReflectDeserializer`] for the given type `T`. + /// Creates a new [`TypedReflectDeserializer`] for the given type `T` + /// without a processor. /// /// # Panics /// @@ -257,6 +307,30 @@ impl<'a> TypedReflectDeserializer<'a> { Self { registration, registry, + processor: None, + } + } +} + +impl<'a, P: ReflectDeserializerProcessor> TypedReflectDeserializer<'a, P> { + /// Creates a typed deserializer with a processor. + /// + /// If you do not need any custom logic for handling certain types, use + /// [`new`]. + /// + /// [`new`]: Self::new + pub fn with_processor( + registration: &'a TypeRegistration, + registry: &'a TypeRegistry, + processor: &'a mut P, + ) -> Self { + #[cfg(feature = "debug_stack")] + TYPE_INFO_STACK.set(crate::type_info_stack::TypeInfoStack::new()); + + Self { + registration, + registry, + processor: Some(processor), } } @@ -264,22 +338,42 @@ impl<'a> TypedReflectDeserializer<'a> { pub(super) fn new_internal( registration: &'a TypeRegistration, registry: &'a TypeRegistry, + processor: Option<&'a mut P>, ) -> Self { Self { registration, registry, + processor, } } } -impl<'a, 'de> DeserializeSeed<'de> for TypedReflectDeserializer<'a> { +impl<'de, P: ReflectDeserializerProcessor> DeserializeSeed<'de> + for TypedReflectDeserializer<'_, P> +{ type Value = Box; - fn deserialize(self, deserializer: D) -> Result + fn deserialize(mut self, deserializer: D) -> Result where D: serde::Deserializer<'de>, { let deserialize_internal = || -> Result { + // First, check if our processor wants to deserialize this type + // This takes priority over any other deserialization operations + let deserializer = if let Some(processor) = self.processor.as_deref_mut() { + match processor.try_deserialize(self.registration, self.registry, deserializer) { + Ok(Ok(value)) => { + return Ok(value); + } + Err(err) => { + return Err(make_custom_error(err)); + } + Ok(Err(deserializer)) => deserializer, + } + } else { + deserializer + }; + let type_path = self.registration.type_info().type_path(); // Handle both Value case and types that have a custom `ReflectDeserialize` @@ -300,7 +394,12 @@ impl<'a, 'de> DeserializeSeed<'de> for TypedReflectDeserializer<'a> { let mut dynamic_struct = deserializer.deserialize_struct( struct_info.type_path_table().ident().unwrap(), struct_info.field_names(), - StructVisitor::new(struct_info, self.registration, self.registry), + StructVisitor { + struct_info, + registration: self.registration, + registry: self.registry, + processor: self.processor, + }, )?; dynamic_struct.set_represented_type(Some(self.registration.type_info())); Ok(Box::new(dynamic_struct)) @@ -311,57 +410,76 @@ impl<'a, 'de> DeserializeSeed<'de> for TypedReflectDeserializer<'a> { { deserializer.deserialize_newtype_struct( tuple_struct_info.type_path_table().ident().unwrap(), - TupleStructVisitor::new( + TupleStructVisitor { tuple_struct_info, - self.registration, - self.registry, - ), + registration: self.registration, + registry: self.registry, + processor: self.processor, + }, )? } else { deserializer.deserialize_tuple_struct( tuple_struct_info.type_path_table().ident().unwrap(), tuple_struct_info.field_len(), - TupleStructVisitor::new( + TupleStructVisitor { tuple_struct_info, - self.registration, - self.registry, - ), + registration: self.registration, + registry: self.registry, + processor: self.processor, + }, )? }; - dynamic_tuple_struct.set_represented_type(Some(self.registration.type_info())); Ok(Box::new(dynamic_tuple_struct)) } TypeInfo::List(list_info) => { - let mut dynamic_list = - deserializer.deserialize_seq(ListVisitor::new(list_info, self.registry))?; + let mut dynamic_list = deserializer.deserialize_seq(ListVisitor { + list_info, + registry: self.registry, + processor: self.processor, + })?; dynamic_list.set_represented_type(Some(self.registration.type_info())); Ok(Box::new(dynamic_list)) } TypeInfo::Array(array_info) => { let mut dynamic_array = deserializer.deserialize_tuple( array_info.capacity(), - ArrayVisitor::new(array_info, self.registry), + ArrayVisitor { + array_info, + registry: self.registry, + processor: self.processor, + }, )?; dynamic_array.set_represented_type(Some(self.registration.type_info())); Ok(Box::new(dynamic_array)) } TypeInfo::Map(map_info) => { - let mut dynamic_map = - deserializer.deserialize_map(MapVisitor::new(map_info, self.registry))?; + let mut dynamic_map = deserializer.deserialize_map(MapVisitor { + map_info, + registry: self.registry, + processor: self.processor, + })?; dynamic_map.set_represented_type(Some(self.registration.type_info())); Ok(Box::new(dynamic_map)) } TypeInfo::Set(set_info) => { - let mut dynamic_set = - deserializer.deserialize_seq(SetVisitor::new(set_info, self.registry))?; + let mut dynamic_set = deserializer.deserialize_seq(SetVisitor { + set_info, + registry: self.registry, + processor: self.processor, + })?; dynamic_set.set_represented_type(Some(self.registration.type_info())); Ok(Box::new(dynamic_set)) } TypeInfo::Tuple(tuple_info) => { let mut dynamic_tuple = deserializer.deserialize_tuple( tuple_info.field_len(), - TupleVisitor::new(tuple_info, self.registration, self.registry), + TupleVisitor { + tuple_info, + registration: self.registration, + registry: self.registry, + processor: self.processor, + }, )?; dynamic_tuple.set_represented_type(Some(self.registration.type_info())); Ok(Box::new(dynamic_tuple)) @@ -371,13 +489,21 @@ impl<'a, 'de> DeserializeSeed<'de> for TypedReflectDeserializer<'a> { == Some("core::option") && enum_info.type_path_table().ident() == Some("Option") { - deserializer - .deserialize_option(OptionVisitor::new(enum_info, self.registry))? + deserializer.deserialize_option(OptionVisitor { + enum_info, + registry: self.registry, + processor: self.processor, + })? } else { deserializer.deserialize_enum( enum_info.type_path_table().ident().unwrap(), enum_info.variant_names(), - EnumVisitor::new(enum_info, self.registration, self.registry), + EnumVisitor { + enum_info, + registration: self.registration, + registry: self.registry, + processor: self.processor, + }, )? }; dynamic_enum.set_represented_type(Some(self.registration.type_info())); diff --git a/crates/bevy_reflect/src/serde/de/enums.rs b/crates/bevy_reflect/src/serde/de/enums.rs index ecdcc1435c6b3..21cbf46f9ecb1 100644 --- a/crates/bevy_reflect/src/serde/de/enums.rs +++ b/crates/bevy_reflect/src/serde/de/enums.rs @@ -15,30 +15,19 @@ use crate::{ use core::{fmt, fmt::Formatter}; use serde::de::{DeserializeSeed, EnumAccess, Error, MapAccess, SeqAccess, VariantAccess, Visitor}; +use super::ReflectDeserializerProcessor; + /// A [`Visitor`] for deserializing [`Enum`] values. /// /// [`Enum`]: crate::Enum -pub(super) struct EnumVisitor<'a> { - enum_info: &'static EnumInfo, - registration: &'a TypeRegistration, - registry: &'a TypeRegistry, +pub(super) struct EnumVisitor<'a, P> { + pub enum_info: &'static EnumInfo, + pub registration: &'a TypeRegistration, + pub registry: &'a TypeRegistry, + pub processor: Option<&'a mut P>, } -impl<'a> EnumVisitor<'a> { - pub fn new( - enum_info: &'static EnumInfo, - registration: &'a TypeRegistration, - registry: &'a TypeRegistry, - ) -> Self { - Self { - enum_info, - registration, - registry, - } - } -} - -impl<'a, 'de> Visitor<'de> for EnumVisitor<'a> { +impl<'de, P: ReflectDeserializerProcessor> Visitor<'de> for EnumVisitor<'_, P> { type Value = DynamicEnum; fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { @@ -63,6 +52,7 @@ impl<'a, 'de> Visitor<'de> for EnumVisitor<'a> { struct_info, registration: self.registration, registry: self.registry, + processor: self.processor, }, )? .into(), @@ -71,9 +61,12 @@ impl<'a, 'de> Visitor<'de> for EnumVisitor<'a> { *TupleLikeInfo::field_at(tuple_info, 0)?.ty(), self.registry, )?; - let value = variant.newtype_variant_seed( - TypedReflectDeserializer::new_internal(registration, self.registry), - )?; + let value = + variant.newtype_variant_seed(TypedReflectDeserializer::new_internal( + registration, + self.registry, + self.processor, + ))?; let mut dynamic_tuple = DynamicTuple::default(); dynamic_tuple.insert_boxed(value); dynamic_tuple.into() @@ -85,6 +78,7 @@ impl<'a, 'de> Visitor<'de> for EnumVisitor<'a> { tuple_info, registration: self.registration, registry: self.registry, + processor: self.processor, }, )? .into(), @@ -151,13 +145,14 @@ impl<'de> DeserializeSeed<'de> for VariantDeserializer { } } -struct StructVariantVisitor<'a> { +struct StructVariantVisitor<'a, P> { struct_info: &'static StructVariantInfo, registration: &'a TypeRegistration, registry: &'a TypeRegistry, + processor: Option<&'a mut P>, } -impl<'a, 'de> Visitor<'de> for StructVariantVisitor<'a> { +impl<'de, P: ReflectDeserializerProcessor> Visitor<'de> for StructVariantVisitor<'_, P> { type Value = DynamicStruct; fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { @@ -168,24 +163,37 @@ impl<'a, 'de> Visitor<'de> for StructVariantVisitor<'a> { where A: SeqAccess<'de>, { - visit_struct_seq(&mut seq, self.struct_info, self.registration, self.registry) + visit_struct_seq( + &mut seq, + self.struct_info, + self.registration, + self.registry, + self.processor, + ) } fn visit_map(self, mut map: V) -> Result where V: MapAccess<'de>, { - visit_struct(&mut map, self.struct_info, self.registration, self.registry) + visit_struct( + &mut map, + self.struct_info, + self.registration, + self.registry, + self.processor, + ) } } -struct TupleVariantVisitor<'a> { +struct TupleVariantVisitor<'a, P> { tuple_info: &'static TupleVariantInfo, registration: &'a TypeRegistration, registry: &'a TypeRegistry, + processor: Option<&'a mut P>, } -impl<'a, 'de> Visitor<'de> for TupleVariantVisitor<'a> { +impl<'de, P: ReflectDeserializerProcessor> Visitor<'de> for TupleVariantVisitor<'_, P> { type Value = DynamicTuple; fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { @@ -196,6 +204,12 @@ impl<'a, 'de> Visitor<'de> for TupleVariantVisitor<'a> { where V: SeqAccess<'de>, { - visit_tuple(&mut seq, self.tuple_info, self.registration, self.registry) + visit_tuple( + &mut seq, + self.tuple_info, + self.registration, + self.registry, + self.processor, + ) } } diff --git a/crates/bevy_reflect/src/serde/de/lists.rs b/crates/bevy_reflect/src/serde/de/lists.rs index 7d4ab44f8aea9..b85e46874a335 100644 --- a/crates/bevy_reflect/src/serde/de/lists.rs +++ b/crates/bevy_reflect/src/serde/de/lists.rs @@ -5,31 +5,25 @@ use crate::{ use core::{fmt, fmt::Formatter}; use serde::de::{SeqAccess, Visitor}; +use super::ReflectDeserializerProcessor; + /// A [`Visitor`] for deserializing [`List`] values. /// /// [`List`]: crate::List -pub(super) struct ListVisitor<'a> { - list_info: &'static ListInfo, - registry: &'a TypeRegistry, -} - -impl<'a> ListVisitor<'a> { - pub fn new(list_info: &'static ListInfo, registry: &'a TypeRegistry) -> Self { - Self { - list_info, - registry, - } - } +pub(super) struct ListVisitor<'a, P> { + pub list_info: &'static ListInfo, + pub registry: &'a TypeRegistry, + pub processor: Option<&'a mut P>, } -impl<'a, 'de> Visitor<'de> for ListVisitor<'a> { +impl<'de, P: ReflectDeserializerProcessor> Visitor<'de> for ListVisitor<'_, P> { type Value = DynamicList; fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { formatter.write_str("reflected list value") } - fn visit_seq(self, mut seq: V) -> Result + fn visit_seq(mut self, mut seq: V) -> Result where V: SeqAccess<'de>, { @@ -38,6 +32,7 @@ impl<'a, 'de> Visitor<'de> for ListVisitor<'a> { while let Some(value) = seq.next_element_seed(TypedReflectDeserializer::new_internal( registration, self.registry, + self.processor.as_deref_mut(), ))? { list.push_box(value); } diff --git a/crates/bevy_reflect/src/serde/de/maps.rs b/crates/bevy_reflect/src/serde/de/maps.rs index fbb5f9d449208..95b1c1f83e25b 100644 --- a/crates/bevy_reflect/src/serde/de/maps.rs +++ b/crates/bevy_reflect/src/serde/de/maps.rs @@ -5,28 +5,25 @@ use crate::{ use core::{fmt, fmt::Formatter}; use serde::de::{MapAccess, Visitor}; +use super::ReflectDeserializerProcessor; + /// A [`Visitor`] for deserializing [`Map`] values. /// /// [`Map`]: crate::Map -pub(super) struct MapVisitor<'a> { - map_info: &'static MapInfo, - registry: &'a TypeRegistry, -} - -impl<'a> MapVisitor<'a> { - pub fn new(map_info: &'static MapInfo, registry: &'a TypeRegistry) -> Self { - Self { map_info, registry } - } +pub(super) struct MapVisitor<'a, P> { + pub map_info: &'static MapInfo, + pub registry: &'a TypeRegistry, + pub processor: Option<&'a mut P>, } -impl<'a, 'de> Visitor<'de> for MapVisitor<'a> { +impl<'de, P: ReflectDeserializerProcessor> Visitor<'de> for MapVisitor<'_, P> { type Value = DynamicMap; fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { formatter.write_str("reflected map value") } - fn visit_map(self, mut map: V) -> Result + fn visit_map(mut self, mut map: V) -> Result where V: MapAccess<'de>, { @@ -36,10 +33,12 @@ impl<'a, 'de> Visitor<'de> for MapVisitor<'a> { while let Some(key) = map.next_key_seed(TypedReflectDeserializer::new_internal( key_registration, self.registry, + self.processor.as_deref_mut(), ))? { let value = map.next_value_seed(TypedReflectDeserializer::new_internal( value_registration, self.registry, + self.processor.as_deref_mut(), ))?; dynamic_map.insert_boxed(key, value); } diff --git a/crates/bevy_reflect/src/serde/de/mod.rs b/crates/bevy_reflect/src/serde/de/mod.rs index 92a43ed17565d..b1ada1835e961 100644 --- a/crates/bevy_reflect/src/serde/de/mod.rs +++ b/crates/bevy_reflect/src/serde/de/mod.rs @@ -1,5 +1,6 @@ pub use deserialize_with_registry::*; pub use deserializer::*; +pub use processor::*; pub use registrations::*; mod arrays; @@ -11,6 +12,7 @@ mod helpers; mod lists; mod maps; mod options; +mod processor; mod registration_utils; mod registrations; mod sets; @@ -24,12 +26,15 @@ mod tuples; mod tests { use bincode::Options; use core::{any::TypeId, f32::consts::PI, ops::RangeInclusive}; + use serde::de::IgnoredAny; + use serde::Deserializer; use serde::{de::DeserializeSeed, Deserialize}; use bevy_utils::{HashMap, HashSet}; - use crate as bevy_reflect; + use crate::serde::ReflectDeserializerProcessor; + use crate::{self as bevy_reflect, TypeRegistration}; use crate::{ serde::{ReflectDeserializer, ReflectSerializer, TypedReflectDeserializer}, DynamicEnum, FromReflect, PartialReflect, Reflect, ReflectDeserialize, TypeRegistry, @@ -275,15 +280,14 @@ mod tests { let mut registry = get_registry(); registry.register::(); let registration = registry.get(TypeId::of::()).unwrap(); - let reflect_deserializer = TypedReflectDeserializer::new_internal(registration, ®istry); + let reflect_deserializer = TypedReflectDeserializer::new(registration, ®istry); let mut ron_deserializer = ron::de::Deserializer::from_str(input).unwrap(); let dynamic_output = reflect_deserializer .deserialize(&mut ron_deserializer) .unwrap(); let output = - ::from_reflect(dynamic_output.as_ref().as_partial_reflect()) - .unwrap(); + ::from_reflect(dynamic_output.as_partial_reflect()).unwrap(); assert_eq!(expected, output); } @@ -517,6 +521,243 @@ mod tests { assert_eq!(error, ron::Error::Message("type `core::ops::RangeInclusive` did not register the `ReflectDeserialize` type data. For certain types, this may need to be registered manually using `register_type_data`".to_string())); } + #[test] + fn should_use_processor_for_custom_deserialization() { + #[derive(Reflect, Debug, PartialEq)] + struct Foo { + bar: i32, + qux: i64, + } + + struct FooProcessor; + + impl ReflectDeserializerProcessor for FooProcessor { + fn try_deserialize<'de, D>( + &mut self, + registration: &TypeRegistration, + _: &TypeRegistry, + deserializer: D, + ) -> Result, D>, D::Error> + where + D: Deserializer<'de>, + { + if registration.type_id() == TypeId::of::() { + let _ = deserializer.deserialize_ignored_any(IgnoredAny); + Ok(Ok(Box::new(456_i64))) + } else { + Ok(Err(deserializer)) + } + } + } + + let expected = Foo { bar: 123, qux: 456 }; + + let input = r#"( + bar: 123, + qux: 123, + )"#; + + let mut registry = get_registry(); + registry.register::(); + let registration = registry.get(TypeId::of::()).unwrap(); + let mut processor = FooProcessor; + let reflect_deserializer = + TypedReflectDeserializer::with_processor(registration, ®istry, &mut processor); + let mut ron_deserializer = ron::de::Deserializer::from_str(input).unwrap(); + let dynamic_output = reflect_deserializer + .deserialize(&mut ron_deserializer) + .unwrap(); + + let output = + ::from_reflect(dynamic_output.as_partial_reflect()).unwrap(); + assert_eq!(expected, output); + } + + #[test] + fn should_use_processor_for_multiple_registrations() { + #[derive(Reflect, Debug, PartialEq)] + struct Foo { + bar: i32, + qux: i64, + } + + struct FooProcessor; + + impl ReflectDeserializerProcessor for FooProcessor { + fn try_deserialize<'de, D>( + &mut self, + registration: &TypeRegistration, + _: &TypeRegistry, + deserializer: D, + ) -> Result, D>, D::Error> + where + D: Deserializer<'de>, + { + if registration.type_id() == TypeId::of::() { + let _ = deserializer.deserialize_ignored_any(IgnoredAny); + Ok(Ok(Box::new(123_i32))) + } else if registration.type_id() == TypeId::of::() { + let _ = deserializer.deserialize_ignored_any(IgnoredAny); + Ok(Ok(Box::new(456_i64))) + } else { + Ok(Err(deserializer)) + } + } + } + + let expected = Foo { bar: 123, qux: 456 }; + + let input = r#"( + bar: 0, + qux: 0, + )"#; + + let mut registry = get_registry(); + registry.register::(); + let registration = registry.get(TypeId::of::()).unwrap(); + let mut processor = FooProcessor; + let reflect_deserializer = + TypedReflectDeserializer::with_processor(registration, ®istry, &mut processor); + let mut ron_deserializer = ron::de::Deserializer::from_str(input).unwrap(); + let dynamic_output = reflect_deserializer + .deserialize(&mut ron_deserializer) + .unwrap(); + + let output = + ::from_reflect(dynamic_output.as_partial_reflect()).unwrap(); + assert_eq!(expected, output); + } + + #[test] + fn should_propagate_processor_deserialize_error() { + struct ErroringProcessor; + + impl ReflectDeserializerProcessor for ErroringProcessor { + fn try_deserialize<'de, D>( + &mut self, + registration: &TypeRegistration, + _: &TypeRegistry, + deserializer: D, + ) -> Result, D>, D::Error> + where + D: Deserializer<'de>, + { + if registration.type_id() == TypeId::of::() { + Err(serde::de::Error::custom("my custom deserialize error")) + } else { + Ok(Err(deserializer)) + } + } + } + + let registry = get_registry(); + + let input = r#"{"i32":123}"#; + let mut deserializer = ron::de::Deserializer::from_str(input).unwrap(); + let mut processor = ErroringProcessor; + let reflect_deserializer = ReflectDeserializer::with_processor(®istry, &mut processor); + let error = reflect_deserializer + .deserialize(&mut deserializer) + .unwrap_err(); + + #[cfg(feature = "debug_stack")] + assert_eq!( + error, + ron::Error::Message("my custom deserialize error (stack: `i32`)".to_string()) + ); + #[cfg(not(feature = "debug_stack"))] + assert_eq!( + error, + ron::Error::Message("my custom deserialize error".to_string()) + ); + } + + #[test] + fn should_access_local_scope_in_processor() { + struct ValueCountingProcessor<'a> { + values_found: &'a mut usize, + } + + impl ReflectDeserializerProcessor for ValueCountingProcessor<'_> { + fn try_deserialize<'de, D>( + &mut self, + _: &TypeRegistration, + _: &TypeRegistry, + deserializer: D, + ) -> Result, D>, D::Error> + where + D: Deserializer<'de>, + { + let _ = deserializer.deserialize_ignored_any(IgnoredAny)?; + *self.values_found += 1; + Ok(Ok(Box::new(123_i32))) + } + } + + let registry = get_registry(); + + let input = r#"{"i32":0}"#; + + let mut values_found = 0_usize; + let mut deserializer_processor = ValueCountingProcessor { + values_found: &mut values_found, + }; + + let mut deserializer = ron::de::Deserializer::from_str(input).unwrap(); + let reflect_deserializer = + ReflectDeserializer::with_processor(®istry, &mut deserializer_processor); + reflect_deserializer.deserialize(&mut deserializer).unwrap(); + assert_eq!(1, values_found); + } + + #[test] + fn should_fail_from_reflect_if_processor_returns_wrong_typed_value() { + #[derive(Reflect, Debug, PartialEq)] + struct Foo { + bar: i32, + qux: i64, + } + + struct WrongTypeProcessor; + + impl ReflectDeserializerProcessor for WrongTypeProcessor { + fn try_deserialize<'de, D>( + &mut self, + registration: &TypeRegistration, + _registry: &TypeRegistry, + deserializer: D, + ) -> Result, D>, D::Error> + where + D: Deserializer<'de>, + { + if registration.type_id() == TypeId::of::() { + let _ = deserializer.deserialize_ignored_any(IgnoredAny); + Ok(Ok(Box::new(42_i64))) + } else { + Ok(Err(deserializer)) + } + } + } + + let input = r#"( + bar: 123, + qux: 123, + )"#; + + let mut registry = get_registry(); + registry.register::(); + let registration = registry.get(TypeId::of::()).unwrap(); + let mut processor = WrongTypeProcessor; + let reflect_deserializer = + TypedReflectDeserializer::with_processor(registration, ®istry, &mut processor); + let mut ron_deserializer = ron::de::Deserializer::from_str(input).unwrap(); + let dynamic_output = reflect_deserializer + .deserialize(&mut ron_deserializer) + .unwrap(); + + assert!(::from_reflect(dynamic_output.as_partial_reflect()).is_none()); + } + #[cfg(feature = "functions")] mod functions { use super::*; diff --git a/crates/bevy_reflect/src/serde/de/options.rs b/crates/bevy_reflect/src/serde/de/options.rs index de9ded50750cb..f347c4a67d8e3 100644 --- a/crates/bevy_reflect/src/serde/de/options.rs +++ b/crates/bevy_reflect/src/serde/de/options.rs @@ -8,22 +8,16 @@ use crate::{ use core::{fmt, fmt::Formatter}; use serde::de::{DeserializeSeed, Error, Visitor}; -/// A [`Visitor`] for deserializing [`Option`] values. -pub(super) struct OptionVisitor<'a> { - enum_info: &'static EnumInfo, - registry: &'a TypeRegistry, -} +use super::ReflectDeserializerProcessor; -impl<'a> OptionVisitor<'a> { - pub fn new(enum_info: &'static EnumInfo, registry: &'a TypeRegistry) -> Self { - Self { - enum_info, - registry, - } - } +/// A [`Visitor`] for deserializing [`Option`] values. +pub(super) struct OptionVisitor<'a, P> { + pub enum_info: &'static EnumInfo, + pub registry: &'a TypeRegistry, + pub processor: Option<&'a mut P>, } -impl<'a, 'de> Visitor<'de> for OptionVisitor<'a> { +impl<'de, P: ReflectDeserializerProcessor> Visitor<'de> for OptionVisitor<'_, P> { type Value = DynamicEnum; fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { @@ -49,7 +43,11 @@ impl<'a, 'de> Visitor<'de> for OptionVisitor<'a> { VariantInfo::Tuple(tuple_info) if tuple_info.field_len() == 1 => { let field = tuple_info.field_at(0).unwrap(); let registration = try_get_registration(*field.ty(), self.registry)?; - let de = TypedReflectDeserializer::new_internal(registration, self.registry); + let de = TypedReflectDeserializer::new_internal( + registration, + self.registry, + self.processor, + ); let mut value = DynamicTuple::default(); value.insert_boxed(de.deserialize(deserializer)?); let mut option = DynamicEnum::default(); diff --git a/crates/bevy_reflect/src/serde/de/processor.rs b/crates/bevy_reflect/src/serde/de/processor.rs new file mode 100644 index 0000000000000..1d4d81f4b9fc1 --- /dev/null +++ b/crates/bevy_reflect/src/serde/de/processor.rs @@ -0,0 +1,216 @@ +use crate::{PartialReflect, TypeRegistration, TypeRegistry}; + +/// Allows overriding the default deserialization behavior of +/// [`ReflectDeserializer`] and [`TypedReflectDeserializer`] for specific +/// [`TypeRegistration`]s. +/// +/// When deserializing a reflected value, you may want to override the default +/// behavior and use your own logic for deserialization. This logic may also +/// be context-dependent, and only apply for a single use of your +/// [`ReflectDeserializer`]. To achieve this, you can create a processor and +/// pass it in to your deserializer. +/// +/// Whenever the deserializer attempts to deserialize a value, it will first +/// call [`try_deserialize`] on your processor, which may take ownership of the +/// deserializer and give back a [`Box`], or return +/// ownership of the deserializer back, and continue with the default logic. +/// +/// The serialization equivalent of this is [`ReflectSerializerProcessor`]. +/// +/// # Compared to [`DeserializeWithRegistry`] +/// +/// [`DeserializeWithRegistry`] allows you to define how your type will be +/// deserialized by a [`TypedReflectDeserializer`], given the extra context of +/// the [`TypeRegistry`]. If your type can be deserialized entirely from that, +/// then you should prefer implementing that trait instead of using a processor. +/// +/// However, you may need more context-dependent data which is only present in +/// the scope where you create the [`TypedReflectDeserializer`]. For example, in +/// an asset loader, the `&mut LoadContext` you get is only valid from within +/// the `load` function. This is where a processor is useful, as the processor +/// can capture local variables. +/// +/// A [`ReflectDeserializerProcessor`] always takes priority over a +/// [`DeserializeWithRegistry`] implementation, so this is also useful for +/// overriding deserialization behavior if you need to do something custom. +/// +/// # Examples +/// +/// Deserializing a reflected value in an asset loader, and replacing asset +/// handles with a loaded equivalent: +/// +/// ``` +/// # use bevy_reflect::serde::{ReflectDeserializer, ReflectDeserializerProcessor}; +/// # use bevy_reflect::{PartialReflect, Reflect, TypeData, TypeRegistration, TypeRegistry}; +/// # use serde::de::{DeserializeSeed, Deserializer, Visitor}; +/// # use std::marker::PhantomData; +/// # +/// # #[derive(Debug, Clone, Reflect)] +/// # struct LoadedUntypedAsset; +/// # #[derive(Debug, Clone, Reflect)] +/// # struct Handle(T); +/// # #[derive(Debug, Clone, Reflect)] +/// # struct Mesh; +/// # +/// # struct LoadContext; +/// # impl LoadContext { +/// # fn load(&mut self) -> &mut Self { unimplemented!() } +/// # fn with_asset_type_id(&mut self, (): ()) -> &mut Self { unimplemented!() } +/// # fn untyped(&mut self) -> &mut Self { unimplemented!() } +/// # fn load_asset(&mut self, (): ()) -> Handle { unimplemented!() } +/// # } +/// # +/// # struct ReflectHandle; +/// # impl TypeData for ReflectHandle { +/// # fn clone_type_data(&self) -> Box { +/// # unimplemented!() +/// # } +/// # } +/// # impl ReflectHandle { +/// # fn asset_type_id(&self) { +/// # unimplemented!() +/// # } +/// # } +/// # +/// # struct AssetPathVisitor; +/// # impl<'de> Visitor<'de> for AssetPathVisitor { +/// # type Value = (); +/// # fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result { unimplemented!() } +/// # } +/// # type AssetError = Box; +/// #[derive(Debug, Clone, Reflect)] +/// struct MyAsset { +/// name: String, +/// mesh: Handle, +/// } +/// +/// fn load( +/// asset_bytes: &[u8], +/// type_registry: &TypeRegistry, +/// load_context: &mut LoadContext, +/// ) -> Result { +/// struct HandleProcessor<'a> { +/// load_context: &'a mut LoadContext, +/// } +/// +/// impl ReflectDeserializerProcessor for HandleProcessor<'_> { +/// fn try_deserialize<'de, D>( +/// &mut self, +/// registration: &TypeRegistration, +/// _registry: &TypeRegistry, +/// deserializer: D, +/// ) -> Result, D>, D::Error> +/// where +/// D: Deserializer<'de>, +/// { +/// let Some(reflect_handle) = registration.data::() else { +/// // we don't want to deserialize this - give the deserializer back +/// return Ok(Err(deserializer)); +/// }; +/// +/// let asset_type_id = reflect_handle.asset_type_id(); +/// let asset_path = deserializer.deserialize_str(AssetPathVisitor)?; +/// +/// let handle: Handle = self.load_context +/// .load() +/// .with_asset_type_id(asset_type_id) +/// .untyped() +/// .load_asset(asset_path); +/// # let _: Result<_, ()> = { +/// Ok(Box::new(handle)) +/// # }; +/// # unimplemented!() +/// } +/// } +/// +/// let mut ron_deserializer = ron::Deserializer::from_bytes(asset_bytes)?; +/// let mut processor = HandleProcessor { load_context }; +/// let reflect_deserializer = +/// ReflectDeserializer::with_processor(type_registry, &mut processor); +/// let asset = reflect_deserializer.deserialize(&mut ron_deserializer)?; +/// # unimplemented!() +/// } +/// ``` +/// +/// [`ReflectDeserializer`]: crate::serde::ReflectDeserializer +/// [`TypedReflectDeserializer`]: crate::serde::TypedReflectDeserializer +/// [`try_deserialize`]: Self::try_deserialize +/// [`DeserializeWithRegistry`]: crate::serde::DeserializeWithRegistry +/// [`ReflectSerializerProcessor`]: crate::serde::ReflectSerializerProcessor +pub trait ReflectDeserializerProcessor { + /// Attempts to deserialize the value which a [`TypedReflectDeserializer`] + /// is currently looking at, and knows the type of. + /// + /// If you've read the `registration` and want to override the default + /// deserialization, return `Ok(Ok(value))` with the boxed reflected value + /// that you want to assign this value to. The type inside the box must + /// be the same one as the `registration` is for, otherwise future + /// reflection operations (such as using [`FromReflect`] to convert the + /// resulting [`Box`] into a concrete type) will fail. + /// + /// If you don't want to override the deserialization, return ownership of + /// the deserializer back via `Ok(Err(deserializer))`. + /// + /// Note that, if you do want to return a value, you *must* read from the + /// deserializer passed to this function (you are free to ignore the result + /// though). Otherwise, the deserializer will be in an inconsistent state, + /// and future value parsing will fail. + /// + /// # Examples + /// + /// Correct way to return a constant value (not using any output from the + /// deserializer): + /// + /// ``` + /// # use bevy_reflect::{TypeRegistration, PartialReflect, TypeRegistry}; + /// # use bevy_reflect::serde::ReflectDeserializerProcessor; + /// # use core::any::TypeId; + /// use serde::de::IgnoredAny; + /// + /// struct ConstantI32Processor; + /// + /// impl ReflectDeserializerProcessor for ConstantI32Processor { + /// fn try_deserialize<'de, D>( + /// &mut self, + /// registration: &TypeRegistration, + /// _registry: &TypeRegistry, + /// deserializer: D, + /// ) -> Result, D>, D::Error> + /// where + /// D: serde::Deserializer<'de> + /// { + /// if registration.type_id() == TypeId::of::() { + /// _ = deserializer.deserialize_ignored_any(IgnoredAny); + /// Ok(Ok(Box::new(42_i32))) + /// } else { + /// Ok(Err(deserializer)) + /// } + /// } + /// } + /// ``` + /// + /// [`TypedReflectDeserializer`]: crate::serde::TypedReflectDeserializer + /// [`FromReflect`]: crate::FromReflect + fn try_deserialize<'de, D>( + &mut self, + registration: &TypeRegistration, + registry: &TypeRegistry, + deserializer: D, + ) -> Result, D>, D::Error> + where + D: serde::Deserializer<'de>; +} + +impl ReflectDeserializerProcessor for () { + fn try_deserialize<'de, D>( + &mut self, + _registration: &TypeRegistration, + _registry: &TypeRegistry, + deserializer: D, + ) -> Result, D>, D::Error> + where + D: serde::Deserializer<'de>, + { + Ok(Err(deserializer)) + } +} diff --git a/crates/bevy_reflect/src/serde/de/sets.rs b/crates/bevy_reflect/src/serde/de/sets.rs index faecf5bc396e8..ed1a469df3d7d 100644 --- a/crates/bevy_reflect/src/serde/de/sets.rs +++ b/crates/bevy_reflect/src/serde/de/sets.rs @@ -5,28 +5,25 @@ use crate::{ use core::{fmt, fmt::Formatter}; use serde::de::{SeqAccess, Visitor}; +use super::ReflectDeserializerProcessor; + /// A [`Visitor`] for deserializing [`Set`] values. /// /// [`Set`]: crate::Set -pub(super) struct SetVisitor<'a> { - set_info: &'static SetInfo, - registry: &'a TypeRegistry, -} - -impl<'a> SetVisitor<'a> { - pub fn new(set_info: &'static SetInfo, registry: &'a TypeRegistry) -> Self { - Self { set_info, registry } - } +pub(super) struct SetVisitor<'a, P> { + pub set_info: &'static SetInfo, + pub registry: &'a TypeRegistry, + pub processor: Option<&'a mut P>, } -impl<'a, 'de> Visitor<'de> for SetVisitor<'a> { +impl<'de, P: ReflectDeserializerProcessor> Visitor<'de> for SetVisitor<'_, P> { type Value = DynamicSet; fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { formatter.write_str("reflected set value") } - fn visit_seq(self, mut set: V) -> Result + fn visit_seq(mut self, mut set: V) -> Result where V: SeqAccess<'de>, { @@ -35,6 +32,7 @@ impl<'a, 'de> Visitor<'de> for SetVisitor<'a> { while let Some(value) = set.next_element_seed(TypedReflectDeserializer::new_internal( value_registration, self.registry, + self.processor.as_deref_mut(), ))? { dynamic_set.insert_boxed(value); } diff --git a/crates/bevy_reflect/src/serde/de/struct_utils.rs b/crates/bevy_reflect/src/serde/de/struct_utils.rs index dc0c5fbd3aa8b..9ac56d9a4a88b 100644 --- a/crates/bevy_reflect/src/serde/de/struct_utils.rs +++ b/crates/bevy_reflect/src/serde/de/struct_utils.rs @@ -13,6 +13,8 @@ use alloc::string::ToString; use core::slice::Iter; use serde::de::{Error, MapAccess, SeqAccess}; +use super::ReflectDeserializerProcessor; + /// A helper trait for accessing type information from struct-like types. pub(super) trait StructLikeInfo { fn field(&self, name: &str) -> Result<&NamedField, E>; @@ -84,15 +86,17 @@ impl StructLikeInfo for StructVariantInfo { /// Deserializes a [struct-like] type from a mapping of fields, returning a [`DynamicStruct`]. /// /// [struct-like]: StructLikeInfo -pub(super) fn visit_struct<'de, T, V>( +pub(super) fn visit_struct<'de, T, V, P>( map: &mut V, info: &'static T, registration: &TypeRegistration, registry: &TypeRegistry, + mut processor: Option<&mut P>, ) -> Result where T: StructLikeInfo, V: MapAccess<'de>, + P: ReflectDeserializerProcessor, { let mut dynamic_struct = DynamicStruct::default(); while let Some(Ident(key)) = map.next_key::()? { @@ -108,6 +112,7 @@ where let value = map.next_value_seed(TypedReflectDeserializer::new_internal( registration, registry, + processor.as_deref_mut(), ))?; dynamic_struct.insert_boxed(&key, value); } @@ -130,15 +135,17 @@ where /// Deserializes a [struct-like] type from a sequence of fields, returning a [`DynamicStruct`]. /// /// [struct-like]: StructLikeInfo -pub(super) fn visit_struct_seq<'de, T, V>( +pub(super) fn visit_struct_seq<'de, T, V, P>( seq: &mut V, info: &T, registration: &TypeRegistration, registry: &TypeRegistry, + mut processor: Option<&mut P>, ) -> Result where T: StructLikeInfo, V: SeqAccess<'de>, + P: ReflectDeserializerProcessor, { let mut dynamic_struct = DynamicStruct::default(); @@ -168,6 +175,7 @@ where .next_element_seed(TypedReflectDeserializer::new_internal( try_get_registration(*info.field_at(index)?.ty(), registry)?, registry, + processor.as_deref_mut(), ))? .ok_or_else(|| Error::invalid_length(index, &len.to_string().as_str()))?; dynamic_struct.insert_boxed(name, value); diff --git a/crates/bevy_reflect/src/serde/de/structs.rs b/crates/bevy_reflect/src/serde/de/structs.rs index 750c739e8ef5a..0135e96358580 100644 --- a/crates/bevy_reflect/src/serde/de/structs.rs +++ b/crates/bevy_reflect/src/serde/de/structs.rs @@ -5,30 +5,19 @@ use crate::{ use core::{fmt, fmt::Formatter}; use serde::de::{MapAccess, SeqAccess, Visitor}; +use super::ReflectDeserializerProcessor; + /// A [`Visitor`] for deserializing [`Struct`] values. /// /// [`Struct`]: crate::Struct -pub(super) struct StructVisitor<'a> { - struct_info: &'static StructInfo, - registration: &'a TypeRegistration, - registry: &'a TypeRegistry, -} - -impl<'a> StructVisitor<'a> { - pub fn new( - struct_info: &'static StructInfo, - registration: &'a TypeRegistration, - registry: &'a TypeRegistry, - ) -> Self { - Self { - struct_info, - registration, - registry, - } - } +pub(super) struct StructVisitor<'a, P> { + pub struct_info: &'static StructInfo, + pub registration: &'a TypeRegistration, + pub registry: &'a TypeRegistry, + pub processor: Option<&'a mut P>, } -impl<'a, 'de> Visitor<'de> for StructVisitor<'a> { +impl<'de, P: ReflectDeserializerProcessor> Visitor<'de> for StructVisitor<'_, P> { type Value = DynamicStruct; fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { @@ -39,13 +28,25 @@ impl<'a, 'de> Visitor<'de> for StructVisitor<'a> { where A: SeqAccess<'de>, { - visit_struct_seq(&mut seq, self.struct_info, self.registration, self.registry) + visit_struct_seq( + &mut seq, + self.struct_info, + self.registration, + self.registry, + self.processor, + ) } fn visit_map(self, mut map: V) -> Result where V: MapAccess<'de>, { - visit_struct(&mut map, self.struct_info, self.registration, self.registry) + visit_struct( + &mut map, + self.struct_info, + self.registration, + self.registry, + self.processor, + ) } } diff --git a/crates/bevy_reflect/src/serde/de/tuple_structs.rs b/crates/bevy_reflect/src/serde/de/tuple_structs.rs index c9703cb504f75..af33b8d0b3c33 100644 --- a/crates/bevy_reflect/src/serde/de/tuple_structs.rs +++ b/crates/bevy_reflect/src/serde/de/tuple_structs.rs @@ -7,30 +7,19 @@ use serde::de::{DeserializeSeed, SeqAccess, Visitor}; use super::{registration_utils::try_get_registration, TypedReflectDeserializer}; +use super::ReflectDeserializerProcessor; + /// A [`Visitor`] for deserializing [`TupleStruct`] values. /// /// [`TupleStruct`]: crate::TupleStruct -pub(super) struct TupleStructVisitor<'a> { - tuple_struct_info: &'static TupleStructInfo, - registration: &'a TypeRegistration, - registry: &'a TypeRegistry, -} - -impl<'a> TupleStructVisitor<'a> { - pub fn new( - tuple_struct_info: &'static TupleStructInfo, - registration: &'a TypeRegistration, - registry: &'a TypeRegistry, - ) -> Self { - Self { - tuple_struct_info, - registration, - registry, - } - } +pub(super) struct TupleStructVisitor<'a, P> { + pub tuple_struct_info: &'static TupleStructInfo, + pub registration: &'a TypeRegistration, + pub registry: &'a TypeRegistry, + pub processor: Option<&'a mut P>, } -impl<'a, 'de> Visitor<'de> for TupleStructVisitor<'a> { +impl<'de, P: ReflectDeserializerProcessor> Visitor<'de> for TupleStructVisitor<'_, P> { type Value = DynamicTupleStruct; fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { @@ -46,6 +35,7 @@ impl<'a, 'de> Visitor<'de> for TupleStructVisitor<'a> { self.tuple_struct_info, self.registration, self.registry, + self.processor, ) .map(DynamicTupleStruct::from) } @@ -71,7 +61,7 @@ impl<'a, 'de> Visitor<'de> for TupleStructVisitor<'a> { self.registry, )?; let reflect_deserializer = - TypedReflectDeserializer::new_internal(registration, self.registry); + TypedReflectDeserializer::new_internal(registration, self.registry, self.processor); let value = reflect_deserializer.deserialize(deserializer)?; tuple.insert_boxed(value.into_partial_reflect()); diff --git a/crates/bevy_reflect/src/serde/de/tuple_utils.rs b/crates/bevy_reflect/src/serde/de/tuple_utils.rs index 18c90e025906a..13a65b4dd64ec 100644 --- a/crates/bevy_reflect/src/serde/de/tuple_utils.rs +++ b/crates/bevy_reflect/src/serde/de/tuple_utils.rs @@ -9,6 +9,8 @@ use crate::{ use alloc::string::ToString; use serde::de::{Error, SeqAccess}; +use super::ReflectDeserializerProcessor; + pub(super) trait TupleLikeInfo { fn field_at(&self, index: usize) -> Result<&UnnamedField, E>; fn field_len(&self) -> usize; @@ -65,15 +67,17 @@ impl TupleLikeInfo for TupleVariantInfo { /// Deserializes a [tuple-like] type from a sequence of elements, returning a [`DynamicTuple`]. /// /// [tuple-like]: TupleLikeInfo -pub(super) fn visit_tuple<'de, T, V>( +pub(super) fn visit_tuple<'de, T, V, P>( seq: &mut V, info: &T, registration: &TypeRegistration, registry: &TypeRegistry, + mut processor: Option<&mut P>, ) -> Result where T: TupleLikeInfo, V: SeqAccess<'de>, + P: ReflectDeserializerProcessor, { let mut tuple = DynamicTuple::default(); @@ -96,6 +100,7 @@ where .next_element_seed(TypedReflectDeserializer::new_internal( try_get_registration(*info.field_at(index)?.ty(), registry)?, registry, + processor.as_deref_mut(), ))? .ok_or_else(|| Error::invalid_length(index, &len.to_string().as_str()))?; tuple.insert_boxed(value); diff --git a/crates/bevy_reflect/src/serde/de/tuples.rs b/crates/bevy_reflect/src/serde/de/tuples.rs index ea06ad3154db8..87b9fc81c9439 100644 --- a/crates/bevy_reflect/src/serde/de/tuples.rs +++ b/crates/bevy_reflect/src/serde/de/tuples.rs @@ -4,30 +4,19 @@ use crate::{ use core::{fmt, fmt::Formatter}; use serde::de::{SeqAccess, Visitor}; +use super::ReflectDeserializerProcessor; + /// A [`Visitor`] for deserializing [`Tuple`] values. /// /// [`Tuple`]: crate::Tuple -pub(super) struct TupleVisitor<'a> { - tuple_info: &'static TupleInfo, - registration: &'a TypeRegistration, - registry: &'a TypeRegistry, -} - -impl<'a> TupleVisitor<'a> { - pub fn new( - tuple_info: &'static TupleInfo, - registration: &'a TypeRegistration, - registry: &'a TypeRegistry, - ) -> Self { - Self { - tuple_info, - registration, - registry, - } - } +pub(super) struct TupleVisitor<'a, P> { + pub tuple_info: &'static TupleInfo, + pub registration: &'a TypeRegistration, + pub registry: &'a TypeRegistry, + pub processor: Option<&'a mut P>, } -impl<'a, 'de> Visitor<'de> for TupleVisitor<'a> { +impl<'de, P: ReflectDeserializerProcessor> Visitor<'de> for TupleVisitor<'_, P> { type Value = DynamicTuple; fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { @@ -38,6 +27,12 @@ impl<'a, 'de> Visitor<'de> for TupleVisitor<'a> { where V: SeqAccess<'de>, { - visit_tuple(&mut seq, self.tuple_info, self.registration, self.registry) + visit_tuple( + &mut seq, + self.tuple_info, + self.registration, + self.registry, + self.processor, + ) } } diff --git a/crates/bevy_reflect/src/serde/ser/arrays.rs b/crates/bevy_reflect/src/serde/ser/arrays.rs index 16c741a68059a..9d565344aeb44 100644 --- a/crates/bevy_reflect/src/serde/ser/arrays.rs +++ b/crates/bevy_reflect/src/serde/ser/arrays.rs @@ -1,26 +1,27 @@ use crate::{serde::TypedReflectSerializer, Array, TypeRegistry}; use serde::{ser::SerializeTuple, Serialize}; -/// A serializer for [`Array`] values. -pub(super) struct ArraySerializer<'a> { - array: &'a dyn Array, - registry: &'a TypeRegistry, -} +use super::ReflectSerializerProcessor; -impl<'a> ArraySerializer<'a> { - pub fn new(array: &'a dyn Array, registry: &'a TypeRegistry) -> Self { - Self { array, registry } - } +/// A serializer for [`Array`] values. +pub(super) struct ArraySerializer<'a, P> { + pub array: &'a dyn Array, + pub registry: &'a TypeRegistry, + pub processor: Option<&'a P>, } -impl<'a> Serialize for ArraySerializer<'a> { +impl Serialize for ArraySerializer<'_, P> { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { let mut state = serializer.serialize_tuple(self.array.len())?; for value in self.array.iter() { - state.serialize_element(&TypedReflectSerializer::new_internal(value, self.registry))?; + state.serialize_element(&TypedReflectSerializer::new_internal( + value, + self.registry, + self.processor, + ))?; } state.end() } diff --git a/crates/bevy_reflect/src/serde/ser/enums.rs b/crates/bevy_reflect/src/serde/ser/enums.rs index 4b71207a9700d..86c4360ce037b 100644 --- a/crates/bevy_reflect/src/serde/ser/enums.rs +++ b/crates/bevy_reflect/src/serde/ser/enums.rs @@ -7,22 +7,16 @@ use serde::{ Serialize, }; -/// A serializer for [`Enum`] values. -pub(super) struct EnumSerializer<'a> { - enum_value: &'a dyn Enum, - registry: &'a TypeRegistry, -} +use super::ReflectSerializerProcessor; -impl<'a> EnumSerializer<'a> { - pub fn new(enum_value: &'a dyn Enum, registry: &'a TypeRegistry) -> Self { - Self { - enum_value, - registry, - } - } +/// A serializer for [`Enum`] values. +pub(super) struct EnumSerializer<'a, P> { + pub enum_value: &'a dyn Enum, + pub registry: &'a TypeRegistry, + pub processor: Option<&'a P>, } -impl<'a> Serialize for EnumSerializer<'a> { +impl Serialize for EnumSerializer<'_, P> { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, @@ -86,7 +80,11 @@ impl<'a> Serialize for EnumSerializer<'a> { let field_info = struct_info.field_at(index).unwrap(); state.serialize_field( field_info.name(), - &TypedReflectSerializer::new_internal(field.value(), self.registry), + &TypedReflectSerializer::new_internal( + field.value(), + self.registry, + self.processor, + ), )?; } state.end() @@ -97,14 +95,17 @@ impl<'a> Serialize for EnumSerializer<'a> { if type_info.type_path_table().module_path() == Some("core::option") && type_info.type_path_table().ident() == Some("Option") { - serializer - .serialize_some(&TypedReflectSerializer::new_internal(field, self.registry)) + serializer.serialize_some(&TypedReflectSerializer::new_internal( + field, + self.registry, + self.processor, + )) } else { serializer.serialize_newtype_variant( enum_name, variant_index, variant_name, - &TypedReflectSerializer::new_internal(field, self.registry), + &TypedReflectSerializer::new_internal(field, self.registry, self.processor), ) } } @@ -119,6 +120,7 @@ impl<'a> Serialize for EnumSerializer<'a> { state.serialize_field(&TypedReflectSerializer::new_internal( field.value(), self.registry, + self.processor, ))?; } state.end() diff --git a/crates/bevy_reflect/src/serde/ser/lists.rs b/crates/bevy_reflect/src/serde/ser/lists.rs index b52d52202f37d..4c3fb6b33dd25 100644 --- a/crates/bevy_reflect/src/serde/ser/lists.rs +++ b/crates/bevy_reflect/src/serde/ser/lists.rs @@ -1,26 +1,27 @@ use crate::{serde::TypedReflectSerializer, List, TypeRegistry}; use serde::{ser::SerializeSeq, Serialize}; -/// A serializer for [`List`] values. -pub(super) struct ListSerializer<'a> { - list: &'a dyn List, - registry: &'a TypeRegistry, -} +use super::ReflectSerializerProcessor; -impl<'a> ListSerializer<'a> { - pub fn new(list: &'a dyn List, registry: &'a TypeRegistry) -> Self { - Self { list, registry } - } +/// A serializer for [`List`] values. +pub(super) struct ListSerializer<'a, P> { + pub list: &'a dyn List, + pub registry: &'a TypeRegistry, + pub processor: Option<&'a P>, } -impl<'a> Serialize for ListSerializer<'a> { +impl Serialize for ListSerializer<'_, P> { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { let mut state = serializer.serialize_seq(Some(self.list.len()))?; for value in self.list.iter() { - state.serialize_element(&TypedReflectSerializer::new_internal(value, self.registry))?; + state.serialize_element(&TypedReflectSerializer::new_internal( + value, + self.registry, + self.processor, + ))?; } state.end() } diff --git a/crates/bevy_reflect/src/serde/ser/maps.rs b/crates/bevy_reflect/src/serde/ser/maps.rs index 354169193b59d..c1a8c287c1b3f 100644 --- a/crates/bevy_reflect/src/serde/ser/maps.rs +++ b/crates/bevy_reflect/src/serde/ser/maps.rs @@ -1,19 +1,16 @@ use crate::{serde::TypedReflectSerializer, Map, TypeRegistry}; use serde::{ser::SerializeMap, Serialize}; -/// A serializer for [`Map`] values. -pub(super) struct MapSerializer<'a> { - map: &'a dyn Map, - registry: &'a TypeRegistry, -} +use super::ReflectSerializerProcessor; -impl<'a> MapSerializer<'a> { - pub fn new(map: &'a dyn Map, registry: &'a TypeRegistry) -> Self { - Self { map, registry } - } +/// A serializer for [`Map`] values. +pub(super) struct MapSerializer<'a, P> { + pub map: &'a dyn Map, + pub registry: &'a TypeRegistry, + pub processor: Option<&'a P>, } -impl<'a> Serialize for MapSerializer<'a> { +impl Serialize for MapSerializer<'_, P> { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, @@ -21,8 +18,8 @@ impl<'a> Serialize for MapSerializer<'a> { let mut state = serializer.serialize_map(Some(self.map.len()))?; for (key, value) in self.map.iter() { state.serialize_entry( - &TypedReflectSerializer::new_internal(key, self.registry), - &TypedReflectSerializer::new_internal(value, self.registry), + &TypedReflectSerializer::new_internal(key, self.registry, self.processor), + &TypedReflectSerializer::new_internal(value, self.registry, self.processor), )?; } state.end() diff --git a/crates/bevy_reflect/src/serde/ser/mod.rs b/crates/bevy_reflect/src/serde/ser/mod.rs index ead36d4819898..ba12b7fb4fc39 100644 --- a/crates/bevy_reflect/src/serde/ser/mod.rs +++ b/crates/bevy_reflect/src/serde/ser/mod.rs @@ -1,3 +1,4 @@ +pub use processor::*; pub use serializable::*; pub use serialize_with_registry::*; pub use serializer::*; @@ -8,6 +9,7 @@ mod enums; mod error_utils; mod lists; mod maps; +mod processor; mod serializable; mod serialize_with_registry; mod serializer; @@ -19,13 +21,15 @@ mod tuples; #[cfg(test)] mod tests { use crate::{ - self as bevy_reflect, serde::ReflectSerializer, PartialReflect, Reflect, ReflectSerialize, - Struct, TypeRegistry, + self as bevy_reflect, + serde::{ReflectSerializer, ReflectSerializerProcessor}, + PartialReflect, Reflect, ReflectSerialize, Struct, TypeRegistry, }; use bevy_utils::{HashMap, HashSet}; + use core::any::TypeId; use core::{f32::consts::PI, ops::RangeInclusive}; use ron::{extensions::Extensions, ser::PrettyConfig}; - use serde::Serialize; + use serde::{Serialize, Serializer}; #[derive(Reflect, Debug, PartialEq)] struct MyStruct { @@ -474,6 +478,172 @@ mod tests { ); } + #[test] + fn should_use_processor_for_custom_serialization() { + #[derive(Reflect, Debug, PartialEq)] + struct Foo { + bar: i32, + qux: i64, + } + + struct FooProcessor; + + impl ReflectSerializerProcessor for FooProcessor { + fn try_serialize( + &self, + value: &dyn PartialReflect, + _: &TypeRegistry, + serializer: S, + ) -> Result, S::Error> + where + S: Serializer, + { + let Some(value) = value.try_as_reflect() else { + return Ok(Err(serializer)); + }; + + let type_id = value.reflect_type_info().type_id(); + if type_id == TypeId::of::() { + Ok(Ok(serializer.serialize_str("custom!")?)) + } else { + Ok(Err(serializer)) + } + } + } + + let value = Foo { bar: 123, qux: 456 }; + + let mut registry = TypeRegistry::new(); + registry.register::(); + + let processor = FooProcessor; + let serializer = ReflectSerializer::with_processor(&value, ®istry, &processor); + + let config = PrettyConfig::default().new_line(String::from("\n")); + let output = ron::ser::to_string_pretty(&serializer, config).unwrap(); + + let expected = r#"{ + "bevy_reflect::serde::ser::tests::Foo": ( + bar: 123, + qux: "custom!", + ), +}"#; + + assert_eq!(expected, output); + } + + #[test] + fn should_use_processor_for_multiple_registrations() { + #[derive(Reflect, Debug, PartialEq)] + struct Foo { + bar: i32, + sub: SubFoo, + } + + #[derive(Reflect, Debug, PartialEq)] + struct SubFoo { + val: i32, + } + + struct FooProcessor; + + impl ReflectSerializerProcessor for FooProcessor { + fn try_serialize( + &self, + value: &dyn PartialReflect, + _: &TypeRegistry, + serializer: S, + ) -> Result, S::Error> + where + S: Serializer, + { + let Some(value) = value.try_as_reflect() else { + return Ok(Err(serializer)); + }; + + let type_id = value.reflect_type_info().type_id(); + if type_id == TypeId::of::() { + Ok(Ok(serializer.serialize_str("an i32")?)) + } else if type_id == TypeId::of::() { + Ok(Ok(serializer.serialize_str("a SubFoo")?)) + } else { + Ok(Err(serializer)) + } + } + } + + let value = Foo { + bar: 123, + sub: SubFoo { val: 456 }, + }; + + let mut registry = TypeRegistry::new(); + registry.register::(); + registry.register::(); + + let processor = FooProcessor; + let serializer = ReflectSerializer::with_processor(&value, ®istry, &processor); + + let config = PrettyConfig::default().new_line(String::from("\n")); + let output = ron::ser::to_string_pretty(&serializer, config).unwrap(); + + let expected = r#"{ + "bevy_reflect::serde::ser::tests::Foo": ( + bar: "an i32", + sub: "a SubFoo", + ), +}"#; + + assert_eq!(expected, output); + } + + #[test] + fn should_propagate_processor_serialize_error() { + struct ErroringProcessor; + + impl ReflectSerializerProcessor for ErroringProcessor { + fn try_serialize( + &self, + value: &dyn PartialReflect, + _: &TypeRegistry, + serializer: S, + ) -> Result, S::Error> + where + S: Serializer, + { + let Some(value) = value.try_as_reflect() else { + return Ok(Err(serializer)); + }; + + let type_id = value.reflect_type_info().type_id(); + if type_id == TypeId::of::() { + Err(serde::ser::Error::custom("my custom serialize error")) + } else { + Ok(Err(serializer)) + } + } + } + + let value = 123_i32; + + let registry = TypeRegistry::new(); + + let processor = ErroringProcessor; + let serializer = ReflectSerializer::with_processor(&value, ®istry, &processor); + let error = ron::ser::to_string_pretty(&serializer, PrettyConfig::default()).unwrap_err(); + + #[cfg(feature = "debug_stack")] + assert_eq!( + error, + ron::Error::Message("my custom serialize error (stack: `i32`)".to_string()) + ); + #[cfg(not(feature = "debug_stack"))] + assert_eq!( + error, + ron::Error::Message("my custom serialize error".to_string()) + ); + } + #[cfg(feature = "functions")] mod functions { use super::*; diff --git a/crates/bevy_reflect/src/serde/ser/processor.rs b/crates/bevy_reflect/src/serde/ser/processor.rs new file mode 100644 index 0000000000000..cf31ab7566791 --- /dev/null +++ b/crates/bevy_reflect/src/serde/ser/processor.rs @@ -0,0 +1,196 @@ +use serde::Serializer; + +use crate::{PartialReflect, TypeRegistry}; + +/// Allows overriding the default serialization behavior of +/// [`ReflectSerializer`] and [`TypedReflectSerializer`] for specific values. +/// +/// When serializing a reflected value, you may want to override the default +/// behavior and use your own logic for serialization. This logic may also be +/// context-dependent, and only apply for a single use of your +/// [`ReflectSerializer`]. To achieve this, you can create a processor and pass +/// it into your serializer. +/// +/// Whenever the serializer attempts to serialize a value, it will first call +/// [`try_serialize`] on your processor, which may take ownership of the +/// serializer and write into the serializer (successfully or not), or return +/// ownership of the serializer back, and continue with the default logic. +/// +/// The deserialization equivalent of this is [`ReflectDeserializerProcessor`]. +/// +/// # Compared to [`SerializeWithRegistry`] +/// +/// [`SerializeWithRegistry`] allows you to define how your type will be +/// serialized by a [`TypedReflectSerializer`], given the extra context of the +/// [`TypeRegistry`]. If your type can be serialized entirely using that, then +/// you should prefer implementing that trait instead of using a processor. +/// +/// However, you may need more context-dependent data which is only present in +/// the scope where you create the [`TypedReflectSerializer`]. For example, if +/// you need to use a reference to a value while serializing, then there is no +/// way to do this with [`SerializeWithRegistry`] as you can't pass that +/// reference into anywhere. This is where a processor is useful, as the +/// processor can capture local variables. +/// +/// A [`ReflectSerializerProcessor`] always takes priority over a +/// [`SerializeWithRegistry`] implementation, so this is also useful for +/// overriding serialization behavior if you need to do something custom. +/// +/// # Examples +/// +/// Serializing a reflected value when saving an asset to disk, and replacing +/// asset handles with the handle path (if it has one): +/// +/// ``` +/// # use core::any::Any; +/// # use serde::Serialize; +/// # use bevy_reflect::{PartialReflect, Reflect, TypeData, TypeRegistry}; +/// # use bevy_reflect::serde::{ReflectSerializer, ReflectSerializerProcessor}; +/// # +/// # #[derive(Debug, Clone, Reflect)] +/// # struct Handle(T); +/// # #[derive(Debug, Clone, Reflect)] +/// # struct Mesh; +/// # +/// # struct ReflectHandle; +/// # impl TypeData for ReflectHandle { +/// # fn clone_type_data(&self) -> Box { +/// # unimplemented!() +/// # } +/// # } +/// # impl ReflectHandle { +/// # fn downcast_handle_untyped(&self, handle: &(dyn Any + 'static)) -> Option { +/// # unimplemented!() +/// # } +/// # } +/// # +/// # #[derive(Debug, Clone)] +/// # struct UntypedHandle; +/// # impl UntypedHandle { +/// # fn path(&self) -> Option<&str> { +/// # unimplemented!() +/// # } +/// # } +/// # type AssetError = Box; +/// # +/// #[derive(Debug, Clone, Reflect)] +/// struct MyAsset { +/// name: String, +/// mesh: Handle, +/// } +/// +/// struct HandleProcessor; +/// +/// impl ReflectSerializerProcessor for HandleProcessor { +/// fn try_serialize( +/// &self, +/// value: &dyn PartialReflect, +/// registry: &TypeRegistry, +/// serializer: S, +/// ) -> Result, S::Error> +/// where +/// S: serde::Serializer, +/// { +/// let Some(value) = value.try_as_reflect() else { +/// // we don't have any info on this type; do the default serialization logic +/// return Ok(Err(serializer)); +/// }; +/// let type_id = value.reflect_type_info().type_id(); +/// let Some(reflect_handle) = registry.get_type_data::(type_id) else { +/// // this isn't a `Handle` +/// return Ok(Err(serializer)); +/// }; +/// +/// let untyped_handle = reflect_handle +/// .downcast_handle_untyped(value.as_any()) +/// .unwrap(); +/// if let Some(path) = untyped_handle.path() { +/// Ok(Ok(serializer.serialize_str(path)?)) +/// } else { +/// Ok(Ok(serializer.serialize_unit()?)) +/// } +/// } +/// } +/// +/// fn save(type_registry: &TypeRegistry, asset: &MyAsset) -> Result, AssetError> { +/// let mut asset_bytes = Vec::new(); +/// +/// let processor = HandleProcessor; +/// let serializer = ReflectSerializer::with_processor(asset, type_registry, &processor); +/// let mut ron_serializer = ron::Serializer::new(&mut asset_bytes, None)?; +/// +/// serializer.serialize(&mut ron_serializer)?; +/// Ok(asset_bytes) +/// } +/// ``` +/// +/// [`ReflectSerializer`]: crate::serde::ReflectSerializer +/// [`TypedReflectSerializer`]: crate::serde::TypedReflectSerializer +/// [`try_serialize`]: Self::try_serialize +/// [`SerializeWithRegistry`]: crate::serde::SerializeWithRegistry +/// [`ReflectDeserializerProcessor`]: crate::serde::ReflectDeserializerProcessor +pub trait ReflectSerializerProcessor { + /// Attempts to serialize the value which a [`TypedReflectSerializer`] is + /// currently looking at. + /// + /// If you want to override the default serialization, return + /// `Ok(Ok(value))` with an `Ok` output from the serializer. + /// + /// If you don't want to override the serialization, return ownership of + /// the serializer back via `Ok(Err(serializer))`. + /// + /// You can use the type registry to read info about the type you're + /// serializing, or just try to downcast the value directly: + /// + /// ``` + /// # use bevy_reflect::{TypeRegistration, TypeRegistry, PartialReflect}; + /// # use bevy_reflect::serde::ReflectSerializerProcessor; + /// # use core::any::TypeId; + /// struct I32AsStringProcessor; + /// + /// impl ReflectSerializerProcessor for I32AsStringProcessor { + /// fn try_serialize( + /// &self, + /// value: &dyn PartialReflect, + /// registry: &TypeRegistry, + /// serializer: S, + /// ) -> Result, S::Error> + /// where + /// S: serde::Serializer + /// { + /// if let Some(value) = value.try_downcast_ref::() { + /// let value_as_string = format!("{value:?}"); + /// Ok(Ok(serializer.serialize_str(&value_as_string)?)) + /// } else { + /// // Not an `i32`, just do the default serialization + /// Ok(Err(serializer)) + /// } + /// } + /// } + /// ``` + /// + /// [`TypedReflectSerializer`]: crate::serde::TypedReflectSerializer + /// [`Reflect`]: crate::Reflect + fn try_serialize( + &self, + value: &dyn PartialReflect, + registry: &TypeRegistry, + serializer: S, + ) -> Result, S::Error> + where + S: Serializer; +} + +impl ReflectSerializerProcessor for () { + fn try_serialize( + &self, + _value: &dyn PartialReflect, + _registry: &TypeRegistry, + serializer: S, + ) -> Result, S::Error> + where + S: Serializer, + { + Ok(Err(serializer)) + } +} diff --git a/crates/bevy_reflect/src/serde/ser/serializer.rs b/crates/bevy_reflect/src/serde/ser/serializer.rs index a803399829376..afe6b56b1dfaf 100644 --- a/crates/bevy_reflect/src/serde/ser/serializer.rs +++ b/crates/bevy_reflect/src/serde/ser/serializer.rs @@ -9,7 +9,9 @@ use crate::{ }, PartialReflect, ReflectRef, TypeRegistry, }; -use serde::{ser::SerializeMap, Serialize}; +use serde::{ser::SerializeMap, Serialize, Serializer}; + +use super::ReflectSerializerProcessor; /// A general purpose serializer for reflected types. /// @@ -23,6 +25,10 @@ use serde::{ser::SerializeMap, Serialize}; /// where the key is the _full_ [type path] of the reflected type /// and the value is the serialized data. /// +/// If you want to override serialization for specific values, you can pass in +/// a reference to a [`ReflectSerializerProcessor`] which will take priority +/// over all other serialization methods - see [`with_processor`]. +/// /// # Example /// /// ``` @@ -47,21 +53,53 @@ use serde::{ser::SerializeMap, Serialize}; /// /// [`ReflectDeserializer`]: crate::serde::ReflectDeserializer /// [type path]: crate::TypePath::type_path -pub struct ReflectSerializer<'a> { +/// [`with_processor`]: Self::with_processor +pub struct ReflectSerializer<'a, P = ()> { value: &'a dyn PartialReflect, registry: &'a TypeRegistry, + processor: Option<&'a P>, } -impl<'a> ReflectSerializer<'a> { +impl<'a> ReflectSerializer<'a, ()> { + /// Creates a serializer with no processor. + /// + /// If you want to add custom logic for serializing certain values, use + /// [`with_processor`]. + /// + /// [`with_processor`]: Self::with_processor pub fn new(value: &'a dyn PartialReflect, registry: &'a TypeRegistry) -> Self { - Self { value, registry } + Self { + value, + registry, + processor: None, + } } } -impl<'a> Serialize for ReflectSerializer<'a> { +impl<'a, P: ReflectSerializerProcessor> ReflectSerializer<'a, P> { + /// Creates a serializer with a processor. + /// + /// If you do not need any custom logic for handling certain values, use + /// [`new`]. + /// + /// [`new`]: Self::new + pub fn with_processor( + value: &'a dyn PartialReflect, + registry: &'a TypeRegistry, + processor: &'a P, + ) -> Self { + Self { + value, + registry, + processor: Some(processor), + } + } +} + +impl Serialize for ReflectSerializer<'_, P> { fn serialize(&self, serializer: S) -> Result where - S: serde::Serializer, + S: Serializer, { let mut state = serializer.serialize_map(Some(1))?; state.serialize_entry( @@ -81,7 +119,7 @@ impl<'a> Serialize for ReflectSerializer<'a> { } })? .type_path(), - &TypedReflectSerializer::new(self.value, self.registry), + &TypedReflectSerializer::new_internal(self.value, self.registry, self.processor), )?; state.end() } @@ -101,6 +139,10 @@ impl<'a> Serialize for ReflectSerializer<'a> { /// /// Instead, it will output just the serialized data. /// +/// If you want to override serialization for specific values, you can pass in +/// a reference to a [`ReflectSerializerProcessor`] which will take priority +/// over all other serialization methods - see [`with_processor`]. +/// /// # Example /// /// ``` @@ -125,29 +167,72 @@ impl<'a> Serialize for ReflectSerializer<'a> { /// /// [`TypedReflectDeserializer`]: crate::serde::TypedReflectDeserializer /// [type path]: crate::TypePath::type_path -pub struct TypedReflectSerializer<'a> { +/// [`with_processor`]: Self::with_processor +pub struct TypedReflectSerializer<'a, P = ()> { value: &'a dyn PartialReflect, registry: &'a TypeRegistry, + processor: Option<&'a P>, } -impl<'a> TypedReflectSerializer<'a> { +impl<'a> TypedReflectSerializer<'a, ()> { + /// Creates a serializer with no processor. + /// + /// If you want to add custom logic for serializing certain values, use + /// [`with_processor`]. + /// + /// [`with_processor`]: Self::with_processor pub fn new(value: &'a dyn PartialReflect, registry: &'a TypeRegistry) -> Self { #[cfg(feature = "debug_stack")] TYPE_INFO_STACK.set(crate::type_info_stack::TypeInfoStack::new()); - Self { value, registry } + Self { + value, + registry, + processor: None, + } + } +} + +impl<'a, P> TypedReflectSerializer<'a, P> { + /// Creates a serializer with a processor. + /// + /// If you do not need any custom logic for handling certain values, use + /// [`new`]. + /// + /// [`new`]: Self::new + pub fn with_processor( + value: &'a dyn PartialReflect, + registry: &'a TypeRegistry, + processor: &'a P, + ) -> Self { + #[cfg(feature = "debug_stack")] + TYPE_INFO_STACK.set(crate::type_info_stack::TypeInfoStack::new()); + + Self { + value, + registry, + processor: Some(processor), + } } /// An internal constructor for creating a serializer without resetting the type info stack. - pub(super) fn new_internal(value: &'a dyn PartialReflect, registry: &'a TypeRegistry) -> Self { - Self { value, registry } + pub(super) fn new_internal( + value: &'a dyn PartialReflect, + registry: &'a TypeRegistry, + processor: Option<&'a P>, + ) -> Self { + Self { + value, + registry, + processor, + } } } -impl<'a> Serialize for TypedReflectSerializer<'a> { +impl Serialize for TypedReflectSerializer<'_, P> { fn serialize(&self, serializer: S) -> Result where - S: serde::Serializer, + S: Serializer, { #[cfg(feature = "debug_stack")] { @@ -155,6 +240,23 @@ impl<'a> Serialize for TypedReflectSerializer<'a> { TYPE_INFO_STACK.with_borrow_mut(|stack| stack.push(info)); } } + + // First, check if our processor wants to serialize this type + // This takes priority over any other serialization operations + let serializer = if let Some(processor) = self.processor { + match processor.try_serialize(self.value, self.registry, serializer) { + Ok(Ok(value)) => { + return Ok(value); + } + Err(err) => { + return Err(make_custom_error(err)); + } + Ok(Err(serializer)) => serializer, + } + } else { + serializer + }; + // Handle both Value case and types that have a custom `Serialize` let (serializer, error) = match try_custom_serialize(self.value, self.registry, serializer) { @@ -163,30 +265,54 @@ impl<'a> Serialize for TypedReflectSerializer<'a> { }; let output = match self.value.reflect_ref() { - ReflectRef::Struct(value) => { - StructSerializer::new(value, self.registry).serialize(serializer) + ReflectRef::Struct(struct_value) => StructSerializer { + struct_value, + registry: self.registry, + processor: self.processor, } - ReflectRef::TupleStruct(value) => { - TupleStructSerializer::new(value, self.registry).serialize(serializer) + .serialize(serializer), + ReflectRef::TupleStruct(tuple_struct) => TupleStructSerializer { + tuple_struct, + registry: self.registry, + processor: self.processor, } - ReflectRef::Tuple(value) => { - TupleSerializer::new(value, self.registry).serialize(serializer) + .serialize(serializer), + ReflectRef::Tuple(tuple) => TupleSerializer { + tuple, + registry: self.registry, + processor: self.processor, } - ReflectRef::List(value) => { - ListSerializer::new(value, self.registry).serialize(serializer) + .serialize(serializer), + ReflectRef::List(list) => ListSerializer { + list, + registry: self.registry, + processor: self.processor, } - ReflectRef::Array(value) => { - ArraySerializer::new(value, self.registry).serialize(serializer) + .serialize(serializer), + ReflectRef::Array(array) => ArraySerializer { + array, + registry: self.registry, + processor: self.processor, } - ReflectRef::Map(value) => { - MapSerializer::new(value, self.registry).serialize(serializer) + .serialize(serializer), + ReflectRef::Map(map) => MapSerializer { + map, + registry: self.registry, + processor: self.processor, } - ReflectRef::Set(value) => { - SetSerializer::new(value, self.registry).serialize(serializer) + .serialize(serializer), + ReflectRef::Set(set) => SetSerializer { + set, + registry: self.registry, + processor: self.processor, } - ReflectRef::Enum(value) => { - EnumSerializer::new(value, self.registry).serialize(serializer) + .serialize(serializer), + ReflectRef::Enum(enum_value) => EnumSerializer { + enum_value, + registry: self.registry, + processor: self.processor, } + .serialize(serializer), #[cfg(feature = "functions")] ReflectRef::Function(_) => Err(make_custom_error("functions cannot be serialized")), ReflectRef::Opaque(_) => Err(error), diff --git a/crates/bevy_reflect/src/serde/ser/sets.rs b/crates/bevy_reflect/src/serde/ser/sets.rs index 343c253eb213e..34e8899d58c95 100644 --- a/crates/bevy_reflect/src/serde/ser/sets.rs +++ b/crates/bevy_reflect/src/serde/ser/sets.rs @@ -1,26 +1,27 @@ use crate::{serde::TypedReflectSerializer, Set, TypeRegistry}; use serde::{ser::SerializeSeq, Serialize}; -/// A serializer for [`Set`] values. -pub(super) struct SetSerializer<'a> { - set: &'a dyn Set, - registry: &'a TypeRegistry, -} +use super::ReflectSerializerProcessor; -impl<'a> SetSerializer<'a> { - pub fn new(set: &'a dyn Set, registry: &'a TypeRegistry) -> Self { - Self { set, registry } - } +/// A serializer for [`Set`] values. +pub(super) struct SetSerializer<'a, P> { + pub set: &'a dyn Set, + pub registry: &'a TypeRegistry, + pub processor: Option<&'a P>, } -impl<'a> Serialize for SetSerializer<'a> { +impl Serialize for SetSerializer<'_, P> { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { let mut state = serializer.serialize_seq(Some(self.set.len()))?; for value in self.set.iter() { - state.serialize_element(&TypedReflectSerializer::new_internal(value, self.registry))?; + state.serialize_element(&TypedReflectSerializer::new_internal( + value, + self.registry, + self.processor, + ))?; } state.end() } diff --git a/crates/bevy_reflect/src/serde/ser/structs.rs b/crates/bevy_reflect/src/serde/ser/structs.rs index c8c2b87b44360..4eb3e76700d57 100644 --- a/crates/bevy_reflect/src/serde/ser/structs.rs +++ b/crates/bevy_reflect/src/serde/ser/structs.rs @@ -4,22 +4,16 @@ use crate::{ }; use serde::{ser::SerializeStruct, Serialize}; -/// A serializer for [`Struct`] values. -pub(super) struct StructSerializer<'a> { - struct_value: &'a dyn Struct, - registry: &'a TypeRegistry, -} +use super::ReflectSerializerProcessor; -impl<'a> StructSerializer<'a> { - pub fn new(struct_value: &'a dyn Struct, registry: &'a TypeRegistry) -> Self { - Self { - struct_value, - registry, - } - } +/// A serializer for [`Struct`] values. +pub(super) struct StructSerializer<'a, P> { + pub struct_value: &'a dyn Struct, + pub registry: &'a TypeRegistry, + pub processor: Option<&'a P>, } -impl<'a> Serialize for StructSerializer<'a> { +impl Serialize for StructSerializer<'_, P> { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, @@ -63,7 +57,7 @@ impl<'a> Serialize for StructSerializer<'a> { let key = struct_info.field_at(index).unwrap().name(); state.serialize_field( key, - &TypedReflectSerializer::new_internal(value, self.registry), + &TypedReflectSerializer::new_internal(value, self.registry, self.processor), )?; } state.end() diff --git a/crates/bevy_reflect/src/serde/ser/tuple_structs.rs b/crates/bevy_reflect/src/serde/ser/tuple_structs.rs index 55e171feae473..5bf2ec64ae7e0 100644 --- a/crates/bevy_reflect/src/serde/ser/tuple_structs.rs +++ b/crates/bevy_reflect/src/serde/ser/tuple_structs.rs @@ -4,22 +4,16 @@ use crate::{ }; use serde::{ser::SerializeTupleStruct, Serialize}; -/// A serializer for [`TupleStruct`] values. -pub(super) struct TupleStructSerializer<'a> { - tuple_struct: &'a dyn TupleStruct, - registry: &'a TypeRegistry, -} +use super::ReflectSerializerProcessor; -impl<'a> TupleStructSerializer<'a> { - pub fn new(tuple_struct: &'a dyn TupleStruct, registry: &'a TypeRegistry) -> Self { - Self { - tuple_struct, - registry, - } - } +/// A serializer for [`TupleStruct`] values. +pub(super) struct TupleStructSerializer<'a, P> { + pub tuple_struct: &'a dyn TupleStruct, + pub registry: &'a TypeRegistry, + pub processor: Option<&'a P>, } -impl<'a> Serialize for TupleStructSerializer<'a> { +impl Serialize for TupleStructSerializer<'_, P> { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, @@ -53,7 +47,7 @@ impl<'a> Serialize for TupleStructSerializer<'a> { let field = self.tuple_struct.field(0).unwrap(); return serializer.serialize_newtype_struct( tuple_struct_info.type_path_table().ident().unwrap(), - &TypedReflectSerializer::new_internal(field, self.registry), + &TypedReflectSerializer::new_internal(field, self.registry, self.processor), ); } @@ -69,7 +63,11 @@ impl<'a> Serialize for TupleStructSerializer<'a> { { continue; } - state.serialize_field(&TypedReflectSerializer::new_internal(value, self.registry))?; + state.serialize_field(&TypedReflectSerializer::new_internal( + value, + self.registry, + self.processor, + ))?; } state.end() } diff --git a/crates/bevy_reflect/src/serde/ser/tuples.rs b/crates/bevy_reflect/src/serde/ser/tuples.rs index 133818dfa905d..195d1e492d76b 100644 --- a/crates/bevy_reflect/src/serde/ser/tuples.rs +++ b/crates/bevy_reflect/src/serde/ser/tuples.rs @@ -1,19 +1,16 @@ use crate::{serde::TypedReflectSerializer, Tuple, TypeRegistry}; use serde::{ser::SerializeTuple, Serialize}; -/// A serializer for [`Tuple`] values. -pub(super) struct TupleSerializer<'a> { - tuple: &'a dyn Tuple, - registry: &'a TypeRegistry, -} +use super::ReflectSerializerProcessor; -impl<'a> TupleSerializer<'a> { - pub fn new(tuple: &'a dyn Tuple, registry: &'a TypeRegistry) -> Self { - Self { tuple, registry } - } +/// A serializer for [`Tuple`] values. +pub(super) struct TupleSerializer<'a, P> { + pub tuple: &'a dyn Tuple, + pub registry: &'a TypeRegistry, + pub processor: Option<&'a P>, } -impl<'a> Serialize for TupleSerializer<'a> { +impl Serialize for TupleSerializer<'_, P> { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, @@ -21,7 +18,11 @@ impl<'a> Serialize for TupleSerializer<'a> { let mut state = serializer.serialize_tuple(self.tuple.field_len())?; for value in self.tuple.iter_fields() { - state.serialize_element(&TypedReflectSerializer::new_internal(value, self.registry))?; + state.serialize_element(&TypedReflectSerializer::new_internal( + value, + self.registry, + self.processor, + ))?; } state.end() } diff --git a/crates/bevy_remote/src/lib.rs b/crates/bevy_remote/src/lib.rs index 9dab9fc4f17a6..576ee7dbf18bc 100644 --- a/crates/bevy_remote/src/lib.rs +++ b/crates/bevy_remote/src/lib.rs @@ -305,7 +305,7 @@ use bevy_app::{prelude::*, MainScheduleOrder}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ entity::Entity, - schedule::{IntoSystemConfigs, ScheduleLabel}, + schedule::{IntoSystemConfigs, IntoSystemSetConfigs, ScheduleLabel, SystemSet}, system::{Commands, In, IntoSystem, ResMut, Resource, System, SystemId}, world::World, }; @@ -442,21 +442,36 @@ impl Plugin for RemotePlugin { app.insert_resource(remote_methods) .init_resource::() .add_systems(PreStartup, setup_mailbox_channel) + .configure_sets( + RemoteLast, + (RemoteSet::ProcessRequests, RemoteSet::Cleanup).chain(), + ) .add_systems( RemoteLast, ( - process_remote_requests, - process_ongoing_watching_requests, - remove_closed_watching_requests, - ) - .chain(), + (process_remote_requests, process_ongoing_watching_requests) + .chain() + .in_set(RemoteSet::ProcessRequests), + remove_closed_watching_requests.in_set(RemoteSet::Cleanup), + ), ); } } /// Schedule that contains all systems to process Bevy Remote Protocol requests #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] -struct RemoteLast; +pub struct RemoteLast; + +/// The systems sets of the [`RemoteLast`] schedule. +/// +/// These can be useful for ordering. +#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] +pub enum RemoteSet { + /// Processing of remote requests. + ProcessRequests, + /// Cleanup (remove closed watchers etc) + Cleanup, +} /// A type to hold the allowed types of systems to be used as method handlers. #[derive(Debug)] diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index 0ab710527b7b1..8a28253c5ecc1 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -30,6 +30,7 @@ ci_limits = [] webgl = ["wgpu/webgl"] webgpu = ["wgpu/webgpu"] ios_simulator = [] +detailed_trace = [] [dependencies] # bevy @@ -67,7 +68,7 @@ codespan-reporting = "0.11.0" # 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 = "23", default-features = false, features = [ +wgpu = { version = "23.0.1", default-features = false, features = [ "wgsl", "dx12", "metal", diff --git a/crates/bevy_render/macros/src/as_bind_group.rs b/crates/bevy_render/macros/src/as_bind_group.rs index d0edd18dd633c..8f871bd0e00c6 100644 --- a/crates/bevy_render/macros/src/as_bind_group.rs +++ b/crates/bevy_render/macros/src/as_bind_group.rs @@ -41,6 +41,7 @@ enum BindingState<'a> { pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { let manifest = BevyManifest::default(); let render_path = manifest.get_path("bevy_render"); + let image_path = manifest.get_path("bevy_image"); let asset_path = manifest.get_path("bevy_asset"); let ecs_path = manifest.get_path("bevy_ecs"); @@ -264,7 +265,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { 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(); + let handle: Option<&#asset_path::Handle<#image_path::Image>> = (&self.#field_name).into(); if let Some(handle) = handle { images.get(handle).ok_or_else(|| #render_path::render_resource::AsBindGroupError::RetryNextUpdate)?.texture_view.clone() } else { @@ -305,7 +306,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { ( #binding_index, #render_path::render_resource::OwnedBindingResource::TextureView({ - let handle: Option<&#asset_path::Handle<#render_path::texture::Image>> = (&self.#field_name).into(); + let handle: Option<&#asset_path::Handle<#image_path::Image>> = (&self.#field_name).into(); if let Some(handle) = handle { images.get(handle).ok_or_else(|| #render_path::render_resource::AsBindGroupError::RetryNextUpdate)?.texture_view.clone() } else { @@ -362,7 +363,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { ( #binding_index, #render_path::render_resource::OwnedBindingResource::Sampler({ - let handle: Option<&#asset_path::Handle<#render_path::texture::Image>> = (&self.#field_name).into(); + let handle: Option<&#asset_path::Handle<#image_path::Image>> = (&self.#field_name).into(); if let Some(handle) = handle { let image = images.get(handle).ok_or_else(|| #render_path::render_resource::AsBindGroupError::RetryNextUpdate)?; diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index f00ebb5e9d5f3..689734c4fba4e 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -1,19 +1,17 @@ use super::{ClearColorConfig, Projection}; -use crate::sync_world::TemporaryRenderEntity; -use crate::view::RenderVisibleEntities; use crate::{ batching::gpu_preprocessing::GpuPreprocessingSupport, camera::{CameraProjection, ManualTextureViewHandle, ManualTextureViews}, - prelude::Image, primitives::Frustum, render_asset::RenderAssets, render_graph::{InternedRenderSubGraph, RenderSubGraph}, render_resource::TextureView, + sync_world::TemporaryRenderEntity, sync_world::{RenderEntity, SyncToRenderWorld}, texture::GpuImage, view::{ - ColorGrading, ExtractedView, ExtractedWindows, GpuCulling, Msaa, RenderLayers, Visibility, - VisibleEntities, + ColorGrading, ExtractedView, ExtractedWindows, GpuCulling, Msaa, RenderLayers, + RenderVisibleEntities, ViewUniformOffset, Visibility, VisibleEntities, }, Extract, }; @@ -30,6 +28,7 @@ use bevy_ecs::{ system::{Commands, Query, Res, ResMut, Resource}, world::DeferredWorld, }; +use bevy_image::Image; use bevy_math::{ops, vec2, Dir3, Mat4, Ray3d, Rect, URect, UVec2, UVec4, Vec2, Vec3}; use bevy_reflect::prelude::*; use bevy_render_macros::ExtractComponent; @@ -441,7 +440,10 @@ impl Camera { #[inline] pub fn target_scaling_factor(&self) -> Option { - self.computed.target_info.as_ref().map(|t| t.scale_factor) + self.computed + .target_info + .as_ref() + .map(|t: &RenderTargetInfo| t.scale_factor) } /// The projection matrix computed using this camera's [`CameraProjection`]. @@ -1062,6 +1064,7 @@ pub fn extract_cameras( RenderLayers, Projection, GpuCulling, + ViewUniformOffset, )>(); continue; } diff --git a/crates/bevy_render/src/camera/manual_texture_view.rs b/crates/bevy_render/src/camera/manual_texture_view.rs index a1353b9d0f9c9..11d82364a611a 100644 --- a/crates/bevy_render/src/camera/manual_texture_view.rs +++ b/crates/bevy_render/src/camera/manual_texture_view.rs @@ -1,7 +1,6 @@ -use crate::{ - extract_resource::ExtractResource, render_resource::TextureView, texture::BevyDefault, -}; +use crate::{extract_resource::ExtractResource, render_resource::TextureView}; use bevy_ecs::{prelude::Component, reflect::ReflectComponent, system::Resource}; +use bevy_image::BevyDefault as _; use bevy_math::UVec2; use bevy_reflect::prelude::*; use bevy_utils::HashMap; diff --git a/crates/bevy_render/src/gpu_readback.rs b/crates/bevy_render/src/gpu_readback.rs index 434b4603cac7c..bb0092edf15bf 100644 --- a/crates/bevy_render/src/gpu_readback.rs +++ b/crates/bevy_render/src/gpu_readback.rs @@ -1,12 +1,11 @@ use crate::{ extract_component::ExtractComponentPlugin, - prelude::Image, render_asset::RenderAssets, render_resource::{Buffer, BufferUsages, Extent3d, ImageDataLayout, Texture, TextureFormat}, renderer::{render_system, RenderDevice}, storage::{GpuShaderStorageBuffer, ShaderStorageBuffer}, sync_world::MainEntity, - texture::{GpuImage, TextureFormatPixelInfo}, + texture::GpuImage, ExtractSchedule, MainWorld, Render, RenderApp, RenderSet, }; use async_channel::{Receiver, Sender}; @@ -21,6 +20,7 @@ use bevy_ecs::{ prelude::{Component, Resource, World}, system::{Query, Res}, }; +use bevy_image::{Image, TextureFormatPixelInfo}; use bevy_reflect::Reflect; use bevy_render_macros::ExtractComponent; use bevy_utils::{default, tracing::warn, HashMap}; diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index a682f2b954413..2923afbad6048 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -61,12 +61,11 @@ pub mod prelude { }, render_resource::Shader, spatial_bundle::SpatialBundle, - texture::{Image, ImagePlugin, IntoDynamicImageError}, + texture::ImagePlugin, view::{InheritedVisibility, Msaa, ViewVisibility, Visibility, VisibilityBundle}, ExtractSchedule, }; } - use batching::gpu_preprocessing::BatchingPlugin; use bevy_ecs::schedule::ScheduleBuildSettings; use bevy_utils::prelude::default; @@ -77,7 +76,8 @@ use bevy_window::{PrimaryWindow, RawHandleWrapperHolder}; use extract_resource::ExtractResourcePlugin; use globals::GlobalsPlugin; use render_asset::RenderAssetBytesPerFrame; -use renderer::{RenderAdapter, RenderAdapterInfo, RenderDevice, RenderQueue}; +use renderer::{RenderDevice, RenderQueue}; +use settings::RenderResources; use sync_world::{ despawn_temporary_render_entities, entity_sync_system, SyncToRenderWorld, SyncWorldPlugin, }; @@ -96,7 +96,7 @@ use crate::{ use alloc::sync::Arc; use bevy_app::{App, AppLabel, Plugin, SubApp}; use bevy_asset::{load_internal_asset, AssetApp, AssetServer, Handle}; -use bevy_ecs::{prelude::*, schedule::ScheduleLabel, system::SystemState}; +use bevy_ecs::{prelude::*, schedule::ScheduleLabel}; use bevy_utils::tracing::debug; use core::ops::{Deref, DerefMut}; use std::sync::Mutex; @@ -241,19 +241,7 @@ pub mod graph { } #[derive(Resource)] -struct FutureRendererResources( - Arc< - Mutex< - Option<( - RenderDevice, - RenderQueue, - RenderAdapterInfo, - RenderAdapter, - RenderInstance, - )>, - >, - >, -); +struct FutureRenderResources(Arc>>); /// A label for the rendering sub-app. #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, AppLabel)] @@ -272,31 +260,27 @@ impl Plugin for RenderPlugin { .init_asset_loader::(); match &self.render_creation { - RenderCreation::Manual(device, queue, adapter_info, adapter, instance) => { - let future_renderer_resources_wrapper = Arc::new(Mutex::new(Some(( - device.clone(), - queue.clone(), - adapter_info.clone(), - adapter.clone(), - instance.clone(), - )))); - app.insert_resource(FutureRendererResources( - future_renderer_resources_wrapper.clone(), + RenderCreation::Manual(resources) => { + let future_render_resources_wrapper = Arc::new(Mutex::new(Some(resources.clone()))); + app.insert_resource(FutureRenderResources( + future_render_resources_wrapper.clone(), )); // SAFETY: Plugins should be set up on the main thread. unsafe { initialize_render_app(app) }; } RenderCreation::Automatic(render_creation) => { if let Some(backends) = render_creation.backends { - let future_renderer_resources_wrapper = Arc::new(Mutex::new(None)); - app.insert_resource(FutureRendererResources( - future_renderer_resources_wrapper.clone(), + let future_render_resources_wrapper = Arc::new(Mutex::new(None)); + app.insert_resource(FutureRenderResources( + future_render_resources_wrapper.clone(), )); - let mut system_state: SystemState< - Query<&RawHandleWrapperHolder, With>, - > = SystemState::new(app.world_mut()); - let primary_window = system_state.get(app.world()).get_single().ok().cloned(); + let primary_window = app + .world_mut() + .query_filtered::<&RawHandleWrapperHolder, With>() + .get_single(app.world()) + .ok() + .cloned(); let settings = render_creation.clone(); let async_renderer = async move { let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { @@ -306,13 +290,13 @@ impl Plugin for RenderPlugin { gles_minor_version: settings.gles3_minor_version, }); - // SAFETY: Plugins should be set up on the main thread. - let surface = primary_window.and_then(|wrapper| unsafe { + let surface = primary_window.and_then(|wrapper| { let maybe_handle = wrapper.0.lock().expect( "Couldn't get the window handle in time for renderer initialization", ); if let Some(wrapper) = maybe_handle.as_ref() { - let handle = wrapper.get_handle(); + // SAFETY: Plugins should be set up on the main thread. + let handle = unsafe { wrapper.get_handle() }; Some( instance .create_surface(handle) @@ -338,9 +322,9 @@ impl Plugin for RenderPlugin { .await; debug!("Configured wgpu adapter Limits: {:#?}", device.limits()); debug!("Configured wgpu adapter Features: {:#?}", device.features()); - let mut future_renderer_resources_inner = - future_renderer_resources_wrapper.lock().unwrap(); - *future_renderer_resources_inner = Some(( + let mut future_render_resources_inner = + future_render_resources_wrapper.lock().unwrap(); + *future_render_resources_inner = Some(RenderResources( device, queue, adapter_info, @@ -392,7 +376,7 @@ impl Plugin for RenderPlugin { fn ready(&self, app: &App) -> bool { app.world() - .get_resource::() + .get_resource::() .and_then(|frr| frr.0.try_lock().map(|locked| locked.is_some()).ok()) .unwrap_or(true) } @@ -405,11 +389,11 @@ impl Plugin for RenderPlugin { "color_operations.wgsl", Shader::from_wgsl ); - if let Some(future_renderer_resources) = - app.world_mut().remove_resource::() + if let Some(future_render_resources) = + app.world_mut().remove_resource::() { - let (device, queue, adapter_info, render_adapter, instance) = - future_renderer_resources.0.lock().unwrap().take().unwrap(); + let RenderResources(device, queue, adapter_info, render_adapter, instance) = + future_render_resources.0.lock().unwrap().take().unwrap(); app.insert_resource(device.clone()) .insert_resource(queue.clone()) diff --git a/crates/bevy_render/src/mesh/allocator.rs b/crates/bevy_render/src/mesh/allocator.rs index 3493f7dcf1722..506d3adeff305 100644 --- a/crates/bevy_render/src/mesh/allocator.rs +++ b/crates/bevy_render/src/mesh/allocator.rs @@ -955,12 +955,18 @@ impl ElementLayout { /// Creates an [`ElementLayout`] for mesh data of the given class (vertex or /// index) with the given byte size. fn new(class: ElementClass, size: u64) -> ElementLayout { + const { + assert!(4 == COPY_BUFFER_ALIGNMENT); + } + // this is equivalent to `4 / gcd(4,size)` but lets us not implement gcd. + // ping @atlv if above assert ever fails (likely never) + let elements_per_slot = [1, 4, 2, 4][size as usize & 3]; ElementLayout { class, size, // Make sure that slot boundaries begin and end on // `COPY_BUFFER_ALIGNMENT`-byte (4-byte) boundaries. - elements_per_slot: (COPY_BUFFER_ALIGNMENT / gcd(size, COPY_BUFFER_ALIGNMENT)) as u32, + elements_per_slot, } } @@ -1000,18 +1006,6 @@ impl GeneralSlab { } } -/// Returns the greatest common divisor of the two numbers. -/// -/// -fn gcd(mut a: u64, mut b: u64) -> u64 { - while b != 0 { - let t = b; - b = a % b; - a = t; - } - a -} - /// Returns a string describing the given buffer usages. fn buffer_usages_to_str(buffer_usages: BufferUsages) -> &'static str { if buffer_usages.contains(BufferUsages::VERTEX) { diff --git a/crates/bevy_render/src/primitives/mod.rs b/crates/bevy_render/src/primitives/mod.rs index 70a06ae635840..48c0652fef3b9 100644 --- a/crates/bevy_render/src/primitives/mod.rs +++ b/crates/bevy_render/src/primitives/mod.rs @@ -95,6 +95,20 @@ impl Aabb { pub fn max(&self) -> Vec3A { self.center + self.half_extents } + + /// Check if the AABB is at the front side of the bisecting plane. + /// Referenced from: [AABB Plane intersection](https://gdbooks.gitbooks.io/3dcollisions/content/Chapter2/static_aabb_plane.html) + #[inline] + pub fn is_in_half_space(&self, half_space: &HalfSpace, world_from_local: &Affine3A) -> bool { + // transform the half-extents into world space. + let half_extents_world = world_from_local.matrix3.abs() * self.half_extents.abs(); + // collapse the half-extents onto the plane normal. + let p_normal = half_space.normal(); + let r = half_extents_world.dot(p_normal.abs()); + let aabb_center_world = world_from_local.transform_point3a(self.center); + let signed_distance = p_normal.dot(aabb_center_world) + half_space.d(); + signed_distance > r + } } impl From for Aabb { @@ -298,6 +312,18 @@ impl Frustum { } true } + + /// Check if the frustum contains the Axis-Aligned Bounding Box (AABB). + /// Referenced from: [Frustum Culling](https://learnopengl.com/Guest-Articles/2021/Scene/Frustum-Culling) + #[inline] + pub fn contains_aabb(&self, aabb: &Aabb, world_from_local: &Affine3A) -> bool { + for half_space in &self.half_spaces { + if !aabb.is_in_half_space(half_space, world_from_local) { + return false; + } + } + true + } } #[derive(Component, Clone, Debug, Default, Reflect)] @@ -325,6 +351,13 @@ pub struct CascadesFrusta { #[cfg(test)] mod tests { + use core::f32::consts::PI; + + use bevy_math::{ops, Quat}; + use bevy_transform::components::GlobalTransform; + + use crate::camera::{CameraProjection, PerspectiveProjection}; + use super::*; // A big, offset frustum @@ -502,4 +535,92 @@ mod tests { Aabb::from_min_max(Vec3::new(-1.0, -5.0, 0.0), Vec3::new(2.0, 0.0, 1.0)) ); } + + // A frustum with an offset for testing the [`Frustum::contains_aabb`] algorithm. + fn contains_aabb_test_frustum() -> Frustum { + let proj = PerspectiveProjection { + fov: 90.0_f32.to_radians(), + aspect_ratio: 1.0, + near: 1.0, + far: 100.0, + }; + proj.compute_frustum(&GlobalTransform::from_translation(Vec3::new(2.0, 2.0, 0.0))) + } + + fn contains_aabb_test_frustum_with_rotation() -> Frustum { + let half_extent_world = (((49.5 * 49.5) * 0.5) as f32).sqrt() + 0.5f32.sqrt(); + let near = 50.5 - half_extent_world; + let far = near + 2.0 * half_extent_world; + let fov = 2.0 * ops::atan(half_extent_world / near); + let proj = PerspectiveProjection { + aspect_ratio: 1.0, + near, + far, + fov, + }; + proj.compute_frustum(&GlobalTransform::IDENTITY) + } + + #[test] + fn aabb_inside_frustum() { + let frustum = contains_aabb_test_frustum(); + let aabb = Aabb { + center: Vec3A::ZERO, + half_extents: Vec3A::new(0.99, 0.99, 49.49), + }; + let model = Affine3A::from_translation(Vec3::new(2.0, 2.0, -50.5)); + assert!(frustum.contains_aabb(&aabb, &model)); + } + + #[test] + fn aabb_intersect_frustum() { + let frustum = contains_aabb_test_frustum(); + let aabb = Aabb { + center: Vec3A::ZERO, + half_extents: Vec3A::new(0.99, 0.99, 49.6), + }; + let model = Affine3A::from_translation(Vec3::new(2.0, 2.0, -50.5)); + assert!(!frustum.contains_aabb(&aabb, &model)); + } + + #[test] + fn aabb_outside_frustum() { + let frustum = contains_aabb_test_frustum(); + let aabb = Aabb { + center: Vec3A::ZERO, + half_extents: Vec3A::new(0.99, 0.99, 0.99), + }; + let model = Affine3A::from_translation(Vec3::new(0.0, 0.0, 49.6)); + assert!(!frustum.contains_aabb(&aabb, &model)); + } + + #[test] + fn aabb_inside_frustum_rotation() { + let frustum = contains_aabb_test_frustum_with_rotation(); + let aabb = Aabb { + center: Vec3A::new(0.0, 0.0, 0.0), + half_extents: Vec3A::new(0.99, 0.99, 49.49), + }; + + let model = Affine3A::from_rotation_translation( + Quat::from_rotation_x(PI / 4.0), + Vec3::new(0.0, 0.0, -50.5), + ); + assert!(frustum.contains_aabb(&aabb, &model)); + } + + #[test] + fn aabb_intersect_frustum_rotation() { + let frustum = contains_aabb_test_frustum_with_rotation(); + let aabb = Aabb { + center: Vec3A::new(0.0, 0.0, 0.0), + half_extents: Vec3A::new(0.99, 0.99, 49.6), + }; + + let model = Affine3A::from_rotation_translation( + Quat::from_rotation_x(PI / 4.0), + Vec3::new(0.0, 0.0, -50.5), + ); + assert!(!frustum.contains_aabb(&aabb, &model)); + } } diff --git a/crates/bevy_render/src/render_phase/mod.rs b/crates/bevy_render/src/render_phase/mod.rs index 5f05bf1870c53..f460a37d0441c 100644 --- a/crates/bevy_render/src/render_phase/mod.rs +++ b/crates/bevy_render/src/render_phase/mod.rs @@ -101,7 +101,7 @@ where /// /// Each bin corresponds to a single batch set. For unbatchable entities, /// prefer `unbatchable_values` instead. - pub(crate) batchable_mesh_values: HashMap>, + pub batchable_mesh_values: HashMap>, /// A list of `BinKey`s for unbatchable items. /// @@ -112,7 +112,7 @@ where /// The unbatchable bins. /// /// Each entity here is rendered in a separate drawcall. - pub(crate) unbatchable_mesh_values: HashMap, + pub unbatchable_mesh_values: HashMap, /// Items in the bin that aren't meshes at all. /// @@ -155,9 +155,9 @@ pub struct BinnedRenderPhaseBatch { } /// Information about the unbatchable entities in a bin. -pub(crate) struct UnbatchableBinnedEntities { +pub struct UnbatchableBinnedEntities { /// The entities. - pub(crate) entities: Vec<(Entity, MainEntity)>, + pub entities: Vec<(Entity, MainEntity)>, /// The GPU array buffer indices of each unbatchable binned entity. pub(crate) buffer_indices: UnbatchableBinnedEntityIndexSet, diff --git a/crates/bevy_render/src/render_resource/bind_group.rs b/crates/bevy_render/src/render_resource/bind_group.rs index 73e25794066b8..d851389fa7d48 100644 --- a/crates/bevy_render/src/render_resource/bind_group.rs +++ b/crates/bevy_render/src/render_resource/bind_group.rs @@ -78,7 +78,7 @@ impl Deref for BindGroup { /// ok to do "expensive" work here, such as creating a [`Buffer`] for a uniform. /// /// If for some reason a [`BindGroup`] cannot be created yet (for example, the [`Texture`](crate::render_resource::Texture) -/// for an [`Image`](crate::texture::Image) hasn't loaded yet), just return [`AsBindGroupError::RetryNextUpdate`], which signals that the caller +/// for an [`Image`](bevy_image::Image) hasn't loaded yet), just return [`AsBindGroupError::RetryNextUpdate`], which signals that the caller /// should retry again later. /// /// # Deriving @@ -87,7 +87,8 @@ impl Deref for BindGroup { /// what their binding type is, and what index they should be bound at: /// /// ``` -/// # use bevy_render::{render_resource::*, texture::Image}; +/// # use bevy_render::render_resource::*; +/// # use bevy_image::Image; /// # use bevy_color::LinearRgba; /// # use bevy_asset::Handle; /// # use bevy_render::storage::ShaderStorageBuffer; @@ -133,7 +134,7 @@ impl Deref for BindGroup { /// GPU resource, which will be bound as a texture in shaders. The field will be assumed to implement [`Into>>`]. In practice, /// most fields should be a [`Handle`](bevy_asset::Handle) or [`Option>`]. If the value of an [`Option>`] is /// [`None`], the [`crate::texture::FallbackImage`] resource will be used instead. This attribute can be used in conjunction with a `sampler` binding attribute -/// (with a different binding index) if a binding of the sampler for the [`Image`](crate::texture::Image) is also required. +/// (with a different binding index) if a binding of the sampler for the [`Image`](bevy_image::Image) is also required. /// /// | Arguments | Values | Default | /// |-----------------------|-------------------------------------------------------------------------|----------------------| @@ -161,7 +162,7 @@ impl Deref for BindGroup { /// resource, which will be bound as a sampler in shaders. The field will be assumed to implement [`Into>>`]. In practice, /// most fields should be a [`Handle`](bevy_asset::Handle) or [`Option>`]. If the value of an [`Option>`] is /// [`None`], the [`crate::texture::FallbackImage`] resource will be used instead. This attribute can be used in conjunction with a `texture` binding attribute -/// (with a different binding index) if a binding of the texture for the [`Image`](crate::texture::Image) is also required. +/// (with a different binding index) if a binding of the texture for the [`Image`](bevy_image::Image) is also required. /// /// | Arguments | Values | Default | /// |------------------------|-------------------------------------------------------------------------|------------------------| @@ -171,7 +172,7 @@ impl Deref for BindGroup { /// * The field's [`Handle`](bevy_asset::Handle) will be used to look up the matching [`Buffer`] GPU resource, which /// will be bound as a storage buffer in shaders. If the `storage` attribute is used, the field is expected a raw /// buffer, and the buffer will be bound as a storage buffer in shaders. -/// * It supports and optional `read_only` parameter. Defaults to false if not present. +/// * It supports an optional `read_only` parameter. Defaults to false if not present. /// /// | Arguments | Values | Default | /// |------------------------|-------------------------------------------------------------------------|----------------------| @@ -194,9 +195,10 @@ impl Deref for BindGroup { /// /// As mentioned above, [`Option>`] is also supported: /// ``` -/// # use bevy_render::{render_resource::AsBindGroup, texture::Image}; -/// # use bevy_color::LinearRgba; /// # use bevy_asset::Handle; +/// # use bevy_color::LinearRgba; +/// # use bevy_image::Image; +/// # use bevy_render::render_resource::AsBindGroup; /// #[derive(AsBindGroup)] /// struct CoolMaterial { /// #[uniform(0)] @@ -433,8 +435,9 @@ where #[cfg(test)] mod test { use super::*; - use crate::{self as bevy_render, prelude::Image}; + use crate as bevy_render; use bevy_asset::Handle; + use bevy_image::Image; #[test] fn texture_visibility() { diff --git a/crates/bevy_render/src/render_resource/pipeline.rs b/crates/bevy_render/src/render_resource/pipeline.rs index 6b70f93e86c00..be400a02e9b0f 100644 --- a/crates/bevy_render/src/render_resource/pipeline.rs +++ b/crates/bevy_render/src/render_resource/pipeline.rs @@ -108,6 +108,9 @@ pub struct RenderPipelineDescriptor { pub multisample: MultisampleState, /// The compiled fragment stage, its entry point, and the color targets. pub fragment: Option, + /// Whether to zero-initialize workgroup memory by default. If you're not sure, set this to true. + /// If this is false, reading from workgroup variables before writing to them will result in garbage values. + pub zero_initialize_workgroup_memory: bool, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -147,4 +150,7 @@ pub struct ComputePipelineDescriptor { /// The name of the entry point in the compiled shader. There must be a /// function with this name in the shader. pub entry_point: Cow<'static, str>, + /// Whether to zero-initialize workgroup memory by default. If you're not sure, set this to true. + /// If this is false, reading from workgroup variables before writing to them will result in garbage values. + pub zero_initialize_workgroup_memory: bool, } diff --git a/crates/bevy_render/src/render_resource/pipeline_cache.rs b/crates/bevy_render/src/render_resource/pipeline_cache.rs index c3ed894fc4e15..5396b720e3055 100644 --- a/crates/bevy_render/src/render_resource/pipeline_cache.rs +++ b/crates/bevy_render/src/render_resource/pipeline_cache.rs @@ -669,6 +669,7 @@ impl PipelineCache { let device = self.device.clone(); let shader_cache = self.shader_cache.clone(); let layout_cache = self.layout_cache.clone(); + create_pipeline_task( async move { let mut shader_cache = shader_cache.lock().unwrap(); @@ -731,10 +732,10 @@ impl PipelineCache { ) }); - // TODO: Expose this somehow + // TODO: Expose the rest of this somehow let compilation_options = PipelineCompilationOptions { - constants: &std::collections::HashMap::new(), - zero_initialize_workgroup_memory: false, + constants: &default(), + zero_initialize_workgroup_memory: descriptor.zero_initialize_workgroup_memory, }; let descriptor = RawRenderPipelineDescriptor { @@ -779,6 +780,7 @@ impl PipelineCache { let device = self.device.clone(); let shader_cache = self.shader_cache.clone(); let layout_cache = self.layout_cache.clone(); + create_pipeline_task( async move { let mut shader_cache = shader_cache.lock().unwrap(); @@ -812,10 +814,11 @@ impl PipelineCache { layout: layout.as_ref().map(|layout| -> &PipelineLayout { layout }), module: &compute_module, entry_point: Some(&descriptor.entry_point), - // TODO: Expose this somehow + // TODO: Expose the rest of this somehow compilation_options: PipelineCompilationOptions { - constants: &std::collections::HashMap::new(), - zero_initialize_workgroup_memory: false, + constants: &default(), + zero_initialize_workgroup_memory: descriptor + .zero_initialize_workgroup_memory, }, cache: None, }; diff --git a/crates/bevy_render/src/render_resource/storage_buffer.rs b/crates/bevy_render/src/render_resource/storage_buffer.rs index fbf8ffdfe5c35..c559712a76389 100644 --- a/crates/bevy_render/src/render_resource/storage_buffer.rs +++ b/crates/bevy_render/src/render_resource/storage_buffer.rs @@ -6,7 +6,7 @@ use encase::{ internal::WriteInto, DynamicStorageBuffer as DynamicStorageBufferWrapper, ShaderType, StorageBuffer as StorageBufferWrapper, }; -use wgpu::{util::BufferInitDescriptor, BindingResource, BufferBinding, BufferUsages}; +use wgpu::{util::BufferInitDescriptor, BindingResource, BufferBinding, BufferSize, BufferUsages}; use super::IntoBinding; @@ -39,6 +39,7 @@ pub struct StorageBuffer { label: Option, changed: bool, buffer_usage: BufferUsages, + last_written_size: Option, } impl From for StorageBuffer { @@ -50,6 +51,7 @@ impl From for StorageBuffer { label: None, changed: false, buffer_usage: BufferUsages::COPY_DST | BufferUsages::STORAGE, + last_written_size: None, } } } @@ -63,6 +65,7 @@ impl Default for StorageBuffer { label: None, changed: false, buffer_usage: BufferUsages::COPY_DST | BufferUsages::STORAGE, + last_written_size: None, } } } @@ -75,9 +78,11 @@ impl StorageBuffer { #[inline] pub fn binding(&self) -> Option { - Some(BindingResource::Buffer( - self.buffer()?.as_entire_buffer_binding(), - )) + Some(BindingResource::Buffer(BufferBinding { + buffer: self.buffer()?, + offset: 0, + size: self.last_written_size, + })) } pub fn set(&mut self, value: T) { @@ -137,16 +142,15 @@ impl StorageBuffer { } else if let Some(buffer) = &self.buffer { queue.write_buffer(buffer, 0, self.scratch.as_ref()); } + + self.last_written_size = BufferSize::new(size); } } impl<'a, T: ShaderType + WriteInto> IntoBinding<'a> for &'a StorageBuffer { #[inline] fn into_binding(self) -> BindingResource<'a> { - self.buffer() - .expect("Failed to get buffer") - .as_entire_buffer_binding() - .into_binding() + self.binding().expect("Failed to get buffer") } } @@ -180,6 +184,7 @@ pub struct DynamicStorageBuffer { label: Option, changed: bool, buffer_usage: BufferUsages, + last_written_size: Option, _marker: PhantomData T>, } @@ -191,6 +196,7 @@ impl Default for DynamicStorageBuffer { label: None, changed: false, buffer_usage: BufferUsages::COPY_DST | BufferUsages::STORAGE, + last_written_size: None, _marker: PhantomData, } } @@ -207,7 +213,7 @@ impl DynamicStorageBuffer { Some(BindingResource::Buffer(BufferBinding { buffer: self.buffer()?, offset: 0, - size: Some(T::min_size()), + size: self.last_written_size, })) } @@ -260,6 +266,8 @@ impl DynamicStorageBuffer { } else if let Some(buffer) = &self.buffer { queue.write_buffer(buffer, 0, self.scratch.as_ref()); } + + self.last_written_size = BufferSize::new(size); } #[inline] @@ -272,6 +280,6 @@ impl DynamicStorageBuffer { impl<'a, T: ShaderType + WriteInto> IntoBinding<'a> for &'a DynamicStorageBuffer { #[inline] fn into_binding(self) -> BindingResource<'a> { - self.binding().unwrap() + self.binding().expect("Failed to get buffer") } } diff --git a/crates/bevy_render/src/settings.rs b/crates/bevy_render/src/settings.rs index bf1db17ff110c..fe8933656a28c 100644 --- a/crates/bevy_render/src/settings.rs +++ b/crates/bevy_render/src/settings.rs @@ -124,16 +124,19 @@ impl Default for WgpuSettings { } } +#[derive(Clone)] +pub struct RenderResources( + pub RenderDevice, + pub RenderQueue, + pub RenderAdapterInfo, + pub RenderAdapter, + pub RenderInstance, +); + /// An enum describing how the renderer will initialize resources. This is used when creating the [`RenderPlugin`](crate::RenderPlugin). pub enum RenderCreation { /// Allows renderer resource initialization to happen outside of the rendering plugin. - Manual( - RenderDevice, - RenderQueue, - RenderAdapterInfo, - RenderAdapter, - RenderInstance, - ), + Manual(RenderResources), /// Lets the rendering plugin create resources itself. Automatic(WgpuSettings), } @@ -147,7 +150,13 @@ impl RenderCreation { adapter: RenderAdapter, instance: RenderInstance, ) -> Self { - Self::Manual(device, queue, adapter_info, adapter, instance) + RenderResources(device, queue, adapter_info, adapter, instance).into() + } +} + +impl From for RenderCreation { + fn from(value: RenderResources) -> Self { + Self::Manual(value) } } diff --git a/crates/bevy_render/src/sync_world.rs b/crates/bevy_render/src/sync_world.rs index 6cee2885d0669..69a8050ed78a9 100644 --- a/crates/bevy_render/src/sync_world.rs +++ b/crates/bevy_render/src/sync_world.rs @@ -117,7 +117,7 @@ impl Plugin for SyncWorldPlugin { /// /// [`ExtractComponentPlugin`]: crate::extract_component::ExtractComponentPlugin /// [`SyncComponentPlugin`]: crate::sync_component::SyncComponentPlugin -#[derive(Component, Clone, Debug, Default, Reflect)] +#[derive(Component, Copy, Clone, Debug, Default, Reflect)] #[reflect[Component]] #[component(storage = "SparseSet")] pub struct SyncToRenderWorld; @@ -165,8 +165,7 @@ pub type MainEntityHashMap = hashbrown::HashMap; pub type MainEntityHashSet = hashbrown::HashSet; /// Marker component that indicates that its entity needs to be despawned at the end of the frame. -#[derive(Component, Clone, Debug, Default, Reflect)] -#[component(storage = "SparseSet")] +#[derive(Component, Copy, Clone, Debug, Default, Reflect)] pub struct TemporaryRenderEntity; /// A record enum to what entities with [`SyncToRenderWorld`] have been added or removed. diff --git a/crates/bevy_render/src/texture/mod.rs b/crates/bevy_render/src/texture/mod.rs index 351a356be24a1..3671b7a0c83db 100644 --- a/crates/bevy_render/src/texture/mod.rs +++ b/crates/bevy_render/src/texture/mod.rs @@ -4,18 +4,11 @@ mod texture_attachment; mod texture_cache; pub use crate::render_resource::DefaultImageSampler; -#[cfg(feature = "exr")] -pub use bevy_image::ExrTextureLoader; -#[cfg(feature = "hdr")] -pub use bevy_image::HdrTextureLoader; -pub use bevy_image::{ - BevyDefault, CompressedImageFormats, FileTextureError, Image, ImageAddressMode, - ImageFilterMode, ImageFormat, ImageFormatSetting, ImageLoader, ImageLoaderError, - ImageLoaderSettings, ImageSampler, ImageSamplerDescriptor, ImageType, IntoDynamicImageError, - TextureError, TextureFormatPixelInfo, -}; #[cfg(feature = "basis-universal")] -pub use bevy_image::{CompressedImageSaver, CompressedImageSaverError}; +use bevy_image::CompressedImageSaver; +#[cfg(feature = "hdr")] +use bevy_image::HdrTextureLoader; +use bevy_image::{CompressedImageFormats, Image, ImageLoader, ImageSamplerDescriptor}; pub use fallback_image::*; pub use gpu_image::*; pub use texture_attachment::*; @@ -39,7 +32,7 @@ pub const TRANSPARENT_IMAGE_HANDLE: Handle = // TODO: replace Texture names with Image names? /// Adds the [`Image`] as an asset and makes sure that they are extracted and prepared for the GPU. pub struct ImagePlugin { - /// The default image sampler to use when [`ImageSampler`] is set to `Default`. + /// The default image sampler to use when [`bevy_image::ImageSampler`] is set to `Default`. pub default_sampler: ImageSamplerDescriptor, } @@ -69,7 +62,7 @@ impl Plugin for ImagePlugin { fn build(&self, app: &mut App) { #[cfg(feature = "exr")] { - app.init_asset_loader::(); + app.init_asset_loader::(); } #[cfg(feature = "hdr")] diff --git a/crates/bevy_render/src/texture/texture_attachment.rs b/crates/bevy_render/src/texture/texture_attachment.rs index e0947e997207e..ac3854227ff31 100644 --- a/crates/bevy_render/src/texture/texture_attachment.rs +++ b/crates/bevy_render/src/texture/texture_attachment.rs @@ -80,6 +80,7 @@ impl ColorAttachment { } /// A wrapper for a [`TextureView`] that is used as a depth-only [`RenderPassDepthStencilAttachment`]. +#[derive(Clone)] pub struct DepthAttachment { pub view: TextureView, clear_value: Option, diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index 2e306d610f369..22af7ffb44b21 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -18,8 +18,8 @@ use crate::{ render_resource::{DynamicUniformBuffer, ShaderType, Texture, TextureView}, renderer::{RenderDevice, RenderQueue}, texture::{ - BevyDefault, CachedTexture, ColorAttachment, DepthAttachment, GpuImage, - OutputColorAttachment, TextureCache, + CachedTexture, ColorAttachment, DepthAttachment, GpuImage, OutputColorAttachment, + TextureCache, }, Render, RenderApp, RenderSet, }; @@ -28,6 +28,7 @@ use bevy_app::{App, Plugin}; use bevy_color::LinearRgba; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::prelude::*; +use bevy_image::BevyDefault as _; use bevy_math::{mat3, vec2, vec3, Mat3, Mat4, UVec4, Vec2, Vec3, Vec4, Vec4Swizzles}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render_macros::ExtractComponent; @@ -185,6 +186,35 @@ impl Msaa { #[derive(Component)] pub struct ExtractedView { + /// Typically a right-handed projection matrix, one of either: + /// + /// Perspective (infinite reverse z) + /// ```text + /// f = 1 / tan(fov_y_radians / 2) + /// + /// ⎡ f / aspect 0 0 0 ⎤ + /// ⎢ 0 f 0 0 ⎥ + /// ⎢ 0 0 0 -1 ⎥ + /// ⎣ 0 0 near 0 ⎦ + /// ``` + /// + /// Orthographic + /// ```text + /// w = right - left + /// h = top - bottom + /// d = near - far + /// cw = -right - left + /// ch = -top - bottom + /// + /// ⎡ 2 / w 0 0 0 ⎤ + /// ⎢ 0 2 / h 0 0 ⎥ + /// ⎢ 0 0 1 / d 0 ⎥ + /// ⎣ cw / w ch / h near / d 1 ⎦ + /// ``` + /// + /// `clip_from_view[3][3] == 1.0` is the standard way to check if a projection is orthographic + /// + /// Custom projections are also possible however. pub clip_from_view: Mat4, pub world_from_view: GlobalTransform, // The view-projection matrix. When provided it is used instead of deriving it from @@ -422,12 +452,44 @@ pub struct ViewUniform { pub world_from_clip: Mat4, pub world_from_view: Mat4, pub view_from_world: Mat4, + /// Typically a right-handed projection matrix, one of either: + /// + /// Perspective (infinite reverse z) + /// ```text + /// f = 1 / tan(fov_y_radians / 2) + /// + /// ⎡ f / aspect 0 0 0 ⎤ + /// ⎢ 0 f 0 0 ⎥ + /// ⎢ 0 0 0 -1 ⎥ + /// ⎣ 0 0 near 0 ⎦ + /// ``` + /// + /// Orthographic + /// ```text + /// w = right - left + /// h = top - bottom + /// d = near - far + /// cw = -right - left + /// ch = -top - bottom + /// + /// ⎡ 2 / w 0 0 0 ⎤ + /// ⎢ 0 2 / h 0 0 ⎥ + /// ⎢ 0 0 1 / d 0 ⎥ + /// ⎣ cw / w ch / h near / d 1 ⎦ + /// ``` + /// + /// `clip_from_view[3][3] == 1.0` is the standard way to check if a projection is orthographic + /// + /// Custom projections are also possible however. pub clip_from_view: Mat4, pub view_from_clip: Mat4, pub world_position: Vec3, pub exposure: f32, // viewport(x_origin, y_origin, width, height) pub viewport: Vec4, + /// 6 world-space half spaces (normal: vec3, distance: f32) ordered left, right, top, bottom, near, far. + /// The normal vectors point towards the interior of the frustum. + /// A half space contains `p` if `normal.dot(p) + distance > 0.` pub frustum: [Vec4; 6], pub color_grading: ColorGradingUniform, pub mip_bias: f32, diff --git a/crates/bevy_render/src/view/view.wgsl b/crates/bevy_render/src/view/view.wgsl index c67f382c23b6e..ed08599758a07 100644 --- a/crates/bevy_render/src/view/view.wgsl +++ b/crates/bevy_render/src/view/view.wgsl @@ -19,12 +19,44 @@ struct View { world_from_clip: mat4x4, world_from_view: mat4x4, view_from_world: mat4x4, + // Typically a right-handed projection matrix, one of either: + // + // Perspective (infinite reverse z) + // ``` + // f = 1 / tan(fov_y_radians / 2) + // + // ⎡ f / aspect 0 0 0 ⎤ + // ⎢ 0 f 0 0 ⎥ + // ⎢ 0 0 0 -1 ⎥ + // ⎣ 0 0 near 0 ⎦ + // ``` + // + // Orthographic + // ``` + // w = right - left + // h = top - bottom + // d = near - far + // cw = -right - left + // ch = -top - bottom + // + // ⎡ 2 / w 0 0 0 ⎤ + // ⎢ 0 2 / h 0 0 ⎥ + // ⎢ 0 0 1 / d 0 ⎥ + // ⎣ cw / w ch / h near / d 1 ⎦ + // ``` + // + // `clip_from_view[3][3] == 1.0` is the standard way to check if a projection is orthographic + // + // Custom projections are also possible however. clip_from_view: mat4x4, view_from_clip: mat4x4, world_position: vec3, exposure: f32, // viewport(x_origin, y_origin, width, height) viewport: vec4, + // 6 world-space half spaces (normal: vec3, distance: f32) ordered left, right, top, bottom, near, far. + // The normal vectors point towards the interior of the frustum. + // A half space contains `p` if `normal.dot(p) + distance > 0.` frustum: array, 6>, color_grading: ColorGrading, mip_bias: f32, diff --git a/crates/bevy_render/src/view/visibility/range.rs b/crates/bevy_render/src/view/visibility/range.rs index c476c9e9c640e..9ea69db7f9770 100644 --- a/crates/bevy_render/src/view/visibility/range.rs +++ b/crates/bevy_render/src/view/visibility/range.rs @@ -12,6 +12,7 @@ use bevy_ecs::{ entity::{Entity, EntityHashMap}, query::{Changed, With}, reflect::ReflectComponent, + removal_detection::RemovedComponents, schedule::IntoSystemConfigs as _, system::{Query, Res, ResMut, Resource}, }; @@ -111,7 +112,7 @@ impl Plugin for VisibilityRangePlugin { /// that the `end_margin` of a higher LOD is always identical to the /// `start_margin` of the next lower LOD; this is important for the crossfade /// effect to function properly. -#[derive(Component, Clone, PartialEq, Reflect)] +#[derive(Component, Clone, PartialEq, Default, Reflect)] #[reflect(Component, PartialEq, Hash)] pub struct VisibilityRange { /// The range of distances, in world units, between which this entity will @@ -131,6 +132,20 @@ pub struct VisibilityRange { /// /// `end_margin.start` must be greater than or equal to `start_margin.end`. pub end_margin: Range, + + /// If set to true, Bevy will use the center of the axis-aligned bounding + /// box ([`Aabb`]) as the position of the mesh for the purposes of + /// visibility range computation. + /// + /// Otherwise, if this field is set to false, Bevy will use the origin of + /// the mesh as the mesh's position. + /// + /// Usually you will want to leave this set to false, because different LODs + /// may have different AABBs, and smooth crossfades between LOD levels + /// require that all LODs of a mesh be at *precisely* the same position. If + /// you aren't using crossfading, however, and your meshes aren't centered + /// around their origins, then this flag may be useful. + pub use_aabb: bool, } impl Eq for VisibilityRange {} @@ -160,6 +175,7 @@ impl VisibilityRange { Self { start_margin: start..start, end_margin: end..end, + use_aabb: false, } } @@ -390,14 +406,17 @@ pub fn check_visibility_ranges( for (entity, entity_transform, maybe_model_aabb, visibility_range) in entity_query.iter_mut() { let mut visibility = 0; for (view_index, &(_, view_position)) in views.iter().enumerate() { - let model_pos = if let Some(model_aabb) = maybe_model_aabb { - let world_from_local = entity_transform.affine(); - world_from_local.transform_point3a(model_aabb.center) - } else { - entity_transform.translation_vec3a() + // If instructed to use the AABB and the model has one, use its + // center as the model position. Otherwise, use the model's + // translation. + let model_position = match (visibility_range.use_aabb, maybe_model_aabb) { + (true, Some(model_aabb)) => entity_transform + .affine() + .transform_point3a(model_aabb.center), + _ => entity_transform.translation_vec3a(), }; - if visibility_range.is_visible_at_all((view_position - model_pos).length()) { + if visibility_range.is_visible_at_all((view_position - model_position).length()) { visibility |= 1 << view_index; } } @@ -416,8 +435,9 @@ pub fn extract_visibility_ranges( mut render_visibility_ranges: ResMut, visibility_ranges_query: Extract>, changed_ranges_query: Extract>>, + mut removed_visibility_ranges: Extract>, ) { - if changed_ranges_query.is_empty() { + if changed_ranges_query.is_empty() && removed_visibility_ranges.read().next().is_none() { return; } diff --git a/crates/bevy_render/src/view/window/screenshot.rs b/crates/bevy_render/src/view/window/screenshot.rs index 38606a9b771ec..ad03ca0920937 100644 --- a/crates/bevy_render/src/view/window/screenshot.rs +++ b/crates/bevy_render/src/view/window/screenshot.rs @@ -2,7 +2,7 @@ use super::ExtractedWindows; use crate::{ camera::{ManualTextureViewHandle, ManualTextureViews, NormalizedRenderTarget, RenderTarget}, gpu_readback, - prelude::{Image, Shader}, + prelude::Shader, render_asset::{RenderAssetUsages, RenderAssets}, render_resource::{ binding_types::texture_2d, BindGroup, BindGroupEntries, BindGroupLayout, @@ -11,7 +11,7 @@ use crate::{ SpecializedRenderPipelines, Texture, TextureUsages, TextureView, VertexState, }, renderer::RenderDevice, - texture::{GpuImage, OutputColorAttachment, TextureFormatPixelInfo}, + texture::{GpuImage, OutputColorAttachment}, view::{prepare_view_attachments, prepare_view_targets, ViewTargetAttachments, WindowSurfaces}, ExtractSchedule, MainWorld, Render, RenderApp, RenderSet, }; @@ -23,6 +23,7 @@ use bevy_ecs::{ entity::EntityHashMap, event::event_update_system, prelude::*, system::SystemState, }; use bevy_hierarchy::DespawnRecursiveExt; +use bevy_image::{Image, TextureFormatPixelInfo}; use bevy_reflect::Reflect; use bevy_tasks::AsyncComputeTaskPool; use bevy_utils::{ @@ -496,6 +497,7 @@ impl SpecializedRenderPipeline for ScreenshotToScreenPipeline { })], }), push_constant_ranges: Vec::new(), + zero_initialize_workgroup_memory: false, } } } diff --git a/crates/bevy_sprite/Cargo.toml b/crates/bevy_sprite/Cargo.toml index 59ee9b8fe1de8..9bbf629ec5098 100644 --- a/crates/bevy_sprite/Cargo.toml +++ b/crates/bevy_sprite/Cargo.toml @@ -9,7 +9,6 @@ license = "MIT OR Apache-2.0" keywords = ["bevy"] [features] -default = ["bevy_sprite_picking_backend"] bevy_sprite_picking_backend = ["bevy_picking", "bevy_window"] serialize = ["dep:serde"] webgl = [] @@ -22,6 +21,7 @@ bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" } bevy_color = { path = "../bevy_color", version = "0.15.0-dev" } bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.15.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.15.0-dev" } bevy_math = { path = "../bevy_math", version = "0.15.0-dev" } bevy_picking = { path = "../bevy_picking", version = "0.15.0-dev", optional = true } bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [ diff --git a/crates/bevy_sprite/src/dynamic_texture_atlas_builder.rs b/crates/bevy_sprite/src/dynamic_texture_atlas_builder.rs index 879a9af3a497a..4c53e321ae4a7 100644 --- a/crates/bevy_sprite/src/dynamic_texture_atlas_builder.rs +++ b/crates/bevy_sprite/src/dynamic_texture_atlas_builder.rs @@ -1,8 +1,9 @@ use crate::TextureAtlasLayout; +use bevy_image::{Image, TextureFormatPixelInfo}; use bevy_math::{URect, UVec2}; use bevy_render::{ render_asset::{RenderAsset, RenderAssetUsages}, - texture::{GpuImage, Image, TextureFormatPixelInfo}, + texture::GpuImage, }; use guillotiere::{size2, Allocation, AtlasAllocator}; diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index 265be52209a1e..c6023b2c3055b 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -51,20 +51,32 @@ use bevy_app::prelude::*; use bevy_asset::{load_internal_asset, AssetApp, Assets, Handle}; use bevy_core_pipeline::core_2d::Transparent2d; use bevy_ecs::{prelude::*, query::QueryItem}; +use bevy_image::Image; use bevy_render::{ extract_component::{ExtractComponent, ExtractComponentPlugin}, mesh::{Mesh, Mesh2d, MeshAabb}, primitives::Aabb, render_phase::AddRenderCommand, render_resource::{Shader, SpecializedRenderPipelines}, - texture::Image, view::{check_visibility, NoFrustumCulling, VisibilitySystems}, ExtractSchedule, Render, RenderApp, RenderSet, }; /// Adds support for 2D sprite rendering. -#[derive(Default)] -pub struct SpritePlugin; +pub struct SpritePlugin { + /// Whether to add the sprite picking backend to the app. + #[cfg(feature = "bevy_sprite_picking_backend")] + pub add_picking: bool, +} + +impl Default for SpritePlugin { + fn default() -> Self { + Self { + #[cfg(feature = "bevy_sprite_picking_backend")] + add_picking: true, + } + } +} pub const SPRITE_SHADER_HANDLE: Handle = Handle::weak_from_u128(2763343953151597127); pub const SPRITE_VIEW_BINDINGS_SHADER_HANDLE: Handle = @@ -135,7 +147,9 @@ impl Plugin for SpritePlugin { ); #[cfg(feature = "bevy_sprite_picking_backend")] - app.add_plugins(picking_backend::SpritePickingPlugin); + if self.add_picking { + app.add_plugins(picking_backend::SpritePickingPlugin); + } if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app diff --git a/crates/bevy_sprite/src/mesh2d/color_material.rs b/crates/bevy_sprite/src/mesh2d/color_material.rs index 3b4ce7641627a..8c3267c40ba4d 100644 --- a/crates/bevy_sprite/src/mesh2d/color_material.rs +++ b/crates/bevy_sprite/src/mesh2d/color_material.rs @@ -4,13 +4,10 @@ use crate::{AlphaMode2d, Material2d, Material2dPlugin, MaterialMesh2dBundle}; use bevy_app::{App, Plugin}; use bevy_asset::{load_internal_asset, Asset, AssetApp, Assets, Handle}; use bevy_color::{Alpha, Color, ColorToComponents, LinearRgba}; +use bevy_image::Image; use bevy_math::Vec4; use bevy_reflect::prelude::*; -use bevy_render::{ - render_asset::RenderAssets, - render_resource::*, - texture::{GpuImage, Image}, -}; +use bevy_render::{render_asset::RenderAssets, render_resource::*, texture::GpuImage}; pub const COLOR_MATERIAL_SHADER_HANDLE: Handle = Handle::weak_from_u128(3253086872234592509); diff --git a/crates/bevy_sprite/src/mesh2d/material.rs b/crates/bevy_sprite/src/mesh2d/material.rs index cfef74be398f3..4eb064de206b4 100644 --- a/crates/bevy_sprite/src/mesh2d/material.rs +++ b/crates/bevy_sprite/src/mesh2d/material.rs @@ -58,8 +58,9 @@ use derive_more::derive::From; /// ``` /// # use bevy_sprite::{Material2d, MeshMaterial2d}; /// # use bevy_ecs::prelude::*; +/// # use bevy_image::Image; /// # use bevy_reflect::TypePath; -/// # use bevy_render::{mesh::{Mesh, Mesh2d}, render_resource::{AsBindGroup, ShaderRef}, texture::Image}; +/// # use bevy_render::{mesh::{Mesh, Mesh2d}, render_resource::{AsBindGroup, ShaderRef}}; /// # use bevy_color::LinearRgba; /// # use bevy_color::palettes::basic::RED; /// # use bevy_asset::{Handle, AssetServer, Assets, Asset}; diff --git a/crates/bevy_sprite/src/mesh2d/mesh.rs b/crates/bevy_sprite/src/mesh2d/mesh.rs index fd65b19d4a02f..bc67c75aa5466 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh.rs +++ b/crates/bevy_sprite/src/mesh2d/mesh.rs @@ -14,8 +14,8 @@ use bevy_ecs::{ query::ROQueryItem, system::{lifetimeless::*, SystemParamItem, SystemState}, }; +use bevy_image::{BevyDefault, Image, ImageSampler, TextureFormatPixelInfo}; use bevy_math::{Affine3, Vec4}; -use bevy_render::sync_world::{MainEntity, MainEntityHashMap}; use bevy_render::{ batching::{ gpu_preprocessing::IndirectParameters, @@ -34,10 +34,8 @@ use bevy_render::{ render_phase::{PhaseItem, RenderCommand, RenderCommandResult, TrackedRenderPass}, render_resource::{binding_types::uniform_buffer, *}, renderer::{RenderDevice, RenderQueue}, - texture::{ - BevyDefault, DefaultImageSampler, FallbackImage, GpuImage, Image, ImageSampler, - TextureFormatPixelInfo, - }, + sync_world::{MainEntity, MainEntityHashMap}, + texture::{DefaultImageSampler, FallbackImage, GpuImage}, view::{ ExtractedView, ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms, ViewVisibility, }, @@ -674,6 +672,7 @@ impl SpecializedMeshPipeline for Mesh2dPipeline { alpha_to_coverage_enabled: false, }, label: Some(label.into()), + zero_initialize_workgroup_memory: false, }) } } diff --git a/crates/bevy_sprite/src/picking_backend.rs b/crates/bevy_sprite/src/picking_backend.rs index 55c06a8ec9ac3..b367efcc00827 100644 --- a/crates/bevy_sprite/src/picking_backend.rs +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -8,6 +8,7 @@ use crate::{Sprite, TextureAtlasLayout}; use bevy_app::prelude::*; use bevy_asset::prelude::*; use bevy_ecs::prelude::*; +use bevy_image::Image; use bevy_math::{prelude::*, FloatExt, FloatOrd}; use bevy_picking::backend::prelude::*; use bevy_render::prelude::*; @@ -70,8 +71,13 @@ pub fn sprite_picking( continue; }; - let Ok(cursor_ray_world) = camera.viewport_to_world(cam_transform, location.position) - else { + let viewport_pos = camera + .logical_viewport_rect() + .map(|v| v.min) + .unwrap_or_default(); + let pos_in_viewport = location.position - viewport_pos; + + let Ok(cursor_ray_world) = camera.viewport_to_world(cam_transform, pos_in_viewport) else { continue; }; let cursor_ray_len = cam_ortho.far - cam_ortho.near; diff --git a/crates/bevy_sprite/src/render/mod.rs b/crates/bevy_sprite/src/render/mod.rs index ac55d47cceca4..9ef692452fcc9 100644 --- a/crates/bevy_sprite/src/render/mod.rs +++ b/crates/bevy_sprite/src/render/mod.rs @@ -18,6 +18,7 @@ use bevy_ecs::{ query::ROQueryItem, system::{lifetimeless::*, SystemParamItem, SystemState}, }; +use bevy_image::{BevyDefault, Image, ImageSampler, TextureFormatPixelInfo}; use bevy_math::{Affine3A, FloatOrd, Quat, Rect, Vec2, Vec4}; use bevy_render::sync_world::MainEntity; use bevy_render::view::RenderVisibleEntities; @@ -33,10 +34,7 @@ use bevy_render::{ }, renderer::{RenderDevice, RenderQueue}, sync_world::{RenderEntity, TemporaryRenderEntity}, - texture::{ - BevyDefault, DefaultImageSampler, FallbackImage, GpuImage, Image, ImageSampler, - TextureFormatPixelInfo, - }, + texture::{DefaultImageSampler, FallbackImage, GpuImage}, view::{ ExtractedView, Msaa, ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms, ViewVisibility, @@ -323,6 +321,7 @@ impl SpecializedRenderPipeline for SpritePipeline { }, label: Some("sprite_pipeline".into()), push_constant_ranges: Vec::new(), + zero_initialize_workgroup_memory: false, } } } diff --git a/crates/bevy_sprite/src/sprite.rs b/crates/bevy_sprite/src/sprite.rs index 88faf4af100f5..f6b8b266d57ea 100644 --- a/crates/bevy_sprite/src/sprite.rs +++ b/crates/bevy_sprite/src/sprite.rs @@ -1,9 +1,10 @@ use bevy_asset::Handle; use bevy_color::Color; use bevy_ecs::{component::Component, reflect::ReflectComponent}; +use bevy_image::Image; use bevy_math::{Rect, Vec2}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -use bevy_render::{sync_world::SyncToRenderWorld, texture::Image, view::Visibility}; +use bevy_render::{sync_world::SyncToRenderWorld, view::Visibility}; use bevy_transform::components::Transform; use crate::{TextureAtlas, TextureSlicer}; diff --git a/crates/bevy_sprite/src/texture_atlas.rs b/crates/bevy_sprite/src/texture_atlas.rs index a4d82b2ca1874..797fb4aa206ff 100644 --- a/crates/bevy_sprite/src/texture_atlas.rs +++ b/crates/bevy_sprite/src/texture_atlas.rs @@ -1,9 +1,9 @@ use bevy_asset::{Asset, AssetId, Assets, Handle}; +use bevy_image::Image; use bevy_math::{URect, UVec2}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; #[cfg(feature = "serialize")] use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; -use bevy_render::texture::Image; use bevy_utils::HashMap; /// Stores a mapping from sub texture handles to the related area index. diff --git a/crates/bevy_sprite/src/texture_atlas_builder.rs b/crates/bevy_sprite/src/texture_atlas_builder.rs index 2428fd4b15298..b59c9423b90e0 100644 --- a/crates/bevy_sprite/src/texture_atlas_builder.rs +++ b/crates/bevy_sprite/src/texture_atlas_builder.rs @@ -1,9 +1,9 @@ use bevy_asset::AssetId; +use bevy_image::{Image, TextureFormatPixelInfo}; use bevy_math::{URect, UVec2}; use bevy_render::{ render_asset::RenderAssetUsages, render_resource::{Extent3d, TextureDimension, TextureFormat}, - texture::{Image, TextureFormatPixelInfo}, }; use bevy_utils::{ tracing::{debug, error, warn}, @@ -179,6 +179,7 @@ impl<'a> TextureAtlasBuilder<'a> { /// # use bevy_ecs::prelude::*; /// # use bevy_asset::*; /// # use bevy_render::prelude::*; + /// # use bevy_image::Image; /// /// fn my_system(mut commands: Commands, mut textures: ResMut>, mut layouts: ResMut>) { /// // Declare your builder diff --git a/crates/bevy_sprite/src/texture_slice/computed_slices.rs b/crates/bevy_sprite/src/texture_slice/computed_slices.rs index 6e2d94198c9d8..490071a6005ed 100644 --- a/crates/bevy_sprite/src/texture_slice/computed_slices.rs +++ b/crates/bevy_sprite/src/texture_slice/computed_slices.rs @@ -3,8 +3,8 @@ use crate::{ExtractedSprite, Sprite, SpriteImageMode, TextureAtlasLayout}; use super::TextureSlice; use bevy_asset::{AssetEvent, Assets}; use bevy_ecs::prelude::*; +use bevy_image::Image; use bevy_math::{Rect, Vec2}; -use bevy_render::texture::Image; use bevy_transform::prelude::*; use bevy_utils::HashSet; diff --git a/crates/bevy_state/macros/src/lib.rs b/crates/bevy_state/macros/src/lib.rs index b18895cdb86ad..8e3300a57b043 100644 --- a/crates/bevy_state/macros/src/lib.rs +++ b/crates/bevy_state/macros/src/lib.rs @@ -9,12 +9,12 @@ mod states; use bevy_macro_utils::BevyManifest; use proc_macro::TokenStream; -#[proc_macro_derive(States)] +#[proc_macro_derive(States, attributes(states))] pub fn derive_states(input: TokenStream) -> TokenStream { states::derive_states(input) } -#[proc_macro_derive(SubStates, attributes(source))] +#[proc_macro_derive(SubStates, attributes(states, source))] pub fn derive_substates(input: TokenStream) -> TokenStream { states::derive_substates(input) } diff --git a/crates/bevy_state/macros/src/states.rs b/crates/bevy_state/macros/src/states.rs index 7a3872fdd135f..52c133f6ee204 100644 --- a/crates/bevy_state/macros/src/states.rs +++ b/crates/bevy_state/macros/src/states.rs @@ -4,9 +4,42 @@ use syn::{parse_macro_input, spanned::Spanned, DeriveInput, Pat, Path, Result}; use crate::bevy_state_path; +pub const STATES: &str = "states"; +pub const SCOPED_ENTITIES: &str = "scoped_entities"; + +struct StatesAttrs { + scoped_entities_enabled: bool, +} + +fn parse_states_attr(ast: &DeriveInput) -> Result { + let mut attrs = StatesAttrs { + scoped_entities_enabled: false, + }; + + for attr in ast.attrs.iter() { + if attr.path().is_ident(STATES) { + attr.parse_nested_meta(|nested| { + if nested.path.is_ident(SCOPED_ENTITIES) { + attrs.scoped_entities_enabled = true; + Ok(()) + } else { + Err(nested.error("Unsupported attribute")) + } + })?; + } + } + + Ok(attrs) +} + pub fn derive_states(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); + let attrs = match parse_states_attr(&ast) { + Ok(attrs) => attrs, + Err(e) => return e.into_compile_error().into(), + }; + let generics = ast.generics; let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); @@ -23,8 +56,12 @@ pub fn derive_states(input: TokenStream) -> TokenStream { let struct_name = &ast.ident; + let scoped_entities_enabled = attrs.scoped_entities_enabled; + quote! { - impl #impl_generics #trait_path for #struct_name #ty_generics #where_clause {} + impl #impl_generics #trait_path for #struct_name #ty_generics #where_clause { + const SCOPED_ENTITIES_ENABLED: bool = #scoped_entities_enabled; + } impl #impl_generics #state_mutation_trait_path for #struct_name #ty_generics #where_clause { } @@ -37,7 +74,7 @@ struct Source { source_value: Pat, } -fn parse_sources_attr(ast: &DeriveInput) -> Result { +fn parse_sources_attr(ast: &DeriveInput) -> Result<(StatesAttrs, Source)> { let mut result = ast .attrs .iter() @@ -73,16 +110,19 @@ fn parse_sources_attr(ast: &DeriveInput) -> Result { )); } + let states_attrs = parse_states_attr(ast)?; + let Some(result) = result.pop() else { return Err(syn::Error::new(ast.span(), "SubStates require a source")); }; - Ok(result) + Ok((states_attrs, result)) } pub fn derive_substates(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); - let sources = parse_sources_attr(&ast).expect("Failed to parse substate sources"); + let (states_attrs, sources) = + parse_sources_attr(&ast).expect("Failed to parse substate sources"); let generics = ast.generics; let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); @@ -113,6 +153,8 @@ pub fn derive_substates(input: TokenStream) -> TokenStream { let source_state_type = sources.source_type; let source_state_value = sources.source_value; + let scoped_entities_enabled = states_attrs.scoped_entities_enabled; + let result = quote! { impl #impl_generics #trait_path for #struct_name #ty_generics #where_clause { type SourceStates = #source_state_type; @@ -124,6 +166,8 @@ pub fn derive_substates(input: TokenStream) -> TokenStream { impl #impl_generics #state_trait_path for #struct_name #ty_generics #where_clause { const DEPENDENCY_DEPTH : usize = ::SourceStates::SET_DEPENDENCY_DEPTH + 1; + + const SCOPED_ENTITIES_ENABLED: bool = #scoped_entities_enabled; } impl #impl_generics #state_mutation_trait_path for #struct_name #ty_generics #where_clause { diff --git a/crates/bevy_state/src/app.rs b/crates/bevy_state/src/app.rs index 666b35ce846a6..c1aaf0933337a 100644 --- a/crates/bevy_state/src/app.rs +++ b/crates/bevy_state/src/app.rs @@ -58,6 +58,9 @@ pub trait AppExtStates { /// Enable state-scoped entity clearing for state `S`. /// + /// If the [`States`] trait was derived with the `#[states(scoped_entities)]` attribute, it + /// will be called automatically. + /// /// For more information refer to [`StateScoped`](crate::state_scoped::StateScoped). fn enable_state_scoped_entities(&mut self) -> &mut Self; @@ -104,6 +107,9 @@ impl AppExtStates for SubApp { exited: None, entered: Some(state), }); + if S::SCOPED_ENTITIES_ENABLED { + self.enable_state_scoped_entities::(); + } } else { let name = core::any::type_name::(); warn!("State {} is already initialized.", name); @@ -126,6 +132,9 @@ impl AppExtStates for SubApp { exited: None, entered: Some(state), }); + if S::SCOPED_ENTITIES_ENABLED { + self.enable_state_scoped_entities::(); + } } else { // Overwrite previous state and initial event self.insert_resource::>(State::new(state.clone())); @@ -160,6 +169,9 @@ impl AppExtStates for SubApp { exited: None, entered: state, }); + if S::SCOPED_ENTITIES_ENABLED { + self.enable_state_scoped_entities::(); + } } else { let name = core::any::type_name::(); warn!("Computed state {} is already initialized.", name); @@ -188,6 +200,9 @@ impl AppExtStates for SubApp { exited: None, entered: state, }); + if S::SCOPED_ENTITIES_ENABLED { + self.enable_state_scoped_entities::(); + } } else { let name = core::any::type_name::(); warn!("Sub state {} is already initialized.", name); diff --git a/crates/bevy_state/src/state/states.rs b/crates/bevy_state/src/state/states.rs index 3d315ab202fd8..8e2422d46a361 100644 --- a/crates/bevy_state/src/state/states.rs +++ b/crates/bevy_state/src/state/states.rs @@ -64,4 +64,8 @@ pub trait States: 'static + Send + Sync + Clone + PartialEq + Eq + Hash + Debug /// Used to help order transitions and de-duplicate [`ComputedStates`](crate::state::ComputedStates), as well as prevent cyclical /// `ComputedState` dependencies. const DEPENDENCY_DEPTH: usize = 1; + + /// Should [`StateScoped`](crate::state_scoped::StateScoped) be enabled for this state? If set to `true`, + /// the `StateScoped` component will be used to remove entities when changing state. + const SCOPED_ENTITIES_ENABLED: bool = false; } diff --git a/crates/bevy_state/src/state_scoped.rs b/crates/bevy_state/src/state_scoped.rs index 5f1b2d0be31b1..3e9f49f51734d 100644 --- a/crates/bevy_state/src/state_scoped.rs +++ b/crates/bevy_state/src/state_scoped.rs @@ -16,8 +16,8 @@ use crate::state::{StateTransitionEvent, States}; /// Entities marked with this component will be removed /// when the world's state of the matching type no longer matches the supplied value. /// -/// To enable this feature remember to configure your application -/// with [`enable_state_scoped_entities`](crate::app::AppExtStates::enable_state_scoped_entities) on your state(s) of choice. +/// To enable this feature remember to add the attribute `#[states(scoped_entities)]` when deriving [`States`]. +/// It's also possible to enable it when adding the state to an app with [`enable_state_scoped_entities`](crate::app::AppExtStates::enable_state_scoped_entities). /// /// If `bevy_hierarchy` feature is enabled, which it is by default, the despawn will be recursive. /// @@ -26,6 +26,7 @@ use crate::state::{StateTransitionEvent, States}; /// use bevy_ecs::prelude::*; /// /// #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] +/// #[states(scoped_entities)] /// enum GameState { /// #[default] /// MainMenu, @@ -53,7 +54,6 @@ use crate::state::{StateTransitionEvent, States}; /// # let mut app = AppMock; /// /// app.init_state::(); -/// app.enable_state_scoped_entities::(); /// app.add_systems(OnEnter(GameState::InGame), spawn_player); /// ``` #[derive(Component, Clone)] diff --git a/crates/bevy_tasks/src/single_threaded_task_pool.rs b/crates/bevy_tasks/src/single_threaded_task_pool.rs index d7f994026a678..054d22260eac4 100644 --- a/crates/bevy_tasks/src/single_threaded_task_pool.rs +++ b/crates/bevy_tasks/src/single_threaded_task_pool.rs @@ -46,6 +46,16 @@ impl TaskPoolBuilder { self } + /// No op on the single threaded task pool + pub fn on_thread_spawn(self, _f: impl Fn() + Send + Sync + 'static) -> Self { + self + } + + /// No op on the single threaded task pool + pub fn on_thread_destroy(self, _f: impl Fn() + Send + Sync + 'static) -> Self { + self + } + /// Creates a new [`TaskPool`] pub fn build(self) -> TaskPool { TaskPool::new_internal() diff --git a/crates/bevy_text/Cargo.toml b/crates/bevy_text/Cargo.toml index ef3d235113007..479ebed5f4a89 100644 --- a/crates/bevy_text/Cargo.toml +++ b/crates/bevy_text/Cargo.toml @@ -19,6 +19,7 @@ bevy_color = { path = "../bevy_color", version = "0.15.0-dev" } bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.15.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.15.0-dev" } bevy_math = { path = "../bevy_math", version = "0.15.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [ "bevy", diff --git a/crates/bevy_text/src/font_atlas.rs b/crates/bevy_text/src/font_atlas.rs index 8f32dade935f1..4ce4ea62072db 100644 --- a/crates/bevy_text/src/font_atlas.rs +++ b/crates/bevy_text/src/font_atlas.rs @@ -1,9 +1,9 @@ use bevy_asset::{Assets, Handle}; +use bevy_image::{Image, ImageSampler}; use bevy_math::{IVec2, UVec2}; use bevy_render::{ render_asset::RenderAssetUsages, render_resource::{Extent3d, TextureDimension, TextureFormat}, - texture::{Image, ImageSampler}, }; use bevy_sprite::{DynamicTextureAtlasBuilder, TextureAtlasLayout}; use bevy_utils::HashMap; diff --git a/crates/bevy_text/src/font_atlas_set.rs b/crates/bevy_text/src/font_atlas_set.rs index 3be8cf74b7fe5..5547ea5fdbd83 100644 --- a/crates/bevy_text/src/font_atlas_set.rs +++ b/crates/bevy_text/src/font_atlas_set.rs @@ -3,12 +3,12 @@ use bevy_ecs::{ event::EventReader, system::{ResMut, Resource}, }; +use bevy_image::Image; use bevy_math::{IVec2, UVec2}; use bevy_reflect::TypePath; use bevy_render::{ render_asset::RenderAssetUsages, render_resource::{Extent3d, TextureDimension, TextureFormat}, - texture::Image, }; use bevy_sprite::TextureAtlasLayout; use bevy_utils::HashMap; diff --git a/crates/bevy_text/src/glyph.rs b/crates/bevy_text/src/glyph.rs index 4b264a8c8909c..b5295c655e76a 100644 --- a/crates/bevy_text/src/glyph.rs +++ b/crates/bevy_text/src/glyph.rs @@ -1,9 +1,9 @@ //! This module exports types related to rendering glyphs. use bevy_asset::Handle; +use bevy_image::Image; use bevy_math::{IVec2, Vec2}; use bevy_reflect::Reflect; -use bevy_render::texture::Image; use bevy_sprite::TextureAtlasLayout; /// A glyph of a font, typically representing a single character, positioned in screen space. diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index f7129c359f423..cd5fba2ca40ef 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -71,7 +71,7 @@ pub mod prelude { }; } -use bevy_app::prelude::*; +use bevy_app::{prelude::*, Animation}; use bevy_asset::AssetApp; #[cfg(feature = "default_font")] use bevy_asset::{load_internal_binary_asset, Handle}; @@ -116,6 +116,8 @@ impl Plugin for TextPlugin { .register_type::() .register_type::() .register_type::() + .register_type::() + .register_type::() .init_asset_loader::() .init_resource::() .init_resource::() @@ -136,7 +138,8 @@ impl Plugin for TextPlugin { calculate_bounds_text2d.in_set(VisibilitySystems::CalculateBounds), ) .chain() - .in_set(Update2dText), + .in_set(Update2dText) + .after(Animation), ) .add_systems(Last, trim_cosmic_cache); diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index cf5fe2f90c2d9..f8e7274ef31f2 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -9,9 +9,9 @@ use bevy_ecs::{ reflect::ReflectComponent, system::{ResMut, Resource}, }; +use bevy_image::Image; use bevy_math::{UVec2, Vec2}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -use bevy_render::texture::Image; use bevy_sprite::TextureAtlasLayout; use bevy_utils::HashMap; @@ -107,6 +107,12 @@ impl TextPipeline { computed.entities.clear(); for (span_index, (entity, depth, span, text_font, color)) in text_spans.enumerate() { + // Save this span entity in the computed text block. + computed.entities.push(TextEntity { entity, depth }); + + if span.is_empty() { + continue; + } // Return early if a font is not loaded yet. if !fonts.contains(text_font.font.id()) { spans.clear(); @@ -122,9 +128,6 @@ impl TextPipeline { return Err(TextError::NoSuchFont); } - // Save this span entity in the computed text block. - computed.entities.push(TextEntity { entity, depth }); - // Get max font size for use in cosmic Metrics. font_size = font_size.max(text_font.font_size); diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 8543daea3d145..0effb361aa302 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -28,7 +28,8 @@ impl Default for CosmicBuffer { /// A sub-entity of a [`ComputedTextBlock`]. /// /// Returned by [`ComputedTextBlock::entities`]. -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, Reflect)] +#[reflect(Debug)] pub struct TextEntity { /// The entity. pub entity: Entity, @@ -41,7 +42,8 @@ pub struct TextEntity { /// See [`TextLayout`]. /// /// Automatically updated by 2d and UI text systems. -#[derive(Component, Debug, Clone)] +#[derive(Component, Debug, Clone, Reflect)] +#[reflect(Component, Debug, Default)] pub struct ComputedTextBlock { /// Buffer for managing text layout and creating [`TextLayoutInfo`]. /// @@ -49,6 +51,7 @@ pub struct ComputedTextBlock { /// `TextLayoutInfo`. If you want to control the buffer contents manually or use the `cosmic-text` /// editor, then you need to not use `TextLayout` and instead manually implement the conversion to /// `TextLayoutInfo`. + #[reflect(ignore)] pub(crate) buffer: CosmicBuffer, /// Entities for all text spans in the block, including the root-level text. /// @@ -286,6 +289,28 @@ pub struct TextFont { } impl TextFont { + /// Returns a new [`TextFont`] with the specified font face handle. + pub fn from_font(font: Handle) -> Self { + Self::default().with_font(font) + } + + /// Returns a new [`TextFont`] with the specified font size. + pub fn from_font_size(font_size: f32) -> Self { + Self::default().with_font_size(font_size) + } + + /// Returns this [`TextFont`] with the specified font face handle. + pub fn with_font(mut self, font: Handle) -> Self { + self.font = font; + self + } + + /// Returns this [`TextFont`] with the specified font size. + pub const fn with_font_size(mut self, font_size: f32) -> Self { + self.font_size = font_size; + self + } + /// Returns this [`TextFont`] with the specified [`FontSmoothing`]. pub const fn with_font_smoothing(mut self, font_smoothing: FontSmoothing) -> Self { self.font_smoothing = font_smoothing; diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index 509108cb501e2..03dd84d073aa0 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -11,18 +11,17 @@ use bevy_ecs::component::Component; use bevy_ecs::{ change_detection::{DetectChanges, Ref}, entity::Entity, - event::EventReader, prelude::{ReflectComponent, With}, query::{Changed, Without}, system::{Commands, Local, Query, Res, ResMut}, }; +use bevy_image::Image; use bevy_math::Vec2; use bevy_reflect::{prelude::ReflectDefault, Reflect}; use bevy_render::sync_world::TemporaryRenderEntity; use bevy_render::view::Visibility; use bevy_render::{ primitives::Aabb, - texture::Image, view::{NoFrustumCulling, ViewVisibility}, Extract, }; @@ -30,7 +29,7 @@ use bevy_sprite::{Anchor, ExtractedSprite, ExtractedSprites, SpriteSource, Textu use bevy_transform::components::Transform; use bevy_transform::prelude::GlobalTransform; use bevy_utils::HashSet; -use bevy_window::{PrimaryWindow, Window, WindowScaleFactorChanged}; +use bevy_window::{PrimaryWindow, Window}; /// [`Text2dBundle`] was removed in favor of required components. /// The core component is now [`Text2d`] which can contain a single text segment. @@ -235,12 +234,12 @@ pub fn extract_text2d_sprite( /// It does not modify or observe existing ones. #[allow(clippy::too_many_arguments)] pub fn update_text2d_layout( + mut last_scale_factor: Local, // Text items which should be reprocessed again, generally when the font hasn't loaded yet. mut queue: Local>, mut textures: ResMut>, fonts: Res>, windows: Query<&Window, With>, - mut scale_factor_changed: EventReader, mut texture_atlases: ResMut>, mut font_atlas_sets: ResMut, mut text_pipeline: ResMut, @@ -255,9 +254,6 @@ pub fn update_text2d_layout( mut font_system: ResMut, mut swash_cache: ResMut, ) { - // We need to consume the entire iterator, hence `last` - let factor_changed = scale_factor_changed.read().last().is_some(); - // TODO: Support window-independent scaling: https://github.com/bevyengine/bevy/issues/5621 let scale_factor = windows .get_single() @@ -266,6 +262,9 @@ pub fn update_text2d_layout( let inverse_scale_factor = scale_factor.recip(); + let factor_changed = *last_scale_factor != scale_factor; + *last_scale_factor = scale_factor; + for (entity, block, bounds, text_layout_info, mut computed) in &mut text_query { if factor_changed || computed.needs_rerender() @@ -359,7 +358,7 @@ mod tests { use bevy_app::{App, Update}; use bevy_asset::{load_internal_binary_asset, Handle}; - use bevy_ecs::{event::Events, schedule::IntoSystemConfigs}; + use bevy_ecs::schedule::IntoSystemConfigs; use crate::{detect_text_needs_rerender, TextIterScratch}; @@ -374,7 +373,6 @@ mod tests { .init_resource::>() .init_resource::>() .init_resource::() - .init_resource::>() .init_resource::() .init_resource::() .init_resource::() diff --git a/crates/bevy_ui/Cargo.toml b/crates/bevy_ui/Cargo.toml index cc6098b02fed5..321388d63b0b3 100644 --- a/crates/bevy_ui/Cargo.toml +++ b/crates/bevy_ui/Cargo.toml @@ -18,22 +18,22 @@ bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.15.0-dev" } bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.15.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.15.0-dev" } bevy_input = { path = "../bevy_input", version = "0.15.0-dev" } bevy_math = { path = "../bevy_math", version = "0.15.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [ "bevy", ] } bevy_render = { path = "../bevy_render", version = "0.15.0-dev" } -bevy_animation = { path = "../bevy_animation", version = "0.15.0-dev" } bevy_sprite = { path = "../bevy_sprite", version = "0.15.0-dev" } -bevy_text = { path = "../bevy_text", version = "0.15.0-dev", optional = true } +bevy_text = { path = "../bevy_text", version = "0.15.0-dev" } bevy_picking = { path = "../bevy_picking", version = "0.15.0-dev", optional = true } bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" } bevy_window = { path = "../bevy_window", version = "0.15.0-dev" } bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } # other -taffy = { version = "0.5" } +taffy = { version = "0.6" } serde = { version = "1", features = ["derive"], optional = true } bytemuck = { version = "1.5", features = ["derive"] } derive_more = { version = "1", default-features = false, features = [ @@ -43,9 +43,9 @@ derive_more = { version = "1", default-features = false, features = [ ] } nonmax = "0.5" smallvec = "1.11" +accesskit = "0.17" [features] -default = ["bevy_ui_picking_backend"] serialize = ["serde", "smallvec/serde", "bevy_math/serialize"] bevy_ui_picking_backend = ["bevy_picking"] diff --git a/crates/bevy_ui/src/accessibility.rs b/crates/bevy_ui/src/accessibility.rs index f4d331b9ca7cd..95686530ce37e 100644 --- a/crates/bevy_ui/src/accessibility.rs +++ b/crates/bevy_ui/src/accessibility.rs @@ -1,13 +1,10 @@ use crate::{ experimental::UiChildren, prelude::{Button, Label}, - widget::{TextUiReader, UiImage}, + widget::{ImageNode, TextUiReader}, ComputedNode, }; -use bevy_a11y::{ - accesskit::{Node, Rect, Role}, - AccessibilityNode, -}; +use bevy_a11y::AccessibilityNode; use bevy_app::{App, Plugin, PostUpdate}; use bevy_ecs::{ prelude::{DetectChanges, Entity}, @@ -19,6 +16,8 @@ use bevy_ecs::{ use bevy_render::{camera::CameraUpdateSystem, prelude::Camera}; use bevy_transform::prelude::GlobalTransform; +use accesskit::{Node, Rect, Role}; + fn calc_label( text_reader: &mut TextUiReader, children: impl Iterator, @@ -92,7 +91,10 @@ fn button_changed( fn image_changed( mut commands: Commands, - mut query: Query<(Entity, Option<&mut AccessibilityNode>), (Changed, Without